动态执行是 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 起,性能分析包含有关 worker 执行的数据,包括在动态执行竞争失败后完成工作请求所花费的时间。如果您发现动态执行工作线程花费大量时间获取资源,或者在 async-worker-finish
中花费大量时间,则可能存在一些缓慢的本地操作延迟了工作线程。
在上述使用 8 个 Javac worker 的配置文件中,我们看到许多 Javac worker 在竞争中失败,并在 async-worker-finish
线程上完成其工作。这是因为非工作器助记符占用了足够的资源来延迟工作器。
如果仅使用动态执行运行 Javac,那么在开始工作后,只有大约一半的已启动工作器最终会输掉竞争。
之前推荐的 --experimental_spawn_scheduler
标志已弃用。它会开启动态执行,并将 dynamic
设置为所有助记符的默认策略,这通常会导致此类问题。
性能
动态执行方法假设本地和远程有足够的可用资源,值得花费一些额外的资源来提高整体性能。但过多的资源使用可能会减慢 Bazel 本身或其运行的机器的速度,或者给远程系统带来意想不到的压力。您可以通过多种方式更改动态执行的行为:
--dynamic_local_execution_delay
会在远程分支启动后延迟本地分支的启动时间(以毫秒为单位),但前提是当前 build 期间发生了远程缓存命中。这样一来,当缓存中可能包含大部分输出时,可从远程缓存中受益的 build 就不会浪费本地资源。根据缓存的质量,减少此值可能会提高 build 速度,但会消耗更多本地资源。
--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
的情况下运行,或者在沙盒中运行本地操作。这可能会稍微减慢 build 速度(如果您使用的是 Mac 或 Windows,请参阅上文),但会消除一些可能导致失败的原因。