在 Web 开发中,将程序作为 Docker 镜像交付,然后在 Kubernetes / Amazon ECS / Docker Compose / 等中运行它们是标准做法。
近年来,随着 ARM 处理器的流行,程序员面临着准备多架构 Docker 镜像的额外步骤(这意味着同一个镜像应该能够在 x86-64/amd64 和 aarch64/arm64 处理器上本地运行)。
为解释型语言准备 Docker 镜像通常不是问题:
使用 Docker Buildx,它在模拟器内部运行,并为每个架构原生构建镜像。
- NodeJS / Python 只需通过 npm / pip 安装依赖项,复制项目代码,进行少许润色,就这么简单。
- 即使在编译后的 Go 中,这种方法也能正常工作,因为 Go 拥有广泛的标准库。
对于 Rust 来说,即使编译一个简单的 Web 应用程序也是对“宇宙”的重建——几乎任何 Web 应用程序都需要:HTTP 服务器、HTTP 客户端(这反过来需要一个库来处理 https 加密)、异步运行时(tokie 等)、序列化/反序列化(serde、serde_json);在 Rust 中,应该将其作为外部库(crate)安装,并应在每次构建程序时进行编译。
尽管 Rust 编译器有很多工作要做,但它可以快速完成。即使不是最强大的 CI,也可以在几分钟内构建一个普通项目。
但是,这仅适用于在本机架构上构建的情况(例如在 amd64 处理器上构建 amd64 二进制文件)。一旦我们需要构建多架构镜像,我们就必须进行模拟,编译速度就会急剧下降。
例如,在简单公共项目ghstats上,从头开始构建多架构镜像大约需要 50 分钟,而本机架构的相同构建只需 2-3 分钟。
通过正确使用 Docker 层可以优化构建时间,这样只有在依赖项实际发生变化时才会发生重建步骤。因此,只有第一次构建会很长,后续构建会很快。不幸的是,Rust 基础设施在这一点上存在问题——文件中的任何更改Cargo.toml(例如版本号)都会使 Docker 层无效并触发所有依赖项的完全重建。
问题定义
因此,为 Rust 项目构建多架构 Docker 镜像存在两个问题:
- 任何更改发生时,层失效并完全重建Cargo.toml
- 由于模拟,多架构构建速度非常慢
更好的依赖关系建立
解决第一个问题最简单的方法是使用cargo-chef专门为它创建的。cargo-chef将Cargo.toml&转换Cargo.lock为一个特殊recipe.json文件,该文件将保持不变,直到项目依赖项发生变化。然后我们可以使用此 json 文件来缓存编译依赖项的昂贵 Docker 层。
Dockerfilecargo-chef将使用多阶段构建,分为 5 个部分:
- 安装cargo-chef并构建依赖项(OpenSSL、linux-headers 等)
- 准备recipe.json包含项目依赖项描述的文件
- 安装并构建项目依赖项recipe.json
- 建设整体项目
- 将二进制文件复制到运行时镜像并进行最终完善
# (1) 安装 cargo-chef 和构建工具 |
这种方法将加速 Docker 镜像构建,同时项目依赖关系不会改变。由于 Docker 单独构建阶段,因此每次都会执行规划器cargo-chef阶段(但速度很快!),而构建器阶段将被部分缓存,直到recipe.json文件相同。
如果您需要单一架构构建,这种方法就很好了。但如果您需要多架构镜像,这种方法仍然很慢。
具有交叉编译功能的多架构镜像
由于 QEMU 模拟,使用 Docker Buildx 进行多架构构建运行速度非常慢。如果我们摆脱模拟,编译将全速进行。Rust 为其他架构内置了交叉编译,因此我们在 Docker 构建中对其进行了调整。
Rust 本身的交叉编译工作得很好,但有些包是基于 C 库的(OpenSSL、SQLite 等)。编译和链接 C 代码相当复杂,而且并不总是很清楚(通常你必须在 Stack Overflow 或 Github Issues 上寻找错误代码,直到你得到正确的编译器/头文件集)。还有另一个工具令人惊讶地很好地解决了交叉编译 C 代码的问题——Zig(实际上这是一种编程语言,但他们也有构建工具链)。
为了将 Zig 构建工具链与 Rust 连接起来,我将使用cargo-zigbuildcrate。我的其他 Docker 文件看起来与我们的cargo-chef变体非常相似,只是我在 Cargo 中添加了第二个目标来构建并将cargo build替换为cargo zigbuild。
# (1) 此阶段将始终在当前 arch上运行 |
总体来说就是这样。这种构建方法会更快。对于我的项目:
- 初始构建 50 分钟 -> 13 分钟 (3.8 倍)
- 代码更新时间从 7 分钟 -> 3 分钟 (2.3 倍)