动态执行

报告问题 查看源代码

动态执行是 Bazel 中的一项功能,可以在本地和远程并行启动同一操作,使用完成的第一个分支的输出,取消另一个分支。它将远程构建系统的执行能力和/或大型共享缓存与本地执行的低延迟优势相结合,在干净构建和增量构建中均有优势。

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

要启用动态执行功能吗?

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

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

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

如果您的远程系统具有缓存,--dynamic_local_execution_delay 标志会在远程系统指示缓存命中后向本地执行添加延迟(以毫秒为单位)。这样可以避免在可能出现更多缓存命中时运行本地执行。默认值为 1000 毫秒,但应调整为略高于缓存命中通常所需的时间。实际时间取决于远程系统和往返所用的时间。通常,对于给定远程系统的所有用户,此值都是相同的,除非其中某些用户因距离太远而增加往返延迟时间。您可以使用 Bazel 性能剖析功能来了解典型缓存命中需要多长时间。

动态执行可以与本地沙盒策略以及永久性工作器结合使用。与动态执行搭配使用时,永久性工作器将自动通过沙盒运行,并且不能使用多路复用工作器。在 Darwin 和 Windows 系统中,沙盒化策略可能速度很慢;您可以传递 --reuse_sandbox_directories 以减少在这些系统上创建沙盒的开销。

动态执行也可以使用 standalone 策略运行,不过,由于 standalone 策略在开始执行时必须获取输出锁,因此可以有效地阻止远程策略先完成。--experimental_local_lockfree_output 标志允许本地执行直接写入输出,但如果远程执行先完成,则会取消该输出,从而解决此问题。

如果动态执行的其中一个分支先完成但失败了,则整个操作都会失败。这是一种有意选择,可防止本地执行和远程执行之间的差异被忽视。

如需了解有关动态执行及其锁定工作原理的更多背景信息,请参阅 Julio Merino 的精彩博文

何时应使用动态执行?

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

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

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

在动态执行性能不佳时分析数据

在上面的配置文件(使用 8 个 Javac 工作器)中,我们看到许多 Javac 工作器落败了比赛,并在 async-worker-finish 线程上完成了其工作。这是由于非工作器助记符占用了足够的资源来延迟工作器。

通过更好的动态执行性能分析数据

当只有 Javac 通过动态执行运行时,只有大约一半的已启动工作器在开始工作后最终输掉比赛。

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

性能

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

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

--experimental_dynamic_local_load_factor 是一个实验性高级资源管理选项。如果关闭此功能,则取值范围为 0 到 1, 0。 当设置为大于 0 的值时,如果有大量操作等待调度,则 Bazel 会调整本地预定操作的数量。将该值设置为 1 会允许调度尽可能多的操作,只要有可用的 CPU 即可(根据 --local_cpu_resources)。较低的值会将调度的操作数量设置为相应地减少,因为可运行的操作数量越多。这听起来可能不合常理,但有了良好的远程系统,当运行很多操作时,本地执行的作用就没什么用处,而本地 CPU 更适合用来管理远程操作。

如果远程分支已经运行了至少这么长的时间,--experimental_dynamic_slow_remote_time 会优先启动本地分支。通常,最近调度的操作会优先考虑,因为它最有可能赢得比赛,但如果远程系统有时挂起或耗时过长,则可能会导致 build 继续进行。该功能默认处于停用状态,因为它可能会隐藏应该修复的远程系统问题。如果启用此选项,请务必监控远程系统性能。

使用 --experimental_dynamic_ignore_local_signals,当本地派生实例因给定信号而退出时,远程分支可用于接管远程分支。这主要与工作器资源限制(请参阅 --experimental_worker_memory_limit_mb--experimental_worker_sandbox_hardening--experimental_sandbox_memory_limit_mb)一起发挥作用,如果工作器进程使用的资源过多,它们可能会被终止。

JSON 跟踪记录配置文件包含许多与性能相关的图表,可帮助您找到各种方式来权衡性能和资源使用情况。

问题排查

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

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