上一篇【第04篇】云原生是什么鬼——CNCF技术版图漫游指南
下一篇【第06篇】第一个K8s应用——3分钟部署你的Hello World
摘要
2020 年底,K8s 社区扔了颗"炸弹":宣布从 v1.20 开始弃用 dockershim,v1.24 正式移除。一时间,“K8s 不支持 Docker 了”"Docker 要凉了"的标题党满天飞。论坛上人心惶惶,很多刚学会 Docker 的同学吓得连夜发帖:“我刚学完 Docker 就要被淘汰了?”
别慌,我来告诉你真相:K8s 废弃的只是 dockershim(一个适配层),不是你手里的 Docker。你照样可以docker build构建镜像,Docker Hub 照样有上百万镜像,docker run照样好用。本文把这件事的来龙去脉讲清楚:K8s 为什么这么干、CRI 是什么标准、containerd 和 CRI-O 有什么区别、对你有什么实际影响——帮你把这块知识彻底吃透。
一、事件复盘——K8s 到底"干掉"了什么?
先把事实摆清楚。K8s 从 v1.24 起不再内置 dockershim 组件(一个让 K8s 能和 Docker 通信的适配层)。媒体和自媒体的标题是"K8s drops Docker support",听起来像是离婚声明。但事情的真相用一个比喻就懂了:
【dockershim 是什么——"翻译官"的比喻】 K8s 只会说"CRI 语言" Docker 只会说"自己的语言" ┌──────────────┐ ┌──────────────┐ │ kubelet │ │ Docker │ │ "CRI Request│ │ daemon │ │ Please" │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ 这俩语言不通,没法直接沟通 │ │ │ │ ┌───────────────┐ │ └────────►│ dockershim │◄─────────┘ │ (翻译官) │ │ │ │ CRI ←→ Docker│ │ API 互译 │ └───────────────┘ 问题是:这位"翻译官"住在 K8s 代码仓库里 ── 每次 K8s 发新版本,翻译官也得跟着更新 ── Docker 底层改了,翻译官也要改 ── 维护成本高,还容易出 bug要点:K8s 废弃 dockershim,就像公司把外包翻译辞退了——不是因为翻译不好,而是因为人家内部已经有了更直接的沟通方式。Docker 自己拆出了一个叫 containerd 的组件,这个组件原生就说 CRI 语言,根本不再需要翻译。
事件时间线
| 时间 | 事件 |
|---|---|
| 2016.12 | K8s 引入 CRI(Container Runtime Interface)标准,同时内置 dockershim 作为 Docker 的适配层 |
| 2017 | Docker 将 containerd 捐给 CNCF,成为独立项目 |
| 2017-2019 | CRI-O 和 containerd 逐渐成熟,开始原生支持 CRI |
| 2020.12 | K8s 宣布 dockershim 进入弃用倒计时 |
| 2021.04 | K8s v1.21 dockershim 开始输出弃用警告 |
| 2022.05 | K8s v1.24 dockershim 正式移除 |
| 至今 | containerd 和 CRI-O 成为两大主流 CRI 运行时 |
二、CRI是什么——K8s的"操作系统接口"
CRI(Container Runtime Interface)是 K8s 定义的一套标准接口,规定了kubelet 和容器运行时之间怎么通信。你可以把它理解成 K8s 世界的"POSIX 标准"——只要你的容器运行时实现了 CRI 接口,就能被 K8s 使用。
【CRI 标准架构】 ┌────────────────────────────────────────────────────┐ │ Kubernetes │ │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ kubelet │ │ │ │ (每个节点上的"工头",负责管理本节点的容器) │ │ │ └────────────────────┬───────────────────────────┘ │ │ │ gRPC (CRI 协议) │ │ ▼ │ │ ┌────────────────────────────────────────────────┐ │ │ │ CRI gRPC Server │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ RuntimeService │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ RunPodSandbox │ │ ◄── 创建Pod沙箱 │ │ │ │ │ │ CreateContainer │ │ ◄── 创建容器 │ │ │ │ │ │ StartContainer │ │ ◄── 启动容器 │ │ │ │ │ │ StopContainer │ │ ◄── 停止容器 │ │ │ │ │ │ ListContainers │ │ ◄── 列出容器 │ │ │ │ │ │ ContainerStatus │ │ ◄── 容器状态 │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └──────────────────────┘ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ ImageService │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ PullImage │ │ ◄── 拉取镜像 │ │ │ │ │ │ ListImages │ │ ◄── 列出镜像 │ │ │ │ │ │ RemoveImage │ │ ◄── 删除镜像 │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └──────────────────────┘ │ │ │ └────────────────────────────────────────────────┘ │ │ │ │ 任何实现了这两个 gRPC Service 的运行时 │ │ 都可以无缝接入 K8s │ └────────────────────────────────────────────────────┘要点:CRI 的核心价值是解耦。K8s 不关心你底层用的是 containerd 还是 CRI-O 还是什么新的运行时——只要你的 gRPC 接口符合 CRI 规范,kubelet 就能指挥你干活。就像 Linux 上可以跑 ext4、XFS、Btrfs 等多种文件系统一样,K8s 上也可以跑多种容器运行时。
CRI 定义的两种 gRPC Service
| Service | 职责 | 关键方法 |
|---|---|---|
| RuntimeService | 管理 Pod 和容器的生命周期 | RunPodSandbox, CreateContainer, StartContainer, StopContainer, RemoveContainer |
| ImageService | 管理容器镜像 | PullImage, ListImages, RemoveImage, ImageStatus |
kubelet 通过调用这两个 gRPC Service,完成 Pod 和容器的所有操作。当然,底层还要配合 CNI(Container Network Interface)管理网络,以及 CSI(Container Storage Interface)管理存储——这是 K8s 的"三驾马车"接口标准。
三、containerd vs CRI-O——两大运行时正面对比
K8s v1.24 之后,主流选择就是 containerd 和 CRI-O。它们都原生实现了 CRI 接口,都经过了大规模生产验证。
【containerd vs CRI-O 架构对比】 containerd CRI-O ┌─────────────────┐ ┌─────────────────┐ │ kubelet │ │ kubelet │ └────────┬────────┘ └────────┬────────┘ │ CRI │ CRI ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ containerd │ │ CRI-O │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │CRI Plugin │ │ │ │CRI Server │ │ │ └───────────┘ │ │ └───────────┘ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ Content │ │ │ │ Storage │ │ │ │ Store │ │ │ │ (镜像存储) │ │ │ └───────────┘ │ │ └───────────┘ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ Snapshot│ │ │ │ Container │ │ │ │ (文件系统)│ │ │ │ (容器管理) │ │ │ └───────────┘ │ │ └───────────┘ │ └────────┬────────┘ └────────┬────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ runc │ │ runc │ │ (OCI 运行时) │ │ (OCI 运行时) │ └─────────────────┘ └─────────────────┘ 来源:从 Docker 拆出 来源:Red Hat 主导 背景:K8s 社区推荐 背景:为 K8s 量身打造详细对比
| 对比维度 | containerd | CRI-O |
|---|---|---|
| 出身 | 从 Docker 中拆分出来的独立运行时 | Red Hat 主导,专为 K8s 设计的运行时 |
| 设计理念 | 通用容器运行时(不仅服务于 K8s) | 纯 K8s 运行时(只做 K8s 需要的事) |
| CRI 实现 | 内置 CRI 插件 | 原生 CRI 实现,更纯粹 |
| OCI 兼容 | ✅ 完全兼容 | ✅ 完全兼容 |
| 镜像管理 | 完整的镜像拉取/存储/管理 | 聚焦 K8s 镜像管理 |
| Docker 镜像兼容 | ✅ 完全兼容(本身就是 Docker 底层) | ✅ 兼容(都是 OCI 格式) |
| 社区维护 | CNCF 毕业项目,社区庞大 | CNCF 孵化项目,Red Hat 主要维护 |
| 默认使用场景 | K8s 官方推荐,kind/minikube 默认 | OpenShift 默认,Red Hat 生态 |
| 学习成本 | 低(Docker 用户几乎无感) | 中(概念更 K8s 原生) |
| 安全特性 | 支持 seccomp/AppArmor/SELinux | 原生集成 SELinux(Red Hat 强项) |
| 性能 | 优秀 | 优秀(略有差异化场景) |
| 兼容性 | Docker CLI 可直连(docker -H) | 有 crictl 工具 |
| ctl 工具 | ctr(低层)/nerdctl(Docker 兼容) | crictl(K8s 调试专用) |
要点:对于学 K8s 来说,选 containerd 还是 CRI-O 真不关键——两者都原生实现 CRI,对上层 K8s 完全透明。就像你不会关心你的 Linux 用 ext4 还是 XFS 文件系统一样。kind 默认用 containerd,minikube 两者都支持,n个发行版也各有所好——不管哪个,你的
kubectl命令都一样。
crictl——K8s 调试专用容器工具
Docker 被"干掉"以后,你没法在 K8s 节点上docker ps看容器了。这时你需要的是crictl——K8s 社区提供的 CRI 调试工具:
# crictl 常用命令(如果你不装,问题也不大)crictlps# 查看运行中的容器(docker ps 等效)crictl pods# 查看所有 Podcrictl images# 查看节点上的镜像crictl logs<container-id># 查看容器日志crictlexec-it<container-id>bash# 进入容器# 注意:crictl 需要配置 runtime-endpoint# containerd: unix:///var/run/containerd/containerd.sock# CRI-O: unix:///var/run/crio/crio.sock# 配置方法(以 containerd 为例):cat>/etc/crictl.yaml<<EOF runtime-endpoint: unix:///var/run/containerd/containerd.sock image-endpoint: unix:///var/run/containerd/containerd.sock timeout: 10 debug: false EOF四、对你有什么影响——答案:几乎没影响
这是很多人最关心的问题,我直接用表格列出来:
| 操作 | 影响? | 说明 |
|---|---|---|
docker build | ✅ 照常用 | Docker 构建镜像和 K8s 运行时选择无关,仍然是最主流的镜像构建方式 |
docker run本地测试 | ✅ 照常用 | 你还是可以在本地用 Docker 启动容器做开发测试 |
docker push推送镜像 | ✅ 照常用 | 你构建的镜像仍然推送到 Docker Hub 或其他 Registry,K8s 从那里拉取 |
| Dockerfile 写法 | ✅ 不用改 | 构建出来的镜像是 OCI 标准格式,containerd 和 CRI-O 都能拉取运行 |
| docker-compose | ✅ 照常用 | 本地开发环境依然可以用 compose 编排多容器 |
| Docker Desktop | ✅ 照常用 | Docker Desktop 内置的 K8s 已经切换为 containerd |
docker ps在 K8s 节点上 | ⚠️ 不灵了 | K8s 节点用了 containerd,需要用crictl ps或nerdctl ps |
| K8s YAML 写法 | ✅ 不改 | 容器镜像字段写镜像名就行,完全不变 |
【镜像构建 vs 容器运行的分离】 构建镜像(Development) 运行容器(Production) ┌─────────────────┐ ┌─────────────────┐ │ Docker CLI │ │ K8s (kubelet) │ │ docker build │ │ │ │ │ docker push │ │ │ CRI │ └────────┬────────┘ │ ▼ │ │ │ ┌───────────┐ │ │ push │ │containerd │ │ ▼ │ │ or CRI-O │ │ ┌─────────────────┐ │ └───────────┘ │ │ Repository │ pull └─────────────────┘ │ (Docker Hub / │◄───────────────── │ Harbor / ECR) │ └─────────────────┘ docker build 从来就不是 K8s 的一部分! 它是构建工具,不是运行工具。 就好比:你把菜做好(docker build),送进冰箱(Registry), K8s 只是负责从冰箱取菜的人——它不关心菜是谁做的。要点:Docker 是一个"全家桶"——它包含了 CLI、API、build、run、push、pull、compose……而 K8s 只需要其中的
run能力。K8s 废弃 dockershim,相当于说"我不要你全家桶了,我只要里面那个叫 containerd 的组件"。但你作为开发者,该用全家桶还接着用——构建和运行本来就可以分开。
五、K8s v1.24+如何检查你的容器运行时
如果你想确认自己 K8s 集群用的是什么容器运行时:
# 方法一:查看节点详情kubectl get nodes-owide# 最后一列 CONTAINER-RUNTIME 会显示# 方法二:查看节点详细信息kubectl describenode<node-name>|grep"Container Runtime Version"# Container Runtime Version: containerd://1.7.15# 方法三:直接进节点看# 如果是 kind 集群dockerexec<control-plane-container>crictlps# 如果是 minikubeminikubesshcrictlps# 查看 containerd 的 K8s 相关配置cat/etc/containerd/config.toml|grep-A10"plugins.'io.containerd.grpc.v1.cri'"nerdctl——Docker 用户最无感的 containerd 命令
如果你习惯了 Docker CLI 的用法,又想在只用 containerd 的环境中操作容器,nerdctl几乎完美替代 Docker CLI:
# nerdctl 安装# macOS:brewinstallnerdctl# Linux:wgethttps://github.com/containerd/nerdctl/releases/download/v1.7.6/nerdctl-1.7.6-linux-amd64.tar.gztarxzf nerdctl-*.tar.gz-C/usr/local/bin/# 用法几乎和 Docker CLI 一模一样nerdctl run-d--namenginx-p8080:80 nginx:alpine nerdctlpsnerdctl images nerdctl build-tmyapp.nerdctl compose up-d# 甚至支持 compose!要点:nerdctl 是由 containerd 社区维护的 Docker CLI 兼容工具。它的命令格式、参数名称、行为都与 Docker CLI 高度一致,甚至支持
nerdctl compose。如果你只是想在 containerd 环境里用熟悉的命令操作容器,装 nerdctl 就行了,几乎零学习成本。
六、容器运行时的演进路线——一张图看懂历史
【容器运行时演进史】 2013 2016-2017 2020-2022 现在 │ │ │ │ ▼ ▼ ▼ ▼ Docker 横空出世 K8s 推出 CRI 标准 dockershim 被废 三足鼎立 ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Docker │ │ dockershim│ │ dockershim│ │containerd│ │ 全家桶 │ │ 翻译官 │ │ ❌ 移除 │ │✅ 原生CRI │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │containerd│ │containerd│ │ CRI-O │ │ │ │ 新秀登场 │ │ 日渐成熟 │ │✅ 原生CRI │ │ │ └──────────┘ └──────────┘ └──────────┘ └────────┘ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ CRI-O │ │ CRI-O │ │ Docker │ ┌────────┐ │ Red Hat搞│ │ 用于OpenShift│ │(构建工具)│ │ rkt │ └──────────┘ └──────────┘ │ ✅ 继续用 │ │CoreOS搞 │ └──────────┘ └────────┘ ┌──────────┐ (已弃用) │ runc │ ┌──────────┐ ┌──────────┐ │ OCI标准 │ │ 所有运行 │ │ OCI 标准 │ │ 参考实现 │ │ 时底层 │ │ 统一天下 │ └──────────┘ │ 都是 runc│ └──────────┘ └──────────┘ 关键趋势:所有容器运行时底层都统一到 OCI 标准(runc/crun/youki) 上层通过 CRI 接口对接 K8s 中间层的选择(containerd 还是 CRI-O)对用户透明要点:容器运行时的演进趋势非常清晰——标准化是不可逆转的。OCI 定义了镜像和运行时的标准,CRI 定义了 K8s 和运行时的接口。只要你的镜像符合 OCI 标准,任何实现了 CRI 接口的运行时都能跑。这个设计让整个生态充满了可替换性——没有任何一个组件是不可替代的,包括 K8s 本身。
本篇小结
让我们把几个关键结论钉在墙上:
- K8s 废弃的不是 Docker,是 dockershim——一个为了兼容 Docker 内核 API 而存在的适配层。Docker 本身活得好好的,你照样用它 build/push/run。
- CRI 是一套标准接口——K8s 通过它告诉容器运行时"创建 Pod"“启动容器”“拉取镜像”。只要你的运行时实现了 CRI,就能被 K8s 用。
- containerd 和 CRI-O 是两大主流 CRI 运行时——containerd 出身于 Docker,更通用;CRI-O 由 Red Hat 主导,更聚焦 K8s。对学 K8s 来说,选哪个没区别。
- 对你的实际影响接近于零——docker build 照用,Dockerfile 不改,K8s YAML 不变。唯一的小变化是 K8s 节点上不能用
docker ps,用crictl替代就行。
下一篇,我们趁热打铁——刚才搭好的 kind 集群还热乎着吧?让我们一起把第一个 K8s 应用跑起来,从一句 YAML 到浏览器看到 Hello World,全程不超过 3 分钟。
上一篇【第04篇】云原生是什么鬼——CNCF技术版图漫游指南
下一篇【第06篇】第一个K8s应用——3分钟部署你的Hello World