Docker、docker-ce、containerd、CRI、CRI-O、shim 是啥关系?顺便把"以前那条链路"也讲透
聊容器运行时,最常见的困惑就是:
"我以前装个 Docker 就能跑容器,怎么现在 Kubernetes 又说 containerd,又说 CRI,还冒出个 shim?Docker 不是运行时吗?那 docker-ce 又是什么?CRI-O 又是哪路神仙?"
说白了,这不是谁取代谁,而是 K8s 把容器这摊事拆得更清楚了:上层只管提需求,中间用标准接口对接,底层专心把进程跑起来,再用 shim 把运行过程稳住。
你把"现在的链路"和"以前的链路"放一起看,就不会乱。
先把几条链路摆出来:一眼就懂它们在换什么
以前(K8s 早期常见,强绑定 Docker):
kubelet → dockershim → Docker Engine(dockerd) → containerd → runc → 容器进程
现在(K8s 主流之一:containerd 直接对接):
kubelet → CRI → containerd → shim → runc → 容器进程
现在(另一条主流:CRI-O 对接):
kubelet → CRI → CRI-O →(shim/OCI runtime)→ runc → 容器进程
看出来了吗?底层终点几乎都一样,都是 runc + 容器进程。变化主要在上层两件事:
-
以前 kubelet 需要靠 dockershim 去"翻译"Docker 的接口。
-
现在 kubelet 只认 CRI,至于底层是 containerd 还是 CRI-O,随你选(只要实现 CRI)。

Docker 和 docker-ce:一个是"概念/产品体系",一个是"你安装的发行版"
很多人把 Docker 当成一个单一组件,其实它更像一个产品集合:CLI、镜像构建、镜像分发、运行容器、网络、卷、日志、API......早期是一条龙,确实省心。
那 docker-ce 是什么?简单讲就是:
Docker 的社区版发行方式/安装包。
你在 Linux 上 yum install docker-ce / apt install docker-ce 装的那套,通常就包含:
-
dockerCLI(你敲命令用的) -
dockerd(Docker Engine) -
以及它依赖的一些底层组件(例如 containerd、runc 等)
所以 docker-ce 不是另一种运行时,而是 Docker 这个体系在社区版里怎么被打包交付。
Docker:以前像"一条龙",现在更像"上层工具箱"
Docker 最早为什么火?因为它把一堆事做成了一条龙:
-
build 镜像、pull/push 镜像
-
run 容器
-
网络、卷、日志、API 都一起包好
体验当然好,但问题也很现实:太大、太全、太像一个产品。
Kubernetes 需要的是一个更"专注跑容器"的底层能力,而不是一个把所有功能都打包的巨无霸。
所以今天很多场景里 Docker 的定位更像:
开发和运维常用的容器工具箱。
在集群里真正负责跑容器的核心,往往不是"Docker 的大引擎",而是它下面的运行时组件(containerd/runc)。
containerd:专职"管理容器生命周期"的运行时核心
containerd 可以理解成:专心把容器跑起来,别的尽量少管。它负责的都是硬活:
-
拉镜像、解压、管理 snapshot
-
创建/启动/停止/删除容器
-
调用更底层 runtime(比如 runc)把容器进程真正拉起来
-
管理容器 I/O、状态等
所以你现在看到 Kubernetes 默认倾向 containerd,并不奇怪:它更轻、更专注、更符合"运行时组件"该有的样子。
CRI:Kubernetes 的"标准插槽",不是运行时
CRI(Container Runtime Interface)最容易被误解成某个程序,其实它是接口标准。
kubelet 为什么要 CRI?一句话:不想跟某一个运行时绑死。
如果 kubelet 直接适配 Docker、适配 containerd、适配 CRI-O......那 kubelet 的代码得多乱?维护成本得多高?
所以 Kubernetes 的做法是:
我 kubelet 只会说 CRI 这门语言,你们运行时自己来实现。
于是现在链路变得很干净:
kubelet → CRI →(containerd / CRI-O / 其他 runtime)
CRI-O:为 Kubernetes 而生的运行时实现(更"纯粹")
CRI-O 你可以理解成:
专门为 Kubernetes 设计的 CRI 运行时。
它不像 Docker 那样做镜像构建体验,也不像 Docker 那样带一堆平台功能。CRI-O 的目标非常明确:
-
实现 CRI,让 kubelet 能用标准接口管理容器
-
遵循 OCI,调用 runc 等 runtime 拉起容器进程
-
尽量保持轻量、专注、贴合 K8s
"containerd 和 CRI-O 有啥区别?"
两者都能当 K8s 运行时,但 CRI-O 更像"为了 K8s 而定制的后端",而 containerd 更像"通用型运行时管理器"。
shim:为什么要有"垫片"?为了稳,不是为了多一层
shim 听起来像"中间商",但它解决的都是生产里非常实在的问题。
以 containerd 为例,一个容器通常会对应一个 shim(例如 containerd-shim-runc-v2)。它的价值主要在三点:
让容器不依赖 containerd 的生命周期
你真的希望 containerd 重启一下,所有容器跟着死吗?当然不希望。
shim 让容器进程可以继续跑,containerd 重启后再重新接管。
处理容器 I/O 和退出状态
stdout/stderr 谁接?容器退出后 exit code 谁收?状态怎么回报?
这些都需要一个"离容器很近、一直在线"的管家,shim 就干这个。
隔离底层 runtime 的细节
containerd 不必直接黏着 runc 的各种细节,通过 shim 隔离开,系统更稳,演进也更容易。
所以 shim 的存在,核心就是两个字:稳定。
以前那条链路:dockershim 到底是干嘛的?为什么后来不用了?
早期 Kubernetes 很依赖 Docker 生态,但 kubelet 需要一种方式驱动 Docker,于是有了 dockershim。
你可以把 dockershim 理解成:
kubelet 和 Docker Engine(dockerd) 之间的翻译官/胶水层。
它把 kubelet 的需求转成 Docker 能理解的调用方式,让 Kubernetes 能"用 Docker 跑 Pod"。
所以以前链路是:
kubelet → dockershim → dockerd → containerd → runc → 容器进程
有趣的是:很多人以为 Docker 直接跑容器,但 Docker Engine 下面早就有 containerd/runc。底层能力一直在那里,只是以前你不需要关心。
那为什么 dockershim 后来逐步退场?也很现实:
-
Kubernetes 不想和 Docker 的实现细节强绑定
-
运行时生态更丰富了(containerd、CRI-O 等)
-
需要统一标准(CRI)来降低维护成本、提升一致性
于是大家改走 CRI 标准化路线。
现在的链路:更标准、更可插拔,也更清爽
现在主流链路就是:
-
kubelet → CRI → containerd → shim → runc → 容器进程 -
或
kubelet → CRI → CRI-O →(shim/OCI runtime)→ runc → 容器进程
它带来的变化很直接:
-
kubelet 只对 CRI 负责,运行时更可插拔
-
containerd/CRI-O 专注运行时职责,边界更清晰
-
shim 把稳定性兜住,容器不怕 runtime 重启
-
分层更清楚,排障也更容易定位
总而言之:把它们当成"分工明确的一条流水线"
-
Docker:容器工具箱/平台体验(开发运维常用)
-
docker-ce:Docker 的社区版发行包,你装的 Docker 通常就是它
-
CRI:kubelet 和运行时之间的标准接口(kubelet 只认它)
-
containerd:通用型运行时管理器(K8s 常用)
-
CRI-O:为 Kubernetes 定制的 CRI 运行时实现(更专注、更原生)
-
shim:容器的管家/垫片,让容器更稳、更独立
-
最终都落到 runc + 容器进程:你的应用真正跑在这里
以前是 kubelet 通过 dockershim "绑"着 Docker 跑;现在是 kubelet 通过 CRI "插"上 containerd 或 CRI-O 跑。Docker/docker-ce 负责好用,containerd/CRI-O 负责执行,shim 负责稳定。