永久性工作器

本页面介绍了如何使用永久性工作器、其优势、要求以及工作器对沙盒的影响。

永久性工作器是由 Bazel 服务器启动的长时间运行的进程,它充当实际工具(通常是编译器)的封装容器,或者本身就是“工具”。为了从永久性工作器中受益,该工具必须支持执行一系列编译,并且封装容器需要在该工具的 API 与下述请求/响应格式之间进行转换。同一 build 中同一工作器在调用时可以有 --persistent_worker 标记,可以有负责启动和与该工具通信,以及在退出时关闭工作器。每个工作器实例在 <outputBase>/bazel-workers 下被分配(但未获得 root 权限)到一个单独的工作目录。

使用永久性工作器是一种执行策略,可减少启动开销,允许进行更多 JIT 编译,并支持在操作执行中缓存抽象语法树等。此策略通过向长时间运行的进程发送多个请求来实现这些改进。

永久性工作器适用于多种语言,包括 Java、ScalaKotlin 等。

使用 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 策略,但您仍然可以使用 localsandboxed 作为后备。

选择工作器数量

每个助记符的默认工作器实例数量为 4 个,但可以使用 worker_max_instances 标志进行调整。您需要充分利用可用 CPU 与获得的 JIT 编译和缓存命中量之间进行权衡。使用更多的工作器,运行非 JIT 代码并触发冷缓存时,需要支付更多目标费用。如果您要构建的目标数量很少,单个工作器可以在编译速度和资源使用之间实现最佳权衡(例如,请参阅问题 8586)。worker_max_instances 标志可设置每个助记符和标志集的工作器实例数上限(见下文),因此,在混合系统中,如果您保留默认值,则最终可能会占用大量内存。对于增量构建,多个工作器实例的优势更小。

此图显示了 Bazel(目标 //src:bazel)在 64 GB RAM 下的 6 核超线程 Intel Xeon 3.5 GHz Linux 工作站上的从头开始编译时间。对于每个工作器配置,系统会运行五个干净构建,然后取后四个 build 的平均值。

整洁构建的性能提升情况图表

图 1. 干净构建的性能改进图表。

对于此配置,两个工作器的编译速度最快,但与一个工作器相比,只提高了 14%。如果您想要减少内存用量,一个工作器是一个不错的选择。

增量编译通常更有利。整洁构建相对来说很少见,但在编译之间更改单个文件很常见,特别是在测试驱动的开发中。上面的示例也包含一些非 Java 打包操作,可能会覆盖增量编译时间。

通过在 AbstractContainerizingSandboxedSpawn.java 中更改内部字符串常量后,仅重新编译 Java 源代码 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar) 可使速度提升 3 倍(平均 20 个增量构建,可舍弃一个预热 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 的请求如下所示:

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 标志单独启用此沙盒化功能。如需了解详情,请参阅设计文档

更多详情

如需详细了解永久性工作器,请参阅: