笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 接入与通信

    • 游戏基础架构与工具
    • 接入网关(长连接接入层)
    • 消息总线(共享内存 IPC)
    • 帧同步(Lockstep)
    • 全栈极限低延迟(物理层→应用层)
    • KCP 与 QUIC
  • 网络与服务网格

    • CNI 与 K8s 网络插件
    • 容器运行时:Docker 与 containerd
    • Istio 与 Cilium 服务网格
    • 服务网格:中心化 vs 去中心化
    • eBPF 原理与在网络/可观测/安全的落地
    • 自研 Mesh 服务网格 × K8s 部署
    • 自研 Mesh × K8s · 数据面深水区
    • Tco 协程框架 · 运行时深水区
    • K8s 异构 & 网络插件
  • 有状态服务

    • 分布式游戏存储(分布式 KV)
    • 有状态服务的数据迁移
    • 有状态服务的数据恢复与容灾
    • 有状态服务的 K8s 编排层治理(防漂移 + 节点崩溃恢复)
    • 内存配置热刷新与更新机制
  • 算法与协程

    • 一致性哈希四算法实现(Ring Hash / Ketama / Maglev / Jump Hash)
    • 蓄水池抽样(Reservoir Sampling)与加权扩展
    • C++ 协程:有栈 vs 无栈 vs C++20 原生(函数染色与 libco)
    • Raft 与 Gossip:CP 强一致共识 vs AP 最终一致传播
  • 流量与承载

    • 游戏秒杀场景承载
    • 令牌桶与漏桶
    • 限流与熔断
    • 限流:互联网 vs 游戏
    • 灰度发布 · Canary / BlueGreen / A-B / Shadow
    • 服务器日常系统开发与维护 SOP
    • 极限发压工具设计(多长连接 + 发压任务)
    • 性能分析与优化方法论(六条排查线)
  • 编译与排查

    • 编译优化与 LLVM/Clang/GCC
    • Sanitizer 工具链与内存泄漏定位

容器运行时:Docker 与 containerd

Docker vs containerd · CRI / OCI 两套标准 · dockershim 移除 · runc/gVisor/Kata 隔离谱系

一句话结论

K8s 只认 CRI,Docker 那套 CLI/构建/API 对它是冗余;1.24 删掉 dockershim 直连 containerd,镜像格式(OCI)不变、构建与运行彻底解耦。

场景问题

面试高频题:「K8s 1.24 之后不支持 Docker 了,我的镜像还能用吗?」——一句话答案是镜像照用,变的只是运行时那一层。但要讲清楚为什么,得先分清一堆容易混的名词:Docker、dockerd、containerd、containerd-shim、runc、CRI、OCI。

问题的根子是分层与标准化:容器技术早期 Docker 一家全包(构建镜像、拉镜像、管网络卷、起容器、CLI、daemon 全都在 dockerd 里)。K8s 要调度容器,早期只能通过一个叫 dockershim 的适配层去喊 Docker。可 Docker 这一大坨里,K8s 真正需要的只有"按 OCI 规范把一个容器跑起来"这一小块,其余构建/CLI/网络对 kubelet 全是冗余负担,还得 K8s 自己维护 shim 去适配 Docker 的私有 API。于是社区把职责逐层拆开、定标准,最后 kubelet 直连更轻的 containerd,把 Docker 从生产运行链路里请了出去。

打个比方:Docker 早期像一家「前店后厂」的餐馆——点菜(CLI)、收银(API)、备料(构建镜像)、后厨炒菜(跑容器)全捏在一个老板手里。K8s 这个大客户其实只要「后厨炒菜」这一项服务,却被迫走前台点菜的流程,还得雇个翻译(dockershim)把自己的话转成这家店的黑话。后来行业出了统一菜谱格式(OCI 镜像/运行时规范)和统一点单协议(CRI),K8s 就能绕开前台,直接对着标准化的后厨(containerd → runc)下单。类比失效边界:请走的是「前台点菜那套 Docker 专属流程」,不是「后厨」——containerd 本就是 Docker 自己拆出来捐给 CNCF 的后厨,runc 也还是原来那口锅;菜谱(镜像)是 OCI 标准格式,换不换前台都能照做。所以"不支持 Docker"≠"容器换了内核",只是 kubelet 不再绕 Docker 的前台而已;你本地用 docker build 打的镜像,集群照样跑。

实现方案

调用链:从 kubelet 到容器进程

一个容器被拉起来,是一条逐层下探、职责递减的调用链:

  • kubelet:K8s 节点代理,通过 CRI(gRPC)向运行时下"起 Pod/起容器/拉镜像"的指令。
  • containerd:高层运行时(daemon),管镜像拉取、快照(rootfs)、容器生命周期,暴露 CRI 服务(cri 插件)。CRI-O 是同层的另一选择。
  • containerd-shim:每个容器一个 shim 进程,作为容器的"父进程守护"——即使 containerd 重启,容器也不受影响(解耦 daemon 与容器生命周期)。
  • runc:低层运行时(OCI runtime),真正调用内核的 namespace + cgroup + rootfs pivot 把进程"关进"容器,然后自己退出(容器进程由 shim 托管)。

Docker 的 dockerd 如今只是 containerd 之上再包一层:加了构建(BuildKit)、Compose、Swarm、docker CLI、镜像管理 API。这些对开发本地很方便,但对 kubelet 是纯冗余。

两套标准:CRI 管上、OCI 管下

容器生态能"换零件不换整机",靠两套正交的标准把接口钉死:

标准全称谁和谁之间约定什么
CRIContainer Runtime Interfacekubelet ↔ 高层运行时gRPC 接口:RunPodSandbox / CreateContainer / PullImage… K8s 只认这个
OCI runtime-specOpen Container Initiative高层运行时 ↔ 低层运行时一个 rootfs + config.json,runc/crun/kata-runtime 都按它跑
OCI image-specOpen Container Initiative镜像的存储/分发格式分层 tar + manifest + config,docker build 产物即 OCI 镜像

一句话:CRI 让 K8s 不绑定某个运行时,OCI 让镜像和低层运行时可互换。 正因为镜像是 OCI 标准格式,"去 Docker"才不影响镜像可用性。

K8s 1.24 移除 dockershim:来龙去脉

  • 为什么删:dockershim 是 kubelet 里专门适配 Docker 私有 API 的一段代码,由 K8s 团队维护。Docker 本身不实现 CRI,而 containerd/CRI-O 原生支持 CRI,留着 shim 等于为一个"外壳"长期背维护包袱。1.20 起标记弃用,1.24 正式移除。
  • 影响面(关键结论):
    • 镜像不受影响:OCI 镜像格式不变,docker build 的镜像照常运行、docker push 照常分发。
    • 构建与运行解耦:本地开发照用 Docker/BuildKit 构建;生产集群节点的运行时换成 containerd 或 CRI-O。
    • 要改的是节点运维:排障命令从 docker ps 换成 crictl ps;日志/监控里对 dockershim 的假设要清理。
    • Docker 镜像仓库/Registry 不受影响:那是分发层,与运行时无关。
  • 一句避坑:面试别答"镜像不能用了"——变的是运行时组件,不是镜像格式。

为什么这么做

  • 分层解耦:把"构建 / 分发 / 运行"拆成独立组件,各自演进。kubelet 只依赖 CRI,运行时可插拔。
  • containerd/shim 解耦生命周期:shim 让容器脱离 daemon 独立存活,containerd 升级/重启不杀容器——这是生产稳定性的关键设计。
  • 去掉冗余:生产节点不需要 Docker 的 CLI/构建/Swarm,containerd 更轻、攻击面更小、资源占用更低。

镜像分层与 snapshotter

  • 镜像 = 只读层叠加:每条 Dockerfile 指令生成一层(OCI layer,本质是 tar diff),运行时用 overlayfs 把多只读层 + 一层可写层联合挂载成容器 rootfs。
  • snapshotter:containerd 用 snapshotter 插件管理这套层叠快照,默认 overlayfs,也有 native/btrfs/stargz(懒加载、边拉边跑,加速冷启动)。
  • 相同底层被多镜像共享,省磁盘省拉取——这也是"改一行代码只重传顶层"的原因(层缓存)。

runc / gVisor / Kata:隔离谱系

低层运行时按隔离强度 × 性能开销排成一条谱系,通过 K8s 的 RuntimeClass 为不同 Pod 选择:

运行时隔离方式隔离强度开销适用
runc共享宿主内核 + namespace/cgroup弱(共享内核,一个内核漏洞全穿)近乎零默认,可信负载
gVisor用户态内核(Sentry 拦截 syscall)中(syscall 不直达宿主内核)syscall 变慢多租户、跑不可信代码
Kata轻量虚拟机(每容器一个微型 VM)强(独立内核 + 硬件虚拟化)内存/启动开销强隔离、混部不可信租户

与 k8s-network 里提到的"多 Runtime + RuntimeClass"是同一件事:网络侧关注选哪个 CNI,运行时侧关注选哪种隔离。两者由 RuntimeClass/CNI 各管一段,互不重复。

为什么别的选择不行

常见误区

说法 / 做法为什么不对
"K8s 1.24 后 Docker 镜像不能用了"混淆运行时与镜像格式。镜像是 OCI 标准,照用;变的只是节点运行时组件
生产节点继续留着完整 DockerCLI/构建/Swarm 对 kubelet 是冗余,徒增资源占用与攻击面;containerd 更轻
不可信代码直接用 runc 跑runc 共享宿主内核,一个内核提权漏洞即逃逸;应上 gVisor/Kata
用 docker ps 排查 K8s 容器节点已是 containerd,Docker 看不到 CRI 容器,要用 crictl
  • 仍用 Docker 作 K8s 运行时(Mirantis cri-dockerd):技术上可行(把 shim 外置成独立组件),但等于自己扛维护,收益是零——不如直接上 containerd。
  • CRI-O 替代 containerd:CRI-O 更"K8s 专用"(只做 CRI,不做通用容器管理),OpenShift 默认;containerd 生态更广、也能被 Docker/nerdctl 复用。两者都对,按发行版/生态选。

排障:从 docker 到 crictl / nerdctl

  • crictl:面向 CRI 的排障工具,直接问 kubelet 用的运行时。crictl ps / crictl images / crictl logs <id> / crictl inspect——K8s 节点上取代 docker ps。
  • nerdctl:containerd 的 Docker 兼容 CLI(命令几乎和 docker 一样,还支持 BuildKit、Compose、懒加载镜像),适合在 containerd 节点上"像用 Docker 一样"操作。
  • ctr:containerd 自带的底层调试 CLI(按 namespace,如 k8s.io),偏底层、不适合日常。
  • 一句话:K8s 容器用 crictl,想要 Docker 手感用 nerdctl,底层 debug 才用 ctr。

沉淀结论

结论

  • 调用链:kubelet →(CRI)→ containerd →(shim)→ runc →(namespace/cgroup)→ 容器进程;越往下职责越少。
  • 两标准:CRI 管 kubelet↔运行时,OCI 管运行时↔内核与镜像格式。K8s 只认 CRI,镜像认 OCI。
  • 1.24 删 dockershim:去掉 K8s 自维护的 Docker 适配层,直连 containerd/CRI-O;镜像格式不变、构建与运行解耦,改的是节点运行时与排障命令。
  • 隔离谱系:runc(共享内核·快)→ gVisor(用户态内核)→ Kata(轻量 VM·强隔离),按 RuntimeClass 选。

相关专题:K8s 异构 & 网络 · CNI 网络插件 · 秒杀场景承载(网络相关内容见前两篇,本篇不复述)

记忆口诀

  • 调用链:kubelet →CRI→ containerd →shim→ runc →内核(namespace/cgroup)
  • 两标准:CRI 管上(kubelet↔运行时)/ OCI 管下(运行时↔内核 + 镜像格式)
  • 删 shim:Docker 不实现 CRI / shim 是 K8s 包袱 / 1.24 移除 / 镜像照用不变
  • 隔离谱系:runc 共享内核快 / gVisor 用户态内核 / Kata 轻量 VM 最强
  • 排障:K8s 用 crictl / Docker 手感用 nerdctl / 底层 debug 用 ctr

内容来源

综合整理。参考方向:OCI runtime-spec / image-spec,Kubernetes CRI 设计文档与 dockershim 移除公告(1.20 弃用、1.24 移除),containerd / CRI-O / runc / gVisor / Kata Containers 官方文档,overlayfs 与 containerd snapshotter 文档,crictl/nerdctl/ctr 使用文档。

扩展阅读

  • server-daily-ops —— 容器运行时排障作为日常 SOP 的排障入口:本篇讲运行时原理(CRI/OCI 分层、containerd 调用链、隔离谱系),server-daily-ops 讲日常巡检和值班响应中的排障动作。

自测:合上资料能说清楚吗?

  1. K8s 1.24 移除 dockershim 后,我用 docker build 打的镜像还能在集群跑吗?为什么?
参考答案

能。变的是运行时组件(kubelet 从经 dockershim 调 Docker,改为经 CRI 直连 containerd/CRI-O),不是镜像格式。镜像是 OCI 标准格式,docker build/docker push 产物照常运行与分发。要改的是节点运行时与排障命令(docker ps→crictl ps),镜像仓库不受影响。

  1. 讲清 kubelet 到容器进程的调用链,以及 CRI 和 OCI 各管哪一段。
参考答案

kubelet →(CRI gRPC)→ containerd →(每容器一个 containerd-shim)→ runc →(namespace/cgroup/rootfs)→ 容器进程。CRI 约定 kubelet↔高层运行时的接口(K8s 只认它,故可换 containerd/CRI-O);OCI 约定高层↔低层运行时(runtime-spec,runc/crun/kata 通用)以及镜像格式(image-spec)。shim 让容器脱离 daemon 独立存活。

  1. dockershim 为什么被删?留着 Docker 当运行时有什么坏处?
参考答案

dockershim 是 kubelet 里适配 Docker 私有 API 的一段代码,由 K8s 团队维护;Docker 不实现 CRI,而 containerd/CRI-O 原生支持 CRI,留着 shim 等于为一个"外壳"长期背维护包袱(1.20 弃用、1.24 移除)。生产节点留完整 Docker,其 CLI/构建/Swarm 对 kubelet 全是冗余,徒增资源与攻击面;containerd 更轻。

  1. runc、gVisor、Kata 的隔离方式和取舍是什么?跑不可信代码该选哪个?
参考答案

runc 共享宿主内核 + namespace/cgroup,开销近零但隔离最弱(内核漏洞即逃逸)。gVisor 用用户态内核(Sentry)拦截 syscall,不直达宿主内核,隔离更强但 syscall 变慢。Kata 每容器一个轻量 VM(独立内核 + 硬件虚拟化),隔离最强、有内存/启动开销。跑不可信代码/多租户选 gVisor 或 Kata,通过 RuntimeClass 指定。

  1. 在 containerd 节点上排查一个跑挂的 K8s 容器,你用什么命令?为什么不是 docker?
参考答案

用 crictl(面向 CRI):crictl ps 看容器、crictl logs <id> 看日志、crictl inspect 看详情。节点运行时是 containerd,docker 命令看不到 CRI 管理的容器。想要 Docker 手感可用 nerdctl(containerd 的 Docker 兼容 CLI),底层调试才用 ctr(注意 -n k8s.io namespace)。

最近更新: 2026/9/10 11:38
Prev
CNI 与 K8s 网络插件
Next
Istio 与 Cilium 服务网格