ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

containerd实战指南:K8s容器运行时原理、命令与排障

containerd实战指南:K8s容器运行时原理、命令与排障 最近排查生产环境K8s节点上的容器异常时很多问题的根因都落在同一个地方containerd这个容器运行时。不少刚接触云原生的同事第一反应是先问我“containerd到底是什么它是Docker的替代品吗K8s里面提到containerd命令到底是指哪个命令”这些问题看起来很基础但一旦搞不清楚后面排查起来就会绕很多弯路。在Kubernetes 1.24之后kubelet默认对接的就是containerd你每天在节点上看的容器日志、镜像列表、Pod状态背后基本都是它在工作。这篇文章我会结合自己实际部署和排障的经验把这个容器运行时从设计思路、核心组件、实操命令到常见坑位完整搂一遍。新同学可以借此建立一份完整的认知地图老手可以直接跳到后面的故障排查部分看案例。1. 这个容器运行时到底在系统里扮演什么角色1.1 从Docker里拆出来的“后厨总管”理解containerd最好的角度是看它从哪里来。很多年前Docker如日中天的时候它的引擎内部就有一个负责管理容器生命周期的组件这个组件的名字就叫containerd。后来Docker公司把这个组件捐给了CNCF由云原生社区继续维护并且独立成了今天的开源项目。所以你可以把containerd理解成是一个从Docker内部“拆出来单飞”的后厨总管——它不去管那些花哨的前台装修比如docker build的镜像构建、docker compose的编排语法而是专心搞定一件最核心的事把容器镜像变成真正跑起来的容器进程并且管好它们的生死。打个生活化的比方。如果你把容器比作一个个外卖订单那么Docker是那个你每天都在手机上点开的美团APP它负责展示菜单、处理支付、给出漂亮的界面。而containerd就是后厨里那个接到订单后真正洗菜、切菜、下锅、装盒的厨师主管。用户平时不直接跟厨师说话但在后厨这个环节里厨师主管说了算。Kubernetes就是另一个大商家它不想被美团APP绑架于是直接找厨师主管对接——这就是为什么K8s默认用containerd而不是Docker。这个定位决定了它的使用场景你不是通过containerd写Dockerfile也不是用它执行docker-compose.yml而是让K8s、或者云原生平台里的其他调度系统通过标准的gRPC接口把“请帮我跑一个容器”这种请求交给它。它负责拉镜像、解压镜像、创建快照层、启动进程、监控状态以及在容器退出后清理环境。这一整条链路是K8s每个Pod能稳定运行的地基。1.2 为什么Kubernetes最终选中了它K8s最早期的版本确实是通过Docker来直接管理容器的那时候kubelet调用Docker的APIDocker再调containerdcontainerd再调runc去创建容器链路多了一层。这条路能跑但Docker daemon作为一个庞大进程本身要维护大量网络、存储、构建相关的状态在K8s大量创建和销毁容器的场景下Docker daemon经常成为单点瓶颈。而且Docker为开发体验设计的很多功能对K8s这种编排系统来说根本用不上。于是K8s社区搞了一个CRIContainer Runtime Interface标准定义成kubelet和容器运行时之间的统一接口。containerd直接在这个接口上实现了自己的CRI插件kubelet通过gRPC直接跟containerd交流。这样链路就变成了kubelet - containerd - runc。少了一个daemon延迟更低资源占用更少问题定位的路径也更短。从1.20版本开始Kubernetes宣布弃用Docker作为运行时到1.24版本正式移除了dockershim之后的K8s发行版里默认的容器运行时就是containerd。现在无论是你自建的集群还是各大云厂商的托管集群底层基本都跑着containerd。所以如果你在节点上看到containerd这个进程占了不少CPU或内存别觉得奇怪它就是个在认真干活的管家。1.3 它和runc、Docker的分工边界要搞清楚containerd命令怎么用先要把生态里的角色分清楚。这里有张对比表可以看得很明确组件层级主要负责典型位置runcOCI运行时真正调用Linux内核能力创建/销毁容器进程管理cgroup、namespace容器启动的最底层containerdCRI运行时镜像管理、快照管理、容器生命周期调度、shim进程管理、暴露gRPC接口节点上的常驻DaemonDocker Engine用户侧工具镜像构建、CLI体验、网络/存储编排、老式编排开发环境常用生产K8s已不依赖runc是做什么的它是按OCI标准实现的小工具负责fork出容器进程、设置好各种隔离参数。你可以把runc理解成那位真的把菜端到出餐口、并且告诉客人“可以开动了”的传菜员。containerd是主管它自己不下厨而是给runc下指令“现在启动这个容器、现在停掉这个容器”。Docker是前台App现在下单方式变了前台App可以不经过但后厨工作依然由containerd加runc这对搭档完成。生产环境排障时很多人分不清谁是哪一层结果查了半天异常其实是在查docker这个不存在的进程。记住一条节点上有containerd和containerd-shim这两个进程是正常的runc是在某个容器启动瞬间临时出现的如果你用ps aux | grep runc看不到常驻runc进程不是出故障了而是它干完活就退出了。2. 三大进程与三个客户端containerd核心细节拆解2.1 containerd、shim、runc是怎么配合工作的每个节点上运行containerd之后你会看到几类进程。第一类是containerd本身它是一个常驻后台的daemon启动的时候会读取/etc/containerd/config.toml配置文件监听一个Unix socket/run/containerd/containerd.sock。所有客户端请求比如“拉镜像”“运行容器”都是通过这个socket以gRPC方式发给它的。第二类是containerd-shim它是每个容器一个的守护子进程。你可能觉得奇怪为什么每个容器都要有一个shim设计原因很实际当runc把容器真正启动起来之后如果containerd daemon本身因为升级、重启导致短暂中断容器不应该跟着挂掉。shim的作用就是替containerd看着已经启动的容器进程把容器的标准输入输出、退出状态转发给daemon。这样daemon重启了、升级了容器也能保持运行。这就像主管临时离开工位每个灶台边都有个副手盯着火菜不会糊。第三类是runc它是真正执行clone系统调用创建进程的那个家伙。整个流程大致是用户调用ctr或crictl发起“创建一个容器”请求containerd收下后做镜像挂载、快照准备然后启动对应容器的shimshim再调用runc完成最后的进程创建runc退出shim继续接管。等容器退出时shim负责清理cgroup、停止进程、报告状态。理解了这条链路你会发现很多故障排查的顺序自然就有了先看daemon日志再看shim状态必要时在pod所在节点用runc那个叫runc state id的命令查看容器状态。2.2 命名空间机制为什么ctr看到的“东西”和crictl不一样用过一段时间ctr的人都会有一个困惑我在节点上用ctr images list明明看到了一堆镜像怎么用crictl images list看到的却是另一堆甚至用ctr containers list里是空的这不是缓存问题而是containerd自己搞了一套命名空间机制。containerd官方文档里的namespace跟Linux内核里的容器隔离namespace完全不是一回事它是containerd用于逻辑隔离不同客户端状态的空间。默认情况下ctr客户端访问的是default命名空间而K8s的kubelet通过CRI插件访问的是k8s.io命名空间。所以你在default命名空间里拉了一个busybox镜像在k8s.io里是看不见的kubelet也不会用这个镜像去创建Pod。用ctr namespace ls可以列出当前节点上的所有命名空间正常情况下你会看到至少两个default和k8s.io。排查容器“为什么没起来”的时候如果你一直用ctr操作default命名空间会完全看不到K8s创建的容器就会得出“节点上根本没有容器在跑”的错觉。下次记得到了生产节点先看命名空间再用客户端工具进到正确的命名空间里去查。2.3 三个命令行客户端该用哪个别搞混围绕containerd现在常见的有三个命令行工具很多人在这里被绕晕。我直接把它们各自的侧重点写在表格里客户端面向视角典型命令适合场景ctrcontainerd原生ctr images pull / ctr run调试containerd本身、观察底层状态crictlCRI标准视角crictl ps / crictl logs / crictl pullK8s节点日常排障、Pod状态排查nerdctlDocker兼容体验nerdctl run / nerdctl compose想用类似docker命令直接管理containerdctr是最底层的调试工具。它不关心你所用的编排系统只跟containerd对话。如果你只想测试某个镜像能不能正常拉取到节点用ctr images pull最直接。crictl是K8s排障最好用的工具。它走的是CRI接口看到的是kubelet视角Pod、容器、镜像的状态和K8s里看到的基本一致。查Pod对应的容器ID、看容器日志、强制停止容器都用这个工具。nerdctl是后来社区做的兼容工具它模仿docker命令风格让从Docker迁移过来的人不用重新背命令。但它不是K8s节点的标配一般是你自己装一个需要额外下载生产节点上要克制一点别随意往节点里塞工具。我在生产上维护集群有一个习惯凡是只查问题一定用crictl凡是怀疑containerd本身配置错了才用ctr打开default命名空间验证nerdctl基本只在测试环境里体验省得自己写一堆复杂参数。这个选择方式也是建议新手直接抄的。3. 从安装到跑通containerd完整实操记录3.1 安装和关键配置项有些开关不改会吃亏安装containerd有很多种姿势常见的有apt/yum安装、二进制文件直接解压部署、以及用K8s节点的初始化脚本一并装好。我个人最推荐二进制部署方式因为版本可控、升级方便。从GitHub release页面下载对应系统架构的压缩包解压后把bin目录里的containerd、containerd-shim、ctr、runc放到/usr/local/bin下就行。不过手动部署时要记得补全配置文件和服务管理。装好之后先运行一句命令生成默认配置sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml生成的默认配置很全需要改的主要有三个点。第一个是SystemdCgroup这个参数必须设成true。Kubernetes节点的cgroup驱动如果用的是systemd而containerd这里没有打开SystemdCgroup那么容器启动后会和使用systemd的kubelet产生cgroup驱动不一致的问题轻则Pod状态异常重则节点上所有容器被杀掉。第二个是镜像加速器地址国内大部分网络环境下直接从Docker Hub拉镜像很慢需要在配置文件的plugins.io.containerd.grpc.v1.cri.registry.mirrors段里加上加速地址。第三个是sandbox_image参数它指定了每个Pod里的pause容器镜像一旦这个镜像拉不下来整个Pod都会卡在ContainerCreating。改完配置记得重启sudo systemctl restart containerd sudo journalctl -u containerd -f看到日志里出现了“Start registering signal handler”和“serving... via /run/containerd/containerd.sock”这类信息说明daemon已经就绪。3.2 用ctr快速验证拉镜像和跑容器配置完成后先用ctr做一次最小验证。这一步不需要K8s参与纯测试containerd本身的能力。sudo ctr images pull docker.io/library/nginx:alpine sudo ctr images list输出里能看到nginx:alpine镜像说明拉取链路没问题。注意我这里的镜像名写全了没有写成nginx:alpine因为ctr对镜像名的规范性要求严格不写完整的docker.io前缀它也认但社区镜像仓库地址经常变写完整更稳。接下来可以试着跑一个容器sudo ctr run --rm -t docker.io/library/nginx:alpine nginx-test /bin/sh这个命令的意思是从alpine镜像里启动一个名为nginx-test的容器进入交互式shell。如果不带--rm标志退出容器后它会留在created状态要用ctr tasks kill nginx-test来杀掉。这里要提醒一个细节ctr跑出来的容器在default命名空间里你在运行的过程中可以另开一个终端用ctr tasks ls看到它的状态这个命令对应的是“任务”不是“容器”。容器是静态描述任务是实际跑起来的进程理解这个区别之后看ctr containers list里面有个容器但tasks里没有对应进程说明它没在运行排查就会更快。3.3 对接Kubernetes并验证节点ReadyK8s节点使用containerd的关键配置其实不在kubelet的yaml里也没那么玄妙。主流的Kubeadm部署方式里在kubeadm init的时候加--cri-socket /run/containerd/containerd.sock参数或者直接编辑/var/lib/kubelet/kubeadm-flags.env在KUBELET_EXTRA_ARGS里加上--container-runtime-endpointunix:///run/containerd/containerd.sock如果你的K8s版本较新--container-runtime这个参数不用再显式指定默认就是remote方式。然后初始化节点或者直接重启kubeletsudo systemctl restart containerd sudo systemctl restart kubelet此时观察节点状态kubectl get nodes节点状态变成Ready说明kubelet已经通过CRI接口成功连接了containerd。再进一步验证可以在这个节点上通过crictl看看目前有没有正在创建的Podsudo crictl ps -a如果看到有Pod的容器处于创建的初始状态或者日志里出现了StartContainer字样说明链路已经打通了。我自己的习惯是接下来立刻部署一个echo-server或者nginx让Pod跑起来然后crictl logs拉一段日志确认运行正常。这一步不要省直接用空集群验证比盲等业务容器起来再发现底层问题要高效得多。4. 生产环境常踩的坑以及排查手记4.1 Pod持续Creating但看不到报错原因这是我在线上被问过最多次的问题Pod状态一直是ContainerCreatingdescribe出来看不到很明确的镜像错误事件里只有“Failed to create pod sandbox”。排查思路要分成两步。第一步先确定是不是sandbox镜像拉取失败。sandbox镜像就是pause镜像负责为整个Pod提供网络、PID等隔离基础。在每个节点上执行sudo crictl pull registry.k8s.io/pause:3.9拉不下来就说明网络或镜像地址有问题。第二步如果镜像能拉就去containerd日志找更详细的错误sudo journalctl -u containerd --since 10 minutes ago | grep -i error我遇到过很隐蔽的一种情况某个自建机房节点上没配DNScontainerd解析不了registry域名重试了一段时间后把拉镜像这个任务标记为失败但kubelet那边的报错信息非常含糊。此时在节点上nslookup registry.k8s.io发现问题直接在/etc/resolv.conf里补充上游DNS就好了。凡是sandbox镜像相关的“看着像网络但实际不一定是网络”的坑都建议先看看节点DNS配置再拉加速器。4.2 crictl看不到容器ctr却能看见一堆这个问题的答案在前面2.2节已经讲清楚了大部分情况就是命名空间区域没对。注意一下你执行命令的用户和配置。生产节点上crictl默认读/etc/crictl.yaml配置如果这份配置没有指定runtime-endpointcrictl会连默认的/run/containerd/containerd.sock正常是能通的。如果当时那台机器上crictl配置指向了别的运行时时也会出现不同工具看到不同内容的错觉。另一种场景是Pod被驱逐或者容器已退出crictl ps不带-a时当然是空的。所以排查时先带着-a看所有状态的容器再用crictl logs container-id确认退出原因这个顺序几乎能覆盖大部分“看不到容器”的现场。4.3 镜像占用越来越大怎么安全清理containerd不会自动清理那些已退出的容器和相关镜像层它在设计上把仓库GC的触发策略留给了上层调用者或运维人员。如果你发现df -h显示节点磁盘快满第一时间去看containerd的数据目录默认是/var/lib/containerd。占空间最大的通常是/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs这个目录。安全清理思路不要直接rm -rf /var/lib/containerd那样会把正在运行的容器快照层全删了。先用crictl清理sudo crictl rm --force --all sudo crictl rmi --prune这两条命令会把已退出的容器和未被任何容器使用的镜像删掉。如果还嫌不够可以再配置containerd定期GC策略或者写一个crontab定时执行。我这边线上就是每30分钟跑一次crictl rmi --prune再配合Prometheus监控磁盘告警之后基本没收到过磁盘爆满的夜班电话。4.4 节点重启后containerd起不来节点重启后containerd偶尔会起不来现象是systemctl状态显示failed日志里有类似于timeout或者sock地址被占用的报错。排查时先确认一下系统里是不是同时装并启用了Docker自带的containerd服务比如某些发行版装了docker-ce后/lib/systemd/system/containerd.service可能是被docker安装包覆盖过的版本。两个containerd抢同一个socket必然起冲突。处理方案也比较直接检查/etc/containerd/config.toml里的root和state路径确保没有别的基础组件占用再看下journalctl -u containerd -n 50如果有listen unix /run/containerd/containerd.sock: bind: address already in use就找到那个残留进程杀掉然后重启服务。一个我要特别加固的建议是生产节点不要同时安装docker和cri-containerd-cni这两个版本混杂的组件要么纯containerd要么Docker内置的版本保持统一混装容易把二进制文件命名覆盖得乱七八糟。4.5 常见问题速查表现象可能原因快速处理Pod状态ContainerCreatingsandbox镜像拉取失败、节点DNS异常crictl pull pause镜像换源修复DNSctr看不到K8s的Pod镜像命名空间不同用crictl或指定ctr -n k8s.io容器日志看不到内容CRI日志输出未重定向到日志文件检查是否用crictl logs读取对应容器IDkubelet报runtime not readycontainerd未启动或socket路径错误检查systemd服务reload后重启kubelet镜像拉取超时加速器未配置或配额限制配置registry mirror磁盘空间被snapshotter占满旧容器镜像层未回收crictl rmi --prune 定时GC容器一启动就退出镜像entrypoint非法/runc版本不匹配crictl logs查看最终错误码5. 一个重要但容易被忽略的经验实际操作中还有个细节很多人是用血泪教训换来的containerd的日志文件默认不按容器ID做子目录存放而是按namespace、pod名字和容器ID组合排列在/var/log/containers、/var/log/pods和/var/log/pods的软链关系里。排查Pod崩溃时kubectl logs已经够用但如果你发现kubectl logs -p拿不到上一个退出实例的日志可以直接去/var/log/containers目录下找对应软链指向的真实文件往往能挖到被kubectl“过滤”掉的信息。这一点在Pod频繁重启、event又被清理掉的场景下经常是唯一靠谱的线索来源。我自己的经验是把containerd的手感练成本能装完先看config默认值改完配置一定重启验证日常排查先用crictl拉开状态再下结论。这套习惯帮我绕过了很多生产事故希望你也能在实践里沉淀出自己的排查路线。
返回列表