同一份应用文件在本机运行时只是进程;打包为镜像之后也不会自动变为“正在运行的容器”。要解释一次请求为什么失败,先区分磁盘上的制品、运行时创建的对象、内核里的进程和应用的就绪状态。这四者不处在同一层;把它们都叫“容器”会使故障归因失去落点。

从制品到请求

OCI Image Specification 约定索引、描述符、配置和层等制品表示;Runtime Specification 约定 bundle 配置与执行环境。镜像的摘要标识内容,不是 PID;bundle 描述将要执行的命令与隔离、挂载等参数,不是已经建立的内核对象。运行时准备根文件系统、进程配置,创建或加入 namespace、应用资源及权限约束,最后让内核执行程序。一个处于不同 namespace 的 Linux 进程仍与同机进程共享一个内核;VM 内的 Linux 容器共享 VM 的内核,而非桌面宿主内核。

实际排查时需要沿对象逐一核验:镜像引用解析到哪个 digest;运行时对象的 ID 是什么;运行程序在宿主侧 PID 是什么;/proc/<pid>/ns/* 指向哪些 inode;/proc/<pid>/cgroup 落在哪个层级;服务的监听地址及一次请求的响应是什么。PID 是可能重用的临时号,不能独自承担对象身份。不同 namespace 中的同一个数字 PID 也不能直接判定为同一进程。节点若在虚拟机里,宿主侧 PID 指 VM 内的宿主,而非物理机 PID。

为什么“镜像里有这个文件”不足以解释请求

把源文件复制到镜像,镜像布局里会出现描述符和内容 blob;运行时还得选择和机器匹配的平台、核对并解压相应层,准备一个可供进程读取的根文件系统。运行时是否用 OverlayFS、其他 snapshotter 或传统目录,不由 OCI 层的 tar media type 唯一指定。因此“配置里写着 /app/probe_app.py”和“该路径在启动时的根目录下确实存在”是两次检查。即使路径存在,脚本所需解释器或动态链接库也可能不存在,执行会失败;一个合法的镜像摘要只能支持制品字节未偏离该摘要,不能证明可执行性。

进程建立之后,应用看到的根路径来自该进程的挂载与根目录状态。/proc/<pid>/root 可以帮助宿主侧查看其根路径,但权限、被观察进程是否已经退出、挂载 namespace 以及文件描述符保留情况都会影响观察。应用的工作目录或 bind mount 又可能覆盖镜像中的同名路径;观察某个文件存在时,必须进一步回答“它来自哪一个挂载源,当前容器删除之后谁仍持有这份数据”。这些步骤分别由 04、11 与 16 追问,不可一次性从镜像 digest 推导出最终写入位置。

PID、namespace、cgroup 怎样拼成同一行证据

操作一个尚在运行的实验容器时,先取运行时给出的对象 ID 与宿主 PID,再在同一个时间窗口打开 /proc/<宿主PID>/status、/proc/<宿主PID>/ns/ 和 /proc/<宿主PID>/cgroup。PID namespace 可能让进程在容器内报告 1 而宿主报告另一个数字,两个视图需要通过同一个实际进程对应,不能用数字相等作条件;/proc/<pid>/ns/pid 链接标识 namespace 对象,两个链接的 inode 相等只表示该类型视图相同,不能证明进程的内存或用户身份也相同。/proc/<pid>/cgroup 给出资源控制归属,实际限额还需读取对应控制器文件,不能因为看见某个层级路径就断定 CPU 被节流。

这个映射有竞争窗口:读取运行时状态之后进程可能立即退出,宿主 PID 后来又可能分配给不相关的新进程。可靠记录应一起保存容器 ID、进程启动时间、采样时间、namespace inode 和实际请求结果;无法取得时明确标出哪段关联仍待确认。只读到一次 ps 不能证明该进程确实产生了刚才看到的 HTTP 响应。容器内 /proc 的挂载也有独立前提:在新 PID namespace 中沿用旧 procfs,可能让内侧看到外侧的 PID 列表,这不意味着 PID 隔离根本没有发生。03 会用本机的实际受限案例具体拆解这种错位。

与 VM、rootless 的边界

Linux 容器并不引入另一个 guest kernel;多进程共享运行处的 Linux 内核和其系统调用接口。传统 VM 有自己的 guest kernel,却仍与其他 VM 共享底层硬件和 hypervisor,不应把“独立内核”夸大为“没有共享面”。容器运行在 VM 里时,容器里的 uname 与 VM 宿主相同不构成异常:桌面机器可能只是控制终端,它的版本与远端 daemon 所在的内核甚至不属于同一计算节点。

namespace 首先约束某类内核资源的可见性;user namespace 涉及 UID 映射和权限上下文;cgroup v2 的控制器限制或统计资源;capabilities、seccomp 和 LSM 则在不同环节约束可执行的操作。这些机制彼此不自动启用,不能从“容器有 PID 1”倒推出它有内存限额,也不能从 id 显示 root 倒推出它可写宿主系统目录。特别是 rootless daemon 与 Dockerfile 的 USER 不是同一个设置;00 只确定观察对象,后文再按一项一项实验核对边界。

实验基线:先检验观测对象

这里的命令使用同名目录中的最小源码包,依赖和入口见运行说明。在仓库工作区运行 sh examples/containers/env_probe.sh;再运行 python3 examples/containers/probe_app.py --state-dir /tmp/containers-my-own-state --port 18080,访问 /identity、/health 和 /ready。/identity 报告应用实际 PID、UID、namespace inode、cgroup 字段;uname -a 只证明当前进程看见的内核版本,不能独自辨认宿主与 VM。每次运行保留命令、退出码及清理动作,避免把教程截图当本机证据。

本环境的原始基线见环境日志,普通进程 HTTP 记录见服务日志。当前是 Ubuntu 24.04 用户空间、Linux 5.10.134 内核、cgroup v1 只读挂载;没有 Docker、containerd、runc、kind、ip 命令。普通探针的 /health 返回 200,但设置 1 秒就绪延迟后的 /ready 返回 503,两个状态并不冲突:前者说明处理程序还活着,后者表明它暂未满足本探针的就绪条件。日志中 HTTP 调用的退出码为 0 仅表明 curl 成功收到了响应,不能把 HTTP 503 当做零故障。探针后来被 TERM 终止,wait 得到 143;临时状态目录删除成功。

这两份记录不是一段预设答案:环境探针逐条执行 uname、id、/proc/self/cgroup、namespace 链接与工具查找,并保留每条命令的退出码;HTTP 探针则真的绑定 loopback 随机端口,分别请求活性、就绪、身份及状态。服务收到 TERM 后,外层 shell 的 wait 显示 143,而不应把这一数字直接翻译成“发生了 OOM”:发送信号的命令和运行环境在日志里有更直接的证据。服务器自己的日志时间采用环境默认时区,脚本额外记录 UTC 开始/结束,两个表面不同的钟面数字不能直接当作实验耗时。/state 返回空值只表示这次尚未写入状态文件,不表示所有 volume 都不持久。

这里尚无容器侧 PID、镜像 digest、容器 ID 和挂载关系,故“普通进程与容器进程对照”仍是 NOT_RUN。补跑需要专用 Linux VM 中安装并记录固定 Docker 或 containerd/runc 版本,再以同一探针构建镜像:记录构建产物 digest、inspect 返回的容器 ID 和宿主 PID,在两个视角分别读取 uname -a、/proc/self/ns/*、/proc/self/cgroup,请求相同端点,最后删除只属于实验的容器和镜像。不能用当前普通进程记录填充容器列。

一个反例:镜像存在却没有进程

拉取了镜像,即使摘要已校验,本机也可能没有任何以它为制品的运行进程;进程已启动却只监听 127.0.0.1,另一网络视图也不一定能访问。相反,探针还活着而 /ready 返回 503 是应用自定义状态,不意味着进程已退出。定位失败时先问“我在观察哪个对象”,再比较同一时间窗口的制品、运行时、进程与请求。namespace 隔离并非完整安全沙箱,限制资源和权限还需要后续章节的独立证据。

再考虑一个经常被混入“容器失败”的边界:命令行客户端与 daemon 分居不同机器。客户端上的 uname -r、/proc/self/ns/pid 是客户端本身的状态;服务端 daemon 创建的进程却在另一台 Linux 节点,按客户端数据解释服务端 cgroup 就从采集位置开始错了。实验记录应在每条命令旁标明执行位置,并把 daemon 版本与客户端版本分列。若状态检查只拿到镜像名没有 digest,还须记录其解析时刻,因为可变 tag 的指向变化可能使两次同名运行使用不同字节。上述反例都没有要求容器已经启动,因此能把排查的第一步放在制品与运行时层,而不是先修改应用代码。

最后再限定“成功”的范围:镜像 blob 的 hash 匹配仅说明读到的字节一致;运行时对象处于 running 仅说明某个执行环境和主进程已启动;应用自己的 /ready=200 说明该探针按当时定义报告就绪。业务数据是否写入预期卷、并发请求是否持续成功、终止时在途工作是否完成,都需要不同的检查。把四层状态贴在同一行但逐列保留未知值,比在一个“正常”单元格里同时暗示数据、网络、安全和可用性都已验收,更容易让下一位排障者找到缺失证据。

现象 观察对象 成立边界
镜像能查到,服务不可达 digest、运行时对象、宿主 PID 内容存在不保证已启动
PID 存在但请求失败 监听 socket、路由、就绪响应 PID 可重用;需关联容器 ID
内外报告同一内核 VM 边界和 uname 内核共享不等于 rootfs 相同

练习

  1. 先预测 x86_64 Linux 镜像是否可在任意 CPU 架构或 Windows 内核直接执行;再检查所选镜像索引的 platform 列表,并说明平台匹配与实际 ABI 兼容还差什么。
  2. 先预测容器进程退出后镜像 blob、容器可写层和 volume 是否都消失;再在专用实验 VM 分别检查停止、删除容器与删除镜像三个动作。没有 VM 时记录 NOT_RUN,不要用猜测填写结果。

系列导航与参考资料

本页为全系列入口。每篇材料标明实际能验证的层次;页面生成不代表容器实验通过。

进程与隔离:00 · 01 · 02 · 03 · 04 · 05 · 06 · 07 · 08 · 09

制品与存储:10 · 11 · 12 · 13 · 14 · 15 · 16

运行时:17 · 18 · 19 · 20 · 21

网络:22 · 23 · 24 · 25

Kubernetes 节点:26 · 27 · 28 · 29 · 30

证据与研究:31 · 32 · 33 · 34 · 35 · 36 · 37 · 38 · 39

选修:E01 · E02 · E03 · E04 · E05 · E06 · E07 · E08

下一篇:01:进程启动与退出。命令索引见Docker 完全指南,旧文中的示例输出不作为本篇实验结果。