别再傻傻分不清了!Containerd 和 Docker 到底有啥区别?命令、架构全拆解
前阵子有个刚入行的小伙伴跑来问我:“哥,我听说现在 Kubernetes 都不用 Docker 了,改用 Containerd,那是不是以后 Docker 就废了?我学 Docker 还有意义吗?”
我当时差点一口老血喷出来——这问题问得,就跟“微信被抖音干掉了,我还用不用装微信”一样离谱。其实啊,Docker 没死,只是它的一部分功能被“剥离”出去了,而那个被剥离出来的核心组件,就是 Containerd。
今天咱们就掰开揉碎了讲讲:Containerd 和 Docker 到底有啥区别?从命令行操作到底层架构,一条条给你捋清楚。这篇文章不灌鸡汤,全是我在生产环境里踩过的坑、调过的参数、看过的日志堆出来的干货。
先说个前提:Docker 其实是个“全家桶”。你平时用 docker run、docker build,背后其实是一整套工具链在干活。这套工具链里,真正负责和 Linux 内核打交道、创建容器、管理镜像的,并不是 Docker 本身,而是它内部的一个叫 containerd 的守护进程。
你可以把 Docker 想象成一个“外卖平台”——你点个“宫保鸡丁”,平台接单(Docker CLI),然后派单给后厨(containerd),后厨炒菜(拉镜像、启容器),最后骑手送餐(网络、存储等)。而 Containerd 呢,就是那个后厨,只管做菜,不管接单也不管送餐。
所以,Kubernetes 把 Docker “踢出局”,其实是说:“我不需要你这个外卖平台了,我自己会接单(Kubelet 直接调 containerd),你把后厨交出来就行。”于是 Docker 把 containerd 开源给了 CNCF(云原生基金会),现在它是个独立项目,谁都能用。
那从命令行角度看,差别大吗?
如果你习惯了 docker ps、docker images、docker exec,那你直接切到 containerd 上,会有点懵——因为 containerd 根本没有这些命令!
Containerd 提供的是 ctr 和 crictl 两个命令行工具。注意,这两个不是一回事:
- ctr 是 containerd 自带的调试工具,功能齐全但不稳定,官方明确说“别在生产环境用”。
- crictl 是 Kubernetes CRI(容器运行时接口)标准下的命令行工具,专为 K8s 设计,推荐在集群节点上使用。
举个例子:
你想看当前有哪些容器在跑:
# Docker 方式
docker ps
# ctr 方式(不推荐生产用)
ctr containers list
# crictl 方式(K8s 节点推荐)
crictl ps再比如拉个镜像:
# Docker
docker pull nginx
# ctr
ctr image pull docker.io/library/nginx:latest
# crictl(注意:crictl 不能直接 pull,得通过 pod sandbox 或 runtime 接口间接拉)
# 所以通常 crictl 不用来拉镜像,而是配合 kubelet 自动拉看到没?ctr 的命令更底层,参数也更啰嗦。而且 ctr 拉下来的镜像,默认命名空间是 default,而 K8s 用的是 k8s.io。如果你用 ctr 拉了个镜像,K8s 可能根本看不到——因为不在同一个命名空间里!
这就导致一个经典翻车现场:运维同学在 K8s 节点上用 ctr pull 了一个镜像,然后纳闷“为啥 Pod 还在 ImagePullBackOff”?结果一查,镜像在 default 命名空间,而 kubelet 在 k8s.io 里找,当然找不到。
正确的做法是:
ctr -n k8s.io image pull nginx:latest加个 -n k8s.io 指定命名空间,这样 kubelet 才能认。
再说架构差异,这才是重点。
Docker 的架构是这样的(简化版):
用户 → Docker CLI → Docker Daemon (dockerd) → containerd → runc → Linux Kernel其中:
- Docker CLI:你敲的 docker 命令
- Docker Daemon (dockerd):后台服务,负责解析命令、管理镜像、网络、卷等
- containerd:负责容器生命周期管理(创建、启动、停止等)
- runc:OCI 标准的容器运行时,真正调用 clone()、mount() 等系统调用创建容器
而纯 Containerd 的架构(比如在 K8s 中)是:
Kubelet → CRI Plugin → containerd → runc → Linux Kernel这里少了 Docker Daemon 这一层。Kubelet 通过 CRI(Container Runtime Interface)直接和 containerd 对话。containerd 内置了 CRI 插件(以前是单独的 cri-containerd,现在合并了),所以不需要 dockerd。
这意味着什么?
- 更轻量:少了 dockerd,内存占用更低,启动更快。我们线上一个节点,切换到 containerd 后,系统负载降了 10% 左右。
- 更稳定:dockerd 曾经出过不少 bug,比如镜像清理异常、网络配置冲突。containerd 架构简单,故障面小。
- 更标准:CRI 是 Kubernetes 定义的标准接口,任何符合 CRI 的运行时(比如 containerd、CRI-O)都能无缝接入 K8s。而 Docker 并不原生支持 CRI,得靠 dockershim(一个适配层),这个 shim 在 K8s 1.20 之后就被废弃了。
不过,Docker 也不是一无是处。它的最大优势是开发体验好。本地开发时,docker build、docker-compose 一套下来丝滑流畅。而 containerd 没有 build 功能——它只能运行容器,不能构建镜像。
所以现实中的分工往往是:
- 开发阶段:用 Docker 构建镜像、本地测试
- 生产部署:用 containerd(或 CRI-O)跑在 K8s 上
两者根本不冲突,反而互补。
说到这儿,可能有人要问:“那我能不能在生产环境继续用 Docker?”
技术上当然可以。K8s 1.24 之前都支持 dockershim,就算现在,也有第三方维护的 shim 可以用。但我们团队早就切到 containerd 了,原因很简单:少一层抽象,少一个故障点。
记得去年有一次,dockerd 卡死,导致整个节点上的容器无法更新,Pod 重启失败。排查半天才发现是 dockerd 的镜像元数据锁住了。换成 containerd 后,这种“中间商赚差价”导致的问题基本消失了。
另外,containerd 的配置也更透明。它的配置文件是 /etc/containerd/config.toml,所有参数一目了然。而 Docker 的配置散落在 daemon.json、systemd unit、甚至环境变量里,调起来头疼。
比如你想改镜像仓库地址(mirror),Docker 要改 /etc/docker/daemon.json:
{
"registry-mirrors": ["https://xxx.mirror.aliyuncs.com"]
}而 containerd 要在 config.toml 里找到 [plugins."io.containerd.grpc.v1.cri".registry] 这一段,加上:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://xxx.mirror.aliyuncs.com"]虽然写法麻烦点,但结构清晰,不容易出错。
再聊聊镜像存储。
Docker 默认用 overlay2 存储驱动,镜像存在 /var/lib/docker 下,目录结构复杂,手动清理容易误删。
Containerd 用的是 content store + snapshotter 架构。镜像内容(layers)存在 content store(通常是 /var/lib/containerd/io.containerd.content.v1.content),快照(容器可写层)由 snapshotter 管理(比如 overlayfs)。
好处是:镜像和容器的存储完全解耦,支持多 snapshotter(overlayfs、btrfs、zfs 等),扩展性更强。
不过对运维来说,最直观的感受是:containerd 的磁盘占用更可控。我们曾经遇到 Docker 节点磁盘爆满,查了半天发现是 /var/lib/docker/containers 下的日志文件没轮转。而 containerd 默认不接管日志,日志由 kubelet 或 systemd 管理,更容易统一处理。
最后说点实用技巧。
如果你在 K8s 节点上想调试容器,推荐用 crictl:
# 查看所有 Pod
crictl pods
# 查看容器
crictl ps -a
# 进入容器(类似 docker exec)
crictl exec -it <container_id> sh
# 查看容器日志
crictl logs <container_id>但要注意,crictl 的 container_id 是长 ID,不像 docker ps 显示短 ID。可以用 crictl ps | grep your-app 找。
另外,containerd 支持插件机制。比如你可以替换默认的 runc,换成 kata-runtime 实现安全容器;或者换 snapshotter 为 devmapper 做块设备隔离。这种灵活性是 Docker 很难做到的。
总结一下:
- Docker 是“全家桶”,适合开发和单机部署;Containerd 是“核心引擎”,适合生产环境尤其是 K8s。
- 命令上,Docker 有 docker CLI,Containerd 用 ctr/crictl,后者更底层但更贴近 K8s。
- 架构上,Docker 多了一层 dockerd,Containerd 直接对接 CRI,更轻量、更标准。
- 两者不是替代关系,而是分工协作:Docker 构建,Containerd 运行。
别再纠结“该学哪个”了——都得会。开发用 Docker,运维搞 K8s 就绕不开 Containerd。理解它们的关系,才能在容器世界里游刃有余。
如果你觉得这篇文章帮你理清了思路,不妨点个赞、转发给还在混淆这两者的同事。容器化这条路,踩坑不可怕,可怕的是不知道坑在哪。
关注我,带你少走弯路,多填干货。
公众号:运维躬行录
个人博客:躬行笔记