news 2026/9/13 5:21:03

gVisor 资源模型深度解析:Sentry 如何按需弹性管理 CPU、内存、线程与文件资源

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gVisor 资源模型深度解析:Sentry 如何按需弹性管理 CPU、内存、线程与文件资源

gVisor 资源模型深度解析:Sentry 如何按需弹性管理 CPU、内存、线程与文件资源

【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor

导读

本文基于 gVisor 架构指南中的 Resource Model(资源模型) 章节,系统讲解 gVisor 沙箱在进程、网络、文件、线程、时间、内存与资源限制七个维度上的资源管理设计。gVisor 的核心思想是:不预先固定 vCPU 数量或物理内存大小,而是让沙箱的资源形状紧密跟随被沙箱化进程的形状——忙碌时向上扩展占用大量核心与内存,空闲时把资源交还宿主机。读完本文,你将理解 gVisor 为何能做到接近零 CPU 的空闲开销、应用内存为何在 cgroup 中显示为 shmem 而非 anon,以及如何用runsc usage从沙箱内部获取准确的内存明细。

资源模型总览:沙箱形状跟随工作负载形状

gVisor 的资源模型并不假设存在固定数量的执行线程(vCPU)或固定大小的物理内存。凡是涉及底层物理资源的决策,只要可能,都会委托给拥有全局信息的宿主机系统去优化。这种委托机制使沙箱在资源使用上高度动态化:繁忙时横跨大量 CPU 核与大量内存,空闲时则把资源释放回宿主机。

换言之,沙箱的形状应当紧密跟踪被沙箱化进程的形状。下图直观展示了不同形状(不同资源需求形态)的工作负载如何在 gVisor 沙箱之上运行、并最终共享宿主机资源:

gVisor 资源模型示意图:不同形状的工作负载运行在 gVisor 沙箱之上,共享宿主机资源

进程:沙箱在宿主机上是"一个不透明进程"

与虚拟机类似,一个 gVisor 沙箱在系统上表现为一个不透明的进程。沙箱内部的进程不会以宿主机进程的形式显现——在宿主机上运行top(1)是看不到它们的。沙箱内进程层面的交互需要进入沙箱才能进行,例如通过docker exec进入容器执行命令。

这一点与 gVisor 的 Sentry 实现直接对应:Sentry 维护自己的 PID 表来表示沙箱内的进程(见 pkg/sentry/kernel 中的进程管理实现),这些并非真实的宿主机进程,沙箱内进程调用getpid(2)时,Sentry 直接用自己的 PID 表回答,不触发任何宿主机系统调用。

网络:自有协议栈,资源全部限定在沙箱内

沙箱会向系统挂接一个网络端点(endpoint),但网络协议栈是 gVisor 自己实现的(位于 pkg/tcpip,一个 Go 语言从零实现的网络栈)。除宿主机上"在途"(in flight)的数据包之外,所有网络资源只存在于沙箱内部,并受相应资源限制约束。

你可以像操作普通容器一样与沙箱暴露的网络端点交互,但网络内部的探查(introspection)同样需要进入沙箱才能进行。

文件:Gofer 通过 SCM_RIGHTS 传递文件描述符

沙箱内的文件可能由不同的后端实现支撑。对于宿主机原生文件(已有可用文件描述符的情况),Gofer(一个略高权限的伴生进程,负责代表 Sentry 访问宿主机文件系统)可以通过SCM_RIGHTS将文件描述符传递给 Sentry。

这一机制在源码中有清晰体现:

  • pkg/sentry/socket/control/control.go 中实现了SCMRights类型与NewSCMRightsPackRights等函数,专门处理通过 Unix socket 控制消息传递文件描述符的逻辑;
  • pkg/sentry/control/fs.go 中的FilePayload注释也明确指出其包含通过SCM_RIGHTS发送的文件描述符(例如传给 Gofer 的 socket FD)。

脚注补充:除非启用了宿主网络(host networking),Sentry 自身无法创建或打开宿主机文件描述符,它只能以这种方式从 Gofer 接收文件描述符。

这些文件可以通过标准系统调用读写,也可以映射进关联应用程序的地址空间。这意味着同一份宿主内存可以被多个沙箱共享(例如多个沙箱映射同一文件页)。需要说明的是,这种机制并不排除侧信道攻击的可能,相关讨论见 Security Model(安全模型)。

另外,有些文件系统只存在于沙箱上下文中。例如在很多情况下,tmpfs挂载会出现在/tmp/dev/shm,它直接从沙箱内存文件(见下文"内存"章节的 memfd)中分配内存。这类内存最终会以与宿主机原生文件类似的方式计入相关限制。

线程:goroutine 即绿色线程,宿主线程按需创建

Sentry 用goroutine来为每一个任务线程(task thread)建模。因此:

  • 每个任务线程都是一个轻量级绿色线程(green thread),不一定对应底层的一个宿主线程
  • 但是,应用程序的执行被建模为对 Sentry 的一次阻塞式系统调用。这意味着宿主机可能会创建额外的线程——取决于活跃应用线程的数量

在实际运行中,一个繁忙的应用会趋近于"活跃线程数 = 宿主线程数"的状态,此时宿主机能够对所有应用线程做出合理的调度决策。这种设计让宿主机调度器得以介入沙箱内的线程调度,实现了"沙箱线程随应用负载伸缩"的动态性。

时间:Sentry 自持时钟,空闲时进入"无 tick"模式

沙箱内的时间由 Sentry 通过自己的 vDSO 与时间保持(time-keeping)实现来提供,这与宿主机时间截然不同,且不与宿主机共享任何状态(尽管初始时会用宿主机时钟校准)。

  • 与运行在硬件上的内核类似,Sentry 会运行定时器来感知时间流逝——只不过这里是软件定时器。这些定时器负责:更新 vDSO、提供系统调用返回的时间、记录用于用量/限额跟踪的时间(例如RLIMIT_CPU)。
  • 所有应用线程都空闲时,Sentry 会禁用定时器,直到某个事件唤醒 Sentry 或某个应用线程为止——这与"无 tick 内核"(tickless kernel)类似。这使得空闲应用可以达到接近零的 CPU 占用

gVisor 的 vDSO 实现位于 vdso 目录(如 vdso_time.cc、cycle_clock.h),通过共享映射向应用提供无系统调用的时间读取路径。

内存:单一 memfd 支撑全部应用内存

Sentry 实现了自己的内存管理,包括:

  • 按需分页(demand-paging);
  • 针对无法原生使用的文件,维护Sentry 内部页缓存

关键设计是:一个单独的 memfd 支撑全部应用内存memfd_create(2)的封装位于 pkg/memutil/memfd_linux_unsafe.go,其CreateMemFD函数在实现中明确要求 Linux 3.17 及以上版本(否则报错 "memfd_create(2) is not implemented")。Sentry 内核启动时会创建该 memfd(见 pkg/sentry/kernel/kernel.go 中的相关调用),所有应用内存页都挂在这一个文件上。

地址空间:平台相关,必要时创建 stub 进程

地址空间的创建是平台相关的(例如 Systrap 平台与 KVM 平台的行为不同)。对某些平台,宿主机上可能会创建额外的"stub 进程"来支持额外的地址空间。这些 stub 进程同样受沙箱级别的各种限制约束(例如 PID 限制)。

物理内存:交给宿主机管理,Sentry 启发式批量映射

宿主机可以使用常规手段管理物理内存(例如跟踪工作集、在压力下回收与交换)。Sentry惰性地为应用填充宿主映射,并允许宿主机对这些区域按需分页——这对上述机制的正常运转至关重要。

为避免过高开销,Sentry不会逐页按需分页,而是基于启发式算法选择合适的内存区域批量处理。这带来一个权衡:

  • Sentry 无法轻易判断哪些页是活跃的、哪些不是;
  • 即使逐页触发 fault,宿主机也可能在 Sentry 不知情的情况下回收或交换某些页。

因此,沙箱内的内存使用统计(例如通过 proc 查看)只是近似值。Sentry 内部维护了内存使用的细分账目,可以收集准确信息,但只能通过一个相对昂贵的 API 调用实现;而且从安全角度讲,把宿主机管理内存的精确信息暴露给沙箱也被认为是不明智的。

madvise:释放内存归还宿主

当应用标记某段内存不再需要时(例如调用madvise),Sentry 会立即把这块内存释放回宿主机。这存在性能代价——在很多情况下,保留内存并用它满足其他请求反而更便宜。但立即释放让宿主机能更有效地复用资源、执行全局性的高效策略,这与"资源决策交给宿主机"的整体模型一脉相承。

资源限制与 cgroup:为什么 memory.stat 里 anon 很低?

所有 Sentry 线程和 Sentry 内存都受容器 cgroup约束。但这里有一个关键点:应用的内存使用不会表现为匿名内存(anon)用量,而是被计入那个memfd。也就是说:

  • 所有匿名内存(anon)对应的是Sentry 自身的用量;
  • 应用内存以memfd形式存在,宿主机对容器收取的内存费用按常规方式工作(即计为 file-backed/shmem)。

这带来一个容易困惑的后果:读取 gVisor 沙箱 cgroup 的memory.stat(或memory.current)时,由于应用内存由 memfd 支撑,内核会将其计为shmem而不是anon。因此:

  • 即使容器运行着内存饥渴的应用,memory.stat中的anon字段也会保持很低
  • shmem(或file,视内核版本而定)则反映应用真实内存用量的绝大部分。

如果你需要沙箱内部应用内存用量的明细,有两个途径:

  1. 查看memory.current获取总量;
  2. 使用Sentry 自身的记账runsc usage <container id>

runsc usage 命令实操

runsc usage是 runsc 提供的专门命令,其实现位于 runsc/cmd/usage.go。基本用法:

# 打印内存用量(按类别,以字节为单位) runsc usage <container id> # 枚举所有类别的完整用量明细 runsc usage --full <container id> # 通过既有的 usage FD 获取用量的子集 runsc usage --fd <container id>

从源码看,其输出通过 JSON 编码器美化后打印(usage子命令的 Synopsis 为 "Usage shows application memory usage across various categories in bytes."),--full标志用于枚举所有类别的用量,--fd则通过容器沙箱的 usage FD(cont.Sandbox.UsageFD())快速抓取 Mapped/Unknown/Total 三个数值。

cgroup 可以按常规方式监控标准信号:压力指示(pressure indicators)、阈值通知(threshold notifiers)等,也可以动态调整。值得注意的实现细节是:Sentry 自身可能会监听所在 cgroup 的压力信号,以便清理内部缓存(即内存压力驱动的缓存回收机制,从源码结构看这一行为与 pkg/sentry/kernel 中的内存管理逻辑相关)。

延伸阅读

  • 资源模型背后的安全设计动机,参见 Security Model(安全模型) 与 Introduction to gVisor security;
  • 系统调用与缺页拦截的平台实现(Systrap / KVM),参见 Platforms(平台);
  • gVisor 的网络栈(沙箱自有协议栈)源码位于 pkg/tcpip,Gofer 与文件系统后端相关逻辑可查阅 pkg/sentry/fsimpl 与 runsc/fsgofer;
  • runsc usage等命令的完整命令集可参考 runsc/cmd 目录与 runsc 用户指南。

【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 5:20:33

gRPC-Go 如何借助 HTTP CONNECT 代理转发流量?

gRPC-Go 如何借助 HTTP CONNECT 代理转发流量&#xff1f; 【免费下载链接】grpc-go The Go language implementation of gRPC. HTTP/2 based RPC 项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-go 当 gRPC-Go 客户端运行在受限网络中&#xff0c;出站流量必须…

作者头像 李华
网站建设 2026/9/13 5:20:31

2100柴油机曲轴系设计:材料选择与结构优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:10:27

No such file or directory 报错根源与排查:从GCC编译到跨平台脚本

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华