软件工程师,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。
原文: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 结果记录的输出不一致,就需要执行。 ...
原文:Troubleshooting Bazel with Git Bisect 作者:Son Luong Ngoc · 原文发表于 2025-02-18。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 译注:本文保留 2025 年 2 月使用 Bazel 8.0.1、8.1.0 和 rules_go 排查回归的案例。版本号、commit 和日志均是当时的记录,用来演示二分定位方法。 升级 Bazel 和相关依赖项有时会导致意外问题。这些问题的范围从 构建 故障到运行时错误,而且通常很难排除故障。 今天我们来看看,依赖升级后出现 构建 失败,如何用 git bisect 找出根因。 升级 Bazel 后 构建 失败 如果您像我一样,您会喜欢在项目中拥有最新、最好的工具。而我使用最多的工具是Bazel,所以随着最近8.1.0版本的发布,我决定升级我们的BuildBuddy repository来使用它。 $ echo '8.1.0' > .bazelversion $ bazel test --config=remote-minimal //... ... ERROR: /root/workspace/output-base/external/bazel_tools/tools/build_defs/repo/http.bzl:137:45: An error occurred during the fetch of repository 'rules_cc+': Traceback (most recent call last): File "/root/workspace/output-base/external/bazel_tools/tools/build_defs/repo/http.bzl", line 137, column 45, in _http_archive_impl download_info = ctx.download_and_extract( Error in download_and_extract: com.google.devtools.build.lib.remote.common.CacheNotFoundException: Missing digest: abc605dd850f813bb37004b77db20106a19311a96b2da1c92b789da529d28fe1/178823 ... 哎呀!将 Bazel 升级到 8.1.0 后,构建 似乎失败了。返回到 8.0.1 问题就消失了,这意味着问题是在 Bazel 8.1.0 中引入的。 ...
原文:Distributed Scheduling for Faster Builds 作者:Tyler Williams · 原文发表于 2022-04-07。 译注:文中的“我们”指原作者或 BuildBuddy 团队,时效性表述以原文发布日期为准。Bazel 专有名词保留英文,代码和命令未作翻译。 先向还不了解的读者介绍一下 BuildBuddy。我们为 Bazel 构建提供 UI、分布式缓存和 remote execution 平台,安全地编译代码、缓存 artifacts,并展示结果。使用这套服务,可以在笔记本上用不到 5 分钟构建 Tensorflow 等项目,而原先需要 90 分钟。 显然,要做到这一切,我们必须应对一些棘手的工程挑战,其中之一就是安排远程执行。为此,我们有一个调度程序。调度程序只是将我们的 API 接收到的 actions(基本上是作业)与实际执行工作的远程 workers 相匹配。如果您将 Tensorflow 之类的完整 构建 视为 10 道菜的餐食,那么单个 action 就相当于该餐食一小部分的食谱。为了更容易可视化,这里有一个来自构建 BuildBuddy 的真实 action: # this action just uses gcc to compile zlib/infback.c into an object file, zlib/infback.o /usr/bin/gcc -U_FORTIFY_SOURCE -fstack-protector -Wall -Wunused-but-set-parameter \ -Wno-free-nonheap-object -fno-omit-frame-pointer -g0 -O2 -D_FORTIFY_SOURCE=1 \ -DNDEBUG -ffunction-sections -fdata-sections -MD -MF \ bazel-out/k8-opt-exec-34F00540/bin/external/zlib/_objs/zlib/infback.d \ -frandom-seed=bazel-out/k8-opt-exec-34F00540/bin/external/zlib/_objs/zlib/infback.o \ -iquote external/zlib -iquote bazel-out/k8-opt-exec-34F00540/bin/external/zlib \ -isystem external/zlib/zlib/include \ -isystem bazel-out/k8-opt-exec-34F00540/bin/external/zlib/zlib/include -g0 \ -Wno-unused-variable -Wno-implicit-function-declaration -fno-canonical-system-headers \ -Wno-builtin-macro-redefined -D__DATE__="redacted" -D__TIMESTAMP__="redacted" \ -D__TIME__="redacted" -c external/zlib/infback.c -o \ bazel-out/k8-opt-exec-34F00540/bin/external/zlib/_objs/zlib/infback.o 这是另一个: ...