本文介绍了 Bazel 中的沙盒以及如何调试沙盒环境。
沙盒是一种权限限制策略,可将进程彼此隔离或与系统中的资源隔离。对于 Bazel,这意味着限制文件系统访问权限。
Bazel 的文件系统沙盒在仅包含已知输入的工作目录中运行进程,这样一来,除非编译器和其他工具知道源文件的绝对路径,否则它们不会看到不应访问的源文件。
沙盒不会以任何方式隐藏宿主环境。进程可以自由访问文件系统上的所有文件。不过,在支持用户命名空间的平台上,进程无法修改其工作目录之外的任何文件。这可确保 build 图没有可能会影响 build 可重现性的隐藏依赖项。
更具体地说,Bazel 会为每个操作构建一个 execroot/
目录,该目录在执行时充当操作的工作目录。execroot/
包含操作的所有输入文件,并充当任何生成的输出的容器。然后,Bazel 会使用操作系统提供的技术(Linux 上的容器和 macOS 上的 sandbox-exec
)将操作限制在 execroot/
内。
沙盒处理原因
如果不使用操作沙盒,Bazel 就不知道某个工具是否使用了未声明的输入文件(未在操作的依赖项中明确列出的文件)。当其中一个未声明的输入文件发生更改时,Bazel 仍然认为 build 是最新的,并且不会重新构建操作。这可能会导致增量构建不正确。
远程缓存期间,缓存条目的重复使用不当会导致问题。共享缓存中的错误缓存条目会影响项目中的每位开发者,而擦除整个远程缓存并不可行。
沙盒模拟远程执行的行为,如果构建在沙盒中运行良好,那么在远程执行中也可能会运行良好。通过让远程执行上传所有必要的文件(包括本地工具),您可以显著降低编译集群的维护成本,而无需在每次想试用新编译器或更改现有工具时,都必须在集群中的每台机器上安装这些工具。
使用哪种沙盒策略
您可以使用策略标志选择要使用的沙盒类型(如果有)。使用 sandboxed
策略可让 Bazel 选择下列沙盒实现之一,并优先选择特定于操作系统的沙盒,而不是不太严格的通用沙盒。如果您传递 --worker_sandboxing
标志,持久工作器将在通用沙盒中运行。
local
(也称为 standalone
)策略不会进行任何类型的沙盒处理。
它只是执行操作的命令行,并将工作目录设置为工作区的 execroot。
processwrapper-sandbox
是一种不需要任何“高级”功能的沙盒策略,应该可以在任何 POSIX 系统上开箱即用。它会构建一个由指向原始源文件的符号链接组成的沙盒目录,执行操作的命令行,并将工作目录设置为此目录(而不是 execroot),然后将已知的输出制品从沙盒移到 execroot 并删除沙盒。这样可以防止操作意外使用任何未声明的输入文件,并防止在执行根目录中留下未知输出文件。
linux-sandbox
更进一步,在 processwrapper-sandbox
的基础上构建。与 Docker 在幕后执行的操作类似,它使用 Linux 命名空间(用户、装载、PID、网络和 IPC 命名空间)将操作与主机隔离开来。也就是说,除了沙盒目录之外,它会使整个文件系统变为只读,因此该操作不会意外修改宿主文件系统上的任何内容。这样可以防止出现有缺陷的测试意外运行 rm -rf 命令来删除您的 $HOME 目录等情况。(可选)您还可以阻止该操作访问网络。linux-sandbox
使用 PID 命名空间来防止该操作看到任何其他进程,并可靠地杀死所有进程(即使是该操作生成的守护程序)
darwin-sandbox
类似,但适用于 macOS。它使用 Apple 的 sandbox-exec
工具来实现与 Linux 沙盒大致相同的功能。
由于操作系统提供的机制存在限制,linux-sandbox
和 darwin-sandbox
在“嵌套”场景中均无法正常运行。由于 Docker 也使用 Linux 命名空间来实现其容器魔力,因此您无法在 Docker 容器中轻松运行 linux-sandbox
,除非您使用 docker run --privileged
。在 macOS 上,您无法在已沙盒化的进程内运行 sandbox-exec
。因此,在这些情况下,Bazel 会自动回退到使用 processwrapper-sandbox
。
如果您希望获得构建错误(例如,避免因执行策略不够严格而意外构建),请明确修改 Bazel 尝试使用的执行策略列表(例如 bazel build
--spawn_strategy=worker,linux-sandbox
)。
动态执行通常需要沙盒化才能进行本地执行。如需选择停用,请传递 --experimental_local_lockfree_output
标志。动态执行会以静默方式对持久性 worker 进行沙盒处理。
沙盒的缺点
沙盒会产生额外的设置和拆解费用。此成本的大小取决于多种因素,包括 build 的形状和宿主操作系统的性能。对于 Linux,沙盒式 build 的速度很少会慢于非沙盒式 build 几个百分点。设置
--reuse_sandbox_directories
可以降低设置和拆解成本。沙盒环境会有效停用该工具可能拥有的任何缓存。您可以使用持久工作器来缓解此问题,但这样会降低沙盒保证的可靠性。
多路复用工作器需要明确的工作器支持才能进行沙盒处理。不支持多路沙盒的工作器在动态执行下作为单路工作器运行,这可能会消耗额外的内存。
调试
请按照以下策略调试沙盒问题。
已停用的命名空间
在某些平台(例如 Google Kubernetes Engine 集群节点或 Debian)上,出于安全考虑,用户命名空间默认处于停用状态。如果 /proc/sys/kernel/unprivileged_userns_clone
文件存在且包含 0,您可以运行以下命令来激活用户命名空间:
sudo sysctl kernel.unprivileged_userns_clone=1
规则执行失败
由于系统设置,沙盒可能无法执行规则。如果您看到类似 namespace-sandbox.c:633: execvp(argv[0], argv): No such file or
directory
的消息,请尝试使用 --strategy=Genrule=local
停用 genrule 的沙盒,并使用 --spawn_strategy=local
停用其他规则的沙盒。
针对 build 失败的详细调试
如果 build 失败,请使用 --verbose_failures
和 --sandbox_debug
让 Bazel 显示 build 失败时运行的确切命令,包括设置沙盒的部分。
错误消息示例:
ERROR: path/to/your/project/BUILD:1:1: compilation of rule
'//path/to/your/project:all' failed:
Sandboxed execution failed, which may be legitimate (such as a compiler error),
or due to missing dependencies. To enter the sandbox environment for easier
debugging, run the following command in parentheses. On command failure, a bash
shell running inside the sandbox will then automatically be spawned
namespace-sandbox failed: error executing command
(cd /some/path && \
exec env - \
LANG=en_US \
PATH=/some/path/bin:/bin:/usr/bin \
PYTHONPATH=/usr/local/some/path \
/some/path/namespace-sandbox @/sandbox/root/path/this-sandbox-name.params --
/some/path/to/your/some-compiler --some-params some-target)
您现在可以检查生成的沙盒目录,查看 Bazel 创建了哪些文件,然后再次运行该命令,看看它的行为如何。
请注意,当您使用 --sandbox_debug
时,Bazel 不会删除沙盒目录。除非您正在积极调试,否则应停用 --sandbox_debug
,因为该功能会随着时间的推移而占用大量磁盘空间。