动态执行

动态执行 是 Bazel 中的一项功能,可并行启动同一操作的本地执行和远程执行,并使用第一个完成的分支的输出,取消另一个分支。它结合了远程构建系统的执行能力和/或大型共享缓存与本地执行的低延迟,为干净构建和增量构建提供了两全其美的解决方案。

本页介绍了如何启用、调整和调试动态执行。如果您同时设置了本地执行和远程执行,并且正在尝试调整 Bazel 设置以获得更好的性能,那么本页非常适合您。如果您尚未设置 远程执行,请先参阅 Bazel 远程执行 概览

启用动态执行?

动态执行模块是 Bazel 的一部分,但如需使用动态执行,您必须能够从同一 Bazel 设置在本地和远程进行编译。

如需启用动态执行模块,请将 --internal_spawn_scheduler 标志传递给 Bazel。这会添加一个名为 dynamic 的新执行策略。您现在可以将其用作要动态运行的助记符的策略,例如 --strategy=Javac=dynamic。如需了解如何选择要为其启用动态执行的助记符,请参阅下一部分。

对于使用动态策略的任何助记符,远程执行策略取自 --dynamic_remote_strategy 标志,本地策略取自 --dynamic_local_strategy 标志。传递 --dynamic_local_strategy=worker,sandboxed 会将动态执行的本地分支的默认值设置为按该顺序尝试使用 worker 或沙盒执行。传递 --dynamic_local_strategy=Javac=worker 仅会替换 Javac 助记符的默认值。远程版本的工作方式相同。这两个标志都可以多次指定。如果某项操作无法在本地执行,则会像往常一样在远程执行,反之亦然。

如果您的远程系统有缓存,则在远程系统指示缓存命中后,--dynamic_local_execution_delay 标志会向本地执行添加以毫秒为单位的延迟。这样可以避免在可能发生更多缓存命中时运行本地执行。默认值为 1000 毫秒,但应调整为比缓存命中通常所需的时间稍长。实际时间取决于远程系统以及往返所需的时间。通常,对于给定远程系统的所有用户,该值都是相同的,除非其中一些用户距离足够远,会增加往返延迟时间。您可以使用 Bazel 分析 功能 来查看典型的 缓存命中所需的时间。

动态执行可以与本地沙盒策略以及 持久型 worker 搭配使用。持久型 worker 在与动态执行搭配使用时会自动 使用沙盒运行,并且无法使用 多路复用 worker。在 Darwin 和 Windows 系统上,沙盒策略可能会很慢;您可以传递 --reuse_sandbox_directories 以减少在这些系统上创建沙盒的开销。

动态执行也可以使用 standalone 策略运行,但由于 standalone 策略在开始执行时必须获取输出锁,因此它实际上会阻止远程策略先完成。--experimental_local_lockfree_output 标志通过允许本地执行直接写入输出来解决此问题,但如果远程执行先完成,则会被远程执行中止。

如果动态执行的某个分支先完成但失败,则整个操作都会失败。这是有意为之,以防止本地执行和远程执行之间的差异被忽视。

如需详细了解动态执行及其锁定机制,请参阅 Julio Merino 的精彩博文

我应该在什么情况下使用动态执行?

动态执行需要某种形式的远程执行系统。目前无法使用仅缓存的远程系统,因为缓存未命中会被视为失败的操作。

并非所有类型的操作都非常适合远程执行。最佳 候选对象是那些在本地执行速度更快(例如通过 使用 持久型 worker)的操作,或者那些运行速度足够快 ,以至于远程执行的开销占据执行时间的操作。由于每个本地执行的操作都会锁定一定数量的 CPU 和内存资源,因此运行不属于这些类别的操作只会延迟属于这些类别的操作的执行。

从版本 5.0.0-pre.20210708.4开始, 性能分析包含有关 worker 执行的数据,包括在 动态执行竞争失败后完成工作请求所花费的时间。如果您看到动态执行 worker 线程花费大量时间获取资源,或者在 async-worker-finish 中花费大量时间,则可能有一些缓慢的本地操作延迟了 worker 线程。

动态执行性能较差的分析数据

在上面的配置文件中,我们使用了 8 个 Javac worker,可以看到许多 Javac worker 在竞争中失败,并在 async-worker-finish 线程上完成了工作。这是由非 worker 助记符占用足够的资源来延迟 worker 造成的。

具有更出色的动态执行性能的分析数据

当仅使用动态执行运行 Javac 时,只有大约一半的已启动 worker 在开始工作后最终输掉了竞争。

之前推荐的 --experimental_spawn_scheduler 标志已弃用。 它会启用动态执行并将 dynamic 设置为所有助记符的默认策略,这通常会导致此类问题。

性能

动态执行方法假定本地和远程都有足够的可用资源,因此值得花费一些额外的资源来提高整体性能。但是,过度使用资源可能会降低 Bazel 本身或其运行的机器的速度,或者给远程系统带来意外的压力。您可以通过以下几种方式更改动态执行的行为:

--dynamic_local_execution_delay 会在远程分支启动后延迟本地分支的启动,延迟时间以毫秒为单位,但前提是当前构建期间发生了远程缓存命中。这样一来,受益于远程缓存的构建就不会浪费本地资源,因为大多数输出都可能在缓存中找到。根据缓存的质量,减少此值可能会提高构建速度,但代价是使用更多本地资源。

--experimental_dynamic_local_load_factor 是一项实验性的高级资源管理选项。它接受 0 到 1 之间的值,0 表示关闭此功能。 当设置为大于 0 的值时,Bazel 会在有许多操作等待调度时调整本地调度的操作数。将其设置为 1 可允许调度与可用 CPU 数量(根据 --local_resources)一样多的操作。较低的值会将调度的操作数设置为相应较少的值,因为有更多操作可供运行。这听起来可能违反直觉,但对于良好的远程系统,当运行许多操作时,本地执行的帮助不大,而本地 CPU 更适合管理远程操作。

当远程分支至少运行这么长时间后,--experimental_dynamic_slow_remote_time 会优先启动本地分支。通常,最近调度的操作会获得优先权,因为它赢得竞争的机会最大,但如果远程系统有时挂起或花费的时间过长,这可以使构建继续进行。默认情况下,此功能处于停用状态,因为它可能会隐藏远程系统的问题,而这些问题应该得到修复。如果您启用此选项,请务必监控远程系统性能。

当本地 spawn 因给定信号而退出时,可以使用 --experimental_dynamic_ignore_local_signals 让远程分支接管。这 主要与 worker 资源限制(请参阅 --experimental_worker_memory_limit_mb--experimental_worker_sandbox_hardening、 和 --experimental_sandbox_memory_limit_mb)) 一起使用,其中 worker 进程可能会在使用过多资源时被终止。

JSON 跟踪配置文件包含 许多与性能相关的图表,这些图表有助于确定如何改进 性能和资源用量之间的权衡。

问题排查

动态执行的问题可能很微妙且难以调试,因为它们可能仅在本地执行和远程执行的某些特定组合下才会显现。--debug_spawn_scheduler 会添加来自动态执行系统的额外输出,这些输出有助于调试这些问题。您还可以调整 --dynamic_local_execution_delay 标志以及远程作业与本地作业的数量,以便更轻松地重现问题。

如果您在使用 standalone 策略时遇到动态执行问题,请尝试在不使用 --experimental_local_lockfree_output 的情况下运行,或者在沙盒中运行本地操作。这可能会稍微降低构建速度(如果您使用的是 Mac 或 Windows,请参阅上文),但会消除一些可能导致失败的原因。