本页介绍了如何使用持久工作器、持久工作器的优势、要求以及工作器对沙盒的影响。
持久工作器是由 Bazel 服务器启动的长时间运行的进程,它充当实际工具(通常是编译器)的封装容器,或者本身就是工具。为了从持久工作器中受益,该工具必须支持执行一系列编译,并且封装容器需要在该工具的 API 与下述请求/响应格式之间进行转换。同一 build 中的同一工作器可能会在有 --persistent_worker
标志和没有 --persistent_worker
标志的情况下被调用,并且负责适当地启动工具并与工具通信,以及在退出时关闭工作器。每个工作器实例都会分配(但不会 chroot 到)<outputBase>/bazel-workers
下的单独工作目录。
使用持久工作器是一种执行策略,可减少启动开销、允许更多 JIT 编译,并支持缓存(例如操作执行中的抽象语法树)。此策略通过向长时间运行的进程发送多个请求来实现这些改进。
持久工作器已针对多种语言实现,包括 Java、Scala、Kotlin 等。
使用 NodeJS 运行时的程序可以使用 @bazel/worker 辅助库来实现 worker 协议。
使用持久性工作器
Bazel 0.27 及更高版本在执行 build 时默认使用持久工作器,不过远程执行优先。对于不支持持久性工作器的操作,Bazel 会回退到为每个操作启动一个工具实例。您可以为适用的工具助记符设置 worker
strategy,从而明确将 build 设置为使用持久性 worker。作为最佳实践,此示例包含将 local
指定为 worker
策略的后备:
bazel build //my:target --strategy=Javac=worker,local
根据具体实现情况,使用工作器策略而非本地策略可以显著提高编译速度。对于 Java,构建速度可提高 2-4 倍,有时对于增量编译,速度可提高更多。使用 worker 时,编译 Bazel 的速度大约快 2.5 倍。如需了解详情,请参阅“选择工作器数量”部分。
如果您还有一个与本地构建环境相匹配的远程构建环境,则可以使用实验性的动态策略,该策略会同时执行远程执行和 worker 执行。如需启用动态策略,请传递 --experimental_spawn_scheduler 标志。此策略会自动启用工作器,因此无需指定 worker
策略,但您仍可使用 local
或 sandboxed
作为后备策略。
选择工作器数量
每个助记符的默认工作器实例数为 4,但可以使用 worker_max_instances
标志进行调整。充分利用可用 CPU 与获得的 JIT 编译和缓存命中次数之间存在权衡。随着工作器的增加,更多目标将支付运行非 JIT 编译代码和命中冷缓存的启动成本。如果您要构建的目标数量较少,单个工作器可能在编译速度和资源使用率之间实现最佳平衡(例如,请参阅问题 #8586)。
worker_max_instances
标志用于设置每个助记符和标志集的工人实例数量上限(见下文),因此在混合系统中,如果您保留默认值,最终可能会使用大量内存。对于增量 build,多个工作器实例的优势甚至更小。
此图显示了在具有 64 GB RAM 的 6 核超线程 Intel Xeon 3.5 GHz Linux 工作站上,从头开始编译 Bazel(目标 //src:bazel
)所用的时间。对于每种工作器配置,系统会运行 5 次干净的 build,并取后 4 次的平均值。
图 1. 整洁 build 的性能改进图。
对于此配置,两个工作器可实现最快的编译速度,不过与一个工作器相比,速度仅提高了 14%。如果您想减少内存用量,一个工作器是不错的选择。
增量编译通常会带来更多好处。干净 build 相对较少,但在编译之间更改单个文件很常见,尤其是在测试驱动的开发中。上述示例还包含一些非 Java 打包操作,这些操作可能会掩盖增量编译时间。
在更改 AbstractContainerizingSandboxedSpawn.java 中的内部字符串常量后,仅重新编译 Java 源代码 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
) 可将速度提高 3 倍(20 次增量构建的平均值,其中一次预热构建被舍弃):
图 2. 增量 build 的性能改进图。
加速效果取决于所做的更改。在上述情况下,当更改常用常量时,速度提升了 6 倍。
修改持久性工作器
您可以传递 --worker_extra_flag
标志,以指定工作器的启动标志(按助记符键控)。例如,传递 --worker_extra_flag=javac=--debug
仅会针对 Javac 启用调试。每次使用此标志时只能设置一个 worker 标志,并且只能针对一个助记符设置。
不仅为每个助记符单独创建了 worker,还为启动标志的变体创建了 worker。助记符和启动标志的每种组合都会合并为一个 WorkerKey
,并且对于每个 WorkerKey
,最多可以创建 worker_max_instances
个工作器。如需了解操作配置如何指定设置标志,请参阅下一部分。
您可以使用 --high_priority_workers
标志来指定应优先于正常优先级助记符运行的助记符。这有助于优先处理始终位于关键路径中的操作。如果有两个或更多高优先级工作器正在执行请求,则所有其他工作器都无法运行。此标志可多次使用。
传递 --worker_sandboxing
标志可使每个工作者请求都使用单独的沙盒目录来存储其所有输入。设置沙盒需要花费一些额外时间,尤其是在 macOS 上,但可以更好地保证正确性。
--worker_quit_after_build
标志主要用于调试和分析。此标志会强制所有工作器在构建完成后退出。您还可以传递 --worker_verbose
以获取有关工作器正在执行的操作的更多输出。此标志反映在 WorkRequest
中的 verbosity
字段中,从而允许工作器实现也更加详细。
工作器将其日志存储在 <outputBase>/bazel-workers
目录中,例如 /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
。
文件名包含 worker ID 和助记符。由于每个助记符可以对应多个 WorkerKey
,因此您可能会看到给定助记符对应的日志文件数量超过 worker_max_instances
个。
对于 Android build,请参阅 Android build 性能页面了解详情。
实现持久性 worker
如需详细了解如何创建工作器,请参阅创建持久性工作器页面。
此示例展示了使用 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 格式(因为 requires-worker-protocol
设置为 JSON)在 stdin
上接收此数据。然后,工作器执行相应操作,并通过标准输出将 JSON 格式的 WorkResponse
发送给 Bazel。然后,Bazel 会解析此响应并手动将其转换为 WorkResponse
proto。如需使用二进制编码的 protobuf 而不是 JSON 与关联的工作人员进行通信,请将 requires-worker-protocol
设置为 proto
,如下所示:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
如果您未在执行要求中包含 requires-worker-protocol
,Bazel 会默认使用 protobuf 进行 worker 通信。
Bazel 会根据助记符和共享标志推导出 WorkerKey
,因此如果此配置允许更改 max_mem
参数,则会为使用的每个值生成一个单独的工作器。如果使用过多的变体,可能会导致内存消耗过大。
每个工作器目前一次只能处理一个请求。借助实验性多路复用工作器功能,可以使用多个线程(如果底层工具是多线程的,并且封装容器已设置为可理解这一点)。
在此 GitHub 代码库中,您可以查看以 Java 和 Python 编写的工作器封装容器示例。如果您使用 JavaScript 或 TypeScript,@bazel/worker 软件包和 nodejs worker 示例可能会对您有所帮助。
工作线程如何影响沙盒?
默认情况下使用 worker
策略不会在沙盒中运行操作,这与 local
策略类似。您可以设置 --worker_sandboxing
标志,以在沙盒中运行所有工作器,确保每次执行该工具时,只能看到它应该拥有的输入文件。该工具可能仍会在内部泄露请求之间的信息,例如通过缓存。使用 dynamic
策略需要对工作器进行沙盒处理。
为了让工作器能够正确使用编译器缓存,系统会随每个输入文件一起传递摘要。这样一来,编译器或封装容器无需读取文件即可检查输入是否仍然有效。
即使使用输入摘要来防范不必要的缓存,沙盒工作器的沙盒化程度也比纯沙盒低,因为该工具可能会保留受之前请求影响的其他内部状态。
只有在工作器实现支持的情况下,多路复用工作器才能进行沙盒处理,并且必须使用 --experimental_worker_multiplex_sandboxing
标志单独启用此沙盒处理。如需了解详情,请参阅设计文档)。
深入阅读
如需详细了解持久工作器,请参阅:
- 有关持久性工作器的原始博文
- Haskell 实现说明 {: .external}
- Mike Morearty 的博文 {: .external}
- 使用 Bazel 进行前端开发:Angular/TypeScript 和持久工作器(与 Asana 合作){: .external}
- Bazel 策略说明 {: .external}
- bazel-discuss 邮件列表上有关信息丰富的 worker 策略的讨论 {: .external}