什么是Bazel
Bazel是Google在2015年开源的一款构建工具,目前在GitHub上已获得超过23k的star,明显领先于类似的工具如gradle、maven和cmake。近年来,国内大型互联网公司也逐渐采用Bazel来构建自己的软件。
Bazel之所以备受青睐,正如其口号所言:“正确与快速,二者兼得”,这不仅仅是宣传口号,用户的实际使用感受也印证了这一点。
- 加速构建和测试: Bazel只重建必要的东西,借助本地和分布式缓存、优化的依赖关系分析和并行执行,获得快速和增量构建。
- 一个工具,多种语言: 构建和测试Java、C++、Android、iOS、Go和各种其他语言平台。Bazel可以在Windows、macOS和Linux上运行。
- 可扩展:Bazel帮助您扩展您的组织、代码库和持续集成解决方案。它处理任意大小的代码库,在多个仓库或一个巨大的monorepo中。
- 可根据您的需求进行扩展:使用Bazel熟悉的扩展语言,轻松添加对新语言和平台的支持。分享和重用不断发展的Bazel社区编写的语言规则。
本文不会重点介绍Bazel,而重点讨论如何通过bazel的remote executor方式实现大规模的分布式并发编译。
榨干bazel的性能:remote cache和分布式编译
remote cache
Bazel的Action由构建系统本身进行设计,更加安全,因此就不会出现多个action对同一文件的竞争问题。基于这个特性,我们可以充分利用多核CPU的能力,让Action并行执行。通常我们采用CPU逻辑核心数作为Action执行的并发度,如果开启了远端执行,则可以开启更高的并发度。
除了分布式编译,bazel的远端缓存也可以尽可能地保证我们的构建是增量构建,下图展示了remote cache生效的原理。

每个action的元数据和编译产物artifacts都会被hash为digest,这个digest是基于内容的,也就是说,只有内容发生变化时,才会重新计算digest。一个digest对应一个actionResult,内容寻址的好处是不容易污染存储空间。因此,通过remote cache我们可以持久化存储actionResult,从而加快编译构建。
当然storage不可能是无限容量的,因此一般都有LRU算法来管理CAS中的blob,当写满时,最久没有被访问的blob会被自动淘汰。
remote execute
Bazel在构建时,可以把Action发送给另一台服务器执行,等执行完毕后,服务器可以向CAS上传ActionResult,然后本地再下载这个结果。
这种做法减少了本地执行Action的开销,使得我们设置更高的构建并发度
远程执行的原理如下:

Buildfarm
Buildfarm是一款开源的分布式编译服务,基于spring boot实现。Buildfarm可以分为server和worker两部分。
源码解析
Server端
ExecutionService
ShardInstance
RedisShardBackplane
worker端
ShardWorkerInstance
pipeline.add(matchStage, 4);
pipeline.add(inputFetchStage, 3);
pipeline.add(executeActionStage, 2);
pipeline.add(reportResultStage, 1);
matchStage
inputFetchStage
远程拉取Input
executeActionStage
reportResultStage
从上面的源码可以看到,worker的执行分为4个步骤,通过pipeline串联起来。分别是match, fetchInput, execute和report。
CAS端
ShardCASFileCache
常用的cas都是filesystem的文件cache。
定时reindexCas或者节点发生变化时执行
removeWorkerIndexesFromCas 遍历redis集群的node, 然后执行reindexNode
reindexNode
具体清理逻辑
主要是比较active workers和cas key map set中记录的存储改digest的worker set进行一个求交集
如果交集为空,则把cas key直接删除
如果交集不为空,则对这个caskey的worker set进行一个替换,用新找的交集替换上去
常见问题
- Missing Digest问题
原因
-
首先明确出现问题的CI job均使用了--remote_download_minimal参数。
-
BwoB是一种构建模式,启用时,Bazel会推迟远程执行(或远程缓存)操作的输出下载,直到需要它们时才下载(例如,作为本地操作的输入)。这意味着,在调用过程中,Bazel需要跟踪每个输出的最少信息,以备后面检索。然后,Bazel在必要时使用元数据下载远程blobs。
-
目前,Bazel假设它以前从远程服务器获取的元数据总是可以在以后用于检索相应的blob。然而,对于使用分布式编译服务buildfarm来说,这是不正确的,因为blobs可能会由于各种原因(远程存储空间不足,弹性节点伸缩等)而从远程缓存中被逐出。从Bazel的角度来看,blobs可以在两个不同的时间被驱逐:
- 在调用期间:在当前调用早期远程执行的action的输出被删除,而其成为了另一个正在运行的action的输入。
- 在调用之间:当Bazel没有构建时,blobs被逐出。
解决办法
Bazel 6.2.1支持--experimental_remote_cache_eviction_retries参数,指定后在遇到remote blob eviction时会重新启动一个invocation,试验了几次是能解决Missing Digest问题的。