作者丨 Uber 工程播客
译者丨明知山
策划丨 Tina
简 述
2021 年 11 月,我们决定评估 arm64 架构在 Uber 的可行性。我们的大多数服务是用 Go 或 JAVA 编写的,但我们的构建系统只能编译成 x86_64。现在,得益于开源合作,Uber 拥有了一个独立于系统的构建工具链,可以无缝地支持多种架构。我们使用这个工具链来引导 arm64 主机。本文将分享我们是如何着手去做这件事情的,以及我们早期的想法、遇到的问题、达成的一些成就和未来的方向。
我们从 2021 年 11 月开始使用专门的 linux/x86_64 基础架构,而到了 2023 年 1 月,我们有:
让我们来看看我们是如何做到的。
为什么要考虑 arm64 架构
所有的主流云供应商都在 arm64 上投入巨资,再加上 arm64 与古老的 x86_64 相比所表现出来的平台优势(能耗、价格、计算性能),我们觉得很有必要认真考虑让 arm64 成为我们平台的一部分。
于是,我们开始尝试自己去探究。我们的第一个目标如下所述:
在 arm64 架构上运行一个大型的应用程序,并对可能节省的成本进行度量。
其中一个关键点是最小化运行和基准测试消耗多个核心的服务所需的工作量。我们找到了两种截然不同的方法:
考虑到最小化工作量是我们优先考虑的事项,所以第一个选项似乎更适合我们。毕竟,我们为什么要把时间和金钱投入到有可能被放弃的东西上呢?我们考虑运行一个“并行区域”,它具备 arm64 架构,但在其他方面与生产环境是分离的(并且质量要求更为宽松,方便我们快速前进)。
不久之后,我们有了一个更重要的支持 arm64 的理由:如果我们可以在 arm64 上运行工作负载,就可以让平台的能力多样化,从而让自己处于一个更有利的位置。于是,我们的使命变成了(直到今天仍是如此):
通过在 arm64 上部署一些生产应用程序来降低 Uber 的计算成本、增加容量多样性,以及使我们的平台现代
我们最初是带着原型思维开始的,但现在却有了 180 度转变,形成了一个指导原则:
没有 hack,所有的内容都在主线上(也就是说,没有长期的分支或补丁)。
既然我们的核心基础设施需要提供一流的 arm64 支持,那么这个项目就很自然地被分成两个部分:
那么如何编译成 arm64 二进制文件?当然是直接在 arm64 主机上进行原生构建,或者通过交叉编译。我们有必要先来了解一下原生编译和交叉编译的差异和要求。
原生编译和交叉编译的基础知识
一些我们可能不太熟悉的术语:
下图显示了如何通过原生编译(左)和交叉编译(右)将源文件 mAIn.c 编译成可执行文件。
图 1:输入文件 main.c 原生编译(左)或交叉编译(右)为 aarch64 架构。
原生编译只需要较少的配置和准备工作就可以使用,因为这是大多数编译器工具链的默认模式。从表面上看,我们可以在云供应商的平台上启动一些 arm64 虚拟机,并从那里开始引导我们的工具。但是,我们所有的服务器都使用相同的基础镜像,包括构建主机。基础镜像包含许多从 Go 代码库编译出来的内部工具。因此,我们遇到了一个先有鸡还是先有蛋的问题:如何为我们的第一个 arm64 构建主机编译工具?
示例:使用 GCC 和 Clang 进行交叉编译
让我们在 x86_64 Linux 主机上编译一个 C 文件,目标平台是 Linux aarch64:
GCC 调用目标平台特定的可执行文件(aarch64-linux-gnu-gcc),而 Clang 接受目标平台作为命令行参数(-target <…>):
表面上看,用 GCC 和 Clang 交叉编译 C 源文件似乎很容易,但背后都发生了什么?
基于 LLVM 的 C/C++ 工具链
“clang”使用哪些文件来构建最终的可执行文件?我们来跟踪一下:
以下是这些相关的文件:
现在我们已经知道交叉编译器使用了哪些东西,我们可以将依赖项分为两类:
Uber 需要支持以下这些目标平台:
在撰写本文时,GCC 和 LLVM 都不能交叉编译 macOS 二进制文件。因此,我们维护了一个专门的构建集群来编译 macOS 目标平台。交叉编译 macOS 目标平台是非常有必要的,但我们目前还没有做到这一点。
以下是我们目前支持的主机平台:
下图画出了主机工具链、sysroot 以及它们之间的关系,每个主机工具链(左)都可以使用任意特定于目标平台的 sysroot(右):
图 2:基于 LLVM 的工具链需要每个主机和目标平台的 tarball(“sysroot”)
为了支持这些主机和目标平台,我们需要维护 8 个压缩文件:3 个工具链(每个主机架构需要一个编译的 LLVM)和 5 个目标平台的 sysroot。一个典型的 LLVM 工具链需要 500 到 700MB(压缩包),一个典型的 sysroot 需要 100 到 150MB(压缩包)。在编译代码之前,加上其他工具,总共需要下载和解压约 1.5GB 的压缩文件。Linux x86_64 的 Go 1.20 工具链压缩包为 95MB,是编译代码所需的最大的下载文件。
为了完整起见,我们来看一下 GCC。你可能还记得之前提到 GCC 交叉编译器是 aarch64-linux-gnu-gcc,这意味着每个主机和目标平台都需要一个完整的工具链。因此,如果我们要使用基于 GCC 的工具链,就需要维护 3 5=15 个工具链。如果我们添加一个新的主机平台(例如 Linux aarch64)和两个目标平台(分别针对 x86_64 和 aarch64 的 Linux glibc.2.36),那么需要维护的压缩包数量将跃升至 4
7=28 个!
在购买 Bazel 工具链时,我们评估了 GCC 和基于 LLVM 的工具链。LLVM 更受青睐,因为它需要维护的压缩文件数量的增长是线性的(而不是 GCC 那样的二次幂增长)。但我们能做得更好吗?
Zig 工具链
Zig 采用了不同的方式:它对所有受支持的目标平台使用了相同的工具链。
它在编译时使用了哪些文件?如果我们跟踪一下会发现,它只使用了来自 Zig SDK 的文件(中间文件放在 /tmp 目录下)。主机系统没有受到任何影响,这意味着 Zig 是完全独立的。
为什么 Zig 能做到这样,而 Clang 却不能?Clang 和 Zig 之间主要的差异是什么?Zig 需要的依赖项与 Clang 一样,我们来看一下:
因此,Zig 可以用一个工具链编译所有受支持的目标平台。为了支持我们的 3 个主机和 5 个目标平台,我们需要从 https://ziglang.org/download 下载 3 个 Zig tarball 文件:
图 3:每个主机平台需要 1 个工具链。同一工具链可以编译所有目标平台。
Zig 作者 Andrew Kelley 在他的博客中更详细地解释了 Zig 在 Clang 之上添加了哪些东西。不管我们希望支持多少个目标平台,只需要一个主机工具链,这是非常诱人的。
我们尝试做一些其他工具链无法做到的事情:在 Linux 机器上交叉编译和链接 macOS 可执行文件:
尽管在 2021 年底,Zig 还只是一项未经验证的新技术,但一个主机平台一个 tar 包和交叉编译 macOS 目标的能力赢得了团队的青睐。我们开始使用 Zig,将 zig cc 整合到我们的 Go 代码库中。
Bazel 与 Zig
对于 Bazel 来说,只有一个 C++ 工具链(在本例中是 Zig SDK)是不够的:它还需要一些粘合代码,一个工具链配置。2022 年 2 月,Go 代码库对 zig cc 的初步支持是通过添加到一个配置标志来实现的:
最开始所有的东西都不正常,大部分的测试都无法构建通过,更不用说执行了。我们开始慢慢解决这些问题。到 2022 年 9 月,所有测试都通过了。自 2023 年 1 月起,Zig 工具链可以将 Uber Go 代码库中的所有 C 和 C++ 代码编译到 Linux 目标平台。
Uber 自 2022 年 4 月以来一直在运行 Zig 生成的二进制文件,因此我们对 Zig 信心满满。Bazel 和 Zig 之间的粘合代码最初放在 Adam Bouhenguel 的代码库 bazel-zig-cc 中,后来被 Motiejus Jakštys 克隆并进一步开发,最终转到了 https://Github.com/uber/hermetic_cc_toolchain。
因为与 Zig 软件基金会合作,我们可以寻求对我们来说重要的解决方案。Zig 的人帮助我们发现和修复 Go 和 Zig 中的问题。因为在 2021 年合作进展顺利,Uber 决定将合作关系延长到了 2023 年和 2024 年。Zig 软件基金会所做的所有工作都是开源的,这让更大社区从中受益。
对 arm64 支持的进展
等到工具链足够成熟,可以进行 arm64 平台编译,我们就开始在内部加强对 arm64 的支持。例如:
在能够将程序编译为 arm64 之后,我们开始采用所有可以存储、下载和执行原生二进制文件的系统。现在,我们有:
我们 2023 年的计划包括:
Uber 有使用 Zig 语言吗
可以说有,也可以说没有。例如,ermet_cc_toolchain 中的启动器是我们用 Zig 编写的。嵌入到可执行文件中的运行时库(compiler-rt)是用 Zig 编写的。总而言之,我们的大多数 Go 服务都涉及到了一点 Zig,并且是用 Zig 编写的工具链编译的。
尽管如此,我们还没有将用 Zig 编写的生产应用程序引入到我们的代码库中(虽然工具链已经完全设置好了),因为目前公司中只有少数人知道这门语言。
总 结
截止 2023 年 1 月 16 日,所有发布到生产环境的 C/C++ 代码都通过 hermect_cc_toolchain 进行编译。因为 Zig 现在是我们 Go 代码库的关键组成部分,因此 hermetic_cc_toolchain 的维护得到了财务(与 Zig 软件基金会的合作将到 2024 年底)和 Uber 员工工时的支持。
虽然可以在 arm64 硬件上运行我们的核心基础设施,但我们还没有准备好运行面向客户的应用程序。我们的下一步是在 arm64 上试验面向客户的应用程序,这样就可以测试它的性能并决定未来的方向。
原文链接:
https://www.uber.com/en-SG/blog/bootstrapping-ubers-infrastructure-on-arm64-with-zig