ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

gVisor Rootfs Overlay 深度解析:把容器可写层移入沙箱,将文件系统开销减半

gVisor Rootfs Overlay 深度解析:把容器可写层移入沙箱,将文件系统开销减半 gVisor Rootfs Overlay 深度解析把容器可写层移入沙箱将文件系统开销减半【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisorRootfs Overlay 是 gVisorrunsc中默认启用的文件系统优化它把容器根文件系统的可写“upper 层”从宿主机 overlayfs 移入沙箱内部的 tmpfs使创建、修改、copy-up 等高频写操作不再经过 gofer RPC 和宿主机系统调用。读完本文你将理解该机制为何能消除关键路径上的跨进程开销、self-backed filestore 与 whiteout 如何解决内存膨胀和 K8s 临时存储计量问题以及如何通过--overlay2标志与 OCI 注解完整配置这一特性并用源码定位到实现的关键调用链。为什么 gVisor 中的文件系统访问很昂贵gVisor 使用一个受信任的文件系统代理进程gofer代表沙箱访问宿主文件系统。沙箱进程在 gVisor 的安全模型中是不受信组件它不直接获得容器文件系统的访问权其 seccomp 过滤器见 runsc/boot/filter也不允许文件系统相关的系统调用。因此容器 rootfs 和 bind mount 都被配置为由 gofer 提供服务。当容器需要执行文件系统操作时它会向 gofer 发起一次 RPC由 gofer 在宿主上发起真正的系统调用并处理该请求。这一路径相当昂贵成本主要来自两部分RPC 成本与 gofer 进程通信的开销包括进程调度、消息序列化和 IPC 系统调用。为降低该成本gVisor 开发了一个专用协议 LISAFS它比此前的 gofer 协议高效得多此外gVisor 社区也在试验让沙箱以安全方式直接访问容器文件系统从而让 gofer 完全退出文件系统操作的关键路径该方向即后来的 DirectFS参见 g3doc/user_guide/filesystem.md 中的介绍。系统调用成本真正读写容器文件系统的宿主机系统调用的开销。系统调用会触发进入内核的用户态/内核态上下文切换代价很高。为缓解这一点gVisor 在内存中重度缓存文件系统树stat(2)等针对已缓存文件的操作可以很快完成但mkdir(2)、rename(2)等操作仍必须走到宿主机系统调用。Rootfs overlay 的核心洞察是既然容器对 rootfs 的修改本来就随容器销毁而丢弃那就让这些修改根本不落地到宿主机——把 upper 层搬进沙箱。背景容器 rootfs 与 overlayfs在 Docker 与 Kubernetes 中容器 rootfs 基于镜像打包的文件系统。镜像文件系统不可变容器对 rootfs 的改动被单独保存并随容器销毁而丢弃——这样同一镜像的文件系统可以高效共享给所有运行该镜像的容器。这与 bind mount 不同bind mount 让容器访问被绑定的宿主机目录树其修改始终传播到宿主机并在容器退出后保留。Docker 和 Kubernetes 默认使用 overlay 文件系统overlayfs来配置容器 rootfs。overlayfs 挂载由一个 upper 层和多个 lower 层组成在挂载点呈现所有层的合并视图保证 lower 层只读所有修改写入 upper 层。lower 层构成“镜像层”upper 层是“容器层”。容器销毁时upper 层挂载一并销毁容器对 rootfs 的修改随之被丢弃。旧模型宿主侧 OverlayBefore考虑这样一个例子镜像中有文件foo和baz容器随后覆盖了foo并新建了文件bar。在 rootfs overlay 引入之前gVisor 的做法是沙箱内的 overlay 挂载直接对应宿主机上的一个 overlayfs 挂载lower 指向镜像层upper 指向宿主可写目录容器对 rootfs 的每一次访问/修改都要经过 gofer 落到宿主机上操作这个被 overlay 的目录。上表例中bar的创建、foo的覆盖全部要付出 RPC 宿主系统调用的双重代价。沙箱内 Overlay用 tmpfs 充当 upper 层关键问题在于upper 层反正会随容器销毁从宿主机访问/修改它又这么贵为什么还要把它留在宿主机上gVisor 的做法是把 upper 层移入沙箱内部用沙箱内部的一个 overlay 挂载来覆盖 rootfsupper容器层是沙箱内的 tmpfslower 层是只读的、由 gofer 客户端LISAFS提供的镜像层对 rootfs 的一切修改都保存在 tmpfs内存中访问/修改 upper 层不需要任何 gofer RPC也不需要宿主机系统调用。这显著加速了 upper 层上的文件系统操作——而 upper 层恰好存放着新创建的文件和 copy-up从 lower 拷入 upper 的修改版的文件目录也就是容器运行期最常触碰的部分。代价tmpfs 默认驻留应用内存tmpfs 默认使用沙箱进程自己的内存来存储挂载中所有文件数据。对于文件量大的负载这会推高沙箱内存用量甚至耗尽容器的内存限制。解决办法是把 tmpfs upper 层的全部文件数据落到磁盘上在宿主机文件系统上为 tmpfs 准备一个“filestore”文件。以上面的例子为例这个 filestore 将保存foocopy-up与bar的全部文件数据。从源码看这一机制在 configureOverlay 中组装当挂载配置带有 filestoreIsFilestorePresent()为真即 upper 为self或anon时gofer 端创建的 filestore 文件描述符会被送入沙箱并经createPrivateMemoryFile包装成私有内存文件赋给 tmpfs 的MemoryFile选项if filestoreFD ! nil { // Create memory file for disk-backed overlays. resourceID : checkpoint.ResourceID{ContainerName: c.containerName, Path: dst} mf, err : createPrivateMemoryFile(filestoreFD.ReleaseToFile(overlay-filestore), resourceID, c.containerID, c.l.fsRestore) ... tmpfsOpts.MemoryFile mf }filestore 会把 tmpfs 中所有常规文件“扁平化”进同一个宿主机文件沙箱随后可以mmap(2)该文件在自己的地址空间中高效地访问与修改它既不用经过 gofer RPC也不用发起额外系统调用。同时 tmpfs 选项设置DisableDefaultSizeLimit: true避免被 overlay 的挂载受默认 tmpfs 大小限制约束大小由--overlay2的size参数显式控制。Self-Backed Overlaywhiteout 与 K8s 临时存储计量把 upper 层移入沙箱会引入一个新的问题Kubernetes 支持为容器设置本地临时存储ephemeral storage请求与限制而宿主上 rootfs overlay 的 upper 层会计入该限制。kubelet 通过遍历整个 upper 层目录、对每个文件stat(2)并累加stat.st_blocks * block_size来强制执行该限制。如果 upper 层被搬进沙箱宿主上的 upper 层是空的kubelet 就无从计量了。为此 gVisor 引入了“self-backed”自支撑overlay把 filestore 文件就创建在宿主 upper 层里。这样 kubelet 扫描宿主 upper 层时能发现该文件其st_blocks就能代表沙箱内 upper 层的总文件用量。另一方面必须把这个 filestore 对容器内应用隐藏以免误导应用做法是在沙箱内部的 upper 层里为它创建一个whiteout阻止该文件出现在合并视图中。源码中这一逻辑清晰可见filestore 的命名规则SelfFilestorePath 将 filestore 放在被 overlay 挂载自身的隐藏文件中文件名带沙箱 ID 后缀前缀定义于 pkg/fsutil/fsutil.go以便同一卷被多个沙箱 overlay 时互不冲突whiteout 的创建configureOverlay 末尾若mountConf.IsSelfBacked()为真则调用overlay.CreateWhiteout在 upper 层根下为selfFilestoreName(sandboxID)打上 whiteoutoverlay 文件系统实现见 pkg/sentry/fsimpl/overlay/overlay.go。// We need to hide the filestore from the containerized application. if mountConf.IsSelfBacked() { if err : overlay.CreateWhiteout(ctx, c.l.k.VFS(), creds, vfs.PathOperation{ Root: upperRootVD, Start: upperRootVD, Path: fspath.Parse(selfFilestoreName(c.l.sandboxID)), }); err ! nil { return nil, nil, fmt.Errorf(failed to create whiteout to hide self overlay filestore: %w, err) } }至此rootfs 的最终配置为镜像层只读经 gofer/LISAFS 提供→ 沙箱内 overlayupper 为 tmpfs文件数据经 mmap 的 filestore 落盘→ filestore 位于宿主 overlayfs 的 upper 层且对容器不可见。配置实战--overlay2标志与 OCI 注解挂载配置模型每个 gofer 挂载在沙箱中的形态由 GoferMountConf 描述包含 lower 类型、upper 类型与可选大小三部分type GoferMountConf struct { Lower GoferMountConfLowerType json:lower // none | lisafs | erofs Upper GoferMountConfUpperType json:upper // none | memory | self | anon Size string json:size,omitempty }解析格式为lower:upper[:size…]配套判断函数ShouldUseOverlayfs、ShouldUseTmpfs、ShouldUseLisafs、ShouldUseErofs决定了沙箱内实际叠加哪些文件系统层。upper 类型的语义摘自 runsc/specutils/gofer_conf.goupper 值含义none不加 upper 层此时必须有 lower 层memorytmpfs upper 层由应用内存支撑selftmpfs upper 层filestore 位于挂载源目录内对应用不可见即 self-backedanontmpfs upper 层filestore 位于匿名目录中全局配置--overlay2标志全局为所有容器配置 overlay 使用--overlay2标志格式为--overlay2{mount}:{medium}[,size{size}]定义见 runsc/config/flags.go生效逻辑见 Config.GetOverlay2mountroot仅根文件系统或all所有挂载medium支撑介质memory、self或dir/abs/dir/pathfilestore 创建在指定宿主目录中size可选限制 tmpfs upper 层大小如2g。常用取值示例详见 g3doc/user_guide/filesystem.md配置效果--overlay2root:selfrootfs 覆盖一个自支撑的、文件后端的 tmpfsfilestore 存于 rootfs 自身。这是 runsc 的默认行为--overlay2all:memory所有挂载覆盖内存后端 tmpfs--overlay2root:dir/tmp/overlayrootfs 覆盖文件后端 tmpfsfilestore 存于/tmp/overlay--overlay2none关闭 overlay当需要把 rootfs 修改传播回宿主文件系统时使用几个来自源码的注意点默认值self-backed 的 rootfs overlay 在 runsc 中默认启用以获得性能如文档所述需要把 rootfs 变更传播到宿主机时用--overlay2none关闭。兼容性约束Config.Validate 中overlay2与file-accesssharedrootfs 共享文件访问模式互斥——共享模式下 rootfs 本就直连宿主无需也不允许叠加 overlay。注解通道限制checkOverlay2flags.go只允许none/memory/self通过 OCI 注解下发使用dir…介质必须同时启用--allow-flag-override标志。废弃标志--overlay已废弃等价于--overlay2all:memory两者同时使用会报错。runsc do场景非容器执行路径 runsc/cmd/sandboxexec.go 会默认设置--overlay2root:memory避免对宿主根目录的权限问题。通过 Docker 使用时把参数写入/etc/docker/daemon.json的runtimeArgs并重启 Docker daemon 即可{ runtimes: { runsc: { path: /usr/local/bin/runsc, runtimeArgs: [ --overlay2all:memory ] } } }进阶EROFS 作为 lower 层文档同时说明g3doc/user_guide/filesystem.mdgVisor 支持 EROFSEnhanced Read-Only File Systemrootfs 与挂载它是一个高性能只读文件系统镜像文件被 mmap 进 sentry 后纯内存访问完全无需经由 gofer 与宿主文件系统通信是 rootfs overlay lower 层的理想选择还能让 gVisor 在无其他 gofer 挂载时运行于 gofer-less 模式。通过容器 spec 注解配置annotations: { dev.gvisor.spec.rootfs.source: /tmp/container_image.erofs, dev.gvisor.spec.rootfs.type: erofs, dev.gvisor.spec.rootfs.overlay: memory, dev.gvisor.spec.rootfs.options: size2g }其中source与type必填overlay可选默认不加 overlay得到只读 rootfs取值同前文的支撑介质options可选为逗号分隔的选项列表目前支持size用于限定 tmpfs upper 层大小。性能收益fsstress 微基准与真实构建负载以下基准在 gLinux 桌面上使用 KVM 平台运行用于观察 rootfs overlay 的影响。微基准fsstressLinux Test Project 提供的 fsstress 工具会并发执行大量文件系统操作创建并修改一个形态各异的庞大文件系统树。在容器根文件系统上运行的具体命令为sh -c mkdir /test time fsstress -d /test -n 500 -p 20 -s 1680153482 -X -l 10加-v参数verbose可查看正在执行哪些文件系统操作。结果非常惊人rootfs overlay 将 fsstress 的耗时从 262.79 秒降低到 3.18 秒。但请注意这类微基准不能代表真实应用不应据此外推真实世界的性能。真实世界基准abseil-cpp 构建构建任务是非常吃文件系统的负载读取大量源码文件、编译并写出二进制与目标文件。以用 bazel 构建 abseil-cpp 为例bazel 会在 rootfs 中执行大量文件系统操作其缓存位于~/.cache/bazel/。这类场景在真实世界很常见许多应用都乐于把容器根文件系统当作临时空间scratch space利用其“容器退出即消失”的特性。为更贴近真实abseil-cpp 仓库通过 bind mount 挂入容器——bind mount 没有 overlay。衡量性能时我们关心的是压低沙箱化开销使 gVisor 性能尽量接近无沙箱基线。沙箱开销可按公式overhead (s-n)/n计算其中s是工作负载在 gVisor 沙箱内的运行时间n是同一负载在无沙箱原生环境下的运行时间。结果对 abseil 构建rootfs overlay把沙箱开销减半。小结Rootfs overlay 的核心思想容器层upper layer反正随容器销毁就把它从宿主 overlayfs 搬进沙箱用 tmpfs upper 只读 lowergofer/LISAFS 提供的合并视图服务 rootfs新创建与 copy-up 文件的高频操作不再经过 gofer RPC 与宿主系统调用filestorehost-backed解决了 tmpfs 吃光容器内存的问题所有文件数据扁平化进单个宿主文件沙箱 mmap 后直接内存式读写runsc/boot/vfs.goself-backed 变体进一步把 filestore 放进宿主 upper 层并打上 whiteout 隐藏使 kubelet 的临时存储限额计量继续有效该优化如今在 runsc 中默认启用root:self可用--overlay2调整介质与大小、用--overlay2none关闭或通过 OCI 注解如 EROFS lower 层做更细粒度的配置效果fsstress 微基准耗时从 262.79s 降到 3.18sabseil 构建的沙箱开销减半——文件密集负载在安全性与性能之间不再需要二选一。适用前提与限制上述配置均针对当前仓库中的 runscgVisor 的 OCI 运行时--overlay2与file-accessshared互斥dir介质经注解下发时需--allow-flag-override微基准数据仅说明机制收益量级不代表任何具体环境的预期值。【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表