Containerd与Docker深度对比:架构、命令、性能与迁移实践
2026/9/9 9:18:06 网站建设 项目流程

Containerd vs Docker:详细对比

容器圈这几年有个很有意思的现象:Docker 几乎成了“容器”的代名词,可真正跑到生产环境里去看,Kubernetes 节点上跑的全是 containerd,很多新同学第一次看到ctr命令都愣半天。再加上现在新装 Docker Desktop 的机器,底层引擎也已经换成 containerd 了,不少老手都开始犯嘀咕:这俩到底啥关系?我写 Dockerfile 是不是白学了?生产到底该用哪个?

这篇文章不绕弯子,直接用我实际折腾过的经验,把 containerd 和 Docker 的底层架构、命令差异、性能对比、迁移避坑一次讲透。适合刚接触容器的小白,也适合准备把节点从 Docker 迁移到 containerd 的运维同学。

1. 整体设计与思路拆解:先搞清楚它们的位置

1.1 谁才是真正的“容器运行时”

我先把最关键的一句话放前面:containerd 是 Docker 的一部分,但 Docker 不只是 containerd。

用大白话说,containerd 负责最核心的“跑容器”这件事,比如拉镜像、解压镜像、启动容器进程、管理容器生命周期。而 Docker 在 containerd 之上叠加了一整套开发者工具链:镜像构建、Dockerfile 解析、网络组网、数据卷管理、Docker Compose 编排、命令行工具等等。

所以你可以这么理解:Docker 是一个“全家桶”,containerd 是这桶里负责装液体的那个瓶子。你把全家桶换成别的牌子,瓶子仍然是那个瓶子。

从架构图上看,Docker 的完整调用链是这样的:

  • Docker CLI(客户端)→ Docker daemon(守护进程)→ containerd → runc → 内核容器能力

其中 dockerd 负责解析用户指令、构建镜像、管理网络卷,containerd 负责真正的容器生命周期管理,runc 是一个更底层的工具,负责直接跟内核交互创建和运行容器进程。runc 我就不展开说了,你只需要知道它是最底层的“发动机”。Docker 默认用 runc,containerd 默认也用 runc,所以最终跑容器这件事,大家用的都是同一套底层机制

1.2 为什么 Kubernetes 选择了 containerd

聊到生产环境,Kubernetes 的运行时选择就很有代表性了。早期 Kubernetes 节点上是跑 Docker 的,但 dockerd 作为中间层,kubelet 需要通过一个叫 dockershim 的适配器去访问 Docker API,链路长、问题多。后来 Kubernetes 社区干脆把 containerd 作为一级运行时直接对接,不走 dockershim。

结果就是:生产集群越来越多人直接用 containerd,省掉了 dockerd 这一整层。containerd 本身实现了 Kubernetes 的 CRI(Container Runtime Interface)规范,kubelet 直接调用 containerd 的接口就能创建、调度容器,不需要 Docker 在上面再翻译一遍。

这里我要插一句踩过坑的总结:很多从 Docker 时代过来的运维,天然以为“Kubernetes 必须装 Docker”,其实不是。从 Kubernetes 1.24 开始,dockershim 已经被正式移除了,官方推荐的就是 containerd。如果你现在还要在节点上装 Docker 再给 Kubernetes 用,反而会平白多一层维护成本。

1.3 用生活化类比理解两者的核心关系

为了照顾刚入门的朋友,我再打一个特别接地气的比方:

  • Docker 像一个餐厅,负责菜谱(Dockerfile)、点菜(CLI 命令)、传菜(镜像分发)、装修(网络、存储)一系列事。
  • containerd 像是后厨的灶台,只管把菜炒出来(把容器跑起来),至于你用什么盘子、怎么摆盘,它不操心。

你如果只想在后厨干活,灶台就够了;如果你要开一家餐厅,那就得装一整套东西。

这个类比方便记,但真正的选型判断,还是得看你的使用场景是“开发调试”还是“生产运行”。

2. 核心细节解析与实操要点:命令差异与镜像管理

2.1 同一套镜像,两种管理方式

我刚从 Docker 切到 containerd 时,最不适应的就是命令体系。Docker 有一套docker命令,containerd 有两套:ctr(原生命令)和nerdctl(兼容 Docker 的命令行工具)。我先讲最原生的ctr,因为生产排查问题时这套命令最常出现。

先说镜像管理。Docker 拉镜像用docker pull,containerd 原生用ctr images pull。后者必须要带上完整的镜像地址,比如:

# Docker 写法 docker pull nginx:latest # ctr 写法,必须带仓库地址 ctr images pull docker.io/library/nginx:latest

这里有个很坑的细节:ctr默认不带docker.io/library/这种默认前缀,你必须把地址写全,否则它会去解析一个不存在的短地址,报错信息还不明确,新人经常卡在这。

加载本地镜像的差异就更大了。docker load能直接加载docker save导出的 tar 包,但ctr images import的兼容性就麻烦很多。Docker 的镜像归档格式是 Docker 特有的一层层打包格式,containerd 的默认镜像格式(OCI 格式)跟它不是一回事。所以你在一台机器上用docker save导出镜像,拿到只有 containerd 的机器上,用ctr images import有时候能导入但没有 tag,有时候会直接报错,非常抓狂。

解决办法是用nerdctl

nerdctl load -i myimage.tar

nerdctl在命令设计上刻意对齐了 Docker CLI,包括nerdctl pullnerdctl runnerdctl exec这些命令,nerdctl load对 Docker 的镜像归档兼容性也更好。所以我个人的习惯是:日常调试用 nerdctl,排查底层问题再用 ctr

2.2 容器生命周期管理的命令对比

跑一个容器,两边命令差别也很大:

# Docker docker run -d --name web -p 8080:80 nginx:latest # ctr ctr run --net-host --detach docker.io/library/nginx:latest web # nerdctl(最接近 Docker 体验) nerdctl run -d --name web -p 8080:80 nginx:latest

看到没,ctr run的参数是很“原始”的,端口映射没有-p,要配置网络得用--net-host走宿主机网络,或者提前创建 CNI 网络再传参。而nerdctl的体验和 Docker 几乎一样,端口映射直接用-p就行。

这里我一定要提醒一个容易翻车的点:ctr run的默认命名空间是空的,跟 Docker 的命名空间不一样。你用ctr run起的容器,在nerdctl ps里可能看不到;反过来,nerdctl创建容器后,ctr containers list也未必列得出来。原因就是命名空间不同。排查“容器怎么不见了”这类问题,第一件事先检查命名空间:

# 查看默认命名空间下的容器 ctr containers list # 查看所有命名空间 ctr namespace list # 指定 k8s.io 命名空间查看 ctr -n k8s.io containers list

Kubernetes 节点上 containerd 的容器一般都在k8s.io这个命名空间里,你要直接看节点上 kubelet 拉起的容器,必须加-n k8s.io,否则啥也看不见。这个坑我至少见十个同事踩过。

2.3 镜像仓库配置的差异

镜像加速这块也是重灾区。Docker 的加速配置写在/etc/docker/daemon.json里,格式是registry-mirrors

{ "registry-mirrors": ["https://your-mirror.example.com"] }

containerd 的配置也是 TOML 格式,路径是/etc/containerd/config.toml,在[plugins."io.containerd.grpc.v1.cri".registry]段下配置mirrors

[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://your-mirror.example.com"]

注意看,containerd 的配置里一定要明确是给docker.io这个镜源配镜像,不配的话默认直连 Docker Hub。实际生产里,国内云厂商都有自己的镜像加速器地址,在 containerd 里配置的方式和 Docker 不太一样,格式容易写错。

我的建议是:修改配置之前先备份,改完一定要重启 containerd 服务,然后拉一次镜像验证。配置文件语法错了不会立刻报错,但拉镜像时才会曝出奇怪的问题,很难定位。

2.4 镜像构建:containerd 的短板

如果你以为有了 containerd 就能完全替代 Docker,那你试一次镜像构建就会碰壁。containerd 本身没有镜像构建功能,它不解析 Dockerfile。

Kubernetes 集群里的镜像要么是 CI 系统构建好推到镜像仓库,要么开发在自己电脑上用 Docker 构建再推上去。也就是说:Docker 仍然是镜像构建环节的事实标准,containerd 是运行环节的选手。

如果你非要在只有 containerd 的机器上构建镜像,可以用buildkitnerdctl build,它们底层调用 BuildKit 来完成 Dockerfile 构建。但这属于额外安装组件,除非环境受限,否则我不建议生产节点这么折腾。

3. 实操过程与核心环节实现:手上的技术栈怎么切换

3.1 一台全新机器上完整部署 containerd

我刚入职现在的公司时,接手的第一件事就是把一批新的 Kubernetes 节点从 Docker 切换到 containerd。我在这分享一下完整的部署流程。

随便选一台 CentOS 7.9 或 Ubuntu 22.04 的机器,先装 containerd:

# Ubuntu 上直接按官方文档装 apt-get update && apt-get install -y containerd.io # 生成默认配置 containerd config default > /etc/containerd/config.toml

这里有两个必须调整的地方,否则后续全是坑:

一是设置 systemd cgroup 驱动。在config.toml里找到SystemdCgroup,把它改成true。如果保持默认的false,Kubernetes 的 kubelet 会检测到 cgroup 驱动不一致,直接拒绝调度 Pod,报错信息很让人懵。

二是修改沙箱 pause 镜像地址。containerd 创建 Pod 之前要拉一个 pause 镜像,默认值是registry.k8s.io/pause:3.x,在国内网络环境根本拉不下来。改成你本地的镜像仓库地址。

改完之后重启:

systemctl daemon-reload systemctl restart containerd systemctl enable containerd

再用ctr version确认一下版本,看到containerdrunc两个版本号都正常,就算装好了。

3.2 containerd 镜像导入导出的完整实操

把镜像从 Docker 机器迁移到 containerd 机器,是我几乎每周都要干的事。这里直接给大家一个可以照抄的流程。

第一步,在 Docker 机器上导出镜像。我推荐用docker save的时候指定输出文件名和镜像 tag 写清楚:

docker save -o myapp-v1.0.0.tar myapp:1.0.0

第二步,把 tar 文件传到目标机器上,用nerdctl导入:

nerdctl load -i myapp-v1.0.0.tar

第三步,验证镜像是否正确加载:

nerdctl images | grep myapp

这里有个细节,如果你只有ctr没有nerdctl,也可以导入,但注意命令和命名空间的差异:

# 导入到默认命名空间 ctr -n myns images import myapp-v1.0.0.tar # 检查 ctr -n myns images list

跑容器时也要加上同一个命名空间:

ctr -n myns run --detach myapp:1.0.0 myapp

如果你不指定-n,后面会发现镜像、容器全都找不到,维护起来非常混乱。

3.3 Docker Compose 工作流平移到 containerd

我前阵子帮一个业务团队排查问题,他们内部工具原本用 Docker Compose 起了一堆服务,现在要统一迁到 containerd。Docker Compose 的文件依赖 Docker daemon,containerd 本身跑不了docker-compose up

办法有两个:

  • nerdctl,它支持nerdctl compose -f docker-compose.yml up -d这条命令,兼容大部分 Compose v2 的语法,日常用的buildportsvolumesenvironment都没问题。
  • containerlab或手动起多个ctr run,适合不需要编排的场景。

我实测下来,nerdctl compose对常见项目基本够用,但在遇到 Compose 里用了depends_on的 condition 这种高级语法时,偶尔会忽略条件,直接并行启动容器。如果你的业务强依赖启动顺序,建议在服务里加健康检查,或者调整启动脚本,不要完全依赖 compose 帮你排队。

3.4 性能对比:差多少才是真差距

性能对比是大家最爱问的话题。我先说结论:containerd 比 Docker 轻,但不代表性能有质的飞跃,真正跑业务时差距通常小于 5%。

Docker 多了 dockerd 这一层,意味着每次容器创建、销毁都会有额外的守护进程调用开销、用户态进程切换开销。containerd 去掉这一层,kubelet 直接跟 CRI 对接,创建 Pod 的延迟和内存占用确实更优。

我做过一个简单的压测:在同一台 8C16G 的云主机上,分别用 Docker 和 containerd 连续创建 1000 个容器,对比结果如下:

指标DockerContainerd
1000 容器创建总耗时约 6 分 20 秒约 4 分 50 秒
单个容器平均启动时间约 380ms约 290ms
空闲时守护进程内存占用约 180MB约 50MB
磁盘占用(二进制+依赖)约 1GB 以上约 100MB

可以看到,containerd 的优势主要体现在资源占用和调度速度上。但对于一个单容器应用来说,300ms 和 380ms 的差别你几乎感知不到。真正让你选 containerd 的理由不是性能,而是架构简洁和 Kubernetes 生态统一。如果谁跟你说换了 containerd 业务快了三倍,那一定是在吹牛。

4. 常见问题与排查技巧实录:踩坑整理

4.1 问题速查表

我整理了一份高频问题对照表,基本覆盖我日常排障时碰到的 80% 情况:

问题表现排查思路解决方案
ctr images pull拉不到镜像可能没写完整镜像地址写全docker.io/library/nginx:latest这类完整地址
nerdctl ps看不到容器命名空间不对nerdctl -n k8s.io ps查看对应命名空间
docker load的镜像ctr导入报错镜像格式不兼容nerdctl load导入
kubelet 报 cgroup 驱动错误containerd 的 SystemdCgroup 没开启修改 config.toml 重启 containerd
生产节点拉镜像特别慢缺少镜像加速配置给 containerd 配 mirrors,指向你本地加速地址
ctr run后容器退出了前台容器需要保持进程不退出检查镜像 CMD 是否常驻,或加--detach正确检查日志
containerd 版本升级后 Pod 起不来配置格式变了按新版本重新containerd config default后改差异项

4.2 cgroup 驱动不一致的典型坑

我在第一次部署 containerd 时就踩过这个坑。kubelet 正常装完,一创建 Pod 就报错:

failed to run Kubelet" err="failed to run kubelet"

日志里还有一句failed to validate cgroup v2之类的提示。当时排查了很久,最后才发现是 containerd 默认配置里SystemdCgroupfalse,而 kubelet 默认用 systemd cgroup driver,两边对不上,容器创建被拒绝。

解决思路很简单:在 containerd 的 config.toml 里把SystemdCgroup改成true,重启服务。但如果你用的是 kubeadm 初始化的集群,还得确保 kubelet 配置里的 cgroup driver 也是systemd,保持两边一致。

4.3 restart 策略的缺失

用 Docker 时,我们习惯--restart=always这种自动重启策略。containerd 原生的ctr run没有这种简单的 restart 参数,容器退出后就保持退出状态,不会自动拉起。

我接手一个业务的时候,他们想用纯 containerd 跑一小堆常驻服务,结果半夜进程崩了没人发现,服务就那样挂了一整晚。后来我改用了 systemd 管理这些容器进程。具体思路是给每个容器写一个 systemd unit 文件,通过Restart=always保证崩溃自动拉起,再用ExecStartPre去检查镜像是否存在,不存在就先拉镜像再启动容器。

如果你不想上 systemd,更简单粗暴的方案是起一个循环脚本,检查容器状态,发现退出就重新ctr run。但这种方式我不推荐,因为脚本自身的健壮性也是个问题。containerd 本体的定位是“底层的运行底座”,它不自带“老大哥式”的进程守护策略,这是它和 Docker 在部署体验上一个很大的区别。

4.4 镜像垃圾清理

Docker 有一条docker system prune能清理悬空镜像和停止的容器,containerd 原生没有这么方便的“一键瘦身”命令。我用过一段时间ctr images prune,它只能清理未被容器引用的镜像,对临时构建的中间层镜像帮助有限。

更好的方案是配合nerdctl system prune,或者到 Kubernetes 环境里用kubelet自带的镜像垃圾回收(imageGC)。后者我比较推荐,生产集群里 kubelet 会根据你设置的阈值(比如磁盘使用率超过 85%)自动清理不用的镜像,不需要人工介入。

4.5 containerd 日志和调试手段

containerd 的日志是输出到 journald 的,查日志用:

journalctl -u containerd -f

如果你要看某个容器的日志,Docker 是docker logs <name>,nerdctl 是nerdctl logs <name>,但 ctr 没有直接的 logs 命令。你只能找到容器目录下的日志文件,路径一般在:

/var/log/containers /var/log/pods

/var/log/pods适用于 Kubernetes 环境,每个 Pod 一个目录,子目录下是各容器日志文件,文件名还带着容器 ID,刚开始看会有点懵,但习惯后还是能快速定位问题的。

5. 一些补充建议与个人心得

5.1 日常开发场景怎么选

如果你主要在本地开发程序,或者给客户做交付,Docker 整体体验依然是最顺的。docker builddocker compose updocker logs,这些一套流程非常成熟,资料多、好排查、团队成员接受度高,没必要为了“追求新”而强行切到 containerd。

反过来讲,如果你是运维,或者你负责的集群已经在用 Kubernetes,那节点运行时选 containerd 是更合理的选择。少一层 dockerd,少一份维护,也少了 dockershim 这个历史遗留包袱。

5.2 需要同时管理两种环境时的技巧

我的工作环境是 Docker 和 containerd 共存,为了不让命令混淆,我给自己定了一条规则:

  • 只有 Docker 语法用docker
  • 在 containerd 环境优先用nerdctl,只有当nerdctl查不到底层状态时才用ctr

同时我在每台服务器上都设置了别名,把常用命令简化:

alias ctrn='ctr -n k8s.io' alias nctl='nerdctl -n k8s.io'

这样在 Kubernetes 节点上排查时,直接ctrn images list就能看到 kubelet 拉取的镜像,不用每次手敲-n k8s.io

5.3 迁移切流时记得做“回退演练”

最后想提醒一句:任何方案改动,先小范围验证再批量推广。我们当时切换节点运行时,没有一下子全量切,而是先挑一个非核心节点的业务 Pod 迁移过去,观察了两三天,确认没问题后才逐步扩大。还提前准备了一套回退方案:关键业务每台机器保留 Docker 的 systemd 服务,只是停用但没卸载,万一 containerd 出了兼容性问题,能立刻切回 Docker。

这个思路不限于运行时替换,任何生产环境的架构调整都适用。别怕慢,怕的是上线了才发现问题,却又没有退路。

我在实际使用中发现一个特别容易让人心累的地方:Docker 和 containerd 都在快速迭代,网上的博客教程经常写得互相矛盾。遇到问题千万别急着照搬某个教程,先看官方文档和你本机的版本。同一份 config.toml,在 containerd 1.6 和 2.0 上的字段差异就很大,搞错了镜像加速配置就失效,日志里还不会直接报错。我的做法是,每次升级前先把默认配置导出一份,对比新旧差异,再做调整。

如果你现在还在犹豫“到底要不要从 Docker 切 containerd”,我的建议很简单:开发机上继续用 Docker,服务器上如果不是必须兼容旧工具链,就直接上 containerd。两种运行时可以长期共存,不必焦虑选错。毕竟,不管你用哪一层,底下干活的都是同一套内核机制,容器还是那个容器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询