容器 32:从错误症状回到失败层
启动失败的提示往往出现在最高一层:CLI、Pod condition 或 HTTP 超时。修复需要逆向定位首个出错环节。沿10 的 digest、17 的 bundle、24 的网络对象与28 的就绪状态逐层判断,先记录假设,再以明确证据排除。
“启动不了”本身不是故障层:Pod Pending 可能是调度等待、取不到镜像,也可能卡在挂载或 CNI;容器 Running 但 Service 超时则已经跨过了前面几个阶段。没有同一个 Pod UID、事件时间和实际 containerID,将某一层的日志套到另一层只会反复修错。每个反例先保存错误出现前后对象,确定第一个不可达阶段;修复时只改动造成这一次失败的输入,然后让该 Pod 名称重新创建并以新 UID 保存结果。
四种会混成一个“起不来”的情况
第一,镜像无法解析或 layer blob 缺失:还没有完整根文件,查看拉取响应与 digest。第二,平台不匹配:索引没该平台或启动可执行文件时产生架构错误,核对节点架构与目标文件格式。第三,运行时配置或挂载权限被拒:先找 OCI bundle/config、宿主权限与 errno,不能怪应用网络。第四,进程已经运行但监听 127.0.0.1、readiness 返回 503 或 Service 后端未更新:先比较 PID、目标地址、探针与真实请求。每类都要设计一条可被反例推翻的排除证据,不能只列故障关键词。
先分清控制面说的是哪一步
Pod phase 是摘要而不是错误分类;Pending 包含尚未完成调度、准备网络和获取镜像等情形,Running 指主容器中至少一个已运行或正在启动/重启,不承诺应用探针通过。kubectl get pod 显示的 STATUS 常把一个容器等待原因放在摘要列,那不等于完整 Pod phase。保存原始 JSON 中 .status.phase、各容器 state.waiting.reason/message、lastState.terminated、restartCount 和 conditions;然后按 Pod UID 查相关事件发生时刻。若新旧 Pod 使用相同名称,仅靠 kubectl describe pod 的一段最后事件可能把两次不同故障拼在一起。
固定阅读的 Kubernetes v1.33.4 pkg/kubelet/images/image_manager.go 的 EnsureImageExists 会处理拉取失败与退避,等待状态里的 ImagePullBackOff 是 kubelet 在失败之后再次尝试的一种结果,不是“Registry 已经响应了某个固定 HTTP 状态”的同义词。真实事件可能是鉴权被拒、仓库不可达、digest 不存在、限速或本地 unpack 错误;需连同镜像引用、imagePullPolicy、节点网络和运行时拉取日志查第一个失败处。这里不假设节点装着上述 tag 的 kubelet,源码只提供理解等待状态的实现阅读锚点。
本篇镜像负例用专用仓库的同一引用前缀和一个不存在的 digest,不删除 registry 的任何 blob;先用实际 digest 创建正常 Pod,再应用坏摘要。只有节点能够连上专用仓库且正常镜像确实存在时,坏摘要引出的拉取错误才有排除意义。两边都拉不下来,原因更可能在凭据、证书或网络,不能宣布“已复现 digest 丢失”;一个 ErrImagePull 字符串也不能证明 registry 缺 blob,需要保存运行时和仓库返回的原始错误。回退时替换为先前验证成功的 digest,检查新 Pod 的镜像 ID 与 HTTP 返回,而不是只观察镜像名字看起来相同。
入口执行失败不是镜像拉取失败
第二个负例在已验证可拉取的同一镜像上把 command 改成不存在的绝对路径,使入口不能执行。保存运行时创建/启动错误、Pod waiting.message 和事件;应用进程并未运行,kubectl logs 可能无应用 stdout,不能据此认定日志组件丢数据。相比之下,exec format error 可能由错误架构的 ELF、脚本缺少解释器或其它文件格式问题导致,不应只看一行字就断言平台选择出错。想验证架构不匹配必须另准备已固定 digest 的另一平台制品、核对 OCI index 的 platform、节点架构、镜像内文件格式以及仿真设置;没有这样的受控制品时本篇平台分支标 NOT_RUN,不拿“命令不存在”冒充架构实验。
有时应用启动后立即退出,Pod 又会显示类似启动不稳的症状。此时应区分 OCI runtime 报错发生在进程创建前、进程以非零码退出以及应用进程被 OOM 杀死:前者通常不会有真实应用 PID/应用日志,后两者可以结合 lastState.terminated.reason/exitCode、kubectl logs --previous 与 cgroup 内存事件继续细分。只盯最终 CrashLoopBackOff 看不出它第一次退出是配置错、读不到挂载、还是健康检查反复重启。保留第一次失败的事件、退出时间和重试计数,有助于找到错误发生顺序;退避间隔本身是重复失败后的结果,不是根因。
挂载成功与应用可写仍是两段
第三个负例沿用同一 probe-app,让根目录不可写的只读 emptyDir 挂到 /tmp/containers-state,并让 --state-dir 指向里面尚不存在的子目录。启动时 probe_app.py 需要创建子目录;若挂载成功而进程因此报只读/权限错误,才形成“进程已获得挂载,但应用启动初始化失败”的证据。若 Pod 一直卡在 FailedMount,说明失败发生得更早,应取 CSI/挂载事件,不能把从未执行的 Python mkdir 写成结果。取证要同时保留 volumeMounts.readOnly、实际 mountinfo、容器退出码、应用 traceback 与所用 UID/GID。修复只把同一挂载恢复可写,不降低整个节点的安全策略、也不改共享目录权限。
readOnlyRootFilesystem 与只读 volumeMount 还不是一个开关:前者约束容器根文件系统,挂入的卷可另有自身读写标志。把同一路径错误归因给安全上下文,往往是因为只读挂载已覆盖镜像原有的可写目录。恢复时先保证自己的卷确实是独立 emptyDir 而非别的 Pod PVC,再观察探针 /ready 和写入目标目录;若应用无需写入,启动成功也不能证明目录可写。对于 30 的 PVC 类问题,还需按照驱动和 fsGroup 能力排查,不能把这次 emptyDir 的权限结果外推到真实 CSI 卷。
进程存活但流量入口仍失败
第四个负例不碰镜像或写目录,只把 readinessProbe 的 URL 设为不存在的 /__c32_not_ready__。应用进程照常启动,/identity 仍可在 Pod IP 上直接返回;若探针持续得到 404,Pod Ready 为 false,Service 的 EndpointSlice 后端条件应相应变化。先保存真实 Pod UID、containerID、应用监听地址与直连 HTTP,再查探针状态和 EndpointSlice;如果 Pod 连 IP 都拿不到,或程序根本没监听 18080,就不能把它归因于错误 readiness 路径。Probe 失败从运行状态走到可服务状态这一步,没有逆转镜像已存在的事实;反复重拉镜像通常不会修复路径配置。
读者也可能遇到“readiness 一直成功,但客户端超时”:这时先确认客户端究竟请求的是 Pod IP、Service ClusterIP 还是外部入口,查后端对象、客户端 DNS、NetworkPolicy、跨节点路由与目标监听。localhost 在客户端 Pod 中只指向客户端自己,不能因为服务端在 127.0.0.1 上用 curl 成功,就断定其它 Pod 也能连上它。本篇不在共享网络安装插件或修改 Service 的全局转发;必要时按 29 的同名限域网络矩阵继续验证。四种失败只改变本篇 namespace 内的 Pod 清单,不能拼成“一个案例同时证明镜像、挂载、网络都失效”。
修复要回到同一层留一条成功链
在专用 namespace 先建立一只正常控制 Pod,保存 digest、UID/容器 ID、/identity 与 /ready;依次创建四只各自只有一个故障变量的 Pod,观察每只最早的失败及后续重试。修复不是在同一 Pod 上写一条说明,而是删去该错误 Pod,再以相同名字、相同已核镜像创建无故障替代,确认新 UID 与应用真实响应。若原始失败阶段是镜像,修复后镜像拉取成功仍不代表探针健康;必须继续到 HTTP。若原始失败是只读目录,要核对新进程确实完成 mkdir、持有正确挂载,而不是随手把挂载删掉让写入退回镜像可写层。
对照表只表达定位顺序,不能从单列现象倒推出唯一原因。四种受控失败清单、同名修复模板与专项清单校验在同名目录独立提供;按层取证、逐例回退与清理仅适用于专用 VM,继续复用 共用 probe-app 源码包。无 Docker/containerd/kind 时不能从配置预测固定错误文本或实验 HTTP 响应,所有真实故障注入保持 NOT_RUN。
如果拉取失败出现在宿主网络不通的节点,即使引用的摘要是故意错误,真正首先失败的仍可能是连接握手;要核对正常控制 Pod 在同一个节点成功拉到正常 digest 才能隔离摘要变量。若正常 Pod 被调度到另一个节点,先记录差异,不要从两个节点的结果推断仓库只拒绝坏摘要。挂载负例也要看应用有没有拿到进程 PID:没有挂载就没有 mkdir;有 PID 但初始化失败才是应用写权限路径。坏探针负例则相反,必须证明应用已经能从 Pod IP 回应,才能把 Ready=false 明确归到探针 URL,而非进程退出。每一例的“排除证据”要能把相邻层分开。
修复过程中还有重试与缓存的干扰:Kubelet 可能对镜像拉取和容器重启退避,节点也可能已有相同 digest 的本地镜像。修复 Pod 后保存 .status.containerStatuses[].imageID 而非只抄 Pod spec,记录第一次成功请求时刻;不同 imagePullPolicy 下“这次是不是再次走网络”要从节点事件/运行时原始记录判定。若调度器恰好将修复后的 Pod 放到另一个节点,缓存、CNI 和目录状态都会变化,不能把恢复全部归功于修改的那一行。对于探针,固定 HTTP 路径之后要等到新的 Ready condition 和 EndpointSlice 都更新,再从客户端发请求,不因为编辑了 YAML 就宣称服务已恢复。
入口执行失败和应用退出也要看容器重启次数、PID 是否短暂存在。OCI runtime 的 exec 阶段因文件不存在或权限被拒时,应用逻辑甚至没开始;Python 成功 exec 后在 mkdir 处失败时,应用日志可以保留异常,且 lastState.terminated 往往可以看到退出码。连接 127.0.0.1 的请求成功只验证这只 Pod 的本地回环路径;是否经 Service 可访问还要经过 Pod 监听地址、EndpointSlice 与网络策略。定位顺序的好处不是命令越多越好,而是每次读取的证据都足以否定一个错误层的假设。
每轮实验若未得到预期错误,不应修改记录让它看起来“成功”:保留原始事件、镜像 ID 和实际所处节点,标注这一次未复现的环境条件。尤其是镜像拉取可能受代理、缓存或仓库授权干扰;在排除这些变量之前,结论只能停留在可见的第一层失败。下一轮也应使用新 UID、明确开始时间和同一份可校验清单,避免把前一次退避事件算进本轮修复结果。
在专用实验 VM 用同一 probe-app 分别注入错误镜像摘要、缺失入口命令、只读目录、坏 readiness 路径;四种各由自己的 Pod 隔离,第一种确认镜像来源,其余三种使用相同正常镜像。平台不匹配另需固定第二平台制品才可测,不能混作入口命令故障。为每次保留 Pod UID、容器 ID、PID 是否存在、错误日志、修复后成功请求及清理记录;云端没有运行时或集群,以上全部 NOT_RUN,不把 10 的离线篡改检查混成容器启动错误。
| 现象 | 第一证据 | 误判风险 |
|---|---|---|
| image pull 失败 | 远端 HTTP、manifest digest | 不等于 PID 1 问题 |
exec format error |
ELF 平台与节点架构 | 不等于 CNI 错误 |
| 已 Running、访问超时 | 监听、路由与 endpoint | 不代表镜像不完整 |
练习
- 先预测入口命令不存在与镜像平台不匹配的首个失败日志位置;在专用 VM 分别运行并保存原始错误及清理确认。
- 先预测 Pod Running 但
/ready=503是否该重拉镜像;沿容器 PID、服务监听和 EndpointSlice 排除后再决定修复层。
系列总目录:从进程隔离到运行时与编排。






