连1.0版本都没有,Uber为什么会采用这样一项新技术?
本文最初发布于 Motiejus Jakštys 的个人博客。
免责声明:我在 Uber 工作,我的一部分职责是将 zig cc 引入公司。但这篇文章是我的观点,与 Uber 无关。
我日前在 Zig 的一场交流会上作了题为“Uber 引入 Zig”的 演讲。本文从技术和社交两方面简单介绍了“Uber 是如何使用 Zig 的”,而主要的篇幅是介绍“我把 Zig 带到 Uber 的经验”。
本文要点:
Uber 使用 Zig 来编译其 C/C++ 代码。现在,Uber 只在 Go Monorepo 中使用 bazel-zig-cc,但计划尽可能地将zig cc推广到其他需要 C/C++ 工具链的语言。
与其他工具链相比,zig-cc 提供的 C/C++ 工具链的主要优势是:glibc 版本可配制与 macOS 交叉编译。
Uber 没有任何使用 zig-the-language 的计划。
Uber 与 Zig 软件基金会(ZSF)签署了一份支持协议,以优先修复我们提交的 Bug。ZSF 的财务报告有披露了合同额。
感谢我的团队、Go Monorepo 团队、Go 平台团队、我的主管、财务、法律,当然还有 Zig 软件基金会,是他们让这种关系成为现实。到目前为止,这种关系已经带来了丰硕的成果。
Uber 于 2010 年创立,已拥有超过 150 亿次的出行记录,为此,他们实现了很多很酷的创新技术。Go 和 Java 是通用服务器端语言,Python 和 Node 应用于特定的情况(如 Node 用于前端,Python 用于数据分析 /ML)。C++ 被用于一些底层的库。在后端代码中使用其他语言的情况很少。
我们的 Go Monorepo 比 Linux 内核还要大,有几千名工程师在开发和维护。总而言之,很大。
Abhinav Gupta 是我们的来自 Go 平台团队的同事,其实他描述得比我好:我理解我们只是在使用 Zig 的 C 工具链,而不是将其作为语言使用。Zig 支持基于 C 的代码的交叉编译,能减少对系统 C 编译器的依赖。
2018 年之前,Uber 的 Go 服务都有单独的存储库。2018 年,我们开始将这些服务大规模地迁移到 Go Monorepo。我的团队参与了第一波迁移——我仍然记得那有多复杂。
当时,Go Monorepo 已经使用了一个封闭式的 Go 工具链。因此,用于构建 Go Monorepo 的 Go 编译器不会受系统上安装的编译器影响(如果有的话)。因此,无论在哪个环境下构建,都会使用相同版本的 Go。Bazel 文档对此做了很好的解释。
创建于 2019 年,没有太多变动。
C++ 工具链是一个编译 C/C++ 代码的程序集。不可避免地,我们的一些 Go 代码要使用 CGo,所以它需要一个 C/C++ 编译器。然后,CGo 将 Go 和 C 部分链接成最终的可执行文件。
从 Go Monorepo 创建伊始,C++ 工具链就不是封闭式的:Bazel 会使用它在系统上发现的任何东西。也就是说,在 macOS 上使用 Clang,在 Linux 上使用 GCC(无论什么版本)。在 Bazel 中创建一个封闭式的 C++ 工具链是一项很大的工作(对于我们的 Go Monorepo 来说,需要花费数月时间),没有迫切的需求,也没有足够的痛苦,我们还无法接受做这样一件事。
现在,我们看下非封闭式 C++ 工具链的局限性:
不能交叉编译。所以,如果需要 CGo 的话(我们的许多服务都需要),我们就无法在 Mac 上编译生成 Linux 上的可执行文件。这可以通过...... 而不是交叉编译来解决。
CGo 的可执行文件将链接到系统上发现的 glibc 版本。也就是说,在升级操作系统时(数月的努力),构建机群必须最后升级。否则,如果构建主机上 glibc 的版本比生产主机上的新,那么生成的二进制文件将链接到较新的 glibc 版本,就会与生产主机上的旧版本不兼容。
我们无法使用新的编译器,即使它提供了更好的优化功能,因为我们在构建机群上运行的是旧版本的操作系统(只向后迁移编译器,而不迁移 glibc,本身就有风险)。
Go 的新版本的官方二进制文件在构建时使用的 GCC 版本,比我们的一些构建机器上的新。在这些机器上,我们不得不通过从源代码编译 Go 来解决这个问题。所有这些问题都很令人恼火,但不足以让我们在工具链上进行投资。
我在做一个与 Uber 无关的小项目。该项目是用 Bazel 构建的,并使用了 CGo。我希望我的二进制文件是静态的,但 Bazel 并没有让这个过程变得简单。我花了几个晚上基于 musl.cc 创建了一个 Bazel 工具链,但没走多远,因为当时我无法深入理解 Bazel 的工具链文档,而且也没有找到一个好的示例可以参照。
2021 年 1 月,我发现了 Andrew Kelley 的博文“zig cc:一个功能强大的 GCC/Clang 替代品”。我推荐你读一下这篇文章;它改变了我对编译器的看法(也有助于你更好地理解本文接下来的内容,因为我是讲给 Zig 追随者听的)。综观 Andrew 的文章,zig cc有以下优势:
完全封闭的 C/C++ 编译器,压缩包只有大约 40MB。这比 Clang 的标准发行版要小一个数量级。
可以链接到通过命令行参数提供的 glibc 版本(例如,-target x86_64-linux-gnu.2.28将以 x86_64 Linux 为编译目标并链接到 glibc 2.28)。
主机和目标平台是解耦的。不管是什么主机,针对目标平台linux-aarch64和darwin-x86_64的设置都是一样的。
与 musl 链接“只是一个不同的 libc 版本”:-target x86_64-linux-musl。我开始摆弄zig cc。我随便编译了一些程序,报告了一些问题。我想过把它做成一个 bazel 工具链,但有很多拦路的 bug 或缺失的功能。其中之一就是缺少 Bazel 所依赖的zig ar。
我 向 Zig 报告了 Bug。一个星期都没有动静。我每月捐 50 美元,希望“Zig 的人”能优先处理我所报告的问题。接着又是一个星期的沉默。然后我在#zig:libera.chat中扔了一枚炸弹:
<motiejus> 捐赠后,有什么规约可以用来“申请”开发时间吗?
<andrewrk> ZSF 只接受不附带任何条款的捐赠。
<andrewrk> 你是从哪里获得了不同的印象吗?
当时,我希望无论谁注意到这段对话都立即忘掉它。好吧,一年多以后,我又把这段话写在这里,看着玩吧。
2021 年 6 月,Adam Bouhenguel 创建了一个可以工作的 bazel-zig-cc 原型。基本功能没问题,但仍然缺少一些特性。后来,Andrew 实现了zig ar,这是一个真正可用的 bazel-zig-cc 所缺少的最后一块拼图。我集成了zig ar,完善了文档,并 在 Zig 邮件列表中宣布了我创建的 bazel-zig-cc 分叉。至此,它对我的小项目是有效的。
在公告发布几周后,我为 Uber 的 Go Monorepo 创建了一个“WIP DIFF”:只是按照我的上线说明,天真地将其提交到我们的 CI。几乎所有的测试它都没有通过。
将 bazel-zig-cc 加入 Uber 的 Go Monorepo。
大部分失败都是由系统库依赖导致的。关于这一点,很明显,要想真正搭载 bazel-zig-cc 并编译所有的 C/C++ 代码,需要巨大的投入来消除对系统库的依赖,并偿还大量的技术债务。
Uber 有很多地方可以从一个封闭式的 C++ 交叉编译器中受益,但由于需要大量的投资,再加上没有足够的理由,所以没有获得资助。 bazel-zig-cc 有点用,但 bazel-zig-cc 和 zig cc 已知都存在 Bug。 我无法实现必要的修改或 Bug 修复。所以,我试图实现zig ar,LLVM ar的一个小前端,但失败了。 如果确定一个问题是 Zig 的问题,那么我们就无法预知它什么时候能引起 Zig 开发者的关注。有些问题几天内就得到了解决,有些则花了 6 个多月的时间,而捐款并不会改变zig cc的优先级。 Monorepo 上线差异(monorepo-onboarding diff)正在酝酿之中,等待时机。
我的任务是为 Uber 评估 arm64。撇开评估细节不说,我需要为 linux-arm64 编译软件。大量的软件!由于我们大部分的底层基础设施都在 Go Monorepo 中,我首先需要一个交叉编译器。
我终于有了一个实现交叉编译器的商业理由。现在,时间和金钱都可以投入了。“WIP DIFF”搭配zig cc是一个良好的开始,但距离终点还很远:团队不相信这是正确的事情,DIFF 更多的是一个原型。而且,要让 zig-cc 和 bazel-zig-cc 在任何情况下都可以使用,还有很多工作要做。
在一个大公司里引进这样的技术时,最重要的是风险管理。由于 Zig 是一项新技术(甚至连 1.0 都没有!),建议用它来编译我们所有的 C 和 C++ 代码很不寻常。我们应该做好至少十年内都使用它的计划。人们提了一些问题,并针对这些问题做了认真仔细的评估。为此,我真心感谢 Go Monorepo 团队,特别是 Ken Micklas,为这个未经证实的原型做了大量的工作和研究。
我们需要一个交叉编译器,摆在我们面前的选项有两个:
grailbio/bazel-toolchain:使用普通的 Clang。没有风险。容易理解。显然,这是一个安全、恰当的解决方案。
~motiejus/bazel-zig-cc:使用zig cc。有问题,有风险,不安全,不确定,没有人使用,但却是一个相当诱人的解决方案。与bazel-toolchain相比,zig cc额外提供了以下特性:
可配置的 glibc 版本。使用grailbio,就需要 sysroot(本质上是一个带有系统库的 chroot,因此,程序可以与之链接),而这需要维护。
a working, albeit still buggy, hermetic (cross-)compiler for macOS. 一个存在缺陷,但可以工作的封闭式 macOS(交叉)编译器。使用这两种方法中的任何一种处理 glibc 都没问题,然而,grailbio似乎不大可能编译到 macOS 上,更不用说交叉编译了。依赖开发者笔记本电脑上的系统编译器是不可取的,Go 平台团队亲身感受到了这一点,尤其是在 macOS 升级期间。
对于以 macOS 为目标封闭式工具链,选择的天平偏向了zig cc,连同它所有的缺陷、风险和不稳定性。
还有一个问题需要注意:我们知道,如果我们在重要的地方使用 Zig,会遇到问题,但又可能不具备解决这些问题的专业知识。作为一家大公司,我们该如何降低采用风险,确保严重的 Bug 及时得到处理?我们确信,ZSF 的出发点是好的:显然,如果我们发现并报告一个合理的 Bug,它就会得到修复。但是,怎么才能设定一个等待时间的上限呢?
50 美元的捐款无济于事,也许一份大的服务合同会有帮助?我四处打听,是否可以通过花些钱来降低“交叉编译器”的风险。获得管理层同意大约需要 10 分钟;起草、审批和签署合同大约需要 2 个月。
合同条款大致如下:
Uber 将问题报告到 github.com/ziglang/zig,并通知 Loris。
Loris 将其分配给 ZSF 的某人。
解决问题。
完成后,Loris 输入解决这个问题花费的时间。Uber 有权占用 ZSF 成员的时间。我们对 Zig 没有任何决定权或投票权。我们有权提出建议,但这些建议将和来自其他第三方的建议等量齐观。我们不能要求特殊权利,这在合同中有明确规定,我们也不希望那样。
合同签署了,电汇完成了,在 2022 年 1 月:
我们与 ZSF 签订了服务合同,他们承诺优先处理我们提交的问题。
Go 平台团队承诺为我们的 C++ 工具链实现交叉编译和封闭。
合同金额是公开的,因为 ZSF 是非营利的。
2022 年 2 月,该工具链通过一个命令行标志(--config=hermetic-cc)做了限定。自此,你可以在 Uber 的 Go Monorepo 中调用zig cc了,不需要自定义补丁。
证明我们的提交队列登录了我的 WIP DIFF。
2022 年截至目前的时间线:
今年 4 月,我在米兰演讲期间,我们向生产环境交付了第一个用 zigc-cc 编译的 Debian 软件包。
今年 5 月,我们在所有的 Debian 软件包中启用了zig cc。
下半年,我们希望用zig cc编译所有的 cgo 代码,并将--config= hermatic -cc作为默认设置。
下半年,我们希望将 bazel- zigc -cc 移到 github.com/uber 下。我们已经向 Zig 提交了一些问题,截至发稿时,所有的问题都已解决。有些是由 ZSF 单独处理的,有些则涉及面比较广,需要 ZSF、Uber 和 Go 开发者之间合作。
我开始准备演讲,希望能给出一个大公司如何采用 Zig 的“运行手册”。然而,其实并没有什么“运行手册”;我为采用 zig-cc 所做的努力本可能会因为很多很多原因而失败。
回顾过去,我觉得要想获得成功,最重要的是在适当的时候有一个杀手锏特性。在我们的例子中,有两个:无需 sysroot 的 glibc 版本选择和交叉编译到 macOS。
原文链接:
https://jakstys.lt/2022/how-uber-uses-zig/