CRI 为什么出现,shim 又抓着什么
一句话总结
CRI 是“Kubernetes 与运行时对话的标准语法”,shim 则是“即使 containerd 死亡, 仍能维持容器存活的轻量进程”。两者都是为消除适配器而制定规范的产物。
为什么需要它
没有 CRI 的时代,Kubernetes 能使用的运行时只有 Docker。为了与 Docker Engine 对话, Kubernetes 在自己的代码中携带了适配器,这就是 dockershim。
这段代码在当时是正确的,因为没有它,早期采用就不可能发生。问题在运行时数量增加后显现: 每增加一种运行时,都要向 Kubernetes 本体再加入一个适配器,维护负担就会无限堆积在 Kubernetes 一侧。
于是顺序被反转:要移除适配器,必须先制定规范。项目定义了名为 CRI(Container Runtime Interface)的 gRPC 接口,让运行时一侧实现该接口。dockershim 已在Kubernetes v1.24 中移除, 官方 FAQ 也说明这段代码最初就被设计为临时方案。它的位置由 containerd 和 CRI-O 接替。
这里必须消除一个考试中常见的误解:移除 dockershim 不代表“不能使用 Docker 镜像”。
镜像格式是 OCI Image Spec,所以用 docker build 制作的镜像仍能在所有 CRI 实现上运行。
消失的不是镜像,而是 kubelet 内部的适配器代码。
它如何运作
CRI 提供的两项服务
- RuntimeService——Pod sandbox 与容器的生命周期。
RunPodSandbox,StopPodSandbox,CreateContainer,StartContainer,StopContainer,ListContainers,ContainerStatus,ExecSync,Exec,Attach,PortForward - ImageService——镜像。
PullImage,ListImages,ImageStatus,RemoveImage,ImageFsInfo
一个 Pod 启动时实际发生的顺序
- kubelet 调用
RunPodSandbox - containerd 创建 pause 容器——它是 Pod 网络命名空间的所有者
- CNI 插件被调用,为该命名空间分配 IP
- kubelet 依次调用
PullImage→CreateContainer→StartContainer - containerd 通过 shim 运行容器
为什么需要 pause 容器是 KCNA 的常考内容。Pod 内的容器要共享 IP 与端口空间,
就必须有人先创建网络命名空间,并一直持有到最后。即使应用容器因重启而全部消失,
命名空间也必须继续存在,IP 才不会改变。pause 只用 pause() 系统调用无限等待,
所以仅使用约 1MB 内存;它还作为 PID 命名空间的 init,负责回收僵尸进程。
shim——把 containerd 重启与容器寿命解耦的装置
containerd --> containerd-shim-runc-v2 --> runc --> 컨테이너 프로세스
shim 有四项职责。
- 即使 containerd 重启,也维持容器运行
- 管理容器的 stdin/stdout/stderr
- 收集退出码
- 报告 OOM 事件
容器进程的父进程不是 containerd,而是 shim。shim 会把自己守护进程化, 在与 containerd 不同的会话中运行。因此重启 containerd,容器进程的 PID 也不会变化。 因为“容器异常”而重启 containerd,大多数时候什么也修不好——重启的只是管理平面, 真正维持容器的是 shim。
shim 可以按容器类型替换:containerd-shim-runc-v2(runc)、containerd-shim-kata-v2(轻量 VM)、
containerd-shim-runsc-v1(gVisor)、containerd-shim-wasm(WebAssembly)。RuntimeClass 对象
把这种选择公开为 Pod 级别的配置。
在实际工作中会遇到的情况
作者重建家庭实验室集群时,把 CNI 从 Calico 换成了 Cilium 1.20.1,并且完全没有安装 kube-proxy
(--skip-phases=addon/kube-proxy)。随后不是靠声称,而是通过测量确认它真的正常工作:
**kube-proxy Pod 为 0,节点上的 iptables KUBE- 链为 0。**取而代之的是 eBPF map 中直接存在
10.96.0.1:443/TCP → 10.0.0.120:6443/TCP 这样的映射。
这里,层级讨论变成了现实:更换 CNI 与更换 CRI 是不同层级的工作。 CNI 只负责第 3 步(为命名空间分配 IP),创建容器的第 1、4、5 步保持不变。 因此即使完整替换 CNI,kubelet 与 containerd 之间的对话也一个字都不会改变。
还有一点。正因为层级分离,才会出现只有 exec 无法使用的故障。exec/attach/port-forward
并不直接通过 CRI gRPC 传输;containerd 返回 streaming URL 后,客户端会使用该 URL 重新连接节点。
因此可能出现 Pod 运行完全正常,只有 kubectl exec 超时的情况。
下一次测验要确认什么
本模块以测验结束。从下一模块开始,将连接真实集群,亲自观察目前讨论的各个组成部分 如何呈现为 API 对象。