软件工程师,10 年+ 经验:5 年 Java 后端开发 + 5 年 DevOps / SRE 实践。
📖 《Jenkins 2.x 实践指南》作者(销量 5000+)。自 2015 年笔耕至今,已写下 120+ 篇文章。
✍️ 这里记录我对 DevOps、SRE、Cloud Native、持续交付、领域驱动设计(DDD) 以及 职场成长 的思考与实践。信奉 Everything as Code。
软件工程师,10 年+ 经验:5 年 Java 后端开发 + 5 年 DevOps / SRE 实践。
📖 《Jenkins 2.x 实践指南》作者(销量 5000+)。自 2015 年笔耕至今,已写下 120+ 篇文章。
✍️ 这里记录我对 DevOps、SRE、Cloud Native、持续交付、领域驱动设计(DDD) 以及 职场成长 的思考与实践。信奉 Everything as Code。
译注:本文按用户提供的英文全文翻译,保留原文的章节和论述。CAS 统一译作“内容寻址存储”,首次出现时保留英文全称。原文把内容完整性校验、不可变写入、留存策略与法规合规多次混为一谈,也有依赖具体实现的性能断言;相关位置附有译注,不能将正文中的这些断言直接视为技术保证或合规结论。核对日期为 2026-11-11。 大多数存储系统回答的是“这份数据在哪里”。它们把文件放进目录,分配块地址,再沿着层级结构检索内容。这套模式通常够用,直到同一份文件在备份基础设施里出现了 17 份,直到监管机构要求证明某条记录自 2019 年以来从未被改动,或者直到一次存储审计发现,40% 的容量被无人刻意创建的重复内容占据。 内容寻址存储(Content Addressable Storage,CAS)回答的是另一个问题:“这份数据是什么?”它根据内容本身识别和检索数据,为每个对象生成唯一的密码学指纹,并把这个指纹作为永久地址。这一变化带来了自动去重、内建不可变性,以及验证已存数据未被改动的能力。这些能力源自寻址模型,无须事后另加一套工具。1 本文从架构层面介绍 CAS 的工作方式,并说明它与对象存储的区别,也会介绍企业中的应用场景和大规模部署时需要考虑的问题。 什么是内容寻址存储,它如何工作 内容寻址存储是一种存储架构。每个数据对象都有一个由自身内容生成的唯一标识符,而不是用存储位置来标识。这个标识符是根据数据计算出的密码学哈希,通常使用 SHA-256。哈希值成为对象在系统中的永久地址。读取时提供哈希值;验证时,对取回的内容重新计算哈希,再与保存的标识符比较。一致,说明数据完整;不一致,说明内容发生了变化。2 这种基于内容的寻址方式直接带来两个结果。第一,相同内容总会产生相同哈希。同一份文件即使出现在多个位置,也会映射到同一个标识符,系统只保存一份。去重直接由寻址方式产生,无须管理员单独配置这项功能。第二,内容只要变化,就会产生不同哈希。无法悄悄修改已存对象,再让它以原地址被取回。修改后的内容要么作为新对象写入并获得新地址,要么在校验时因哈希不匹配而被发现。 CAS 架构的三个层次 CAS 系统由三个协同工作的功能层组成。索引引擎负责哈希计算与查找。写入数据时,它先计算哈希,再检查索引中是否已有这个值。如果有,就引用已有对象;如果没有,就写入数据,并把新哈希加入索引。 存储库保存实际的数据对象。对象不可变,写入后就不再修改。文件变化时,新版本作为新对象存储,获得新的哈希。原对象仍可通过原地址访问,因此系统无需额外工具就能跟踪版本。3 元数据层把每个哈希与上下文信息关联起来,包括时间戳、留存策略、访问控制和合规设置。元数据单独存储、独立管理,不影响内容哈希,但决定了对象在整个生命周期中如何被处理。4 数据如何流经 CAS 系统 应用向 CAS 写入数据时,系统按固定顺序处理:计算输入内容的哈希,在索引中查找匹配项,然后创建新记录,或返回对已有对象的引用。读取时,应用提供内容地址,系统在存储库中定位对应对象。如果启用了校验,系统会在返回数据前重新计算哈希,确认内容在存储期间没有被改动。 在分布式部署中,每个节点执行同样的过程。相同内容无论在哪里计算,都会产生同一个哈希。因此,一份文件在芝加哥节点存储,再复制到法兰克福节点,标识符仍然相同。这种全局一致的标识方式,是 CAS 在分布式和多站点环境中的一项实用优势。2 CAS 与对象存储有什么区别 内容寻址存储和对象存储经常被放在一起讨论,因为二者都把数据作为独立对象管理,而不是按目录层级中的文件或卷中的数据块来管理。区别在于如何标识对象,以及系统如何保证对象在长期存储中的完整性。 对象存储在创建对象时分配唯一标识符,通常是 UUID 或系统生成的 key。标识符由系统分配,与对象内容无关。对象内容可以修改,key 保持不变。这使对象存储适合媒体文件、应用资源和配置对象等会变化的数据。内容需要更新,标识符又要在多个版本间保持稳定。5 CAS 根据内容生成标识符。内容变化,标识符也随之变化,每个不同版本因此被保存为独立对象。CAS 对象无法原地更新,只能创建带有新哈希的新版本。这是其不可变性保证的来源,也是主要约束。CAS 适合不应变化的数据,例如合规记录和审计日志,也包括归档文档、医学影像和数字证据。1 两种架构在企业存储中的位置 对象存储适合大规模的活跃数据,以及频繁访问或更新的工作负载。云原生应用、媒体处理流水线和数据湖可以使用 RESTful API 访问数据、进行分层存储,也能更新对象,无须自行管理版本哈希。S3 兼容生态已经成熟,对象存储也容易融入高频读写的应用架构。 CAS 适用于必须保持不可变的工作负载。监管人员或审计人员要求证明文档归档后未被修改时,CAS 可以重新计算内容哈希,与最初的指纹比较,提供验证依据。开启版本控制的对象存储可以展示变更历史,但如果不在存储系统外单独跟踪并校验哈希,就无法保证某个版本从未被改动。5 不少企业同时使用两者。对象存储承载活跃业务和动态内容,CAS 保存归档、合规记录及受法律保全措施约束的数据。数据根据访问模式和留存要求在两层之间迁移。两种架构互为补充。 CAS 中的数据去重与单实例存储 去重是内容寻址最直接的运维收益之一。传统存储需要单独的去重过程:扫描重复数据块,转而引用唯一副本,再移除冗余副本。在 CAS 中,这一步发生在数据写入时。内容到达后,系统计算哈希。如果索引中已有同一哈希,就指向已有对象,不再写入另一份。因此无需单独运行去重任务,无需后处理,也无需管理员配置。6 单实例存储及其容量影响 单实例存储(Single-Instance Storage,SiS)是 CAS 去重带来的存储原则:每份唯一内容在存储库中只保留一个实例。无论引用来自哪个应用、备份任务或用户,都指向同一实例。内容大量重复时,SiS 能显著节省容量。7 备份是最直观的例子。假设数据库每天备份,两次备份之间有 95% 的记录没有变化,那么只有变化的 5% 会作为新对象写入。未变化的记录已按原哈希保存在 CAS 存储库中,备份任务只需引用,无须再次复制。随着时间推移,备份占用增长会比逻辑数据总量增长慢得多,因为存储库积累的是唯一内容,而不是内容大体相同的快照副本。6 ...
原文:Integration tests are best tests 作者:Jay Conrod · 原文发表于 2026-01-15。 作者简介:Jay Conrod 是软件工程师,目前在 EngFlow 改进 Bazel 等构建系统的 remote builds 性能。他此前在 Google 的 Go 语言团队工作,参与过 Go modules、fuzzing、Bazel 支持和 command-line tools 的开发。 译注:文中的“我”指原作者,测试数量和工具的相关描述保留原文发表时的语境。 我想谈一个可能不太受欢迎的测试观点:integration tests 是最重要的一类测试。你应该尽量做好 integration test coverage,把相对少一些的时间花在 unit tests 上。自从参与 Go 的构建工具 cmd/go 的开发以来,我就一直这么想。当时我们有一套很出色的 integration tests,写起来容易,跑得快,也能可靠地发现 regression。 这个观点与通常的建议相反。大家一般认为,应该有充分的 unit test coverage、适量的 integration test coverage,再加上少量的 end-to-end test coverage。这通常被画成 Testing Pyramid: ...
原文:Why you should ditch YAML and use Jsonnet instead 作者:Joachim Bargsten · 原文发表于 2023-01-14。 译注:文中的“我”指原作者,工具和文档的相关描述保留原文发表时的语境。 我说不清自己究竟是什么时候想明白的。不过,看看 YAML 的格式和它的用法,我觉得这准是魔鬼的杰作之一。就像苍蝇被一大坨牛粪吸引,包括我在内的开发者,也被一股无形的力量推向了这种格式。 事情本来可以很简单。能收拾配置乱摊子的替代方案多的是,有类型的、无类型的都有。可不不不,我们偏要用 YAML。我大概是在第一次看到 Helm chart 时想明白这件事的。过了一阵,我才捋清这套逻辑: Helm 是 Kubernetes 的包管理器。 我把 Helm chart 写成“包”,好让别人复用。 压根没人复用这些 chart。真有人复用,就会产生极其紧密的耦合,正是每篇讲 clean code 的博客都在警告的那一种。 反复试错,直到跑通。 就快搞定了,只差把缩进调对。 或者,这就是所谓的“Arbeitsbeschaffungsmaßnahme”:人为增加工作量,好给更多开发者找点活干。 我也说不准。 YAML 的规范复杂得过了头,还会出现 Norway Problem 这样的意外解析结果,可能存在安全问题,而且经常被滥用,拿来解决本不该由 YAML 解决的问题。 还有什么替代方案? TOML HCL DHALL CUE Nix / Nickel 还可以继续列下去…… 我开始尝试 Jsonnet 和 HOCON。HOCON 在 Java/Scala 圈子之外没多少人知道,其他语言对它的支持也就……一般般。 那 Jsonnet 呢?我个人觉得,它在能力和简洁性之间拿捏得刚好。 Jsonnet 简介 Jsonnet 是什么?简单说: 面向应用和工具开发者的 data templating language。 可以把它看成增强版 JSON。从技术上说,它是 JSON 的 superset,加入了一些编程语言结构。因此,Jsonnet 需要先“编译”或转换,通常输出为 JSON。 ...
原文:Why is my Bazel build so slow? 作者:Maggie Lou · 原文署名日期为 2024-04-02。 译注:文中的“我们”指原作者或 BuildBuddy 团队。所译网页已包含 Bazel 8.7、9.1 等后续版本的补充说明,使用 flags 时请核对自己的 Bazel 版本。Bazel 专有名词、命令和代码保持英文;原文几处参数及缓存行为说明的修正见脚注。 Bazel 承诺的是快速构建。可构建还是很慢,该怎么办?去骂当初说服公司迁移到 Bazel 的开发者吗?当然不会!你肯定不会这么做,对吧?🙂 BuildBuddy 可以帮忙。Bazel 提供了大量排查构建耗时的信息,难的是知道该从哪里看起。 也许你连续运行了两次构建,本以为第二次会命中缓存,结果却依然很慢。也许你只运行了一次构建,想弄清时间究竟花在哪里。下面介绍几个排查起点,并结合我们自己的慢构建案例说明。 本文会多次用到 BuildBuddy UI。即使不用它,大部分步骤也能完成,只是需要自己从 Build Event Stream 中查找、解析信息,工作量会大一些。如果你还没有使用 BuildBuddy,可以给构建添加以下 flags,将 Build Events 免费发送到我们的服务器。完整设置步骤见文档。 --bes_results_url=https://app.buildbuddy.io/invocation/ --bes_backend=grpcs://remote.buildbuddy.io 添加后,构建日志中会出现类似 Streaming build results to: https://app.buildbuddy.io/invocation/XXX 的一行。打开这个链接,就能使用下文介绍的许多功能。 1)我使用了正确的 flags 吗? ⭐ Bazel 的配置选项很多,排查时可以先确认本次构建究竟应用了哪些 flags。下面这些 flags 值得检查:它们可能需要添加,也可能已经设置好了。 --nolegacy_important_outputs 去掉重复的文件引用,减少上传的 Build Event Stream 数据量。 --remote_build_event_upload=minimal 原文将它概括为不向缓存上传标记为 no-cache 的 blob,从而减少网络流量。具体的 BEP 上传范围见译注。1 --remote_download_outputs 限制从缓存下载中间构建 artifacts,减少网络流量。这个 flag 需要指定取值,并非只写名称就会生效。2 --experimental_remote_cache_async 在后台进行缓存 I/O,改善 remote cache 性能。 --noremote_upload_local_results 不向 remote cache 上传本地执行的 action 输出,减少上传量。这会降低后续构建的 cache hit rate;但上传速度受限时,例如网络连接较差,可能值得这样取舍。 --remote_cache_compression 压缩缓存中的 blob,提高 remote cache 吞吐量。 --experimental_remote_cache_chunking remote cache 支持该功能时,以 content-defined chunks 上传、下载大型输出,提高吞吐量。该功能适用于 Bazel 8.7 和 9.1+。 --digest_function=BLAKE3 使用更快的哈希算法,改善大文件的 digest 计算性能。该功能适用于 Bazel 6.4+。 --jobs 允许更多 actions 并行执行,提高执行吞吐量。我们建议从 --jobs=50 开始,再逐步调高。 如果希望每次构建都默认使用这些 flags,可以把它们写进 .bazelrc 文件。我们会把 .bazelrc 提交到 Git,让所有开发者使用统一的构建选项。 ...
原文:Action Merging 作者:Iain Macdonald · 原文发表于 2026-01-05。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 BuildBuddy 的 remote execution 引擎会合并相同 actions 尚未完成的执行,为用户节省时间和资源。本文会介绍 action merging 为什么有用、具体如何实现,以及我们多年来在生产环境中积累的经验。 概述 缓存 action 结果是 Bazel 的核心性能优化之一。但本地缓存和 remote Bazel 缓存都不能共享尚未完成的执行结果。比如,Alice 提交一项后端改动,需要运行 BigSlowTest。测试尚未结束,Bob 又提交一处很小的前端改动,Bob 仍得重新运行同一个 BigSlowTest。这会拖慢他的 invocation,还可能重复消耗模拟器许可证或配额等外部资源。 A Alice 和 Bob 未合并的 BigSlowTest 执行的时间线。 这个例子并不只是设想。下文介绍的 action merging 系统,在生产高峰期每秒会合并超过 1,000 个 actions,为某些长时间运行的测试节省接近 3 小时。Stripe 也曾在这篇文章中介绍过他们解决同类问题的内部方案。 背景 Actions 和执行 Actions 是 Bazel 的工作单元,作为更大的 invocation 的一部分运行。 action 可能是编译或链接命令。每个 action 都有一个 ID,称为 Action ID,它是 action 的定义(命令、环境变量)和所有输入的哈希值。 ...
原文:Remote Cache CDC: Reusing Bytes 作者:Tyler French · 原文发表于 2026-05-01。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 目标:移动更改的字节,而不是整个输出。 BuildBuddy 的 Remote Cache 使用内容定义分块(CDC),让大型构建输出也能按增量传输。如果二进制文件、bundle、package 或归档文件的大部分内容未变,BuildBuddy 就能复用已经保存的 chunks,无需重新上传或下载整个文件。 在为 Bazel 实现缓存分块的 PR中,我们对 BuildBuddy 自己的代码库做了基准测试,观察到上传数据量减少 40%,disk cache 体积也减小了 40%。要在 BuildBuddy 上启用客户端 CDC,请使用 Bazel 8.7 或 9.1+,并传入 --experimental_remote_cache_chunking。 背景 构建缓存下一步要减少的,不只是需要执行的 actions,还有需要传输的字节。 构建 缓存已经取得了长足的进步。 Bazel 和 remote caching 允许团队在机器和 CI 作业中重复使用 action 输出,而不是在每次编辑后重建世界。在实践中,构建已经从接近 O(存储库大小)转向 O(变更大小)。 不过,“改动大小”可能会产生误导。真正重要的是,这次编辑影响到的传递性 actions 有多大。即使各个输出实际改变的都只是一小部分,一处很小的源代码修改仍可能波及许多二进制文件、package、bundle 和其他大型输出。 这类缓存失效符合预期:构建系统在输入变化时,本来就应重新运行 action。remote cache 的问题出在后续传输上。即使 blob 的大部分字节都和上一版一样,只要摘要变化,缓存仍会传输整个 blob。 传递式 Actions 链接、捆绑、打包和归档是最常出现的情况。它们将许多传递输入组合成一个输出。 ...
原文:Snapshot, Chunk, Clone: Fast Runners at Scale 作者:Maggie Lou · 原文发表于 2026-01-26。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 在 BuildBuddy,我们的核心使命是加快构建速度。我们使用 Firecracker 快照来按需保持预热 workers 的可用性。 只需一个curl请求,您就可以在warm worker中运行任何bash命令。 背景 想象一下,每天早上您都必须从头开始重新初始化笔记本电脑上的开发环境,重新安装工具并克隆 git 存储库。接着还得下载容器映像并启动各种应用程序。一想到这些时间和精力我就感到不寒而栗。 不仅设置会很慢,而且所有初始工作负载也会更慢。应用程序和内核必须重新填充各种内存和磁盘缓存,并下载工具和依赖项。 Cold start 是计算系统中常见的问题。GitHub Actions 等 CI 平台会为每次任务初始化新的 worker,AI 编程 agent 也通常运行在尚未预热的 cold runners 上。这样能让环境保持干净、一致,并与其他客户的任务隔离,但每次任务也往往需要多花几分钟。 借助可创建 snapshot 的 runners,只需承担一次初始化 cold worker 的耗时,后续任务就能直接克隆已经预热的 worker。 这些 workers 具有许多有价值的特性: 灵活性:每个worker都有自己的内核,提高安全性并支持需要Docker或自定义映像的复杂工作负载。 写入时复制:可以从共享基线克隆并行workers,同时在写入时复制更改,从而允许不同的工作负载运行而不会产生干扰。 可扩展性:能通过网络克隆,为我们提供可持续扩展的快速 runners 集群。 持久性:可以保存到磁盘,让 workers 在机器重启或自动伸缩后仍能恢复。 高容量:可以处理需要高达100GB磁盘和大量内存的大型工作负载,这对于单一存储库或运行ML模型等内存密集型任务通常是必需的。 增量更新:可以随着每个工作负载进行增量更新,从而减少环境偏差,甚至在数周的开发过程中也是如此。 内存持久性:可以跨工作负载保留内存中状态,允许重复使用昂贵的初始化工作(例如Bazel的分析图和JVM堆状态)。 在我们的代码库中,一次完全命中缓存的 Bazel 构建 大约需要 6 分钟。在 GitHub Actions 上,初始化 worker 就要花 3.5 分钟,包括安装工具、对 GitHub repository 做 sparse checkout,以及在 构建 开始前生成 Bazel 的 analysis graph。这意味着,仅准备工作就占了 CI 时间的三分之一以上! ...
原文:Lazily Pulling Container Images with Podman and SOCI Snapshotter 作者:Iain Macdonald · 原文发表于 2023-11-15。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 译注:缓存淘汰一节原文末句中的“only require re-indexing except after”疑有笔误。结合上文关于缓存访问与淘汰的说明,本译文将其理解为:经常使用的镜像通常只在容器 registry 中更新后才需要重新建立索引。 我们很高兴地宣布 podman 的性能改进,使在 BuildBuddy 的托管执行服务上拉取容器镜像的速度提高了约十倍,减少了客户 构建 和测试时间,并提高了我们自动扩展 BuildBuddy executor 池的能力。请继续阅读以了解具体细节。 容器化 在 BuildBuddy,我们使用容器来隔离 actions 并支持在我们托管的 remote execution 服务上执行任意代码。在生产中,我们使用 Podman 作为默认容器运行时,但我们也支持 Docker 和 Firecracker。我们在容器设置中遇到的一个重大挑战是,在 cold 启动后拉取容器镜像可能会非常慢。我们的 executor 镜像约为 1 GB,在我们的 GCP 生产环境中提取它们通常需要大约 30 秒的时间。一些客户提供的镜像超过 10GB,并且可能需要几分钟的时间来拉取和提取。 显着的镜像拉取延迟不仅会直接减慢客户的构建和测试速度,还会限制我们自动扩展 executor 池和响应重负载的能力。当新的 BuildBuddy executors 启动时,它们会根据需要拉取容器镜像。这意味着 executor 首次运行使用某镜像的 action 时,要先等待镜像拉取。队列中使用同一镜像的其他 actions 也会被阻塞。因此,我们提取容器镜像的速度越快,我们就能更快地启动新的 executors 并开始运行客户提交的 actions。我们可以通过始终运行更多的 executors 来解决这个问题,或者在高峰时段抢先扩展 executor 池,但这些技巧最多只能部分解决这个问题,并且还有其他缺点。 ...
原文:How We Use ClickHouse to Analyze Trends Across Millions of Builds 作者:Lulu Zhang · 原文发表于 2022-11-22。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 使用 Buildbuddy 配合 Bazel 构建、测试软件时,Buildbuddy 会记录每次 Bazel invocation 的信息,包括构建次数、构建耗时和 remote cache 性能等。Buildbuddy 的 Trends 页面会把这些数据随时间的变化展示出来。 趋势页面能帮助你判断,所做的优化如何影响平均构建耗时及其他指标,也能暴露待改进的地方。比如,cache hit 率持续下降,可能是构建中存在结果不确定的构建 actions,也可能是新引入的依赖使缓存更频繁地失效。 当我们第一次创建趋势页面时,我们使用 MySQL 查询来聚合 构建 统计数据并生成我们想要显示的数据。有一段时间,这种方法效果很好,但我们很快就遇到了拥有大量构建的客户的性能问题。我们能够通过添加各种索引来暂时提高性能,尽管这有助于减少读取的行数,但这还不够。有些客户每月进行数百万次构建,这些客户的“趋势”页面(可以追溯到一年)需要超过 20 分钟 的加载时间。 趋势页面背后的查询需要多列的聚合,例如cache hits和cache misses。像 MySQL 这样的传统的基于行的数据库并不总是适合这种用例。在基于行的数据库中,数据是逐行存储的。聚合列时,比基于列的数据库需要更多的 I/O 查找,基于列的数据库将每列的数据存储在连续的块中。此外,基于列的数据库具有更高的压缩率,因为同一列的连续值具有相同的类型并且可能重复。 对于基于行的存储,我们可以从该图中看到,计算 cache hit 计数的总和需要我们加载块 1 和块 2。对于基于列的存储,所有 cache hits 数据都存储在同一块中。 因此,我们认为使用 ClickHouse(基于列的数据库)将提高趋势页面所需查询的性能。我们根据我们的用例验证了 ClickHouse 的性能:ClickHouse 处理 150 万行并计算统计数据花了 0.317 秒。相同的查询花费了 MySQL 大约 24 分钟。 ...
原文:Bazel’s Remote Caching and Remote Execution Explained 作者:Brentley Jones · 原文发表于 2022-03-16。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 译注:本文的 flag 名称和默认行为对应写作时的 Bazel 5.0;相关文档与代码链接也指向该版本。文中保留这些历史名称,以便与原文的执行流程和脚注对应。 Bazel 著名的 remote caching 和 remote execution 功能可以改变游戏规则,但如果您不熟悉它们的工作原理,它们可能会有点神秘。 别担心。我会从基础讲起,介绍 remote caching 和 remote execution,再深入到具体细节,也就是 actions 和 spawns 😄,带你了解 Bazel 的远程能力。 Actions 对于一次请求的 构建,构建 graph 中的每个 rule target,都会在 analysis phase 生成零个或多个 actions。这些 actions 组成 action graph,表示 execution phase 期间需要执行的全部 actions。 示例 action graph。 Actions 按创建它们的 targets 进行分组。箭头连接 actions,显示它们之间的依赖关系。 在 execution phase,Bazel 会遍历 action graph,逐个判断 action 是否需要执行。如果 output base 的 action cache 中没有对应的 action 结果,或者 output base 中的输出与 action 结果记录的输出不一致,就需要执行。 ...