永久性工作器

报告问题 查看源代码 每夜 build · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

永久性工作器是由 Bazel 服务器启动的长时间运行进程,可用作实际工具(通常是编译器)的封装容器,或就是工具本身。为了从永久性工作器中受益,该工具必须 支持执行一系列编译,并且封装容器需要将 与下面所述的请求/响应格式相关联。在同一 build 中,系统可能会在有 --persistent_worker 标志和没有 --persistent_worker 标志的情况下调用同一 worker,并负责适当地启动和与工具通信,以及在退出时关闭 worker。系统会为每个工作器实例分配 (但不通过 chroot 到)一个单独的工作目录 <outputBase>/bazel-workers

使用永久性工作器 执行策略, 启动开销,允许更多的 JIT 编译,并支持缓存 操作执行中的抽象语法树示例。此策略 实现这些改进的方法是,向长时间运行的 过程。

持久性工作器针对多种语言实现,包括 Java、 ScalaKotlin 等。

使用 NodeJS 运行时的程序可以使用 @bazel/worker 帮助程序库 实现工作器协议

使用永久性工作器

Bazel 0.27 及更高版本 执行构建时默认使用永久性工作器 优先执行对于不支持永久性工作器的操作,Bazel 会回退为每个操作启动一个工具实例。您可以通过为适用的工具助记符设置 worker 策略,明确设置 build 以使用永久性工作器。最佳实践是,此示例中包含将 local 指定为 worker 策略的回退策略:

bazel build //my:target --strategy=Javac=worker,local

使用 worker 策略而不是本地策略可以增强编译 具体取决于实施情况。对于 Java,build 可以是 2-4 个 更快,有时用于增量编译。使用工作器编译 Bazel 的速度大约是原来的 2.5 倍。有关详情,请参阅 “选择工作器数量”部分。

如果您还有与本地构建环境匹配的远程构建环境,则可以使用实验性动态策略,该策略会对远程执行和 worker 执行进行竞态。如需启用动态策略,请传递 --experimental_spawn_scheduler 标志。此策略会自动启用工作器,因此无需指定 worker 策略,但您仍然可以使用 localsandboxed 作为后备。

选择工作器数量

每个 mnemonic 的默认工作器实例数为 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)从头开始编译所需的时间。对于每个工作器配置,系统会运行五个整洁 build, 计算最后四项指标的平均值。

整洁 build 的性能提升图

图 1. 干净 build 的性能改进图。

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

增量编译通常好处更多。干净 build 相对较少,但在编译之间更改单个文件很常见,尤其是在测试驱动型开发中。上述示例中还包含一些非 Java 打包操作,这些操作可能会掩盖增量编译时间。

仅重新编译 Java 源代码 (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar) 更改内部字符串常量之后, AbstractContainerizingSandboxedSpawn.java 提供 3 倍的速度提升(通过 1 个预热 build 平均进行 20 次增量构建) 已舍弃):

增量构建的性能改进图

图 2. 增量 build 性能提升情况图表。

具体提速幅度取决于所做的更改。在上述更改常用常量的情况下,测量到的加速比为 6 倍。

修改永久性工作器

您可以传递 --worker_extra_flag 标志,以按助记符指定工作器的启动标志。例如,传递 --worker_extra_flag=javac=--debug 会仅为 Javac 启用调试。每次使用此标志时,只能设置一个工作器标志,并且只能为一个助记符设置一个工作器标志。 您不仅要为每个助记符单独创建 Worker, 启动标志。助记符和启动的每种组合 标志会组合成 WorkerKey,并且对于每个 WorkerKey,最多 可以创建 worker_max_instances 个工作器。如需了解操作配置如何还可以指定设置标志,请参阅下一部分。

您可以使用 --high_priority_workers 标志指定应优先于正常优先级的 mnemonic 运行的 mnemonic。这有助于优先考虑始终处于关键 路径。如果有两个或更多高优先级工作器在执行请求,则系统会阻止所有其他工作器运行。此标志可多次使用。

传递 --worker_sandboxing 标志会使每个 worker 请求为其所有输入使用单独的沙盒目录。设置沙盒需要额外一些时间,尤其是在 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 性能”页面

实现持久性工作器

如需详细了解如何创建工作器,请参阅创建永久性工作器页面。

以下示例展示了使用 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 上接收此数据。然后,Worker 会执行操作, 并将 JSON 格式的 WorkResponse 发送到 stdout 上的 Bazel。然后,Bazel 会解析此响应,并将其手动转换为 WorkResponse proto。接收者 使用二进制编码的 protobuf 与关联的 Worker 进行通信,而不是 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 示例 可能有帮助。

工作器如何影响沙盒?

默认情况下,使用 worker 策略不会在 sandbox,类似于 local 策略。您可以将 --worker_sandboxing 标志,用于在沙盒内运行所有工作器,确保每个工作器 工具执行时,只能看到应该包含的输入文件。该工具可能仍会在请求之间内部泄露信息,例如通过缓存。使用 dynamic 策略需要将工作器置于沙盒中

为了让工作器能够正确使用编译器缓存,系统会一并传递摘要 输出结果。因此,编译器或封装容器可以检查输入是否 也无需读取文件。

即使使用输入摘要来防止不必要的缓存, 提供的沙盒不如纯粹的沙盒那么严格,因为该工具可能会 保留受先前请求影响的其他内部状态。

只有在工作器实现支持的情况下,多路复用工作器才能放入沙盒中,并且必须使用 --experimental_worker_multiplex_sandboxing 标志单独启用此沙盒化功能。如需了解更多详情,请访问 设计文档)。

深入阅读

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