☰
Containerd容器生命周期管理:从创建到销毁的工业级实践
2026/9/30 3:38:18 网站建设 项目流程

一、内容整体设计与思路拆解

1.1 Containerd 在工业级环境中的定位

先聊一个很多人问过我的问题:为什么已经有了 Docker,还要在 Kubernetes 这一类编排系统里把运行时替换成 Containerd?答案其实不复杂——Docker 本身是一套“容器解决方案”,而 Containerd 是一个“容器运行时组件”。生产环境里跑着几百上千个节点时,我们需要的不是 Docker 那一整套 API、网络代理和 CLI 工具链,而是一个内核稳定、接口收敛、性能损耗尽量小的执行单元。Containerd 恰好就是这样一块“工业级零件”。

从项目标题里的 Containerd、工业级容器、container-lifecycle-management 这几个关键词就能看出,这篇博文的核心不是讲“怎么把镜像跑起来”,而是要深入容器的一生——从创建到销毁,中间经历启动、运行、暂停、停止,直到最后的资源回收。这套流程在单机 Docker 环境里体验不明显,因为 Docker CLI 把很多状态转换都给用户“代劳”了。但在工业级集群环境中,调度器需要精确掌控每个容器的状态,才能决定什么时候迁移 Pod、什么时候重启容器、什么时候把节点上的负载腾空。这时候,理解 Containerd 对容器生命周期的管理方式,就成了排查问题、优化性能、保障稳定性的基本功。

1.2 生命周期管理为什么值得单独拆出来讲

很多人以为容器生命周期管理无非就是 start、stop、rm 三个命令,实际操作一下就会发现不是这么回事。Containerd 的生命周期模型里,容器和任务(Task)是两个不同实体。容器负责保存配置、挂载点、镜像引用等元数据;任务则是容器里正在运行的进程实例。创建了容器,不代表进程已经启动;进程退出了,也不代表容器对象已经被清理。这个“容器—任务”双层模型是理解整个生命周期管理的关键,也是工业级容器运行时的核心设计。

更进一步看,容器停掉之后,rootfs 和容器层的数据仍然留在磁盘上。调度器和运维人员需要决定这些数据和配置是被保留还是被回收。网络方面,容器停止时 CNI 插件要清理已分配的 IP,虚拟网卡要摘除;存储方面,容器的读写层是否保留,决定了下一次创建时能否复用已有数据。这些细节层层嵌套,构成了“工业级”容器生命周期管理的完整图景。

1.3 这篇博文适合谁看

如果你已经在接触 Kubernetes 节点排障、操作系统连接不上容器运行时、或者频繁遇到 Pod 卡在 Terminating 状态,这篇内容能帮你建立从命令到原理的完整链路。如果你是刚开始接触云原生基础设施、还不清楚 ctr 和 crictl 有什么区别的初学者,这篇博文也会带你以最贴近生产环境的方式快速进入状态。

二、核心细节解析与实操要点

2.1 生命周期状态机的完整视图

Containerd 的容器生命周期不是简单的一条直线,而是一张状态转换图。理解这张图,排查问题会清晰很多。

第一个阶段是 Create。通过 ctr 或 CRI 插件创建容器时,Containerd 会基于镜像生成一个 rootfs,配置好 mounts、env、args,并在它的内部状态里记录当前容器为 created。注意这个阶段没有任何用户进程在跑,只是把“壳”准备好了。

第二个阶段是 Start。Containerd 调用 runc 去执行容器里的 init 进程。runc 完成 namespaces、cgroups、capabilities 的配置后,会把进程拉起,容器进入 running 状态。这里有个细节:如果容器里配置了 init 进程或自定义入口命令,Start 阶段会确保这些进程被正确挂到控制组里。运行状态下的容器通过宿主机上的 containerd-shim 进程保持连接,shim 是容器进程与 containerd 守护进程之间的桥梁,它负责转发信号、收集退出状态、管理 stdio。

第三个阶段是 Pause / Resume。使用 crictl pause 可以把容器里的所有进程冻结在 cgroup freezer 里,CPU 调度停止,但内存和文件句柄不释放。这个操作在给容器做快照或临时隔离时很有用,但需要注意,不是所有工作负载都能接受长时间暂停。

第四个阶段是 Stop / Kill。停止容器本质上是向主进程发送信号。默认情况下,Kubernetes 通过 CRI 发送 SIGTERM,等待默认的优雅退出时间后,再发送 SIGKILL 强制终止。这个设计是为了让业务进程有足够时间做退出前清理,比如关闭数据库连接、刷新日志缓冲等。

最后一个阶段是 Delete。删除容器时,Containerd 会回收 rootfs 和容器层、摘除挂载点、释放 cgroup、清理网络接口。删除之后,容器对象在 containerd 的 metadata 里被标记为 deleted,随后从存储中被移除。

2.2 容器与任务的双层模型

我最初用 ctr 的时候犯过一个错误:ctr c create 创建完容器,然后直接用 ctr c ls 查看,以为容器已经在跑了,直到看了半天才发现根本没有进程。因为 create 和 start 是分开的,create 只完成配置和 rootfs 准备,start 才会真正拉起进程。

在 containerd 的术语体系里,这个正在运行的进程对象叫 Task。容器是静态实体,任务是动态实体。你可以在同一个容器上停止任务、重启任务,容器对象本身不会变化,这就引出了一个实际应用:Kubernetes 里的重启策略,本质上是在复用容器对象的基础上重新创建和拉起 Task。

理解了这个模型,你在排查“容器停止失败”这类问题时,就能快速定位是 Task 没有正常退出,还是容器对象清理流程卡住了。有了这个诊断方向,再去检查 shim 进程、Cgroup 路径、挂载点状态就顺理成章了。

2.3 工业级场景下的状态约束

生产环境下,我们最怕的是“状态不确定”。容器到底是在启动中还是已停止?是在删除中还是删除失败?Containerd 用内部状态机约束了这些转换的合法性,例如:不能直接从一个运行中的容器执行删除,必须先停止;不能对一个尚未创建的容器执行启动;Pause 和 Resume 是互斥操作等。

我曾经在测试环境里用脚本循环删除和创建容器,结果有一天出现了大量状态异常的记录。查到最后是因为脚本绕过了 Stop 阶段直接调用了 Delete,Containerd 虽然能处理这种请求,但状态机的不一致会导致 GC(垃圾回收)任务无法及时清理底层资源。工业级环境的第一原则就是按状态机的合法路径操作,不要跳过中间状态。

三、实操过程与核心环节实现

3.1 工具选型:ctr vs crictl

Containerd 项目中自带两个客户端工具:ctr 和 crictl。它们并不是互为替代品,而是服务于不同场景。

ctr 是 containerd 的原生 CLI,能直接操作 containerd 的所有原生对象,包括 namespace、image、container、task。优点是功能全面,调试底层问题时很直观;缺点是它绕过了 CRI 插件的流程,直接向 containerd 核心 API 发起请求,所以用 ctr 创建的容器不会自动被 kubelet 感知到。

crictl 是 CRI 兼容的容器运行时调试客户端,它模拟的是 kubelet 通过 CRI 接口发起请求的行为。我们日常排查节点上的 Pod 和容器时,用的基本都是 crictl。它会把 Pod 抽象为 sandbox,把业务容器抽象为单一的 container,这与 Kubernetes 的视角完全一致。

建议:生产环境优先使用 crictl 排查问题;在学习和理解底层机制时使用 ctr。两个工具结合使用,既能看清 CRI 层交互,又能深入核心对象。

3.2 使用 ctr 完整演示一个容器的生命周期

为了方便演示,我用一个最小化的容器镜像,这里从公共镜像仓库拉取一个静态工具镜像,用于测试。实际操作时你会选镜像仓库里你自己的镜像或者公共镜像仓库的镜像,原理都是相通的。

先创建 namespace。生产环境中的租户隔离和组件隔离大多依赖 namespace,Kubernetes 的 Pod 和容器一般会在 k8s.io 这个 namespace 下管理,而系统组件则可能放在其他 namespace 中。执行以下命令创建独立 namespace:

ctr namespace create demo-lifecycle

拉取镜像:

ctr -n demo-lifecycle image pull docker.io/library/busybox:latest

创建容器:

ctr -n demo-lifecycle container create docker.io/library/busybox:latest demo-container

这时候容器已经创建完成,但运行状态的 Task 还不存在。你可以用下面这个命令验证:

ctr -n demo-lifecycle task list

执行后会看到列表里只有“已创建但未运行”的容器状态标识。下面创建并启动 Task:

ctr -n demo-lifecycle task start -d demo-container /bin/sleep 3600

命令成功执行后,容器进程在后台运行。再看一下 Task 列表,此时状态应该变为 running。接下来我们演示暂停并恢复容器:

ctr -n demo-lifecycle task pause demo-container ctr -n demo-lifecycle task resume demo-container

暂停时进程被冻结在 cgroup freezer,恢复后继续运行。最后停止容器:

ctr -n demo-lifecycle task kill demo-container ctr -n demo-lifecycle task delete demo-container ctr -n demo-lifecycle container delete demo-container

这个流程虽然简单,但踩过的坑不少。第一次用 kill 之后没有执行 task delete,导致容器对象始终处于“进程已退出但 Task 未清理”的状态。正确的顺序必须是:先 kill 停止任务,再删除 Task,最后删除容器对象。

3.3 使用 crictl 演示 Pod 沙箱与容器生命周期

接下来用 crictl 模拟 kubelet 的视角。首先创建一个 Pod 沙箱,沙箱就是一组共享命名空间的容器集合,对应 Kubernetes 里的 Pod:

crictl runp sandbox-config.json

然后准备好业务容器的配置,通过 run 命令创建并启动容器,指定 Docker 镜像和沙箱 ID:

crictl run container-config.json sandbox-id

crictl run 会自动完成容器创建、任务启动和网络配置。查看当前容器列表:

crictl ps -a

你会看到沙箱容器和业务容器两个条目。这里有个非常容易混淆的地方:Pod 沙箱本身也是一个容器,它运行的是 pause 容器。pause 容器的唯一作用是“占住”一组 Linux namespace,让同一 Pod 内的业务容器可以加入这组 namespace,形成共享网络和 PID 空间的单元。在生命周期管理中,Pod 删除时需要先删除业务容器,再删除沙箱容器,顺序不能乱,否则会导致 namespace 无法正常回收。

停止容器:

crictl stop <container-id>

删除容器和沙箱:

crictl rm <container-id> crictl rmp -f <sandbox-id>

-f 参数用于强制删除处于非活跃状态的沙箱。这个场景经常出现在节点维护和 Pod 驱逐时,理解这里就能明白 kubelet 在节点压力下执行驱逐时经历的状态转换路径。

3.4 生命周期中的存储与网络资源管理

容器生命周期管理核心不只是进程,还包括存储和网络。

存储方面,容器创建时,Containerd 会为容器准备 rootfs。rootfs 由镜像层和容器层组成,镜像层为只读,容器层负责运行时的写入。容器被删除时,默认情况下容器层会一并清理,磁盘空间得以释放。这就是为什么你用 docker 删除容器后,磁盘占用会降低,而工业级场景下我们要格外关注持久化数据卷的生命周期——卷的挂载点与容器生命周期解耦,删除容器并不会自动删除卷数据。

网络方面,每当沙箱创建时,CRI 插件会调用 CNI 配置的网络插件(比如 Calico 或 Flannel),为沙箱分配 IP 并创建网络命名空间。容器生命周期结束时,CNI 的 DELETE 操作会回收 IP 并清理虚拟网卡。如果容器删除后 IP 仍然被占用,基本可以推断是 CNI 调用链路或者第三方插件卡住了,这个思路对排查 IP 泄漏非常有价值。

3.5 状态查询与诊断命令速查

日常工作中,我使用频率最高的是下面这几组命令:

命令用途
crictl ps -a查看所有容器及其状态,包括已退出容器
crictl inspect <container-id>查看容器完整状态信息,包括 PID、挂载点、资源限制
crictl logs <container-id>查看容器日志
crictl stats查看运行时资源使用情况
ctr -n k8s.io task list原生视角查看 Task 列表,排查任务状态
ctr -n k8s.io container ls原生视角查看容器列表
ctr -n k8s.io c info <container-id>查看容器元数据详情

一个实用的排查思路:当 crictl ps 显示容器为 CONTAINER_EXITED,但业务进程在你的监控系统里仍然存在时,不要急着强制杀掉进程,先检查这个进程是不是容器外的残留进程,用nsenter或者ctr task list判断该 PID 归属的 cgroup 路径是否还挂载在已删除的容器之下。

四、常见问题与排查技巧实录

4.1 容器删除失败,卡在 Terminating

这是 Kubernetes 环境里最常见的生命周期问题。现象是 Pod 一直处于 Terminating 状态,几分钟甚至几小时都无法消失。

排查步骤可以按这个顺序走:

第一步,用 crictl 查看容器状态:

crictl ps -a | grep <pod-name>

如果容器状态显示为 CONTAINER_EXITED 但删除卡住,大概率是 Containerd 在等待 kubelet 确认,或者是 CNI 资源清理没结束。

第二步,检查 containerd 的日志。多数情况会看到错误信息显示任务删除超时或 mount point 仍然被占用:

journalctl -u containerd --since "10 minutes ago"

第三步,去宿主机上检查容器对应的 cgroup 路径是否还在。容器删除后,cgroup 应该自动清理。没有清理的话,手动查看并处理挂载:

cat /proc/mounts | grep <container-id>

4.2 停止容器时优雅退出超时,进程被 SIGKILL

业务容器停止时,kubelet 会给容器一个优雅退出时间,默认是 30 秒。如果进程在超时时间内没有退出,kubelet 会发送 SIGKILL 强杀。这个问题常见于业务进程没有处理 SIGTERM 信号,或者存在不可中断的内核线程。

从生命周期管理的角度,建议业务镜像内部实现退出钩子,确保 SIGTERM 到达时业务代码能够主动关闭连接池、落盘未完成的数据。底层基础设施层面则要确保 containerd 和 runc 的版本不低于当前发行版的支持基线,避免因 shim 阻塞导致 SIGKILL 延迟。

4.3 沙箱逃逸与顽固的 pause 容器

crictl ps 里看到一个 pause 容器一直处于 Running,对应的业务容器却全部退出了。这个状态并不一定异常,因为 Pod 的沙箱生命周期可以比业务容器更长。但如果一个 Pod 已经被删掉很长时间,暂停沙箱仍然存在,那就要动手清理了。

此时先利用 crictl inspect 查询沙箱关联的 CNI 网卡和 IP,再去 CNI 插件侧检查对应的 IP 是否还需要回收。确认不会有业务流量后再执行强制删除。工业级运维规范通常要求任何生产节点的强制删除操作都记录操作人、时间和原因,这是避免误伤的重要手段。

4.4 镜像层和容器层占满磁盘

容器生命周期管理的末端是资源回收。如果节点上频繁创建容器、Taint 和驱逐,但磁盘空间越用越满,就要排查 Containerd 的镜像 GC 和高位垃圾回收配置。Containerd 提供了 image GC 和 container GC 两个机制,相关配置位于 /etc/containerd/config.toml。合理的 GC 配置能自动清理未被容器引用的镜像层和悬空容器。

我曾经在一个高负载节点上配置了较短的生命周期回收周期,结果反过来影响了大镜像的正常拉取。调整之后采用分时回收策略,在业务低峰期触发回收任务,磁盘占用和拉取性能同时得到保证。这个平衡需要根据你的业务负载特征持续观察,不存在一个适合所有集群的通用数值。

五、踩过几次坑之后的个人体会

Containerd 的容器生命周期管理,说到底是围绕状态、资源和时间三件事的博弈。状态要清晰,每一步都要合法可追踪;资源要可控,网络、存储、CPU、内存都要随生命周期正确回收;时间要敏感,优雅退出要配置合理,GC 任务要避开业务高峰。

我在实际生产运维中最大的体会是:永远不要在生产节点上绕开 CRI 层去操作核心对象的生命周期。ctr 能修改底层状态,但不会同步通知 kubelet。一旦两边状态不一致,排障成本远高于临时操作节省的时间。如果你维护的是 Kubernetes 集群,优先用 crictl;如果你在深入研究和学习 Containerd 的核心设计,ctr 是不可或缺的工具。两者配合,才能在工业级容器环境里做到既懂原理又处变不惊。最后再分享一个小技巧:在关键操作前给 containerd 的日志目录开启持久化,排障时先看时间线,再定位对象,很多生命周期问题的答案都藏在事件顺序里。

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

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

立即咨询