Docker卷中的缓存Cargo依赖项

cgvd09ve  于 2022-11-28  发布在  Docker
关注(0)|答案(7)|浏览(201)

我正在Docker(rust:1.33.0)中构建一个Rust程序。
每次代码更改时,它都会重新编译(好),同时重新下载所有依赖项(坏)。
我想我可以通过添加VOLUME ["/usr/local/cargo"]来缓存依赖项。edit 我也试过用CARGO_HOME移动这个目录,但没有成功。
我认为将其作为卷可以持久保存下载的依赖项,这些依赖项似乎位于此目录中。
但它没有工作,他们仍然下载每一次。为什么?
停靠文件

FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

仅用docker build .构建。
Cargo.toml

[package]
name = "mwe"
version = "0.1.0"
[dependencies]
log = { version = "0.4.6" }

代码:只是你好世界
更改main.rs后第二次运行的输出:

...
Step 4/6 : COPY Cargo.toml .
---> Using cache
---> 97f180cb6ce2
Step 5/6 : COPY src/ ./src/
---> 835be1ea0541
Step 6/6 : RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]
---> Running in 551299a42907
Updating crates.io index
Downloading crates ...
Downloaded log v0.4.6
Downloaded cfg-if v0.1.6
Compiling cfg-if v0.1.6
Compiling log v0.4.6
Compiling mwe v0.1.0 (/)
Finished dev [unoptimized + debuginfo] target(s) in 17.43s
Removing intermediate container 551299a42907
---> e4626da13204
Successfully built e4626da13204
ni65a41a

ni65a41a1#

Dockerfile中的卷在这里会产生反作用。这会在每个构建步骤中挂载一个匿名卷,并且在运行容器时再次挂载。每个构建步骤中的卷在该步骤完成后会被丢弃,这意味着您需要为需要这些依赖项的任何其他步骤再次下载整个内容。
标准模型是复制依赖项规范,运行依赖项下载,复制代码,然后编译或运行代码,分4个独立的步骤。这让docker以一种高效的方式缓存层。我对rust或cargo并不熟悉,但我相信看起来像这样:

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
RUN cargo fetch # this should download dependencies
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

另一个选择是打开BuildKit(available in 18.09, released 2018-11-08)的一些实验性特性,这样Docker就可以将这些依赖项保存在类似于构建的命名卷中。该目录可以在构建之间重用,但不会添加到映像本身,这使得它对于下载缓存之类的东西很有用。

# syntax=docker/dockerfile:experimental
FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN --mount=type=cache,target=/root/.cargo \
    ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

请注意,上面假设cargo将文件缓存在/root/. cargo中。您需要验证这一点并进行适当的调整。我也没有将mount语法与json exec语法混合,以了解这部分是否有效。您可以在此处阅读更多关于BuildKit实验特性的信息:https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md
从18.09和更高版本打开BuildKit就像export DOCKER_BUILDKIT=1一样简单,然后从该shell运行您的构建。

smdncfj3

smdncfj32#

我会说,更好的解决方案是求助于docker multi-stage build,正如这里和那里指出的那样
这样,您可以为自己创建第一个映像,该映像将构建应用程序和依赖项,然后仅在第二个映像中使用第一个映像中的依赖项文件夹
这是由您对@杰克·戈尔的回答的评论和上面链接的两个问题的评论所启发的。

FROM rust:1.33.0 as dependencies

WORKDIR /usr/src/app

COPY Cargo.toml .

RUN rustup default nightly-2019-01-29 && \
    mkdir -p src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build -Z unstable-options --out-dir /output

FROM rust:1.33.0 as application

# Those are the lines instructing this image to reuse the files 
# from the previous image that was aliased as "dependencies" 
COPY --from=dependencies /usr/src/app/Cargo.toml .
COPY --from=dependencies /usr/local/cargo /usr/local/cargo

COPY src/ src/

VOLUME /output

RUN rustup default nightly-2019-01-29  && \
    cargo build -Z unstable-options --out-dir /output
  • PS:只有一次运行将减少您生成的层的数量;更多信息请点击此处 *
yhuiod9q

yhuiod9q3#

这里有一个可能性的概述。(向下滚动我的原始答案。)

  • 添加Cargo文件,创建假的main.rs/lib.rs,然后编译依赖项。然后删除假的源代码并添加真正的源代码。[缓存依赖项,但有几个假的文件带有工作区]。
  • 添加Cargo文件,创建假的main.rs/lib.rs,然后编译依赖项。然后创建一个新的依赖层,并从那里继续。[类似于上面]。
  • 从外部为缓存目录装入卷。[缓存所有内容,依赖调用程序传递--mount]。
  • 在新的Docker版本的Docker文件中使用RUN --mount=type=cache,target=/the/path cargo build。[缓存所有内容,似乎是一个好方法,但目前对我来说太新了。可执行文件不是图像的一部分。编辑:请参阅此处以获取解决方案。]
  • 在另一个容器或主机上运行sccache,然后在构建过程中连接到它。请参见Cargo issue 2644中的注解。
  • 使用cargo-build-deps。[可能适用于某些工作区,但不支持Cargo工作区(2019年)]。
  • 等待Cargo issue 2644。[有人愿意将此添加到Cargo,但还没有具体的解决方案]。
  • 在Dockerfile中使用VOLUME ["/the/path"]不起作用,这仅针对每层(每个命令)。

注意:可以在Dockerfile中设置CARGO_HOMEENV CARGO_TARGET_DIR来控制下载缓存和编译输出的位置。
另请注意:cargo fetch至少可以缓存依赖项的下载,尽管不进行编译。
Cargo工作区需要手动添加每个Cargo文件,对于某些解决方案,需要生成一打伪main.rs/lib.rs。对于只有一个Cargo文件的项目,这些解决方案效果更好。
通过添加以下内容,我可以使缓存为我的特定情况工作

ENV CARGO_HOME /code/dockerout/cargo
ENV CARGO_TARGET_DIR /code/dockerout/target

其中/code是我装载代码的目录。
这是外部挂载的,而不是来自Dockerfile。

EDIT 1:我很困惑为什么会这样,但是@ b.enoit.be和@BMitch澄清了这是因为在Dockerfile中声明的卷只存在于一个层(一个命令)中。

v1l68za4

v1l68za44#

你不需要使用一个显式的Docker卷来缓存你的依赖项。Docker会自动缓存你的图像的不同“层”。基本上,Docker文件中的每个命令对应于图像的一个层。你所面临的问题是基于Docker图像层缓存的工作原理。
Docker遵循的图像层缓存规则在官方的documentation中列出:

  • 从已经在高速缓存中的父映像开始,将下一个指令与从该基本映像派生的所有子映像进行比较,以查看是否使用完全相同的指令构建了其中的一个子映像。如果不是,则使高速缓存无效。
  • 在大多数情况下,只需将Dockerfile中的指令与其中一个子映像进行比较就足够了。但是,某些指令需要更多的检查和解释。
  • 对于ADD和COPY指令,文件的内容检查映像中的个文件,并计算每个文件的校验和。文件的上次修改时间和上次访问时间在缓存查找过程中,将校验和与现有映像中的校验和进行比较。如果文件中有任何更改,(s),如内容和元数据,则使高速缓存无效。
  • 除了ADD和COPY命令之外,高速缓存检查不会查看容器中的文件来确定高速缓存匹配项。例如,在处理RUN apt-get -y update命令时,不会检查容器中更新的文件来确定是否存在高速缓存命中。在这种情况下,仅使用命令字符串本身来查找匹配项。

一旦缓存失效,所有后续Dockerfile命令都会生成新图像,并且不会使用缓存。
所以问题出在DockerfileCOPY src/ ./src/命令的位置上。只要源文件中有一个更改,缓存就会失效,所有后续命令都不会使用缓存。因此,cargo build命令不会使用Docker缓存。
要解决您的问题,只需将Docker文件中的命令重新排序为:

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

COPY src/ ./src/

这样做,只有当Cargo.toml发生变化时,依赖项才会被重新安装。
希望这对你有帮助。

clj7thdc

clj7thdc5#

随着BuildKit与Docker的集成,如果你能够利用强大的BuildKit后端,现在就可以mount a cache volume during a RUN command,恕我直言,这已经成为缓存货物构建的最佳方式。缓存卷会保留以前运行时写入的数据。
要使用BuildKit,您将挂载两个缓存卷,一个用于cargo dir,它缓存外部crate源文件,另一个用于target dir,它缓存所有构建的工件,包括外部crate以及项目bin和libs。
如果您的基础映像是rust,$CARGO_HOME设置为/usr/local/cargo,则您的命令如下所示:

RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
    --mount=type=cache,target=target \
    cargo build

如果你的基本映像是其他的东西,你将需要改变/usr/local/cargo位为任何$CARGO_HOME的值,或者添加一行ENV CARGO_HOME=/usr/local/cargo。作为一个侧记,聪明的做法是设置字面上的target=$CARGO_HOME,让Docker来做扩展,但它似乎并不工作正确-扩展发生了,但是当您这样做时,buildkit仍然不能在运行中保持相同的卷。
实现Cargo构建缓存的其他选项(包括sccache和cargo wharf项目)在this github issue中描述。

yws3nbqq

yws3nbqq6#

我想出了如何让这个也工作在货物工作区,使用romac的cargo-build-deps的fork。
此示例具有my_app和两个工作区:utilsdb中的一个或多个。

FROM rust:nightly as rust

# Cache deps
WORKDIR /app
RUN sudo chown -R rust:rust .
RUN USER=root cargo new myapp

# Install cache-deps
RUN cargo install --git https://github.com/romac/cargo-build-deps.git

WORKDIR /app/myapp
RUN mkdir -p db/src/ utils/src/

# Copy the Cargo tomls
COPY myapp/Cargo.toml myapp/Cargo.lock ./
COPY myapp/db/Cargo.toml ./db/
COPY myapp/utils/Cargo.toml ./utils/

# Cache the deps
RUN cargo build-deps

# Copy the src folders
COPY myapp/src ./src/
COPY myapp/db/src ./db/src/
COPY myapp/utils/src/ ./utils/src/

# Build for debug
RUN cargo build
cgh8pdjw

cgh8pdjw7#

我相信你可以调整这个代码来使用一个Dockerfile,但是我的wrote a dockerized drop-in replacement for cargo你可以保存到一个包里,并作为./cargo build --release运行。这 * 只适用于 *(大多数)开发(使用rust:latest),但没有为CI或任何东西设置。
用法:./cargo build./cargo build --release
它将使用当前的工作目录并将缓存保存到./.cargo。(您可以在版本控制中忽略整个目录,并且它不需要预先存在。)
在项目文件夹中创建一个名为cargo的文件,对该文件运行chmod +x ./cargo,然后将以下代码放入其中:

#!/bin/bash

# This is a drop-in replacement for `cargo`
# that runs in a Docker container as the current user
# on the latest Rust image
# and saves all generated files to `./cargo/` and `./target/`.
#
# Be sure to make this file executable: `chmod +x ./cargo`
#
# # Examples
#
# - Running app: `./cargo run`
# - Building app: `./cargo build`
# - Building release: `./cargo build --release`
#
# # Installing globally
#
# To run `cargo` from anywhere,
# save this file to `/usr/local/bin`.
# You'll then be able to use `cargo`
# as if you had installed Rust globally.
sudo docker run \
    --rm \
    --user "$(id -u)":"$(id -g)" \
    --mount type=bind,src="$PWD",dst=/usr/src/app \
    --workdir /usr/src/app \
    --env CARGO_HOME=/usr/src/app/.cargo \
    rust:latest \
    cargo "$@"

相关问题