用户态内核沙箱容器运行时,为不可信工作负载提供第二层隔离
gVisor 是 Google 开源的容器运行时沙箱。它的做法和 runc 完全不同:runc 让容器进程直接跑在宿主机内核上,靠 namespace 和 cgroup 做隔离;gVisor 则在应用和宿主机内核之间插进一个用 Go 写的用户态内核(Sentry),应用的所有系统调用先被 Sentry 拦截和实现,只有少数必要操作才转发给真正的内核。
这样做的直接收益是:容器逃逸的攻击面从「整个 Linux 内核」缩小到「Sentry 这个用户态进程 + 少量转发接口」。代价同样直接:Sentry 需要自己实现一遍 Linux 系统调用,兼容性必然有缺口,而且每次 syscall 都要多走一层,性能有损耗。
它服务于一个非常具体的场景:你要跑的是不完全信任的代码——多租户 SaaS 的客户代码、CI 里执行用户提交的构建脚本、在线判题、插件市场里的第三方插件。这些场景下 runc 的隔离强度不够,gVisor 才划算。
gVisor 的「平台(platform)」指它拦截系统调用的机制,直接决定性能与硬件要求:
systrap 通过 seccomp 的 SECCOMP_RET_TRAP 让内核把 SIGSYS 交给 gVisor
2023 年年中起成为默认平台;在虚拟机里跑通常是最优选择
KVM Sentry 同时充当 guest OS 与 VMM,利用硬件虚拟化
只在裸金属上才能发挥全部性能;需要 CPU 虚拟化扩展与 /dev/kvm
ptrace 用 PTRACE_SYSEMU 拦截,上下文切换开销大
官方已标注不再支持,并预告未来会移除
除非有明确理由,直接使用默认的 systrap。
gVisor 以 runsc 二进制发布,同时提供 containerd 的 shim containerd-shim-runsc-v1:
# 官方 apt 源安装(推荐)
sudo apt-get update && sudo apt-get install -y runsc
# 从 release 包安装时,runsc 与 containerd-shim-runsc-v1 必须放在一起
# gvisor-bin/ 目录要与 runsc 相邻,移动时三个一起移
runsc --version
在 containerd 里注册一个 runtime handler:
# containerd 2.x(config version 3)的插件 ID 已经变了
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
# containerd 1.x 的写法(旧教程里常见,2.x 上不生效)
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
# 改完重启 containerd
sudo systemctl restart containerd
然后创建 RuntimeClass 并在 Pod 上引用:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
apiVersion: v1
kind: Pod
metadata:
name: sandboxed
spec:
runtimeClassName: gvisor
containers:
- name: app
image: nginx:alpine
# 确认 RuntimeClass 已经存在并指向正确的 handler
kubectl get runtimeclass
kubectl get runtimeclass gvisor -o yaml
# 确认 Pod 真的跑在沙箱里:容器内执行 dmesg 会看到 gVisor 的启动信息
kubectl exec sandboxed -- dmesg | head -3
kubectl exec sandboxed -- uname -a
托管方案:GKE Sandbox 直接提供同一套能力,节点池级别开启即可,不需要自己维护 runsc。
两者常被放在一起比较,但原理完全不同:
维度 gVisor Kata Containers
隔离机制 用户态内核拦截 syscall 每个 Pod 一个真正的轻量虚拟机
硬件要求 无(纯软件) 需要嵌套虚拟化或裸金属 + /dev/kvm
启动速度 快,与普通容器接近 慢,需要启动 guest 内核
兼容性 系统调用需要被 Sentry 实现 与宿主机内核一致,兼容性更好
内存开销 中等(Sentry 常驻) 较高(每个 Pod 一份 guest 内核)
简单说:gVisor 用兼容性换隔离,不需要虚拟化;Kata 用资源开销换隔离,需要虚拟化。两者可以同时存在于一个集群,靠不同的 RuntimeClass 区分。
gVisor 的问题几乎都出在「启动没问题、跑起来才炸」。上线前按这份清单过一遍:
# 1) 完整跑一遍业务回归,不要只验证容器能起来
# 重点覆盖:启动参数解析、文件读写、子进程创建、信号处理、优雅退出
# 2) 检查是否依赖下列能力(任一命中就要仔细评估)
/proc 与 /sys 的具体字段 gVisor 实现的是子集
mount / 文件系统操作 块设备文件系统不支持
iptables / nftables 规则 只有部分支持
io_uring 默认关闭
自定义设备文件(/dev 下非 GPU/TPU) 基本不支持
在容器里跑虚拟机(KVM) 明确不支持
# 3) 用 strace 观察是否存在大量 EPERM/ENOSYS
kubectl exec sandboxed -- strace -f -e trace=all -p 1 2>&1 | grep -E 'ENOSYS|EPERM'
# 4) 压测对比:同一服务在 runc 与 gvisor 下的 P50 / P99 / CPU 使用率
# 以压测结果决定是否接受,而不是相信任何外部基准
平台选择
裸金属 + CPU 虚拟化可用 → KVM 通常最快
虚拟机里的节点 → systrap(嵌套虚拟化开销大于拦截开销)
不确定 → 用默认 systrap,它是 2023 年以来的默认值
负载类型与损耗
syscall 密集(大量小文件读写、频繁网络往返) 损耗最明显
计算密集(syscall 少) 影响较小
启动阶段 影响小,问题多出在运行期
调整平台用运行时的配置项(在 containerd 的 runtime 配置里给 runsc 加参数):
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc.options]
TypeUrl = "io.containerd.runsc.v1.options"
# 平台、网络实现、缓冲区大小等都在这里配
ENOSYS。上线前必须跑完整的业务回归,而不是只看容器起没起来。limits.cpu 做精确控制的场景要重新评估。ext3、ext4、fat32 在 gVisor 内核里没有原生实现,必须在宿主机挂载好再把目录暴露进去。想在沙箱里跑 mount 的镜像构建类工作负载会直接失败。iptables 只有部分支持(目标是让 Docker-in-gVisor 能用,不保证更多);nftables 同样受限;io_uring 默认关闭,打开后也只支持基础 I/O。容器里跑 VPN、代理、需要精细防火墙规则的软件要慎用。/dev 下专有设备的应用(FPGA、加密卡、串口设备)不要指望 gVisor。runsc --platform=ptrace 的写法要清理掉,它开销大且官方已预告移除,不要在新部署里使用。kubectl exec 进去看到的 /proc、/sys 是 Sentry 实现的版本,内容与真实节点不同;perf、bpftrace、eBPF 类可观测工具在沙箱里取不到数据;strace 看到的 syscall 序列也可能与真实内核不一致。排障时要意识到「你看到的内核不是真的内核」。ps 在节点上看到的是 runsc 的进程树而不是应用的完整进程树,依赖节点侧进程信息的安全 agent(如某些运行时检测工具)会失效或误报。kubectl — Kubernetes集群管理工具kata-containers — 基于虚拟化的另一种强隔离运行时containerd — 注册 runsc runtime handler 的地方crictl — 直接调试 CRI 层面的容器securitycontext — 容器权限与安全字段pod-security-admission — 沙箱工作负载的准入控制confidential-containers — 在 Kata 之上叠加内存加密的方案ebpf-observability — 沙箱内不可用的观测手段