容器改变了我们部署软件的方式。但默认的容器运行时仍然与主机共享内核——这一事实自2017年以来记录的大多数严重容器逃逸事件的根源。gVisor和Kata Containers以不同方式解决这个问题,对于任何运行多租户工作负载或处理敏感数据的团队来说,理解这两种技术都至关重要。
本文将超越营销宣传。我们将研究每种技术实际如何工作,性能权衡是什么,以及它们在真实生产环境中的适用位置。
为什么标准容器不是隔离边界
使用默认runc运行时的Docker容器与其他容器及主机共享Linux内核。命名空间和cgroup创建了分离的假象,但它们是访问控制机制,而非真正的隔离。每个容器进程都直接向主机内核进行系统调用。
这一点很重要,因为Linux内核有巨大的攻击面。被攻破的容器可以利用内核漏洞(如CVE-2022-0492(cgroups v1逃逸)、CVE-2019-5736(runc覆盖)) potentially获得主机root权限或逃逸到相邻容器。
Seccomp配置文件、AppArmor和SELinux策略有助于减少这种攻击面。但它们需要精心维护,经常破坏合法工作负载,并且没有从根本上改变架构:内核代码仍然被信任能够自我保护。
核心问题不在于容器配置错误。而在于共享内核模型要求内核必须完美。但它从来都不是完美的。
更强隔离的两种方法
两种架构从根本上解决了这个问题:
- 用户空间内核拦截(gVisor):拦截来自容器的系统调用,并在用户空间中使用基于Go的精简内核实现来处理它们。
- 硬件虚拟化(Kata Containers):在每个轻量级虚拟机内运行容器,因此每个容器都有自己的内核,而VM硬件边界强制执行隔离。
这些是根本不同的权衡,选择它们取决于工作负载特性,而非哪一个普遍更好。
gVisor:用Go实现的用户空间内核
gVisor(Google于2018年开源)在Go中实现了Linux兼容的系统调用接口,称为Sentry。当容器进程调用read()、connect()或open()时,调用会发送到Sentry而非主机内核。Sentry实现了足够的Linux API来运行大多数容器化工作负载,同时只将一小部分批准的操作传递给真正的内核。
该架构由两个组件组成:
- Sentry: 用户空间内核。处理几乎所有系统调用。使用 Go 编写,极少使用 unsafe 包。以非特权进程运行。
- Gofer: 一个独立进程,代表 Sentry 处理文件系统操作。以有限能力运行并通过 9P 协议通信。
gVisor 通过 OCI 运行时规范与容器运行时集成。要在 Docker 或 containerd 中使用它:
# 安装 runsc (gVisor 运行时二进制文件)
wget https://storage.googleapis.com/gvisor/releases/release/latest/x86_64/runsc
chmod +x runsc && sudo mv runsc /usr/local/bin/
# 配置 Docker 守护进程以使用 runsc
# /etc/docker/daemon.json
{
"runtimes": {
"runsc": {
"path": "/usr/local/bin/runsc"
}
}
}
# 使用 gVisor 运行容器
docker run --runtime=runsc -it ubuntu:22.04 bash
# 验证:在容器内部,内核版本与主机不同
uname -r
# 4.4.0 (gVisor 模拟的内核版本,不是您主机的)
在 Kubernetes 中,您使用 RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
name: secure-workload
spec:
runtimeClassName: gvisor
containers:
- name: app
image: myapp:latest
gVisor 性能特征
系统调用拦截模型有可测量的开销。Google 和独立研究人员发布的基准测试显示:
- 系统调用密集型工作负载(许多小型文件操作,频繁的网络调用):比原生慢 2-5 倍
- CPU 密集型工作负载(数值计算,内存处理):接近原生性能
- 内存分配:由于 Sentry 的内存管理,延迟略高
- 启动时间:比 runc 略高,但比完整虚拟机低得多
对于处理数据库调用的 HTTP 请求的 Web 服务器等工作负载,在实际测量中开销通常为 10-20%,而不是微基准测试中看到的最坏情况数字。
Kata Containers:像容器一样运行的虚拟机
Kata Containers 使用硬件虚拟化。每个容器(或 Kubernetes 中的 pod)在专用的轻量级虚拟机内运行。虚拟机有自己的内核。即使攻击逃逸出容器,仍然需要逃逸出虚拟机——而虚拟机逃逸漏洞很少见,经过严格审计,并且不会与其他容器共享。
架构涉及:
- kata-runtime: OCI 兼容的运行时,为每个容器启动一个虚拟机
- kata-agent: 虚拟机内部处理容器生命周期的进程
- 管理程序: 可以使用 QEMU、Cloud Hypervisor 或 Firecracker 作为后端
Firecracker 后端(也由 AWS 开源)是最具生产环境成熟度的选择。Firecracker 是与 AWS Lambda 和 AWS Fargate 相同的虚拟机监控程序(VM)。它可以在不到 125 毫秒的时间内启动一个微虚拟机(microVM),并采用最小化的设备模型。
# 在 Ubuntu 22.04 上安装 Kata Containers
bash -c "$(curl -fsSL https://raw.githubusercontent.com/kata-containers/kata-containers/main/utils/kata-manager.sh) install-packages"
# 验证安装
kata-runtime check
# 配置 containerd 使用 Kata 和 Firecracker
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-fc]
runtime_type = "io.containerd.kata-fc.v2"
# Kata 的 Kubernetes RuntimeClass
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata-containers
handler: kata-fc
---
# 应用于 pod
spec:
runtimeClassName: kata-containers
Kata 性能特征
Kata 的开销配置与 gVisor 不同:
- 启动延迟: 100-500 毫秒,取决于虚拟机监控程序和内核大小(gVisor 更快)
- 稳态性能: 对于大多数工作负载接近原生性能,因为客户机内核是真实的 Linux 内核
- 内存开销: 每个 VM 需要自己的内核内存(使用精简内核时每个容器约 64-128MB)
- 文件系统操作: 由于 virtio-fs 开销而稍慢,但比早期 Kata 版本好得多
对于在 gVisor 中表现不佳的系统调用密集型工作负载,Kata 通常能提供更好的性能,因为它使用真实内核而非用户空间模拟。
直接对比:gVisor vs Kata Containers
| 因素 | gVisor (runsc) | Kata Containers |
|---|---|---|
| 隔离机制 | 系统调用拦截、用户空间内核 | 硬件 VM 边界 |
| 内核共享 | 否(拥有 Go 内核) | 否(每个 VM 拥有自己的内核) |
| 系统调用密集型性能 | 性能下降(2x-5x) | 接近原生 |
| 启动延迟 | 低(与 runc 相当) | 中等(100-500ms) |
| 内存开销 | 低 | 中等(约 64-128MB/VM) |
| 内核 CVE 保护 | 强(Go 内核,小攻击面) | 强(VM 边界) |
| 兼容性 | 约 90% 的工作负载 | 约 98% 的工作负载 |
| 最适合 | 不受信任的代码、多租户 SaaS | 高性能隔离、受监管的工作负载 |
实际部署模式
多租户 SaaS:使用 gVisor
如果您运行客户代码 — CI/CD 管道、无服务器函数、用户上传的脚本 — gVisor 是正确的选择。它的低开销使其即使在密集部署中也是可行的。Google Cloud Run 对所有容器工作负载使用 gVisor。当频繁扩展到零再扩展回来时,相比 Kata,gVisor 的启动延迟优势明显。
受监管行业工作负载:使用 Kata
医疗、金融和政府工作负载,需要遵守 HIPAA、PCI-DSS 或 FedRAMP 等合规标准,受益于 Kata 的虚拟机级别隔离。审计人员了解虚拟机。隔离模型清晰且可验证。当工作负载的性能需求排除了 gVisor 的系统调用开销时,Kata 为您提供隔离而不牺牲吞吐量。
纵深防御:与 seccomp 结合使用两者
这两种解决方案都不能替代良好的安全卫生习惯。即使使用 gVisor 或 Kata,也应应用 seccomp 配置文件,禁用 Linux 能力,并以非 root 用户运行容器。这些层成本很低,并且能以乘法方式增加攻击者的难度。
# 应用于 gVisor 容器的 Web 服务器的最小 seccomp 配置文件
# 这限制了即使是 Sentry 能传递到主机内核的内容
docker run --runtime=runsc
--security-opt seccomp=/etc/docker/seccomp-profiles/web-server.json
--user 1001:1001
--cap-drop ALL
--cap-add NET_BIND_SERVICE
myapp:latest
部署前您应该了解的限制
gVisor 兼容性差距
gVisor 并不实现所有的 Linux 系统调用。常见的不兼容包括:
- 以不寻常方式大量使用
/proc的工作负载 - 使用
io_uring的应用程序(支持已添加,但截至 2025 年仍不完整) - FUSE 挂载
- 某些 eBPF 操作(它们本身是内核接口)
在生产环境中使用 gVisor 前进行测试是不可协商的。在 runsc 中运行完整的集成测试套件,并在部署前观察失败情况。
Kata 在大规模部署时的开销
在大规模部署中,Kata 的每个虚拟机内存开销会累积。一个运行 200 个容器的节点,仅用于 128MB 大小的虚拟机内核就需要额外的约 25GB RAM。精简内核镜像(kata-containers 提供预构建的精简内核),并为每个 pod 设置明确的内存限制,以保持可管理性。
入门:实用检查清单
- 识别处理不受信任输入、多租户数据或敏感监管数据的工作负载
- 首先使用 gVisor 测试这些工作负载 — 它能覆盖约 90% 的场景且开销更低
- 对于无法通过 gVisor 兼容性测试或需要接近原生系统调用性能的工作负载,评估 Kata 与 Firecracker 的组合
- 为每个安全级别定义 Kubernetes RuntimeClasses:默认(runc)、隔离(gvisor)、高度隔离(kata)
- 添加命名空间级别的准入策略(OPA Gatekeeper 或 Kyverno)以强制敏感命名空间使用 RuntimeClass
- 对您特定的负载进行基准测试 — 通用基准测试很少能反映您的实际系统调用模式
结论
标准容器适用于可信、已充分理解的工作负载。对于任何运行来自外部来源的代码、处理敏感数据或面临合规要求的工作负载,共享内核模型是一种架构风险,仅靠 seccomp 无法充分解决。
gVisor 和 Kata Containers 是 Google、AWS 以及受监管行业团队使用的生产就绪解决方案。它们并非新奇或实验性技术。通过 Kubernetes RuntimeClasses 部署它们的工具已经成熟且文档完善。大多数团队唯一面临的障碍是测试工作负载兼容性和调整资源规划所需的时间。
从您最敏感的工作负载开始,运行兼容性测试,并根据实际流量模式进行基准测试。隔离改进是显著的;权衡是可控的。
