本页介绍了如何使用持久性工作器、持久性工作器的优势、要求,以及 工作器对沙盒的影响。
持久性工作器是由 Bazel 服务器启动的长时间运行的进程,它
充当实际 工具(通常是编译器)的 封装容器,或者就是
该 工具本身。为了从持久性工作器中受益,该工具必须
支持执行一系列编译,并且封装容器需要在工具的 API 和下述请求/响应格式之间进行转换。在同一
build 中,可能会使用或不使用 --persistent_worker 标志调用同一
工作器,并且该工作器负责适当地启动工具并与工具通信,以及在退出时关闭工作器。每个工作器实例都会分配
(但不会 chroot 到)
<outputBase>/bazel-workers下的单独工作目录。
使用持久性工作器是一种 执行策略,可减少 启动开销,允许更多 JIT 编译,并支持缓存操作执行中的 抽象语法树等内容。此策略 通过向长时间运行的 进程发送多个请求来实现这些改进。
持久性工作器已针对多种语言实现,包括 Java, Scala, Kotlin 等。
使用 NodeJS 运行时的程序可以使用 @bazel/worker 帮助程序库来 实现工作器协议。
使用持久性工作器
Bazel 0.27 及更高版本
在执行 build 时默认使用持久性工作器,但远程
执行优先。对于不支持持久性工作器的操作,
Bazel 会回退到为每个操作启动一个工具实例。您可以为适用的工具
助记符设置 worker
策略,以明确将 build 设置为使用持久性工作器。根据最佳实践,此示例包含将 local 指定为
回退到 worker 策略:
bazel build //my:target --strategy=Javac=worker,local根据实现情况,使用工作器策略而不是本地策略可以显著提高编译 速度。对于 Java,build 速度可以提高 2 到 4 倍,有时增量编译的速度会更高。使用工作器编译 Bazel 的速度大约是原来的 2.5 倍。如需了解详情,请参阅 "选择工作器数量"部分。
如果您还有一个与本地 build
环境匹配的远程 build 环境,则可以使用实验性
动态策略,
该策略会同时执行远程执行和工作器执行。如需启用动态
策略,请传递
--experimental_spawn_scheduler
标志。此策略会自动启用工作器,因此无需
指定 worker 策略,但您仍然可以使用 local 或 sandboxed 作为
回退。
选择工作器数量
每个助记符的工作器实例数量默认为 4,但可以使用
with the
worker_max_instances
标志进行调整。充分利用可用 CPU 与您获得的
JIT 编译和缓存命中次数之间存在权衡。工作器越多,需要为运行非 JIT 代码和命中冷
缓存支付启动费用的目标就越多。如果您要构建的目标数量较少,则单个工作器可能会在编译速度和资源使用情况之间提供最佳权衡(例如,请参阅问题 #8586)。worker_max_instances 标志用于设置每个
助记符和标志集(见下文)的工作器实例数量上限,因此在混合系统中,如果您保留默认值,最终可能会使用
大量内存。对于增量 build,
多个工作器实例的优势甚至更小。
此图显示了在具有 6 核超线程 Intel Xeon 3.5 GHz Linux 工作站
(配备 64 GB RAM)上从头开始编译 Bazel(目标
//src:bazel)的时间。对于每个工作器配置,系统会运行五个干净 build,并取最后四个的平均值。

图 1。干净 build 性能提升图。
对于此配置,两个工作器可提供最快的编译速度,但与一个工作器相比,速度仅提升了 14% 。如果您想 使用更少的内存,则一个工作器是不错的选择。
增量编译通常会带来更大的好处。干净 build 相对较少,但在编译之间更改单个文件很常见,尤其是在测试驱动开发中。上述示例还包含一些非 Java 打包操作,这些操作可能会掩盖增量编译时间。
在
AbstractContainerizingSandboxedSpawn.java
中更改内部字符串常量后,仅重新编译 Java 源代码
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar)
可将速度提高 3 倍(20 个增量 build 的平均值,其中一个预热 build
已被舍弃):

图 2.增量 build 性能提升图。
速度提升取决于所做的更改。在上述情况下,当更改常用常量时,速度提升了 6 倍。
修改持久性工作器
您可以传递
--worker_extra_flag
标志,以指定工作器的启动标志,并以助记符作为键。例如,
传递 --worker_extra_flag=javac=--debug 仅会为 Javac 启用调试。
每次使用此标志时,只能设置一个工作器标志,并且只能针对一个助记符。
工作器不仅会为每个助记符单独创建,还会为
启动标志的变体单独创建。助记符和启动
标志的每个组合都会合并到 WorkerKey 中,并且对于每个 WorkerKey,最多可以创建
worker_max_instances 个工作器。如需了解
操作配置如何指定设置标志,请参阅下一部分。
您可以使用
--high_priority_workers
标志指定应优先于正常优先级
助记符运行的助记符。这有助于确定始终位于关键
路径中的操作的优先级。如果有两个或多个高优先级工作器正在执行请求,则所有
其他工作器都无法运行。此标志可以多次使用。
传递
--worker_sandboxing
标志可使每个工作器请求都使用单独的沙盒目录作为其所有
输入。设置 沙盒 需要一些额外的时间,
尤其是在 macOS 上,但可以更好地保证正确性。
The
--worker_quit_after_build
标志主要用于调试和分析。此标志会强制所有工作器
在 build 完成后退出。您还可以传递
--worker_verbose以
获取有关工作器正在执行的操作的更多输出。此标志反映在
verbosity 字段中,从而允许工作器实现也更加
详细。WorkRequest
工作器将其日志存储在 <outputBase>/bazel-workers 目录中,例如
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log。
文件名包含工作器 ID 和助记符。由于每个助记符可以有多个 WorkerKey,因此对于给定的助记符,您可能会看到超过 worker_max_instances
个日志文件。
对于 Android build,请参阅 Android Build 性能页面中的详细信息。
实现持久性工作器
如需详细了解如何创建工作器,请参阅创建持久性工作器页面。
此示例展示了使用 JSON 的工作器的 Starlark 配置:
args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
output = args_file,
content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
mnemonic = "SomeCompiler",
executable = "bin/some_compiler_wrapper",
inputs = inputs,
outputs = outputs,
arguments = [ "-max_mem=4G", "@%s" % args_file.path],
execution_requirements = {
"supports-workers" : "1", "requires-worker-protocol" : "json" }
)
使用此定义,首次使用此操作时,系统会先执行
命令行 /bin/some_compiler -max_mem=4G --persistent_worker。然后,编译 Foo.java 的请求将如下所示:
注意:虽然协议缓冲区规范使用“蛇形命名法”(request_id),
但 JSON 协议使用“驼峰命名法”(requestId)。在本文档中,我们将在 JSON 示例中使用
驼峰命名法,但在讨论字段时,无论协议如何,都将使用
蛇形命名法。
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
工作器以换行符分隔的 JSON 格式在 stdin 上接收此内容(因为
requires-worker-protocol 设置为 JSON)。然后,工作器执行该操作,
并在其 stdout 上向 Bazel 发送 JSON 格式的 WorkResponse。然后,Bazel 会
解析此响应,并手动将其转换为 WorkResponse proto。如需使用二进制编码的 protobuf 而不是
JSON 与关联的工作器通信,requires-worker-protocol 将设置为 proto,如下所示:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
如果您未在执行要求中添加 requires-worker-protocol,
Bazel 会默认将工作器通信设置为使用 protobuf。
Bazel 从助记符和共享标志派生 WorkerKey,因此如果此
配置允许更改 max_mem 参数,则会为每个使用的值生成一个单独的工作器。如果使用的变体过多,可能会导致内存消耗过大。
目前,每个工作器一次只能处理一个请求。实验性 多路复用工作器功能允许使用多个 线程,前提是底层工具是多线程的,并且封装容器已设置为 理解这一点。
在 此 GitHub 代码库中, 您可以查看以 Java 和 Python 编写的工作器封装容器示例。如果您使用的是 JavaScript 或 TypeScript,则@bazel/worker 软件包和nodejs 工作器示例可能会有所帮助。
工作器对沙盒有何影响?
默认情况下,使用 worker 策略不会在
沙盒中运行操作,这与 local 策略类似。您可以设置
--worker_sandboxing 标志,以在沙盒内运行所有工作器,确保工具的每次
执行都只能看到它应该拥有的输入文件。该工具
可能仍会在内部请求之间泄露信息,例如通过
缓存。使用 dynamic 策略
需要对工作器进行沙盒化。
为了允许工作器正确使用编译器缓存,系统会随每个输入文件一起传递摘要。 因此,编译器或封装容器无需读取文件即可检查输入是否 仍然有效。
即使使用输入摘要来防范不必要的缓存,沙盒化 工作器提供的沙盒化也比纯沙盒提供的沙盒化宽松,因为该工具可能 会保留受先前请求影响的其他内部状态。
只有当工作器实现支持多路复用工作器时,才能对多路复用工作器进行沙盒化,
并且必须使用
--experimental_worker_multiplex_sandboxing 标志单独启用此沙盒化。如需了解详情,请参阅
设计文档。
深入阅读
如需详细了解持久性工作器,请参阅: