在如今的容器化时代,我们的项目通常会在容器中进行测试、构建以及部署。那么如何优化容器的体积,以及构建速度是我们需要关注的,本文将通过一个 Rust
项目示例来演示一些容器化构建的优化思路。
注:文章所述的优化思路很容易推广至任意项目,选择
Rust
仅出于演示目的。读者无需提前了解Rust
相关内容。
本文将从一下角度来讲解:
本文主要讲述在项目实践中优化容器打包速度以及容器体积,因此假设读者已经具备如下条件:
首先我们先创建一个 Rust
项目:
docker run --rm --user=$UID --workdir /app -v .:/app rust cargo new container-opt-rs-demo
注:我们使用
Rust
容器来初始化项目,这样我们本地就无需依赖 Rust 工具链
接下来我们为容器添加 Dockerfile :
FROM rust
WORKDIR /app # 指定工作目录
COPY . . # 拷贝项目内容到容器内
RUN cargo b -r # 编译项目
CMD ["/app/target/release/container-opt-rs-demo"] # 指定容器运行入口
执行构建:
docker build -t app:0.1 .
得到如下镜像:
测试一下镜像:
这是一个非常简单的构建过程:
但是仅仅一个Hello World
,我们容器就有 1.4G 的体积。实际上,Rust 编译器将 Rust 程序编译为二进制可执行文件,因此我们的容器在运行期间并不需要 Rust 工具链,我们可以考虑将编译和最终的执行分离到两个镜像中继续,那么如何达到目的呢?我们可以使用 多阶段构建 这个 Docker 官方提供的解决方案,那么就用多阶段构建技术来重构我们的 Dockerfile :
FROM rust as build
WORKDIR /app # 指定工作目录
COPY . . # 拷贝项目内容到容器内
RUN cargo b -r # 编译项目
FROM debian
WORKDIR /app
# 从上一阶段镜像中拷贝编译好的程序
COPY --from=build /app/target/release/container-opt-rs-demo /app
CMD ["/app"] # 指定容器运行入口
构建结果:
我们使用多阶段构建将编译和运行时进行分离,容器体积从 1.4G 降低到了 110Mb 。
在项目实践中我们还可以选择体积更小的基础镜像来继续优化我们的容器体积。但是我们这个 Dockerfile 就足够好了吗?
当我们项目变得庞大,依赖越来越多时,我们就会发现上面的构建文件还是存在问题的:
实际上,容器的构建过程是一层一层叠加上去的,当我们的底层依赖没有改变时,Docker BuildKit 就能够利用缓存来加速构建,而在我们 Dockerfile 中我们使用了 COPY . .
,这会导致项目内容有任何变化时就会导致该命令之后的所有内容重新构建,比如我们改了项目的 README 文件,也会导致容器的重新构建,这显然时不必要的,因此我们必须细化 COPY
的粒度,来优化我们的构建过程:
FROM rust as build
WORKDIR /app # 指定工作目录
COPY Cargo.toml . # 依赖管理文件
COPY src . # 源代码目录
RUN cargo b -r # 编译项目
FROM debian
WORKDIR /app
# 从上一阶段镜像中拷贝编译好的程序
COPY --from=build /app/target/release/container-opt-rs-demo /app
CMD ["/app"] # 指定容器运行入口
注:我们也可以使用
.dockerignore
文件来剔除我们不想拷贝的内容
dockerignore 与 gitignore 文件类似,具体可参考 官方文档
Rust 由于语言特性,很多工作都在编译期间进行,直接导致它的编译过程很慢,我的一个 Rust微服务 在 i9-11900k 上的编译过程大约是2分钟,尽管编译时间并不长,但是这个项目已经算是比较小了,而且当频繁构建时,这个速度还是难以接受的,因此我们需要利用编译缓存来加速构建过程,修改执行编译的 Dockerfile 内容:
RUN --mount=type=cache,id=build-cache,target=target cargo b -r # 编译项目
我们添加了
--mount
参数,这个 Docker Buildx 提供的特性,使得我们在执行RUN
指令时可以添加volume
,我们利用这一特性来缓存我们上次构建的编译内容,这样在下次构建镜像时就可以充分利用缓存来加速构建。
最后,我们再利用 mount
特性,给我们的项目依赖添加缓存:
RUN --mount=type=cache,id=cargo-cache,target=/usr/local/cargo --mount=type=cache,id=build-cache,target=target cargo b -r # 编译项目
这样我们的容器在构建时就不必每次都下载新的项目依赖,从而再次加速了构建过程。 最终,我们的 Dockerfile 内容如下:
FROM rust as build
WORKDIR /app # 指定工作目录
COPY Cargo.toml . # 依赖管理文件
COPY src . # 源代码目录
RUN --mount=type=cache,id=cargo-cache,target=/usr/local/cargo \
--mount=type=cache,id=build-cache,target=target \
cargo b -r # 编译项目
FROM debian
WORKDIR /app
# 从上一阶段镜像中拷贝编译好的程序
COPY --from=build /app/target/release/container-opt-rs-demo /app
CMD ["/app"] # 指定容器运行入口
本文通过一个 Rust 项目实例来介绍一些容器化构建过程的优化实践,大致总结为以下内容:
只要遵循以上一些原则,无论什么项目,我们都能够写出更好,更健壮的 Dockerfile 来使我们的项目实践和 Devops 流程更加高效。
以上仅为我的一些实践经验,欢迎大家积极的交流和指正。