ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Containerd与Docker深度对比:架构、命令、性能与迁移实践

Containerd与Docker深度对比:架构、命令、性能与迁移实践 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 默认用 runccontainerd 默认也用 runc所以最终跑容器这件事大家用的都是同一套底层机制。1.2 为什么 Kubernetes 选择了 containerd聊到生产环境Kubernetes 的运行时选择就很有代表性了。早期 Kubernetes 节点上是跑 Docker 的但 dockerd 作为中间层kubelet 需要通过一个叫 dockershim 的适配器去访问 Docker API链路长、问题多。后来 Kubernetes 社区干脆把 containerd 作为一级运行时直接对接不走 dockershim。结果就是生产集群越来越多人直接用 containerd省掉了 dockerd 这一整层。containerd 本身实现了 Kubernetes 的 CRIContainer 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 pullcontainerd 原生用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有时候会直接报错非常抓狂。解决办法是用nerdctlnerdctl load -i myimage.tarnerdctl在命令设计上刻意对齐了 Docker CLI包括nerdctl pull、nerdctl run、nerdctl 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 listKubernetes 节点上 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 的机器上构建镜像可以用buildkit或nerdctl 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。如果保持默认的falseKubernetes 的 kubelet 会检测到 cgroup 驱动不一致直接拒绝调度 Pod报错信息很让人懵。二是修改沙箱 pause 镜像地址。containerd 创建 Pod 之前要拉一个 pause 镜像默认值是registry.k8s.io/pause:3.x在国内网络环境根本拉不下来。改成你本地的镜像仓库地址。改完之后重启systemctl daemon-reload systemctl restart containerd systemctl enable containerd再用ctr version确认一下版本看到containerd和runc两个版本号都正常就算装好了。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 daemoncontainerd 本身跑不了docker-compose up。办法有两个装nerdctl它支持nerdctl compose -f docker-compose.yml up -d这条命令兼容大部分 Compose v2 的语法日常用的build、ports、volumes、environment都没问题。用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 个容器对比结果如下指标DockerContainerd1000 容器创建总耗时约 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 errfailed to run kubelet日志里还有一句failed to validate cgroup v2之类的提示。当时排查了很久最后才发现是 containerd 默认配置里SystemdCgroup是false而 kubelet 默认用 systemd cgroup driver两边对不上容器创建被拒绝。解决思路很简单在 containerd 的 config.toml 里把SystemdCgroup改成true重启服务。但如果你用的是 kubeadm 初始化的集群还得确保 kubelet 配置里的 cgroup driver 也是systemd保持两边一致。4.3 restart 策略的缺失用 Docker 时我们习惯--restartalways这种自动重启策略。containerd 原生的ctr run没有这种简单的 restart 参数容器退出后就保持退出状态不会自动拉起。我接手一个业务的时候他们想用纯 containerd 跑一小堆常驻服务结果半夜进程崩了没人发现服务就那样挂了一整晚。后来我改用了 systemd 管理这些容器进程。具体思路是给每个容器写一个 systemd unit 文件通过Restartalways保证崩溃自动拉起再用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 namenerdctl 是nerdctl logs name但 ctr 没有直接的 logs 命令。你只能找到容器目录下的日志文件路径一般在/var/log/containers /var/log/pods/var/log/pods适用于 Kubernetes 环境每个 Pod 一个目录子目录下是各容器日志文件文件名还带着容器 ID刚开始看会有点懵但习惯后还是能快速定位问题的。5. 一些补充建议与个人心得5.1 日常开发场景怎么选如果你主要在本地开发程序或者给客户做交付Docker 整体体验依然是最顺的。docker build、docker compose up、docker logs这些一套流程非常成熟资料多、好排查、团队成员接受度高没必要为了“追求新”而强行切到 containerd。反过来讲如果你是运维或者你负责的集群已经在用 Kubernetes那节点运行时选 containerd 是更合理的选择。少一层 dockerd少一份维护也少了 dockershim 这个历史遗留包袱。5.2 需要同时管理两种环境时的技巧我的工作环境是 Docker 和 containerd 共存为了不让命令混淆我给自己定了一条规则只有 Docker 语法用docker在 containerd 环境优先用nerdctl只有当nerdctl查不到底层状态时才用ctr同时我在每台服务器上都设置了别名把常用命令简化alias ctrnctr -n k8s.io alias nctlnerdctl -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。两种运行时可以长期共存不必焦虑选错。毕竟不管你用哪一层底下干活的都是同一套内核机制容器还是那个容器。
返回列表