容器 18:containerd 管的是镜像、任务还是进程
内容引用与进程引用分开
按 containerd 的 Runtime V2 文档,运行时 shim 承担与任务相关的运行时集成,OCI runtime 可用于创建实际进程。content store 保存内容寻址的字节;snapshotter 为运行准备文件系统视图;task 对应运行中的进程生命周期。containerd 的 namespace 是其 API/元数据隔离维度,不能拿它代替 Linux namespace inode,更不能把它当 Kubernetes Namespace 的定义。一个镜像引用可以消失而正在运行的进程仍存在;停止任务也不会自动清空 registry 的原始镜像。
五类对象为何不能合并成一个“容器”
content store 中的 blob 由摘要标识,负责保存镜像 manifest、config 与层等字节;image 是某个名字/引用及其指向的目标 descriptor,不必等同于实际运行任务。snapshotter 将内容准备为可供任务使用的文件系统快照及挂载视图,具体后端可能不同;container metadata 记录一个长期存在的定义性对象;task 才与正在启动或运行的执行实例及其主 PID 关联。删除 image 引用不等于立即清除被其他引用持有的 blob,删除 task 也不等于自动删掉 image 元数据。运行一份镜像至少要跨过引用解析、内容读取、快照准备、container 定义和 task 执行这些对象边界,HTTP 请求则在这条链路之后才发生。
例如应用已经运行,管理员在测试 namespace 中删除某个自建 image tag:如果 container/task 仍在、PID 仍活着,业务请求可能继续得到响应;但这不能推断内容存储马上释放了所有层,是否可回收取决于其他引用和垃圾回收。反过来,停止 task 后镜像引用与已下载的 blob 仍可能存在,下一次创建任务可以复用内容,不应从“现在没有 PID”推断 registry 上的镜像也消失。两种实验需分别保留 image digest、content blob、snapshot key、container ID、task ID/PID 和两次 HTTP 请求结果;只贴 ctr images ls 的一行就无法解释进程为什么还在或消失。
containerd namespace 不是 Linux namespace
CLI 的 --namespace 选择 containerd API 中的命名空间,影响你在同一个 daemon 上查询哪些元数据与 task。Linux 的 /proc/<pid>/ns/pid、ns/mnt、ns/net 则是内核视图对象:前者是管理面作用域,后者是特定进程实际加入的内核 namespace inode。将 ctr -n lab tasks ls 返回空表直接解释成“宿主上已无实验进程”可能错在 CLI 选错了管理命名空间,也可能有已与管理对象失去正常关联的残留进程。需要同时查本次任务 ID、容器 ID、宿主进程树与内核 inode;Kubernetes Namespace 又是集群 API 的资源隔离维度,不能把它的名字直接填进 /proc/<pid>/ns 当 inode。
在专用 VM 里应为本系列新建独立 containerd namespace,并把每条 ctr 命令的 --namespace 写在原始记录里,避免误删别人的镜像或 task。即便专用 namespace 中对象清单为零,也应通过宿主 /proc 查属于自建工作负载的进程是否仍存活,并检查自建的 snapshot 与目录;不能在空表旁顺手清理系统上所有相同名字的进程。当前云端没有 containerd 或 ctr,所以这套交叉检查仍是 VM 补跑动作,不是本机已验证的命名空间清理。
shim 位于哪一段执行链
Runtime V2 文档将 shim 描述为 containerd 与具体 runtime engine 之间的通信和管理层。containerd 可通过 shim 接口创建、启动或停止 task,由 shim 调用适配的 OCI runtime 或其他实现管理实际进程;这解释了为什么 ctr 客户端进程退出不等于容器业务程序结束,也解释了直接运行 runc 与通过 containerd 管 task 时会多出管理进程与 RPC 边界。不要从“有 shim”自动推断每个 Pod 恰好一个 shim,文档说明实现可为单个容器或一组容器提供 shim,不同 shim 协议与版本的进程分组方式也不同。
要追“一次 task 启动”,先固定所选 containerd 版本、实际 runtime plugin 名称和 shim 二进制版本,获取 task 事件中的 container ID,再采集 shim 的宿主 PID、子进程树与 task 主 PID,核对两者何时创建以及退出后谁继续存活。单独一个 shim PID 不是正在运行的应用 PID,应用内部看到的 PID 1 也不必等于宿主 shim 的 PID。shim 的日志或退出状态也不能直接当作业务进程的退出码;需要用 ID、时间窗口和事件关联。
以 containerd v2.1.0 为静态源码对照
这里选择 containerd 源码 tag v2.1.0 研究 Runtime V2,而非假定本机安装了该版本。core/runtime/v2/task_manager.go 的 TaskManager.Create 先准备 bundle,再向 ShimManager.Start 请求 shim,接着通过 shim task 接口发起 task 的 Create;若 task 创建失败,源码对自有 shim/bundle 有回收路径。core/runtime/v2/shim_manager.go 中 ShimManager.Start 会根据 SandboxID 与 bootstrap 参数决定如何连接或启动 shim;其中还明示兼容不同 shim API 版本的分支。这说明“一个 container ID 直接对应一个新 shim 进程”的说法连选定版本的管理代码都未保证,必须看传入的 sandbox ID、实际插件和进程树。
同时注意源码中的 TaskManager.Create 先运行准备步骤,不等于用户应用的主程序已响应 HTTP;它在层次上与 OCI runtime 中的 create/start 不是同一对象模型。runc v1.3.0 的 create.go 是 OCI bundle 级入口,containerd TaskManager.Create 则先处理上层 task/bundle/shim 协作。要确定一次 VM 运行究竟在哪个 create 阶段失败,记录是 ctr 请求失败、shim 建立失败、task.Create 失败、还是用户 process exec 后立刻退出;这四处需要不同的日志与清理证据,不能一个“containerd 出错”抹平。
一条有源有终的正常实验
在专用 VM 安装并锁定实际 containerd、runc、snapshotter 与 shim 后,用本系列唯一的 probe-app 镜像 digest 做输入,先只列出自有 content 的 manifest/config/layer digest;准备自建 snapshot、container metadata,启动 task 并记录 ctr 请求/返回与对应事件。接着采样同一时刻的 container ID、task ID、主 PID、shim PID、/proc/<pid>/ns/*、cgroup 路径和探针 /identity 请求。若镜像 tag 可变,必须额外记录它解析到的 digest;若 probe 不就绪,task 可能仍存在,去查应用日志/状态而非立刻清空 content store。
停止 task 后先检查进程是否退出、shim 如何变化、snapshot/container 元数据是否仍在,再只删除本次创建的 image 引用,核对 task 与 content 的差异。清理顺序由自己创建的对象和版本 API 决定:先停止和等待属于本次任务的进程,再处理 container/snapshot/image 引用,最后检查自有临时目录和进程树;不删除共用 content blob 或其他 namespace 的对象。每一步保留命令、退出码与实际输出;“源码里有 delete 函数”不是这台 VM 清理成功的证据。
相同表象可以来自不同层
若 ctr tasks ls 没有目标任务但 ctr images ls 还有同名 image,最直接的判断是这两个对象处于不同生命周期,不能立即解释为镜像损坏。若 image 已按 digest 在 content store 中,快照创建因底层存储或挂载失败,任务根本没有机会启动,排障应定位 snapshotter 的具体错误。若 task 仍可查但探针无响应,还要确认它的 PID 是否存活、监听地址、网络 namespace 与应用 /ready 响应;只确认 shim 存在没有足够证据推出服务可用。相反,某个应用进程未退出而 CLI 列表暂时查不到,也可能是查询了错误的 containerd namespace,先核对 --namespace 再声称数据消失。
本机没有 ctr/containerd/runc 运行环境,所以 image 到 content、snapshot、task/shim、HTTP 这一整套链路仍 NOT_RUN。文章的可信结论只来自 Runtime V2 官方文档与固定 tag 的代码路径,没有真实 shim 数量、PID、资源占用、任务重启或删除结果。若专用 VM 最终安装的是与研究 tag 不同的版本,要重新取其源码并更新版本表,而不是把当前 tag 的分支当该 VM 的已验证实现。
shim 与进程数量的常见推断为何不可靠
官方 Runtime V2 文档描述 shim 可实现一对一或一对多;其中 io.containerd.runc.v2 会利用分组标识等条件组织容器,同一 Pod 的若干容器可以通过同一个 shim 入口管理,但这是该实现及调用方输入的条件性行为,不能推导出“所有 Pod、所有运行时、所有版本始终一 Pod 一 shim”。同一个 Pod 可能含 sandbox 与业务容器,containerd v2.1.0 的 ShimManager.Start 还会结合 SandboxID、bootstrap 参数、协议版本等决定复用已存在的通信地址还是启动新的进程。即使运行时想复用一个 shim,实际二进制及其版本、已存 sandbox 状态或崩溃后的恢复路径也会改变最终进程树。
因此实验表里要分四列:Kubernetes Pod UID(如果本次确实通过 CRI 运行)、containerd 管理命名空间与 task ID、宿主上真正运行的 shim PID、业务进程宿主 PID。没有集群时不要填一个臆造的 Pod UID;单纯用 ctr 在私有 namespace 启动两个独立 task,得到两个 shim 也不证明 Kubernetes Pod 一定如此。想研究分组就必须确认调度器/CRI 的 sandbox 身份确实作为调用输入送给相同的 shim 实现,并在同一时窗收集两个 task 到 shim 的关联证据。
shim 进程的存活也不能被当成“所有任务都在 running”:它可能为某个组维持 RPC 与 stdio 通道,而一个 task 已停止、另一个还在运行;也可能任务进程还在但 shim 或 daemon 正在异常恢复,导致 CLI 暂时查不到正确状态。故障排查要结合 task 事件、OCI runtime 状态、shim 日志与宿主 /proc,明确谁报告退出、谁真正退出。文档说它能管理进程,不等于某一个 shim 进程崩溃后所有容器具体怎样恢复;后者需要该版本实现源码及专用 VM 故障注入,不能在共享宿主上重启 daemon 做教程。
事件和对象列表能回答的问题不同
ctr images ls 是查询某个 namespace 中的 image 元数据引用,ctr content ls 或对应 API 展示 content store 条目,ctr snapshots 展示特定 snapshotter 维护的键,ctr tasks ls 展示 task 运行状态;它们没有单一列表能同时回答“从哪个 HTTP 请求到这个 PID”的全链路问题。事件记录了对象何时创建、启动或退出,但事件可能和读取当前列表存在采样时间差:刚采集 task ID 后进程就退出,随后再执行 ps 得不到同一 PID 是可能的正常竞态。为避免误配,保存请求起止 UTC、容器 ID、task ID、shim PID、宿主启动时间与 namespace inode,再把事件的时间戳与 HTTP 响应记录相互校对。
例如“镜像存在而任务不存在”:先核对你查的是同一个 namespace,再查是否 snapshot 准备失败、task 创建失败或应用运行后已退出。若日志显示创建 task 失败且 shim 有清理动作,content 仍可被其他容器使用;删掉 content blob 不会修复已经失效的 runtime feature 配置。反过来,“任务存在但镜像 tag 列表为空”:先看 task 运行时具体使用哪个 container/config/rootfs,是否仅从 registry 中删除了 name 引用,不能马上推论程序正从内存中神奇重建镜像。重启 daemon 的真实影响留给 21 在专用 VM 中按对象范围核查,本篇不把静态生命周期词汇当恢复机制。
一个错误链的四个不同返回点
第一种是内容检索失败:manifest 有引用而 blob 缺失或 digest 校验失败,snapshotter 不应凭空生成正确根文件系统。第二种是 snapshotter 准备失败:字节完整但无法在节点底层文件系统建立对应快照或挂载,task 还没开始。第三种是 shim 启动或 task.Create 失败:content/snapshot 可能已准备,运行时特性或 namespace/mount 权限不满足,需检查 shim、OCI runtime 及回滚的自建 bundle。第四种是应用 exec 后报错、进程退出或 /ready 为 503:task 可能曾经成功建立,前一段不应再背全部责任。分段保留错误位置、已建立对象清单及清理返回,才能从外部同样一条 503 精确回溯。
固定的源码片段只展示其中一段:TaskManager.Create 在准备 bundle 后启动 shim,调用 task service 的 Create 失败时有移除自身 shim 映射、调用删除或关闭 shim 的处理路径。源码有清理分支不表示在任何网络、文件系统和超时故障下均无残留;需要 VM 上刻意让自己的 task在安全可控的条件下失败,逐条核对根目录、shim socket、cgroup 与进程树。若没有这份可重现的失败原始输出,文章只能说“源码设计有清理路径”,不能写“已证明无泄漏”。
为什么元数据对象与内核对象的身份要双向核对
containerd container ID 和 task ID 是 API 中的身份,snapshot key 是文件系统视图实现的身份,内容 digest 是字节身份,内核 PID/namespace inode 则标识某一时刻实际进程与隔离对象。一个字符串即使外观看似相同,也不能不经核对就在这些系统间当作同一个对象。例如 container metadata 仍指向某 snapshot,而 task 已退出且旧 PID 在宿主被重用:拿旧 PID 的 /proc/<pid>/cgroup 填当前 container 的资源数据,可能采到完全无关进程。记录 task 创建/退出事件、PID 开始时间、shim 与 task 的父子关系,再从容器请求里采自己的身份,才能把管理状态落回内核观测。
这些关联原则延伸到 Kubernetes 节点时尤其重要。Pod UID 是集群对象身份,容器 ID 及 sandbox ID 通过 CRI 才关联到 containerd task;可能一 Pod 多容器,不能从 kubectl get pod 中一行 Running 就猜节点有一个 shim、一个业务 PID。26 篇才引入 CRI 与 kubelet 的实际链路;18 在没有专用 VM 的时候只按官方 Runtime V2 文档和固定源码描述可验证的映射方法,不提前填任何 Pod UID。
补跑和清理应限制在自己的 namespace
专用 VM 中先创建只用于容器系列的 containerd namespace,把它的名称、containerd socket 和 daemon/CLI 版本写入记录;用固定镜像 digest 创建自己的 snapshot、container 与 task。每次查询都显式带上 --namespace,并将所选 snapshotter 及 runtime handler 的配置写进原始输入,避免拿默认值代替真实插件。正常分支保存服务请求,再按“停任务并核对 PID → 删除自建任务/容器 → 清理自有 snapshot/镜像引用”的顺序逐项检查;异常分支只对自己创建的 task 注入有界失败,保存回滚输出。不停共享 daemon、不清空全局 content store,也不删除别的 namespace 的元数据。
退出码来自哪个命令也要注明:ctr 查询返回 0 只证明 API 请求被处理,不证明业务请求成功;HTTP 200 也不证明任务相关镜像引用仍存在;containerd --version 的字符串只能定位将要查阅的实现,不能代替源码及运行现象。自建实验中每份原始记录都要包含环境、输入、所有命令/退出码、实际 stdout/stderr、预期差异和清理列表;缺少对内容 digest、snapshot、task、shim、进程与请求的一条完整关联,就不达成本篇验收。当前云端只能交付来源与补跑入口,状态表继续保留 NOT_RUN。
关于“停 task 后镜像还在”的两种反例
在第一个 VM 场景里,先让探针正常接收一次请求,再停止任务,记录任务进程退出的事件和后续请求不能再由该 PID 处理;此时如果仍查到同一 image digest,只说明不同对象有不同生命周期,不表示容器“死而复生”。再新建 task,倘若同一 content blob 可以被复用,也应保存新 task ID 和新宿主 PID,不能把第二次正常请求回填到第一次运行的 PID。反过来,若测试时先删了 image name,daemon 可能仍有 image digest 的其他引用或临时内容租约;不要为了让 GC 发生就强制删除仍在运行任务所使用的 snapshot。磁盘剩余量没有立刻增加并不意味着 ctr images rm 失败,其退出码与具体回收条件应分列。
第二个场景通过在自建隔离 namespace 故意错误配置 snapshotter 或 runtime handler 来观察 task 失败前对象留下哪些残留,但必须保证不会改变全局 daemon 配置或其他 namespace 的执行路径。若需要重启 daemon 才能注入配置,就转移到独占 VM 或暂不执行,不能在共享云端操作生产默认服务。记录错误发生在拉取、快照 prepare、shim spawn 还是 task 创建,分别查询 content、snapshot、container 元数据和宿主进程,然后清理自己创建的资源。一个静态 README 上存在 TaskManager.Create 的 rollback 分支并不能替代实际失败清理:用户最关心的是这次出错后还有没有属于自己的进程或挂载。
何时需要检查旧文,何时要回到节点
已有的 kubelet 与容器运行时文章能够帮助理解 CRI 和 Pod 词汇,但不提供本篇的 containerd 版本、task ID 或真实 shim PID。读者要在节点上复核“镜像已存在而 task 失败”,先查本次 containerd socket 与 namespace,拿到具体 image digest;再查 snapshotter,确认是否成功解包和挂载;随后看所选 shim 是否实际创建 task,最后在宿主追 PID 与 cgroup。若旧文给出某个示例输出而当前 VM 无对应对象,只能把旧文作为背景,不可把那段示例迁入本次日志。不同节点内核、runtime 配置和镜像源都会改变故障发生的位置。
文档语言也要限制用途:Runtime V2 允许 shim+engine 或单一 runtime shim 等模式,而这里选的 containerd v2.1.0 仅是阅读源码的定点,它不等于本机安装版本。真正记录“一个 Pod 对应多少个 shim”时还要明确 CRI 插件版本、sandbox ID 传递方式、所选 shim binary 和这次 Pod 的容器数量;即使某一次观测恰是一比一,也不能推广到所有 Kubernetes 配置。把身份、版本和原始进程树作为判断条件,比围绕一个预设比例凑样例更能迁移到别的运行时。
最终的对照不是数进程名称,而是顺着自建探针的一次请求逐层查证:它属于哪个具体 image digest、该 digest 下哪些层字节已读取、由哪个 snapshotter 准备根文件系统、哪个 task 使用该根、哪个 shim 在本次运行管理此 task、应用 PID 在哪个 namespace/cgroup 里处理请求。没有专用 VM 时这些栏目为空,不以文档中的示意流程填入对象 ID。后续若只拿到一个失败的 ctr 返回码,也先判定它属于哪一段,而不是凭 daemon 名称把所有责任归在 containerd 内核管理上。
清理记录也按这些身份逐项完成:退出任务不能顶替删除镜像引用,删引用不能替代宿主 PID 核查,列出 snapshot key 更不能代表它已经卸载。只对本次命名空间中的自建对象执行相应清理,保留每一步的实际结果。
正常对照应固定 containerd 版本,从同一镜像 digest 找到 content blob、snapshot key、container ID、task PID、shim PID 与任务事件,并核对宿主 /proc 及 cgroup;然后先停 task,再删镜像引用,逐次检查哪些对象仍存在。边界例把某 Pod 错误描述成“一 Pod 一 shim”的普遍规律:不同运行时实现和分组方式会影响实际进程数量,必须先按选定版本源码/进程树核对。取证入口要求锁定 tag/commit、shim 实现与 runtime 配置;探针附件提供同一应用输入。
此云端无 ctr/containerd,不能检查所选源码是否与本机行为相符;版本与 shim 分组、task 对照均 NOT_RUN。本篇对官方 Runtime V2 文档和 containerd v2.1.0 固定源码做了 DOC_VERIFIED,不虚构进程树截图。旧文kubelet 与容器运行时是阅读入口,不是本篇实验。
| 现象 | 需核验 | 边界 |
|---|---|---|
| 删镜像后业务继续 | content 引用、task PID | 不等于磁盘 blob 已释放 |
| shim 数量异于 Pod 数量 | 实现版本、task/分组进程 | 不能硬设一对一 |
| 查不到容器 | containerd namespace | Linux namespace 仍可能存在 |
练习
- 先预测“删除 image 引用”和“关闭 task”各自对 HTTP 探针的影响,在专用 VM 分步执行并记录 PID、content、snapshot 与返回码。
- 先预测同一个节点的两个 Pod 一定有两个 shim 是否成立,先选定 runtime 与版本再采集进程树和 CRI ID,不跨实现推广。
上一篇:17:OCI runtime;下一篇:19:从 run 到 exec。
系列总目录:从进程隔离到运行时与编排。






