尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Containerd容器生命周期管理:从ctr命令到Kubernetes故障排查实战

Containerd容器生命周期管理:从ctr命令到Kubernetes故障排查实战 1. 为什么要先把容器生命周期这件事拆清楚1.1 Containerd 并不只是另一个容器引擎谈起 Containerd很多刚接触容器运行时的同学第一反应是这跟 Docker 不是一回事吗。确实日常开发我们都用 docker run 跑容器很少直接碰 Containerd 的命令行。但如果你进入的是工业级环境比如一整套 Kubernetes 集群底层默认的容器运行时很可能就是 Containerd。这时候你排查问题、定位故障、处理容器卡死或者任务不退出手里的工具就不再是 docker 而是 ctr 了。我接触过的不少生产集群节点上的 /run/containerd/containerd.sock 一查就能看到工作负载全部由 Containerd 托管Kubelet 通过 CRI 接口把容器创建、启动、停止这些请求交给它。说白了容器生命周期管理就是 Containerd 最核心的看家本领不理解它很多线上问题你连入手的地方都找不到。拿我自己的一次经历来说有回节点上有个 Pod 一直处于 Terminatingkubectl 删不掉。Kubelet 反复重试清理但容器进程没退干净cgroup 目录还在。用 kubectl 根本看不到下层的任务状态而 ctr 能看到对应容器和 task 还挂在系统里。这种时候如果不懂 Containerd 的生命周期管理逻辑你只能去重启节点代价就太大了。1.2 生命周期背后其实是一条任务状态链在 Containerd 的模型里一个容器对象和它的运行时任务是两回事。容器对象主要是元数据像是镜像、rootfs、配置这些信息的载体而 task 才是真正跑起来的进程实例。生命周期管理管理的就是这条状态链从 create 创建容器对象到 start 启动任务再到 running 运行、pause 暂停、resume 恢复最后 stop/exit 退出并 delete 清理。每一个状态都不是孤立的前面一步出错后面所有环节都会跟着乱。我把最常用的状态流转整理成了一张自己脑补的表实际排查时很有用状态含义常见触发命令created容器对象已创建task 尚未启动ctr containers create、ctr run 内部创建阶段running容器主进程正在运行ctr tasks start、ctr runpaused容器内所有进程被冻结ctr tasks pausestopped/exited主进程已退出task 标记为停止进程退出、ctr tasks killdeleted元数据和资源已释放ctr containers delete、ctr run --rm 自动清理这里最关键的一点是进程退出并不代表资源释放。工业级环境里的磁盘泄漏、僵尸 task、cgroup 残留绝大多数都是因为只看了进程状态没有管理完整的生命周期。后面我展开的每一条命令本质上都是在推动这条状态链往前走一步或者回退一步。2. 生命周期管理的第一道门槛命名空间与镜像准备2.1 命名空间不指定就是白忙活Containerd 里的命名空间namespace和 Linux 内核的 namespace 不是一个概念它是 Containerd 自己做的逻辑隔离用于划分容器、任务、镜像这些资源。Kubernetes 集群里 Kubelet 默认把所有容器的元数据都放在 k8s.io 这个命名空间下而 ctr 命令如果不加 --namespace 参数操作的是默认的 default 命名空间。我见过不止一次这样的场景同事在节点上执行 ctr run 起了个容器然后跑到另一台机器上怎么都找不见镜像原因就是 namespace 不同各看各的。用 ctr 排查节点容器状态几乎每条命令都得带上 -n k8s.io。比如ctr -n k8s.io containers list ctr -n k8s.io tasks list这里如果漏了命名空间列表空空如也你会误以为节点上什么都没跑实际上 Pod 的 pause 容器都在 k8s.io 下面挂着。查看所有命名空间的命令是 ctr namespace ls。排查问题前先确认命名空间是最基本也是最重要的一步而且这个习惯能帮你避免大量明明我创建了却看不到的乌龙。2.2 拉镜像与导入镜像的正确姿势镜像管理是生命周期管理的前置环节。镜像不对后面 create 时第一个报错就是 rootfs 没法准备。Containerd 拉镜像用 ctr images pullctr -n default images pull docker.io/library/nginx:alpine生产节点的 ctr 一般没配 Docker Hub 的认证拉镜像经常需要配置 registry 的 hosts 映射。Containerd 默认从 /etc/containerd/hosts.toml 或者插件配置读取镜像源配置这个和 docker 的 daemon.json 是两套体系不了解的人容易在拉取慢、拉取失败上耗很久。我见过的镜像拉取问题十有八九是 hosts 配置里的 endpoint 写错或者认证信息过期跟网络本身关系不大。还有一个常用姿势是离线导入ctr images import xxx.tar 和 ctr images export和 docker save/load 对标。我实操中一般是把 docker save 出来的 tar 包直接 ctr images import 进 Containerd大多数场景能成功。但要注意有些平台产出的镜像归档格式有差异导入失败时可以先用 ctr images ls 看看有没有产生半成品。镜像在生命周期里的价值是作为只读层Containerd 的 snapshotter 会根据镜像层创建容器可用的 rootfs后续每个容器的可写层都是独立快照。2.3 快照层和容器引用的关系理解快照才能理解为什么删除任务后磁盘空间不一定会马上释放。OverlayFS 是默认 snapshotter镜像的每一层都是一个只读层容器创建时基于镜像挂载一个 copy-up 的可写层。如果容器没删可写层当然还在可即使任务终止了容器对象还在那这个可写层快照也还会被引用。我清理过一次挺典型的问题节点容器已经被 Kubelet 删除但底层 overlay 层还挂在磁盘上df 显示空间快满了。查下来发现有些 container 对象处于 deleted 状态但是引用计数没归零。这种问题在生命周期管理里属于资源回收环节后面第五节我会专门讲怎么排查。这里先记住一条镜像和快照是两码事镜像没了不代表容器可写层没了反过来也一样。3. 从镜像到运行create、start 与 run 的关系3.1 一条 ctr run 命令背后的三步动作很多人以为 ctr run 是一条原子命令其实它背后是准备 rootfs → 创建容器对象 → 启动任务三步。Containerd 创建容器时会为镜像生成 OCI bundlebundle 里包含 config.json运行时配置比如 mounts、process、hooks和 rootfs 目录。然后 containerd 把 bundle 交给 runcrunc 按照 config.json 创建 cgroup、namespace 和 init 进程。从这个角度看ctr run 相当于 ctr containers create 加 ctr tasks start 的一次性封装。我在做问题定位的时候会故意拆开操作比如只 create 不 start方便查看容器对象的配置是否正确再手动 start。命令如下ctr -n default containers create docker.io/library/nginx:alpine demo ctr -n default tasks start demo拆开的好处是能定位到具体失败环节create 失败多半是镜像、快照、权限问题start 失败则是 runc 启动 init 进程时出的问题比如 cgroup 路径不存在、权限不够等。这个排查思路放到 K8s 集群里同样适用Pod Pending 和 ContainerCreating 是 create 阶段的问题而容器启动后立刻 CrashLoopBackOff那就要看 start 之后 init 进程为什么退出。3.2 前台容器与后台容器-d 和 --rm 怎么组合用 ctr run 跑容器时默认是前台运行也就是 ctr 命令本身会附着到容器的终端上CtrlC 会直接中断任务。这在调试时好用但不适用于后台任务。加 -d 参数后容器在后台运行ctr 命令立刻返回。还有一个我常用的参数是 --rm它表示容器退出后自动删除容器对象相当于生命周期走到了 deleted 自动释放。为什么单独强调这对组合因为 Containerd 没有 docker 那么聪明docker run -d 后你 docker rm 能删掉一个停止的容器但 ctr run 不加 --rm容器退出后就变成一个 stopped 状态的残留对象任务虽然没了容器对象和可写层快照都占着元数据。清理时还得手动 ctr containers delete 加 ctr snapshots 清理非常麻烦。所以我在临时跑测试容器时几乎必用 ctr run -d --rm就算崩溃退出也会自动把这堆东西清干净。另外ctr run 时要注意网络参数默认容器没有端口映射和 docker -p 的行为完全不一样。你想让容器使用宿主机网络就得加 --net-host想挂卷用 --mount typebind,src/host,dst/container。这些参数在设计时就决定了容器启动后的行为属于生命周期起点的一部分。3.3 进入运行中的容器task exec 的正确打开方式进入容器的命令不是 ctr exec而是 ctr tasks exec。这里有一个很容易踩坑的地方不指定 exec-id 会直接报错。Containerd 要求每次 exec 操作有个唯一的 exec-id用于标识这个 exec 出来的子进程。我一般用时间戳或者简单数字ctr -n default tasks exec -t -d --exec-id 1 demo /bin/sh-t 代表分配 tty-d 代表后台执行--exec-id 必须唯一否则会报exec-id already exists。如果你 exec 一个不带 -t 的命令比如想在容器里执行个 ls 看看环境输出照样打到你终端上。还有一点exec 是在已有 task 上创建新进程如果容器本身已经退出或处于 paused 状态exec 会失败提示 task 不可用。所以看到 exec 失败先查 tasks list 确认 task 状态再确认容器内有没有对应二进制。容器是精简镜像时经常没有 /bin/bash只有 /bin/sh用 bash 进不去不是权限问题是镜像里根本没装。我习惯在 exec 时写绝对路径比如 /bin/ls、/usr/bin/env因为 exec 子进程的 PATH 不一定跟容器 init 进程一样。3.4 停止任务kill 的信号顺序与超时问题停止容器生命周期的是 kill 命令但 Containerd 里的信号处理值得注意。ctr tasks kill 支持 --signal 参数默认发 SIGTERM给主进程一个优雅退出的机会。工业级容器的优雅退出依赖 app 对 SIGTERM 的处理比如 Java 应用要搞 ShutdownHookNginx 会处理 SIGQUIT 等。如果你直接发 SIGKILL等于不给业务进程任何告别时间可能导致状态文件损坏、事务没提交。我处理线上容器时通常分两步先发 SIGTERM 并观察几秒ctr -n k8s.io tasks kill --signal SIGTERM container-id如果长时间不退再发 SIGKILL。这和 Kubernetes 的 terminationGracePeriodSeconds 模型是吻合的一个合格的运维不会一上来就 SIGKILL。另外要注意任务退出后 container 对象并不会自动删除除非加了 --rm这也是为什么 K8s 里删 Pod 有容器清理环节Kubelet 会负责把元数据清掉。手动用 ctr 测试时记得把这两个状态都处理干净。4. 运行过程中的控制与观测pause、events、shim 与 checkpoint4.1 pause/unpause临时冻结容器的工业级用法Containerd 支持 ctr tasks pause 来冻结容器内所有进程底层是让 runc 使用 SIGSTOP 信号把进程树挂起。这个能力在工业环境里非常有用。比如说你要对容器所在的磁盘做快照、要临时停掉一个对外提供服务的进程去修复文件系统又不想销毁整个容器pause 一下是最安全的做法。恢复的时候执行 ctr tasks resume。注意 pause 不是 stop任务进程被挂起但还活着PID 和网络连接都还在只是不再被调度。所以如果你 pause 的是一台对外服务的容器它所在节点的端口监听还在只是不响应请求。我之前在迁移容器到另一台机器时就先把旧容器 pause 住等新容器起来再把旧容器 kill 掉停顿时间从分钟级降到了秒级。在 Kubernetes 里 kubelet 也会用 pause 容器来维持 Pod 的网络命名空间这也是为什么你在节点上能看到一堆处于 pause 状态的容器。4.2 用 events 捕捉生命周期事件Containerd 有一个事件系统类似 docker events命令是 ctr events。它会输出容器创建、任务启动、任务退出、容器删除等事件流。我调试生命周期问题时特别爱在另一个终端挂着事件监听然后执行 create/start/kill看事件是否按预期顺序出现。ctr -n k8s.io events有一次某个容器一直处于未清理状态我开着事件监听去 delete发现根本没有捕获到删除事件说明删除动作根本没下发到 Containerd问题在 Kubelet 那边。这个定位思路帮我快速切分了故障边界。事件流还可以配合 jq 解析把时间戳、事件类型、对象 ID 存下来做问题复盘。工业级环境里事件流是理解容器到底经历了什么的第一手资料比事后翻日志快得多。4.3 shim 进程生命周期的实际执行者在 Containerd 的架构里每个容器都有一个 containerd-shim 进程它负责和 runc 通信、维护容器内的 IO、上报状态。shim 进程不退出容器任务就能一直维持即使 containerd daemon 重启shim 也能接管继续管理容器。这是工业级容器高可用的重要设计。排查问题时shim 的日志很有价值一般位于 /var/log/containerd/ 下命名跟容器 ID 相关。runc 创建或启动失败的原因很多只能在 shim 日志里看到比如 device cgroup 权限、mount 失败、runc 参数不合法。我遇到任务启动失败时第一步不是反复执行命令而是直接看对应 shim 日志和 journalctl -u containerd。日志的时间戳和堆栈往往比返回的 error message 详细得多。如果把容器生命周期比作一条生产线containerd 是总控台shim 就是每个工位上盯着具体工序的工长。4.4 checkpoint/restore把生命周期暂停到磁盘除了内存级的 pauseContainerd 还支持 checkpoint/restore核心是 CRIU 技术。ctr tasks checkpoint 可以把容器的进程状态、内存内容、文件描述符全部写入磁盘之后在另一台机器上 restore 出来。这个功能在工业环境里主要用来做有状态服务的迁移、热升级和故障恢复。ctr -n default tasks checkpoint --image-path/tmp/checkpoint store demo ctr -n default tasks restore --image-path/tmp/checkpoint demo不过要提醒的是checkpoint 功能在 Containerd 里仍然偏实验性对容器内使用的网络、设备、卷挂载有一些限制。生产环境大规模使用前务必先在测试环境验证你那个特定业务镜像能不能 checkpoint 成功。我自己只用它做过短任务的迁移验证真要迁移有状态服务当前更现实的做法是搭配卷迁移和优雅启停而不是指望一条命令搞定一切。但理解 checkpoint 的存在能帮你判断生命周期管理不只是起停删它还包括把某个中间状态固化下来再恢复的维度。5. 生命周期常见问题与排查技巧实录5.1 容器一直卡在 created 状态不动这种情况多半是 create 成功了但 start 没成功或者 start 后 init 进程无法拉起。先看 tasks list 确认状态再看 shim 日志。常见原因包括runc 没有权限创建某些 namespace、cgroup 路径不存在、镜像 rootfs 权限异常。如果一个集群里所有容器都卡在 created优先怀疑节点上的 runc 版本和 containerd 不兼容或者节点 cgroup 驱动配置和 Kubelet 不一致。我处理过最诡异的一回是容器创建后一直停在 created查完发现是磁盘 quota 特性导致 overlay mount 失败。表面上看起来容器配置都没问题但 shim 日志里明确写着 mount 相关错误。这提醒我排查时永远先看日志再猜原因而不是盯着 API 返回的一些信息反复重试。对于容器长期处于 created 的场景不要反复 delete 再 create先定位根因否则大概率还是会卡在同一个位置。5.2 任务没了磁盘却不释放这个坑在前面已经铺垫过删除容器不等于释放快照。如果使用 ctr containers delete 删除了容器对象但快照层仍被引用磁盘空间不会回来。可以用以下命令检查当前活跃快照ctr -n k8s.io snapshots ls看到大量 orphan 快照无法对应到任何容器时就需要手动清理或者检查 Containerd 的 GC 插件是否正常工作。有些情况是容器对象已经处于 unknown 状态Kubelet 反复删除却删不动这时候我会检查容器对应的 task 是否还存在如果 task 已经不存在说明只是元数据没清可以用 ctr -n k8s.io containers delete -f 强制删除。需要特别提醒的是在 Kubelet 管理的命名空间下手工删容器属于危险操作一定要确认这个容器已经不是某个 ReplicaSet 正在维护的副本否则 Kubelet 会拉一个新容器而残留数据可能造成两个容器同时占用同名卷后果很麻烦。我在做这类操作前一定会先确认对应 Deployment 的期望副本数再决定要不要手工干预。5.3 exec 进不去不一定是命令写错了ctr tasks exec 失败提示 executable file not found 时很多人第一反应是检查容器是否正常运行。没错task 确实在跑但镜像里可能真的没有目标二进制。我会先 exec 一个容器里可靠的 shell比如 /bin/sh并加 -t 尝试如果还不行再看容器的 config.json 里有没有指定 runtime/user安全配置可能导致 exec 请求被拒绝。另一个隐藏坑是 PATH 环境变量。容器进程运行时设置的 PATH 不一定传递到 exec 的子进程里我 exec 时习惯写绝对路径比如 /usr/bin/env、/bin/ls这样能少踩很多环境变量的坑。还有一个常见的现象是 exec 命令本身需要终端但你忘了加 -t导致交互程序行为异常看起来就像命令跑不进去一样。先确认这些基础条件再怀疑 Containerd 本身的问题。5.4 误删容器对象引发的状态不一致线上如果不小心用 ctr containers delete 删了一个还在运行的容器你会发现容器进程可能还在跑但是 Containerd 已经不知道它了。这是因为 delete 只删了元数据没有 kill task。此时必须先 ctr tasks kill 杀掉任务否则进程会变成僵尸而且 cgroup 目录不会被清理后续同名容器也起不来。这个场景在 K8s 集群里要格外小心。Kubelet 通过 CRI 管理容器生命周期你拿着 ctr 绕开 CRI 删掉元数据Kubelet 的状态和 Containerd 状态就会出现分叉。遇到这种局面正确的恢复思路是优先恢复 CRI 视图实在不行再重启 Kubelet 让它重新 reconcile。我自己是不建议在生产节点上随意用 ctr 删除 k8s.io 命名空间下的容器对象的除非你已经很确定那个容器没有业务价值。5.5 用 ctr stats 观测容器运行状态的补充手法生命周期管理不只是控制状态的切换还包括运行期的持续观测。ctr stats 能输出容器的 CPU、内存、PID 等使用情况和 docker stats 对标的ctr -n k8s.io stats我看到这个命令输出后一般会顺手对比一下 cgroup 下的实际使用量确认数据一致。如果发现某个容器 CPU 使用率异常高但业务上又说不清原因我会结合 tasks list 里的 PID 去宿主机上执行 ps 看进程再用 ctr pprof 看 containerd daemon 自身的性能。工业级坑里有一类就是容器卡死但不像死现象是进程在跑就是不响应请求这时候 pause/resume 配合 stats 往往能快速判断是进程被冻结还是单纯负载过高。6. 工业级环境下管理容器生命周期的几条硬经验6.1 排查问题先看状态再动命令我在带新同事的时候常说一句话容器出问题第一件事不是去网上搜解决方案而是先看状态、看日志、看事件。ctr namespace ls、ctr -n xxx containers list、ctr -n xxx tasks list、ctr events 这四条命令几乎能覆盖绝大多数生命周期问题的表象。配合 shim 日志、journalctl -u containerd、kubelet 日志基本上能把故障收敛到一个明确的环节。动手之前想清楚你要推动哪个状态变化。create 卡住了你就不要去 killtask 没起来你就不要急着等 exit快照没释放就不要反复去 mount 检查。状态链的意识越强越少做无用功。有一次我排查就是太心急看到一个容器 state 是 unknown直接反复 delete结果把 Kubelet 正常重试的逻辑打乱了最后只能重启 Kubelet 恢复。后来我给自己立了条规矩非紧急情况下对 k8s.io 命名空间里的对象只读不写先用日志把原因定位清楚。6.2 手工测试容器请把生命周期走完整如果你只是想在节点上快速起个容器做验证建议直接这样跑ctr -n default run -d --rm --env MY_FLAG1 docker.io/library/alpine:latest test-alpine sleep 300验证完后用 ctr -n default tasks kill test-alpine 结束任务再用 ctr -n default containers delete test-alpine 清理对象。别看完输出就拍屁股走人残留的容器对象和快照会一点点吃掉磁盘。上线后养成习惯每次 ctr run 都带上 --rm省得后续手动补清理动作。完整的生命周期操作顺序我习惯是run 之前先确认镜像存在run 之后确认 task 进入 runningkill 之后确认容器进入 stoppeddelete 之后确认容器对象消失再配合 snapshots ls 确认快照被释放。这个流程看起来繁琐但工业级环境里就是这些琐碎检查帮你避开磁盘被写满、元数据残留这种慢性事故。6.3 我的排查命令组合拳最后分享一个我自己的排查习惯。我会把常用的排查命令攒成一段脚本每次节点异常就按顺序执行ctr -n k8s.io containers list ctr -n k8s.io tasks list ctr -n k8s.io snapshots ls journalctl -u containerd --no-pager -n 200这套组合拳能让我在几分钟内判断问题属于镜像、任务还是资源回收。如果日志中出现 runc 相关的 panic 或 OOM 字样再往下追 shim 日志。工业级容器运维说难也难说简单也简单无非是把生命周期里的每个环节都理解透再在排查时按状态链一步步走。Containerd 的命令看着朴素但只要你掌握了这套管理思路换到任何基于 OCI 标准的运行时环境你都能很快上手。
返回列表