13 容器运行时:CRI、Kata Containers 与 gVisor 📅 2026/7/17 2:26:30 系列专栏《深入剖析 Kubernetes》· 基于张磊极客时间专栏思想整理 · 适合 CSDN 发布13 容器运行时CRI、Kata Containers 与 gVisor前面我们讲了 Pod、Deployment、DaemonSet 这些调度层概念。但有一个根本问题一直悬而未决kubelet 收到创建一个容器的指令后到底是谁真正把那个进程拉起来的这就是**容器运行时Container Runtime**的领域。本讲我们下沉到节点最底部看清 CRI 这套接口如何把 K8s 和各种运行时解耦以及为什么在强隔离场景下我们需要 Kata Containers 和 gVisor。一、容器运行时的两层架构容器运行时不是一个单一程序而是分层的┌──────────────────────────────────────────────┐ │ 高层运行时 High-level Runtime │ │ docker(旧) / containerd / CRI-O │ │ 职责镜像管理、解包、调用低层运行时、 │ │ 对接 CRI、管理容器生命周期 │ └───────────────┬──────────────────────────────┘ │ (调用 runc 等) ┌───────────────┴──────────────────────────────┐ │ 低层运行时 Low-level Runtime │ │ runc / runsc(gVisor) / kata-runtime │ │ 职责真正按 OCI spec 创建 │ │ namespace cgroups把进程跑起来 │ └───────────────────────────────────────────────┘高层运行时High-level Runtime面向 K8s负责拉镜像、解包、和管理容器的整个生命周期。代表有containerdDocker 剥离出来的核心、CRI-O专为 K8s 打造Red Hat 主导。低层运行时Low-level Runtime面向内核严格遵循OCI Runtime Spec由runc定义的事实标准负责调用clone()、unshare()、写 cgroup最终把容器进程创建出来。代表是runc默认、runscgVisor、kata-runtimeKata。二者之间靠OCI Runtime Spec这个标准化接口衔接——这正是解耦思想的第一层体现。二、CRI让 K8s 不再绑定某一种运行时早期 K8s 只能对接 Docker代码里写死了调用docker命令行。这显然不灵活K8s 想支持更多运行时就得改自己代码。于是 K8s 在 1.5 引入了CRIContainer Runtime Interface容器运行时接口——一套gRPC 定义的、与具体运行时无关的抽象接口。kubelet │ │ CRI (gRPC: 通用接口) │ ├─ RunPodSandbox / StopPodSandbox │ ├─ CreateContainer / StartContainer │ └─ PullImage / ListImages ... ▼ ┌───────────────┐ │ CRI 插件/垫片 │ (containerd 内置 / CRI-O / 旧 dockershim) └──────┬────────┘ │ OCI Runtime Spec ▼ runc / runsc / kata-runtimeCRI 的核心方法概念级不必死记RunPodSandbox/StopPodSandboxPod 在 CRI 语境下叫 “sandbox”其实对应的是为 Pod 创建那个共享的 pause 容器 / network namespace。CreateContainer/StartContainerPodSandbox内真正创建并启动业务容器。PullImage/RemoveImage镜像管理。只要一个运行时实现了这套 gRPC 接口kubelet 就能用--container-runtime-endpoint指向它无需 K8s 源码有任何改动。这就是 CRI 的价值K8s 只认接口不认实现。dockershim 的废弃一次去耦的必然很多人疑惑“我明明装的是 DockerK8s 怎么用的是 containerd” 历史是这样的早期 K8s 为了支持 Docker在代码里内置了dockershim一个把 CRI 调用翻译成 Docker API 的垫片。这层垫片既丑又重且 Docker 本身并不实现 CRI。K8s 在1.20 宣布废弃 dockershim并在1.24 正式移除。移除后K8s 直接与实现了 CRI 的containerd或CRI-O对话Docker 不再是 K8s 的必选项。注意你用docker build打出来的镜像依然能在 containerd 上跑——因为镜像格式是 OCI 标准构建工具与运行时是解耦的。多数人感觉没变化只是因为 Docker 桌面版默认把 containerd 也带上了。三、为什么 runc 还不够共享内核的风险默认的runc跑出来的容器如第 1 讲所述只是被 namespace 和 cgroups 隔离的进程——它们共享同一个宿主机内核。这对大多数场景够用但在以下场景存在隐患安全多租户云厂商把不同客户的容器放在同一台物理机上一旦某个客户利用内核漏洞如脏牛 Dirty COW、脏管道 Dirty Pipe逃逸就能打到宿主机和其他客户的容器。不可信负载运行用户上传的代码、第三方镜像。强合规金融、政务要求硬隔离。namespace/cgroups 是逻辑隔离不是物理隔离。要更强的隔离就得引入虚拟化或用户态内核。于是 Kata Containers 和 gVisor 登场。四、Kata Containers给每个容器一台轻量虚拟机Kata Containers 的思路非常直白给每个 Pod 起一个极轻量的虚拟机MicroVM容器进程跑在 VM 里的独立内核中而不是宿主机内核。宿主机内核 │ QEMU / Firecracker (轻量 VMM) ▼ ┌──────── VM (microVM) ────────┐ │ guest 内核 (独立, 真隔离) │ │ ├─ 容器进程 A │ │ └─ 容器进程 B (同 Pod) │ └──────────────────────────────┘基于QEMU或更轻的FirecrackerAWS 开源启动快百毫秒级、内存开销小。容器仍能拿到看起来像普通容器的接口对上层完全透明——K8s 通过 CRI 调用 kata-runtime 即可业务无感知。隔离强度 虚拟机级即使容器逃逸也只是逃到自己的 guest 内核碰不到宿主机。代价每个 Pod 多一份内核内存约几十 MB、启动稍慢、某些依赖宿主内核特性的功能受限如 eBPF 在 guest 内。典型场景公有云 Serverless 容器、多租户 CaaS 平台。五、gVisor用用户态内核拦截系统调用gVisorGoogle 开源走了另一条路它不跑完整 VM而是在用户态实现一个假内核Sentry拦截容器进程发出的所有syscall在用户态模拟内核行为。宿主机内核 ▲ 只暴露极少量 syscall经 Go 实现的 runsc 代理 │ ┌──┴── 用户态 ──────────────┐ │ Sentry (gVisor 内核模拟) │ │ 接收 syscall → 模拟返回 │ │ └─ 容器内应用进程 │ └────────────────────────────┘低层运行时叫runscruntime for gVisor Sandbox Container。容器进程表面上觉得自己调了open()/read()其实这些调用被 Sentry 拦截、在用户态处理绝大多数根本不会真正进入宿主机内核。攻击面被压缩到 runsc 这一薄层。优点比 VM 更轻、启动更快、跨平台不依赖硬件虚拟化。缺点系统调用兼容性不是 100%个别依赖特殊内核特性的程序如某些高性能网络栈、特定 ioctl可能跑不了。典型场景运行不可信代码的沙箱、CI 构建隔离、多租户轻量隔离。六、三者选型对照运行时隔离机制隔离强度开销适用场景runcnamespace cgroups进程级共享内核最低绝大多数可信业务Kata轻量 VM独立内核虚拟机级中多份内核强隔离、多租户、合规gVisor用户态内核拦截 syscall接近 VM 级低-中不可信代码沙箱、轻量多租户在 K8s 中还可以用RuntimeClass让同一个集群混合使用多种运行时# 1) 集群管理员定义 runtimeClassapiVersion:node.k8s.io/v1kind:RuntimeClassmetadata:{name:kata}handler:kata# 对应 containerd 里配置的 kata runtime---# 2) 业务 Pod 指定用 kata 隔离apiVersion:v1kind:Podmetadata:{name:tenant-a}spec:runtimeClassName:kata# 该 Pod 走 Kata 强隔离containers:-name:appimage:myrepo/untrusted-user-code:v1spec.runtimeClassName让调度与隔离策略解耦——默认用 runc敏感租户 Pod 标kata实现一套集群、分级隔离。小结 / 核心要点运行时分层高层containerd/CRI-O管镜像与生命周期低层runc/runsc/kata-runtime按 OCI spec 真正创建进程二者靠 OCI Runtime Spec 解耦。CRI 是 K8s 与运行时的契约一套 gRPC 接口让 kubelet 只认接口、不绑实现Pod 在 CRI 里对应 “sandbox”底层是共享的 pause/network namespace。dockershim 已废弃1.24 移除K8s 直连 containerd/CRI-ODocker 仍是可用的构建工具但不再是 K8s 运行时必选项。镜像格式是 OCI 标准构建与运行天然解耦。runc 共享内核 逻辑隔离多租户/不可信负载场景需更强隔离。Kata 轻量 VM独立内核真隔离、gVisor 用户态内核拦截 syscall轻量隔离用RuntimeClass可在同一集群按 Pod 灵活选择隔离级别。至此我们从一个被隔离的进程出发一路走到了节点上真正拉起容器的运行时——整个容器技术栈的自底向上脉络已经清晰。后续内容将更多聚焦于 K8s 的编排与控制循环本身。