Bazel构建系统中的沙箱机制详解
什么是沙箱机制
沙箱(Sandbox)是一种权限限制策略,用于隔离进程之间或进程与系统资源之间的访问。在Bazel构建系统中,沙箱机制主要用于限制文件系统访问。
Bazel的文件系统沙箱会在一个仅包含已知输入文件的工作目录中运行进程,这样编译器和其他工具就无法访问它们不应该访问的源文件,除非这些工具知道这些文件的绝对路径。
沙箱的工作原理
Bazel为每个动作(action)构建一个execroot/
目录,该目录在执行时充当动作的工作目录。execroot/
包含动作的所有输入文件,并作为任何生成输出的容器。Bazel然后使用操作系统提供的技术(Linux上的容器和macOS上的sandbox-exec
)将动作限制在execroot/
内。
为什么需要沙箱机制
-
防止未声明依赖:没有动作沙箱,Bazel无法知道工具是否使用了未声明的输入文件。当这些未声明的输入文件发生变化时,Bazel会认为构建是最新的而不会重新构建动作,导致增量构建不正确。
-
远程缓存问题:错误的缓存条目重用会在远程缓存中造成问题。共享缓存中的错误缓存条目会影响项目中的每个开发者,而清除整个远程缓存并不是可行的解决方案。
-
模拟远程执行:沙箱化模拟了远程执行的行为——如果构建在沙箱中运行良好,那么它在远程执行中也可能运行良好。通过让远程执行上传所有必要的文件(包括本地工具),可以显著降低编译集群的维护成本。
Bazel中的沙箱策略
Bazel提供了多种沙箱策略供选择:
-
local策略:不进行任何形式的沙箱化,只是简单地在工作区的execroot中执行动作的命令行。
-
processwrapper-sandbox:
- 不需要任何"高级"功能,可以在任何POSIX系统上开箱即用
- 构建一个由指向原始源文件的符号链接组成的沙箱目录
- 将工作目录设置为该沙箱目录而非execroot
- 执行后将已知输出工件从沙箱移动到execroot并删除沙箱
-
linux-sandbox:
- 在processwrapper-sandbox基础上构建
- 使用Linux命名空间(User、Mount、PID、Network和IPC)隔离动作与主机
- 使整个文件系统变为只读(沙箱目录除外)
- 可选地阻止动作访问网络
- 使用PID命名空间防止动作看到其他进程
-
darwin-sandbox:
- macOS上的类似实现
- 使用Apple的
sandbox-exec
工具实现类似功能
沙箱机制的局限性
-
性能开销:沙箱化会产生额外的设置和拆卸成本。在Linux上,沙箱化构建通常只会慢几个百分点。可以通过设置
--reuse_sandbox_directories
来缓解。 -
工具缓存失效:沙箱化实际上会禁用工具可能拥有的任何缓存。可以通过使用持久化工作器(persistent workers)来缓解,但会降低沙箱保证。
-
多路复用工作器限制:多路复用工作器需要显式支持才能被沙箱化。不支持多路复用沙箱的工作器在动态执行下会作为单路工作器运行,这可能会消耗额外的内存。
沙箱调试技巧
命名空间未激活问题
在某些平台上,用户命名空间默认是禁用的。如果/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
(针对genrules)或--spawn_strategy=local
(针对其他规则)来停用沙箱。
详细调试构建失败
当构建失败时,使用--verbose_failures
和--sandbox_debug
可以让Bazel显示构建失败时运行的确切命令,包括设置沙箱的部分。
注意:使用--sandbox_debug
时Bazel不会删除沙箱目录。除非正在主动调试,否则应禁用此标志,因为它会随着时间的推移填满磁盘空间。
总结
Bazel的沙箱机制是确保构建可靠性和可重现性的重要工具。通过隔离构建动作,它可以防止未声明的依赖关系,提高远程缓存的安全性,并为远程执行做好准备。虽然沙箱化会带来一定的性能开销,但在大多数情况下,这种开销是可以接受的,特别是考虑到它带来的好处。