使用Zig在arm64上引导Uber的基础设施
2021 年 11 月,我们决定评估 arm64 架构在 Uber 的可行性。我们的大多数服务是用 Go 或 Java 编写的,但我们的构建系统只能编译成 x86_64。现在,得益于开源合作,Uber 拥有了一个独立于系统的构建工具链,可以无缝地支持多种架构。我们使用这个工具链来引导 arm64 主机。本文将分享我们是如何着手去做这件事情的,以及我们早期的想法、遇到的问题、达成的一些成就和未来的方向。
我们从 2021 年 11 月开始使用专门的 Linux/x86_64 基础架构,而到了 2023 年 1 月,我们有:
用于生产环境服务器架构(x86_64 和 arm64)的 C++ 工具链,由 zig cc 提供支持;
一些在 arm64 硬件上运行的核心基础设施服务,为未来的扩展提供了可能性。
让我们来看看我们是如何做到的。
所有的主流云供应商都在 arm64 上投入巨资,再加上 arm64 与古老的 x86_64 相比所表现出来的平台优势(能耗、价格、计算性能),我们觉得很有必要认真考虑让 arm64 成为我们平台的一部分。
于是,我们开始尝试自己去探究。我们的第一个目标如下所述:
在 arm64 架构上运行一个大型的应用程序,并对可能节省的成本进行度量。
其中一个关键点是最小化运行和基准测试消耗多个核心的服务所需的工作量。我们找到了两种截然不同的方法:
在并行区域或现有区域中的独立集群提供基本的 arm64 支持,并在那里运行测试(实验质量);
让所有的核心基础设施都知道现在不止一种架构,然后像生成其他 SKU 一样生成 arm64 主机并测试应用程序。
考虑到最小化工作量是我们优先考虑的事项,所以第一个选项似乎更适合我们。毕竟,我们为什么要把时间和金钱投入到有可能被放弃的东西上呢?我们考虑运行一个“并行区域”,它具备 arm64 架构,但在其他方面与生产环境是分离的(并且质量要求更为宽松,方便我们快速前进)。
不久之后,我们有了一个更重要的支持 arm64 的理由:如果我们可以在 arm64 上运行工作负载,就可以让平台的能力多样化,从而让自己处于一个更有利的位置。于是,我们的使命变成了(直到今天仍是如此):
通过在 arm64 上部署一些生产应用程序来降低 Uber 的计算成本、增加容量多样性,以及使我们的平台现代
我们最初是带着原型思维开始的,但现在却有了 180 度转变,形成了一个指导原则:
没有 hack,所有的内容都在主线上(也就是说,没有长期的分支或补丁)。
既然我们的核心基础设施需要提供一流的 arm64 支持,那么这个项目就很自然地被分成两个部分:
第一个任务是将包含了我们几乎所有基础架构代码的 Go 代码库编译成 arm64 二进制文件;
修改与构建、存储、下载和执行代码相关的所有东西(构建主机、工件存储和调度器),让它们知道现在存在两种架构。
那么如何编译成 arm64 二进制文件?当然是直接在 arm64 主机上进行原生构建,或者通过交叉编译。我们有必要先来了解一下原生编译和交叉编译的差异和要求。
一些我们可能不太熟悉的术语:
二进制文件是由源代码编译而来的机器代码程序。
工具链是将源代码编译为二进制文件所需的一组工具,通常包括预处理器、编译器、链接器等。
密闭(hermetic)工具链是指无论在什么样的环境下,只要给定相同的输入,总是产生相同输出的工具链。这里的“密闭”是指它不使用来自主机的文件,并且包含编译文件所需的所有东西。
主机(host)是指编译二进制文件的机器。
目标平台(target)是指运行二进制文件的机器。
在进行原生编译时,主机和目标是相同的平台(即操作系统、处理器架构和共享库是相同的)。
在进行交叉编译时,主机和目标是不同的平台(例如,从 macOS arm64 (M1) 编译成 x86_64 Linux)。有时候,目标机器可能无法编译代码,但可以运行。例如,一块智能手表可以运行已编译的代码,但不能运行编译器,因此我们可以使用交叉编译器为手表编译程序。
sysroot 是目标平台文件系统的归档。例如,特定于目标平台的头文件、共享库、静态库。通常是交叉编译工具链所必需的,下面将会讨论。
aarch64 或 arm64 是指处理器架构。
下图显示了如何通过原生编译(左)和交叉编译(右)将源文件 main.c 编译成可执行文件。
图 1:输入文件 main.c 原生编译(左)或交叉编译(右)为 aarch64 架构。
原生编译只需要较少的配置和准备工作就可以使用,因为这是大多数编译器工具链的默认模式。从表面上看,我们可以在云供应商的平台上启动一些 arm64 虚拟机,并从那里开始引导我们的工具。但是,我们所有的服务器都使用相同的基础镜像,包括构建主机。基础镜像包含许多从 Go 代码库编译出来的内部工具。因此,我们遇到了一个先有鸡还是先有蛋的问题:如何为我们的第一个 arm64 构建主机编译工具?
让我们在 x86_64 Linux 主机上编译一个 C 文件,目标平台是 Linux aarch64:
GCC 调用目标平台特定的可执行文件(aarch64-linux-gnu-gcc),而 Clang 接受目标平台作为命令行参数(-target <…>):
表面上看,用 GCC 和 Clang 交叉编译 C 源文件似乎很容易,但背后都发生了什么?
“clang”使用哪些文件来构建最终的可执行文件?我们来跟踪一下:
以下是这些相关的文件:
(没有显示出来的)工具:C 编译器(Clang)和链接器(ld)。
/usr/aarch64-linux-gnu/include 中的头文件。这些通常是 GNU C 库头文件。有些程序使用 Linux 内核的公共头文件,但本例中没有。头文件是特定于目标平台的。
编译的、特定于目标架构的库:
动态链接器 /usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1;
C 库,共享对象:/usr/aarch64-linux-gnu/lib/libc.so.6;
程序加载器:crt.o。
其他库:libgcc 和 libc_nonshared。
现在我们已经知道交叉编译器使用了哪些东西,我们可以将依赖项分为两类:
特定于主机的工具(编译器、链接器和其他与目标平台无关的程序);
特定于目标平台的库和头文件,它们是为目标平台编译最终程序所必需的。
Uber 需要支持以下这些目标平台:
Linux x86_64(带有 glibc 2.28);
Linux x86_64(带有 glibc 2.31);
Linux x86_64(带有 musl);
Linux arm64(aarch64,带有 glibc 2.31);
Linux arm64 (aarch64,带有 musl)。
在撰写本文时,GCC 和 LLVM 都不能交叉编译 macOS 二进制文件。因此,我们维护了一个专门的构建集群来编译 macOS 目标平台。交叉编译 macOS 目标平台是非常有必要的,但我们目前还没有做到这一点。
以下是我们目前支持的主机平台:
Linux x86_64:构建集群、DevPod 和开发者笔记本电脑;
macOS x86_64:老一代 macOS 开发者笔记本电脑;
macOS aarch64(Apple Silicon):新一代 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 的工具链,就需要维护 35=15 个工具链。如果我们添加一个新的主机平台(例如 Linux aarch64)和两个目标平台(分别针对 x86_64 和 aarch64 的 Linux glibc.2.36),那么需要维护的压缩包数量将跃升至 4
7=28 个!
在购买 Bazel 工具链时,我们评估了 GCC 和基于 LLVM 的工具链。LLVM 更受青睐,因为它需要维护的压缩文件数量的增长是线性的(而不是 GCC 那样的二次幂增长)。但我们能做得更好吗?
Zig 采用了不同的方式:它对所有受支持的目标平台使用了相同的工具链。
它在编译时使用了哪些文件?如果我们跟踪一下会发现,它只使用了来自 Zig SDK 的文件(中间文件放在 /tmp 目录下)。主机系统没有受到任何影响,这意味着 Zig 是完全独立的。
为什么 Zig 能做到这样,而 Clang 却不能?Clang 和 Zig 之间主要的差异是什么?Zig 需要的依赖项与 Clang 一样,我们来看一下:
工具:C 编译器(Clang)和链接器(lld)。
它们被静态地链接到 Zig 二进制文件中,对于 macOS,Zig 实现了自己的链接器。
/usr/aarch64-linux-gnu/…中的头文件。
Zig 捆绑了多个版本的 glibc、musl libc、linux 内核和其他一些头文件,并自动包含它们。
编译好的特定于目标平台的库:动态链接器、glibc(多版本)、程序加载器。
Zig 根据具体的平台在后台动态编译所有这些文件。
其他库:libgcc 和 libc_nonshared。
Zig 重新实现了这些库中的函数。
因此,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 来说,只有一个 C++ 工具链(在本例中是 Zig SDK)是不够的:它还需要一些粘合代码,一个工具链配置。2022 年 2 月,Go 代码库对 zig cc 的初步支持是通过添加到一个配置标志来实现的:
bazel build –config=hermetic-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 的支持。例如:
当开发人员在 Go 代码库中定义了 Docker 镜像(使用 rules_docker,它相当于 Dockerfile,只是是在 Bazel 中使用),CI 将编译 x86_64 和 arm64 的依赖代码,并且如果无法编译就不允许通过。
我们将 Go 代码库中所有的 Debian 包编译到了 arm64 并发布,尽管它们中的大部分不是我们必需的。与 Docker 镜像类似,CI 确保它们可以编译到 arm64 和 x86_64。目前不可能在我们的 Go 代码库中声明一个不能编译到 arm64 的新的 Debian 包。
在能够将程序编译为 arm64 之后,我们开始采用所有可以存储、下载和执行原生二进制文件的系统。现在,我们有:
开发环境中的 arm64 主机,就像其他 x86_64 主机一样;
运行在 arm64 主机上的几个核心基础设施服务(例如,内部构建的容器调度器和支配程序);
继续扩大 arm64 的使用和支持。
我们 2023 年的计划包括:
为 arm64 增加 Kubernetes 支持;
在 Kubernetes 的 arm64 主机上运行面向客户的服务。
可以说有,也可以说没有。例如,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
声明:本文由 InfoQ 翻译,未经许可禁止转载。
深度:为什么中国数据库领域没有出现像Snowflake这样的巨头?
十七年来奇葩大崩溃!为不让OpenAI和谷歌白拿数据,Reddit 收取巨额API 费用还诽谤开发者,社区爆发大规模抗议
微信扫码关注该文公众号作者