说到生产环境容器运行时这几年绕不开一个名字containerd。从Kubernetes 1.24开始很多集群把默认运行时从Docker换成了containerd。搞容器平台的同学如果还停留在“节点上装个Docker就能跑”的思路那排障时很容易手足无措。这篇文章会把containerd从组件架构、日常操作、生产配置到故障排查完整过一遍适合刚接触containerd的运维也适合已经在用、但对底层机制还没完全吃透的同学。“工业级”这个前缀不是营销话术。它意味着这套运行时在真实生产环境里要满足几件事能扛住频繁的镜像拉取和容器启停能在节点异常时保持容器不散架能给出足够细的日志和指标供排查与告警还要在升级迁移时做到可回滚、可灰度。Containerd在Kubernetes生态里扮演的正是这个承上启下的角色。这次分享不会只讲命令怎么敲重点是把每个关键设计背后的“为什么”说清楚。比如shim进程为什么必须存在、crictl和ctr看到的镜像为什么不一样、磁盘为什么会莫名被快照层占满。这些东西在官方文档里都有但零散地看很容易忽略它们之间的联系。我尽量用实际操作场景把它们串起来。1. 为什么生产环境要单独聊containerd1.1 从Docker到containerd运行时演进的真实原因先理一下时间线。早期Docker是一个大单体从镜像构建、镜像存储、容器启动到网络管理全在docker daemon里。后来容器场景越来越复杂Docker团队把底层容器管理能力抽出来形成了containerd再后来containerd在2017年捐给了CNCF成了独立项目。Kubernetes从1.24起默认停止使用dockershim生产集群的默认运行时事实上变成了containerd。这件事对使用者的影响是直接而具体的。过去你在节点上排查容器问题第一反应是docker ps、docker logs、docker inspect换了containerd之后这些命令要么不存在要么需要换工具、换参数、换socket路径。更重要的是Docker时代的存储目录是/var/lib/dockercontainerd时代是/var/lib/containerd原先基于目录大小的清理脚本、备份方案、磁盘监控全部要调整。我见过不少团队在迁移后踩坑根子在于还拿Docker的思维去理解containerd。比如容器日志去哪了、镜像存在哪、为什么docker images和crictl images看到的结果不一样。这些认知如果不更新出了问题往往不知道从哪里下手。1.2 丢掉Docker之后K8s节点上到底还剩下什么一个纯containerd节点上容器创建路径大概是这样的Kubelet通过CRI接口连接containerd的socketcontainerd的CRI插件解析请求再由containerd调用containerd-shim启动runc最后由runc直接拉起容器进程。这套链路里没有dockerd也没有docker-proxy。Kubelet不再关心“Docker是否健康”只看containerd是否可用。用systemctl status containerd查看服务状态用crictl logs代替docker logs这些都是基本功。有一点容易被忽略containerd自己不带CNI网络插件。Pod网络是由Kubelet调用CNI插件完成的containerd只负责把容器跑起来。如果节点网络异常你先检查的应该是CNI插件和网络策略而不是containerd配置。理解这个边界能节省大量排查时间。2. 组件地图containerd、shim、runc各自干什么2.1 先看懂containerd进程里的四大核心模块containerd本身是一个daemon进程但它内部不是一坨不可拆分的代码。生产调优和故障排查时至少要对四个核心模块有基本认知。Content Store是内容仓库负责保存镜像的层数据。你可以把它理解成一个带寻址能力的文件仓库每一层内容通过摘要算法算出唯一ID并存储。Snapshotter负责把镜像层组装成容器可用的根文件系统常见实现是overlayfs它利用文件系统的写时复制能力让多个容器共享只读层只把各自写入的部分放在可写层。Metadata用来记录镜像、容器、命名空间等对象的元信息相当于整个containerd的账本。Task API则负责进程生命周期管理比如创建、启动、停止容器内部的进程。实际操作中磁盘占用异常时先看/var/lib/containerd下各目录的大小分布。如果io.containerd.snapshotter.v1.overlayfs目录很大说明快照层堆积如果io.containerd.content.v1.content目录很大说明镜像层内容占用高。这个区分能帮你定位是容器写入了大量数据还是镜像本身没被清理。2.2 containerd-shim为什么是“保命”的一层很多人在进程列表里看到containerd-shim会好奇容器不是直接由runc启动的吗怎么还有一层shim这里有个很容易被忽略的工程细节。runc是一个一次性的CLI工具它执行完run命令把容器进程创建出来之后runc本身就会退出。如果容器进程的父进程直接是containerd那containerd一重启或升级容器进程就会变成孤儿甚至可能被系统清理掉。shim就是为了解决这个问题存在的。shim启动后一直驻留接管容器进程的stdio、退出状态、信号转发等工作即使containerd短暂重启shim仍然存活容器进程不会跟着挂掉。这个设计让containerd具备了“daemon轻故障不影响容器运行”的能力。生产环境升级containerd时容器无需重建只要shim还在业务进程就能继续跑。理解了这一点你就明白为什么升级runtime时不能简单粗暴kill掉所有shim进程。2.3 CRI插件与Kubelet的交互细节Kubelet访问containerd时用的不是ctr或nerdctl那套API而是通过CRI插件。CRI插件监听在/run/containerd/containerd.sock上提供PodsAndContainers相关接口比如RunPodSandbox、CreateContainer、StartContainer。创建Pod时Kubelet会先调用RunPodSandbox创建沙箱也就是我们常说的pause容器。pause容器负责持有Pod的网络命名空间业务容器后续通过加入这个命名空间来共享网络。所以一个Pod里的业务容器全挂了pause容器可能还在running状态。排查时不要只看容器列表还要看Pod层面的状态和事件。containerd版本和Kubernetes版本之间也有匹配关系。生产环境建议选择都在各自维护期内的稳定版本不要一方过于激进。这个匹配关系在官方文档里有明确的兼容矩阵升级前一定要核对。3. 日常运维ctr、crictl、nerdctl到底该用哪个3.1 工具选择对照这是新人最容易懵的地方。节点上同时出现ctr、crictl、nerdctl三个命令到底用哪个它们不是同一个工具的三个别名而是面向不同场景的三套客户端。ctr是containerd自带的调试客户端功能最原始适合底层排查但日常操作体验比较简陋。它默认操作的是default命名空间如果没有显式指定命名空间你在Kubernetes节点上用它拉镜像Kubelet根本看不到。crictl是Kubernetes社区提供的CRI调试工具面向Kubelet视角操作的是k8s.io命名空间这也是生产节点上最常用的工具。nerdctl则是一个兼容Docker CLI的containerd客户端支持docker run、docker build、docker compose这类风格命令适合开发环境或从Docker迁过来的场景。三者的核心差异可以列个对照表工具面向场景常用命令示例默认命名空间ctrcontainerd底层调试ctr images pulldefaultcrictlKubernetes节点排查crictl ps、crictl logsk8s.ionerdctlDocker兼容操作nerdctl run -it nginxdefault在Kubernetes节点上日常优先用crictl纯containerd调试场景用ctr本地开发想要Docker体验可以用nerdctl。3.2 命名空间机制为什么同一个节点上镜像列表对不上containerd的namespace是一个逻辑隔离域镜像、容器等元数据归属于某个namespace。ctr默认用defaultcrictl固定用k8s.ioDocker引擎集成containerd时用的是moby。所以你在节点上执行ctr images list看到的是default命名空间下的镜像而crictl images列出来的是k8s.io命名空间下的镜像。两边对不上是正常的不是数据丢失。这个概念特别容易在迁移时坑人。有人把镜像用ctr导入后发现Pod调度到该节点还是报镜像拉取失败就是因为镜像进了default命名空间Pod根本不会去那里找。正确的离线导入姿势是给ctr加 -n k8s.io参数或者干脆用crictl的机制处理。还有一个衍生问题命名空间只做逻辑隔离不做安全隔离。跨命名空间无法直接看到对方元数据但底层磁盘、内核资源是共享的。排查安全问题时不要以为把对象放进不同namespace就高枕无忧。3.3 镜像管理与离线导入实操生产环境经常要在离线或内网环境导入镜像。crictl本身没有pull带认证的复杂参数时好用但离线导入的场景反而ctr更直接。离线导入的标准做法是先在能联网的机器上把镜像保存成tar包再到目标节点导入。在源端执行ctr images export导出目标端执行ctr -n k8s.io images import導入。注意导入时必须带 -n k8s.io否则Kubelet感知不到。用nerdctl的话会更接近Docker体验比如nerdctl load -i image.tar导入nerdctl pull时也能带认证参数。很多从Docker迁移过来的团队会保留nerdctl作为日常工具让研发同事少改习惯这是个合理的过渡方案。不管是哪种工具导完镜像都要做一次校验。用crictl images确认镜像已经出现在k8s.io命名空间再起一个测试Pod验证能否正常拉取和启动。跳过这一步很容易在业务扩容时才暴露镜像缺失问题。4. 生产配置config.toml逐段讲解与调优4.1 基础配置模板containerd默认配置文件在/etc/containerd/config.toml有些发行版还支持config.toml.d目录做配置片段叠加。建议用containerd config default生成一份模板再基于模板修改避免手工编写导致字段缺失或格式错误。一份生产环境可用的基础配置大概长这样version 2 root /var/lib/containerd state /run/containerd [grpc] address /run/containerd/containerd.sock [debug] level info [plugins.io.containerd.grpc.v1.cri] sandbox_image registry.k8s.io/pause:3.9 [plugins.io.containerd.grpc.v1.cri.containerd] default_runtime_name runc [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup trueroot决定数据存储位置state决定运行状态目录。生产环境通常把root放在独立数据盘或大分区上避免和系统盘抢占空间。grpc.address是Kubelet访问的socket路径千万不要乱改否则Kubelet的runtime-endpoint也要跟着改。配置文件的修改需要重启containerd才能生效少数参数支持热加载但保守起见还是走标准重启流程。每次改配置前先把当前文件备份成带时间戳的副本这是能救命的小习惯。4.2 Systemd Cgroup DriverKubelet和containerd的联动关键cgroup driver配置是生产环境最常遇到的坑。在Kubernetes节点上Kubelet通常使用systemd作为cgroup drivercontainerd侧的CRI插件也必须对齐否则节点会直接报misconfiguration或者Pod一直处于ContainerCreating。实现方式就是在runc runtime的options里设置SystemdCgroup true。这一步很关键但很多人只改了Kubernetes侧kubelet的--cgroup-driversystemd忘了同步containerd配置导致节点反复异常。还有一个相关点是容器的cgroup路径。如果你在节点上看systemctl status会发现每个Pod对应一个systemd slice如kubepods-besteffort-podxxxx.slice。这种层次划分对资源统计和回收很友好也让systemd统一管理容器进程的cgroup。如果设置成falsecontainerd会直接写cgroupfs非systemd管理的进程状态容易和节点监控对不上。4.3 镜像加速、垃圾回收和并发控制内网或云环境通常要配置registry mirror。很多云厂商提供容器镜像加速服务配置好了能大幅提升拉取速度尤其在大规模扩容时效果明显。[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://mirror.internal.example.com, https://registry-1.docker.io]endpoint列表有顺序关系containerd会依次尝试如果第一个不可用再走下一条。生产环境建议把自建镜像仓库放在第一位并加上健康检查机制。注意私仓的HTTPS证书要能被节点信任否则会频繁报TLS握手失败。垃圾回收方面containerd默认的image GC阈值和Kubelet的imageGCHighThresholdPercent是配合使用的。Kubelet达到阈值时会主动清理未使用的镜像同时containerd自己也有基于时间的snapshotter GC。生产环境要监控/var/lib/containerd使用率如果磁盘持续上涨优先排查是否有大量异常容器日志和未回收的写层。并发控制上max_concurrent_downloads和max_concurrent_uploads直接影响镜像拉取和推送的吞吐。默认并发不算高在高速内网环境下可以适当调大但不要无脑调因为并发太高会占满磁盘IO和网络带宽反而拖慢整体性能。4.4 快照器选型overlayfs不是唯一选择containerd支持多种快照器常见的有overlayfs、native、btrfs、devmapper。生产环境绝大多数场景默认用overlayfs。它利用内核的OverlayFS能力实现只读层共享、写入层分离既能节省磁盘又比native快。native快照器不依赖OverlayFS直接把镜像层复制出来隔离性更好但占用的磁盘空间成倍上升一般只在OverlayFS不适用时才用。devmapper则需要预先分配存储池配置复杂度高除非有特殊需求否则不建议普通场景硬上。选快照器时要考虑内核版本和文件系统类型。overlayfs在XFS和ext4上表现都很好但需要内核支持相关特性。生产环境升级内核后要顺手验证containerd快照器是否仍处于正常状态我遇到过升级内核后OverlayFS模块异常导致容器启动失败的案例。5. 安全加固与供应链防护5.1 最小权限从进程级别做隔离containerd进程默认以root身份运行这本身就是个大攻击面。生产环境至少要做到几点containerd的socket权限要严格限制不要让普通用户随意调用操作系统层面建议启用SELinux或者AppArmor给容器进程套一层强制访问控制。runc层面可以配置默认的seccomp profile。seccomp会限制容器内进程可用的系统调用默认profile已经屏蔽了很多危险调用。不要因为某个业务程序报“operation not permitted”就随手把seccomp关掉正确做法是精确放行该程序需要的系统调用。图省事关掉隔离等于给入侵者开了一扇侧门。还有一个容易被忽视的点是Capabilities。容器内进程默认带一组能力生产环境应该根据业务需要裁剪。很多高权限漏洞利用的根本原因就是容器以privileged方式运行或者带了过宽的capabilities。5.2 镜像签名验证与私有仓库配置工业级供应链安全里镜像签名验证越来越重要。镜像从构建到拉取中间经过仓库、传输链路、节点缓存等多环节任何一环被篡改都可能带来风险。containerd可以通过外部组件对镜像做签名校验拉取时验证签名通过才允许使用。私有仓库的认证配置建议放在config.toml的registry部分用密钥文件而不是明文密码。生产环境中还要注意仓库的访问控制策略比如哪些节点允许拉取哪些镜像、是否启用镜像不可变标签。镜像一旦打上不可变标签再往相同标签推送会被拒绝这能有效防止生产环境被意外覆盖。5.3 审计、监控与密钥管理节点上的containerd默认日志在journald里同时也可以配置独立的debug日志文件。建议把containerd的info级别日志接入日志采集系统这样当容器频繁创建失败时我们能在日志平台直接检索到相关记录而不是每次都要登节点。密钥管理要单独说。containerd访问私有仓库需要认证信息这些信息不要直接散落在配置文件里。可以将认证放到专门的安全目录并限制权限也可以用支持动态密钥的凭证机制。如果节点被攻破至少不能让攻击者顺着配置直接摸到仓库的长期凭证。6. 线上故障排查手册6.1 Kubelet与cgroup driver不匹配这类问题出现频率非常高。症状通常是kubelet启动时报错节点状态NotReady同时事件里会提示cgroup driver配置不匹配。排查时先看kubelet命令行参数里的--cgroup-driver再看containerd config.toml里是否设置了SystemdCgroup true。两边保持一致后重启containerd和kubelet。注意改配置前备份文件这个步骤看似简单但在批量节点上操作时极其容易遗漏个别节点。6.2 磁盘空间被快照层占满这是经常让人头疼的事故。现象是节点磁盘报警删除了一堆镜像后空间还是不够。原因是运行中的容器和已经停止的容器快照层没有被及时清理。排查步骤是进入/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots目录按目录大小排序找出占用最大的快照。再用crictl ps -a确认对应容器是否已经退出如果退出但快照还存在说明GC没有覆盖到或者容器的写层被某个shim进程引用。清理时要顺着容器ID反查shim进程确认没有进程引用后再删除快照目录。生产环境不要手动删/var/lib/containerd下的文件这只适合应急根本解法是完善GC配置和监控告警。6.3 shim进程残留与僵尸进程shim进程偶尔会变成孤儿比如containerd重启时正在创建或销毁容器。正常情况下容器退出后shim会自行退出。但异常情况下shim可能一直驻留导致容器退出了但端口还占着或者主机进程数持续上涨。排查方法很简单ps aux里数一下containerd-shim的进程数再对照crictl ps -a的容器数。如果shim数量明显多于容器数量说明有残留。可以先尝试通过containerd的task接口删除对应任务不行再手动kill残留shim。但kill前一定要确认容器确实已经退出避免误杀还在运行的容器。6.4 镜像拉取失败网络、认证、格式三板斧镜像拉取失败是最高频的问题。第一步看错误信息在哪个环节超时一般优先怀疑网络和镜像加速配置认证失败基本是仓库凭证配置错误格式不合法或者manifest无法解析要检查镜像架构和仓库兼容性。我见过很多次“镜像明明存在却拉不下来”的情况最后定位是私有仓库的DNS解析异常或者证书过期。这类问题排查要养成看完整错误信息的习惯不要只盯着镜像仓库地址看。这里可以整理一张故障速查表症状可能原因排查方向节点NotReadycgroup driver不匹配对比kubelet和containerd配置磁盘持续上涨快照层或镜像未回收检查overlayfs目录和GC策略容器启动失败runc或seccomp配置问题看containerd日志和事件拉镜像超时网络、mirror、并发配置检查DNS、证书、endpoint顺序Pod长时间Creatingsandbox镜像拉取失败检查pause镜像是否就绪7. 从Docker迁移到containerd的实战经验7.1 迁移前检查清单如果团队还在用Docker作为节点运行时准备切到containerd之前建议先完成几件事。确认自己的Kubernetes版本支持containerd检查现有业务有没有依赖Docker特有能力比如docker build、docker network等。Kubernetes本身只是用CRI接口很多Docker特性对运行在Pod里的业务本来就不可见但如果是节点上直接跑手动docker容器的场景要单独列出来处理。还要确认监控体系能覆盖新的运行时。原来的docker容器指标、镜像清理脚本、节点排障手册都要更新。我的习惯是提前在一台测试机上完整跑一遍迁移流程把出现过的坑记录到内部文档再批量操作。7.2 节点迁移顺序与回滚策略生产集群迁移不能一把梭。正确做法是分批迁移每批选少量节点验证稳定性再逐步扩大。迁移前把/etc/containerd/config.toml和当前运行版本记录下来一旦发现异常可以迅速回滚到Docker。迁移过程中节点上的存量容器会被重建。这意味着流量会发生短暂转移需要确保工作负载有多副本否则迁移节点等于主动制造单点故障。很多业务方没意识到这一点结果迁移一个节点就把整个服务搞挂了。回滚时也要注意Docker服务要恢复并重新设置为运行时Kubelet参数也要改回。保存好旧的systemd配置和kubelet参数是回滚的依赖条件。7.3 可观测性补充切到containerd后原本基于Docker的指标采集要换成CRI兼容方案。比较常见的是通过cAdvisor或者kubelet内置的stats接口采集容器指标节点层面再看containerd的日志和systemd状态。containerd自身暴露了一些Prometheus指标可以通过配置metric插件开启。重点关注容器启动耗时、镜像拉取耗时、快照操作耗时这几类指标。当集群大范围调度或扩容时这些指标能直观反映运行时是否存在瓶颈。日志方面containerd默认日志量不算大但遇到批量操作时可以临时把debug级别调高定位完问题再调回info避免日志刷爆磁盘。最后提醒一点生产环境任何配置变更都要有变更记录和回滚方案。containerd虽然是稳定组件但配置写错、版本不匹配、存储路径变更都可能在不知不觉间影响线上稳定性。我个人的习惯是每台节点保留一份最近可用的config.toml备份文件名带上日期批量操作前至少在一台节点上验证完整流程所有镜像导入都确认namespace正确。这些习惯看起来琐碎但在事故多发的深夜节点操作时能帮你省下大量时间。
阅读完成 · 觉得有帮助?