本页面介绍了如何使用持久性工作器、其优势、要求以及工作器对沙盒的影响。
持久性工作器是由 Bazel 服务器启动的长时间运行的进程,它作为实际工具(通常是编译器)周围的封装容器,或者本身就是工具。为了从持久性工作器中受益,该工具必须支持执行一系列编译,并且封装容器需要在该工具的 API 和如下所述的请求/响应格式之间转换。同一工作器可能会在调用同一 build 时带有和不带 --persistent_worker
标志,负责适当地启动该工具并与之通信,以及在退出时关闭工作器。每个工作器实例在 <outputBase>/bazel-workers
下被分配一个(但并非根级)一个单独的工作目录。
使用永久性工作器是一种执行策略,可减少启动开销,允许进行更多 JIT 编译,并支持对操作执行中的抽象语法树等进行缓存。此策略通过向长时间运行的进程发送多个请求来实现这些改进。
持久性工作器以多种语言(包括 Java、Scala、Kotlin 等)实现。
使用 NodeJS 运行时的程序可以使用 @bazel/worker 帮助程序库来实现工作器协议。
使用永久性工作器
Bazel 0.27 及更高版本在执行构建时默认使用持久性工作器,但系统会优先使用远程执行。对于不支持持久性工作器的操作,Bazel 会回退到为每个操作启动工具实例。您可以通过为适用的工具助记符设置 worker
策略,明确地将您的构建设置为使用永久性工作器。最佳实践包括将 local
指定为 worker
策略的后备:
bazel build //my:target --strategy=Javac=worker,local
使用工作器策略(而不是本地策略)可以显著提高编译速度,具体取决于实现情况。对于 Java,构建速度可以快 2-4 倍,有时对于增量编译来说可能更快。与工作器一起编译 Bazel 的速度大约是 2.5 倍。如需了解详情,请参阅选择工作器数量部分。
如果您还有与本地构建环境匹配的远程构建环境,则可以使用实验性的动态策略,该策略将竞争远程执行和工作器执行。要启用动态策略,请传递 --experimental_spawn_scheduler 标志。此策略会自动启用工作器,因此无需指定 worker
策略,但您仍然可以使用 local
或 sandboxed
作为回退。
选择工作器数量
每个助记符的默认工作器实例数为 4,但可以使用 worker_max_instances
标志进行调整。必须充分利用可用的 CPU 资源与得到的 JIT 编译和缓存命中量。随着工作器的增多,运行非 JIT 代码并达到冷缓存的启动费用也会增加。如果要构建的目标较少,单个工作器可能会在编译速度和资源使用量之间取得最佳平衡(例如,请参阅问题 #8586)。worker_max_instances
标志设置每个助记符和标志集的工作器实例数上限(见下文),因此在混合系统中,如果您保留默认值,则最终会占用大量内存。对于增量构建,多个工作器实例的优势较小。
此图显示了 Bazel(目标为 //src:bazel
)在具有 64 GB RAM 的 6 核超线程 Intel Xeon Linux 3.5 GHz 工作站上的从头开始编译时间。对于每项工作器配置,系统将运行 5 个干净构建,并取后 4 个平均值。
图 1. 干净构建的性能改进图表。
对于此配置,两个工作器的编译速度最快,但只比一个工作器高出 14%。如果您想使用更少的内存,一个工作器是一个不错的选择。
增量编译通常效果更佳。干净 build 相对罕见,但在编译之间更改单个文件很常见,尤其是在测试驱动的开发中。上面的示例中还使用了一些非 Java 打包操作,这些操作可以覆盖增量编译时间。
在 AbstractContainerizingSandboxedSpawn.java 中更改内部字符串常量后,仅重新编译 Java 源代码 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
) 可使速度提升 3 倍(平均增加 20 个增量 build,而舍弃一个预热 build):
图 2. 增量构建的性能改进图表。
速度取决于要进行的更改。当常用常量发生变化时,系统会衡量上述情况下因子 6 的加速幅度。
修改永久性工作器
您可以传递 --worker_extra_flag
标志,向工作器指定启动标志,由助记键控制。例如,传递 --worker_extra_flag=javac=--debug
仅会为 Javac 启用调试。每次使用此标志时只能设置一个工作器标志,且只能为一个助记符设置。
工作器不仅可以针对每个助记符单独创建,还可以为启动标志中的变体创建。助记标记和启动标志的每个组合都会合并到一个 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
。文件名包含工作器 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..." },
],
}
工作器在 stdin
上以换行符分隔的 JSON 格式接收此内容(因为 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
标志单独启用这一沙盒化。如需了解详情,请参阅设计文档。
补充阅读材料
如需详细了解持久性工作器,请参阅: