Kubernetes Pod异常排查:从状态码与Events定位根因 Pod 异常问题排查这件事我在不同规模的集群里反复做过很多次慢慢发现一个挺反直觉的结论大多数人排查卡住不是因为命令掌握得不够多而是因为第一眼就盯错了地方。看到容器起不来下意识就去翻业务日志结果日志里空空如也看到 Pending就先怀疑网络不通实际上调度器压根还没给这个 Pod 分配节点。Pod 的状态字段本身已经把答案写在脸上了只是很多人跳过了这一步。这篇内容我打算把 Pod 异常排查拆成一条可复用的判断链路从状态码读到什么、怎么用事件定位到具体环节、不同异常各自的根因分布、以及哪些故障其实是配置写错造成的假象。适合已经能在集群里跑起工作负载、但遇到问题还习惯性重启试运气的同学也适合想把排查流程沉淀成团队规范的人。下面这些结论基本上都是我在真实环境里踩出来的不是文档的复述。1. Pod 状态是第一手线索先把异常翻译成可验证的假设Pod 异常排查最容易犯的错是把所有起不来当成同一类问题。实际上 Kubernetes 用一个很有限的状态集合已经把故障切分到了不同的责任方调度器、kubelet、容器运行时、镜像仓库、应用进程各自对应不同的状态表现。读懂这张映射表排查范围能立刻缩小一大半。1.1 七种高频状态分别指向谁我习惯在动手之前先做一次责任方判定用下面的对照关系快速缩小范围状态大概率责任方第一手要看的字段Pending调度器 / 存储绑定Events 里的 FailedSchedulingContainerCreatingkubelet / 运行时 / 存储Events 里的挂载与拉取记录ImagePullBackOff镜像仓库 / 凭据 / 镜像名Events 里的拉取失败原因CreateContainerConfigError配置注入引用的 ConfigMap/Secret 是否存在CrashLoopBackOff应用进程本身上一次退出码与--previous日志Running 但不 Ready探针 / 启动时序 / 依赖describe 里的探针失败事件Evicted节点资源压力Pod 的 status.reason 与节点状况这张表的价值在于它把哪里出问题和谁该负责直接绑定了。Pending 你去翻应用日志翻一整天也翻不出东西因为容器根本没被创建不存在日志反过来CrashLoopBackOff 你去查节点资源方向也偏了退出码通常会直接告诉你答案。1.2 为什么 Events 应该排在日志前面kubectl describe pod输出末尾那段 Events是我每次排查的第一站。原因很朴素Events 记录的是控制面和 kubelet 对这个 Pod 做过的所有尝试包含时间戳和失败原因而容器日志只覆盖进程已经跑起来之后的那一小段。容器根本没启动的情况下日志是空的但 Events 通常已经写满了重试记录。我一般会这样取事件按时间排序看整条时间线kubectl get events -n namespace \ --field-selector involvedObject.namepod-name \ --sort-by.lastTimestamp默认事件只保留一小时如果问题发生在一小时之前get events可能已经查不到了。这时候要去看集群的事件持久化方案或者退一步用describe里残留的那部分。这个时间窗口是我踩过坑之后才记住的——有一次排查一个隔夜的问题get events返回空我一度以为集群事件机制坏了其实是早就过期了。1.3 我自己用的三段式排查顺序长期下来我的顺序基本固定成三段中间不做跳跃状态与事件get pod -o wide看状态和节点describe看 Events 和 Conditions确定责任方。进程现场能进容器就进进不去就用--previous日志、kubectl debug临时容器、或者到节点上看运行时日志。横向印证同节点其他 Pod 是否正常、同 Deployment 其他副本是否正常、最近有没有发布或配置变更。第三步经常被忽略但它的性价比极高。如果同一个节点上所有新起的 Pod 都卡在 ContainerCreating那问题大概率在节点或运行时层面而不是你的镜像如果只有你这一个副本异常其他副本健康那基本可以锁定在配置差异或者这个 Pod 被调度到的那个节点上。这个横向对照的动作能省掉大量单点深挖的时间。2. Pending 卡住不动调度器到底在犹豫什么Pending 是最容易被误判的状态。很多人的第一反应是网络问题实际上调度器在这个阶段只做一件事判断有没有节点满足这个 Pod 的全部约束。它不做网络连通性检查也不拉镜像。所以 Pending 的本质是没有节点能容纳我或者我依赖的东西还没准备好。2.1 资源碎片与 requests 的整块占位效应调度是按requests而不是实际用量来算的。这件事听起来是常识但真正理解它的人不多因为它意味着资源利用率高不高和能不能调度上去是两件独立的事。举个具体的例子一个节点有 8 核上面跑了三个 Pod各 request 2 核实际使用率只有 15%。现在你要起一个 request 3 核的 Pod调度器一算8 - 6 2不够直接 Pending。你以为节点很闲调度器眼里它已经快满了。这就是资源碎片——空闲的 2 核零散在物理上凑不出连续的 3 核配额。排查时直接看事件里的数字它写得很清楚0/5 nodes are available: 5 Insufficient cpu.遇到这种情况我有三条处理路径按风险从低到高把新 Pod 的requests调到符合实际用量的水平前提是原来的值定高了给节点池扩容或者调整节点规格用优先级和抢占让低优先级的 Pod 先让位。第三条要慎用抢占会驱逐别人在有状态服务混布的集群里容易引发连锁反应。2.2 亲和性、污点与节点选择器的隐形门槛除了资源调度器还要过一遍各种约束条件。这几类约束在 YAML 里写的时候不觉得出问题的时候报错信息又特别长容易看漏。nodeSelector / nodeAffinity标签写错一个字符就匹配不上。常见的是节点打标签用了disktypessdPod 里写成disk-typessd。污点与容忍节点上有NoSchedule污点而 Pod 没写对应容忍就会被直接排除。很多托管节点池会给新节点打上临时污点等初始化完成才去掉这段时间新建 Pod 会短暂 Pending属于正常现象。Pod 反亲和设了requiredDuringSchedulingIgnoredDuringExecution的反亲和而副本数超过了可用的拓扑域数量多出来的副本就会永远 Pending。这类配置我建议一律用preferred除非你有非常明确的硬性隔离需求。2.3 PVC 未绑定导致的假调度失败有一种 Pending 特别有迷惑性事件里显示调度成功但 Pod 就是起不来或者报pod has unbound immediate PersistentVolumeClaims。这背后的逻辑是Pod 调度分为两个阶段先做节点筛选再走 volume binding。如果 PVC 的storageClassName指定了WaitForFirstConsumer的绑定模式那么 PVC 会等到 Pod 被调度时才真正绑定底层存储。这时候如果存储侧创建失败——配额不足、可用区不匹配、存储类不存在——Pod 就会卡在这两个阶段的中间。排查这类问题要分两路看kubectl describe pvc看 PVC 的事件以及存储侧的实际创建状态。我在一个跨可用区的集群里遇到过Pod 通过亲和性被调度到了 B 区节点但 PVC 的存储类只支持 A 区于是绑定一直失败。事件里只有一句模糊的绑定超时真正的答案在 PVC 那一侧。2.4 从调度器事件读出具体失败的插件如果事件里的FailedScheduling信息不够明确可以用下面的命令把整条调度失败原因完整打出来kubectl get pod pod-name -o jsonpath{.status.conditions[?(.typePodScheduled)].message}这条路径拿到的消息往往比describe里截断显示的更完整尤其是多重约束同时不满足的时候它会逐条列出每个节点的排除理由。我一般会把这段输出直接贴到排查记录里方便后续对比——同一个集群反复出现同一条排除理由说明资源规划本身出了问题而不是某次偶然。3. CrashLoopBackOff 与退出码容器为什么反复自己死掉CrashLoopBackOff 是存在感最强的异常状态因为业务方往往第一时间就能感知到服务不可用。但要注意这个状态名里其实包含两层信息容器确实反复退出了Crash以及 kubelet 在按指数退避策略拉长重启间隔BackOff。后者是保护机制不是故障本身。3.1 退出码是最直接的判据容器退出码遵循 Unix 惯例128 以上表示被信号终止具体是 128 加信号编号。我整理了一张常用对照退出码含义典型场景0正常退出一次性任务跑完主进程是脚本但末尾没阻塞1应用自身报错配置解析失败、依赖连接不上126命令不可执行权限位缺失、脚本没有 shebang127命令找不到入口命令在镜像里不存在、PATH 不对137被 SIGKILL 强制终止OOMKilled或 liveness 探针失败被杀139段错误底层库不兼容、原生扩展崩溃143被 SIGTERM 优雅终止滚动更新、缩容、手动删除查看方式很简单describe里Last State那一栏就有或者kubectl get pod pod-name -o jsonpath{.status.containerStatuses[0].lastState.terminated}这条命令会返回退出码、信号、起止时间和原因信息量比describe里压缩显示的更全。3.2 OOMKilled 的判断链路与 limit 估算137 不代表一定是 OOM它也可能来自 liveness 探针失败后的强制重启。区分方法是看reason字段OOMKilled才是内存超限其他情况通常是探针触发的。确认是 OOM 之后别急着调大 limit先搞清楚内存到底花在哪。我的顺序是看kubectl top pod确认内存曲线是缓慢爬升还是瞬间冲到顶。缓慢爬升通常是内存泄漏或者缓存无界增长瞬间冲顶多半是启动时一次性加载了大量数据。对比requests和limits的差值。差太大意味着这个 Pod 是 Burstable 类型在节点内存紧张时优先级低先调 request 让它更接近真实用量。检查运行时层面对内存的处理。以 JVM 为例堆大小如果用手写的固定值容器 limit 又小于堆 元空间 线程栈 直接内存那就必然在压力下被杀。这类应用我建议开启容器感知让运行时按 cgroup 限制自动算堆。有个细节值得单独提容器的内存指标口径和业务方在应用里看到的口径经常对不上。应用统计的是堆内内存cgroup 统计的是整个进程组包括页缓存。出现过不少次应用说只用了 60%容器却被判 OOM的情况差值基本都在堆外和缓存上。3.3 启动就退出的六种典型原因如果退出码是 1而且日志里只有几行就结束了我通常按这六类往下排入口命令写错command和镜像的ENTRYPOINT叠加使用导致最终执行的命令不是你预期的那个。command会覆盖镜像的 ENTRYPOINTargs会覆盖 CMD两者混用很容易出岔子。配置文件缺失或格式错误配置文件通过 ConfigMap 挂进来但 key 名字对不上挂载点是个空目录进程读到空文件直接退出。依赖服务不可达启动时要连数据库或配置中心连不上就os.Exit(1)。这类问题在滚动发布时特别集中。端口被占用用了 hostPort 或者 hostNetwork同一节点上端口冲突。权限问题以非 root 用户运行但要写日志目录或读挂载卷权限不足直接退出。资源限制过紧limit 设了很小的 CPU启动阶段被限流到几乎跑不动超过启动窗口被杀。3.4 用上一次的日志和临时容器抓现场容器正在重启的间隙里kubectl logs经常是空的。这时候必须加-pkubectl logs pod-name -p --tail200 kubectl logs pod-name -c container-name -p如果容器镜像里连 shell 都没有或者你想看的是文件系统里留下的现场比如写了一半的日志文件、生成的堆转储就要用临时容器kubectl debug -it pod-name --imagebusybox:1.36 \ --targetcontainer-name -- sh--target参数很关键它让临时容器共享目标容器的进程命名空间这样ps能看到应用进程能确认进程到底在不在、是什么状态。不加这个参数你看到的只是一个干净的空环境。这个能力在我排查容器还活着但服务没响应的问题时特别有用。4. 镜像拉取与配置注入那些看起来像网络问题的失败有一类问题特别容易被误诊成网络故障因为报错信息里经常出现连接超时、拒绝连接之类的字眼。但只要往下看一层往往跟网络本身没关系。4.1 ImagePullBackOff 的三层排查这个状态名的意思是镜像拉不下来kubelet 正在退避重试。真实原因通常落在三层里第一层镜像引用本身有问题。仓库地址拼错、tag 不存在、用了latest而仓库里只有具体版本。事件里会明确写manifest unknown或者not found。第二层凭据问题。私有仓库需要在命名空间里有对应的imagePullSecrets。这里有个高频坑Secret 建在了 A 命名空间Pod 跑在 B 命名空间照着文档抄配置的时候漏了这点报错是unauthorized或者no basic auth credentials。第三层节点到仓库的连通性。这一层才真正涉及网络。但即便如此也要先区分是全部节点拉不下来还是个别节点拉不下来。前者多半是仓库侧的策略或配额后者通常是那个节点的网络配置或者本地磁盘满了。describe里的失败原因写得挺直白我一般先扫一眼这几个关键词not found对应镜像名unauthorized对应凭据timeout对应连通性no space left on device对应节点磁盘。4.2 ConfigMap 和 Secret 缺失的两种表现配置引用错误会呈现两种完全不同的症状取决于引用方式以环境变量方式引用且标了optional: false默认Pod 直接进CreateContainerConfigError容器根本不会创建。这种其实算幸运的失败得干脆。以卷挂载方式引用如果 ConfigMap 不存在挂载会失败Pod 卡在 ContainerCreating如果 ConfigMap 存在但 key 缺失挂载点里就是空的进程读到空配置然后在启动时报错退出。第二种更麻烦因为它伪装成了应用问题。我的经验是看到应用报配置项为空先别改代码先去确认挂载点里到底有什么kubectl exec pod-name -- ls -la /etc/app-config/ kubectl exec pod-name -- cat /etc/app-config/app.yaml还有一个时间差问题需要留意以卷方式挂载的 ConfigMap更新后同步到容器里有延迟通常几十秒到一分钟量级而且已经以环境变量方式注入的值永远不会更新必须重建 Pod 才生效。我见过有团队改了 ConfigMap 却怎么都不生效最后发现是环境变量注入白折腾了半天。4.3 挂载失败的报错特征卷挂载相关的失败事件里通常长这样MountVolume.SetUp failed for volume config : configmap app-config not found Unable to attach or mount volumes: unmounted volumes[data], unattached volumes[data]: timed out waiting for the condition第二种是我遇到最多也最难缠的——云环境的块存储挂载超时。通常的原因是节点和存储卷不在同一个可用区、卷还被上一个 Pod 占用未释放、或者节点上的挂载点数达到上限。这类问题在 Pod 被频繁调度或节点异常下线后特别容易出现表现为新 Pod 一直卡在 ContainerCreating事件里反复刷挂载超时。处理这类问题我一般会先确认卷的当前挂载状态如果确认是残留占用清理掉旧 Pod 的挂载记录后就能恢复。这件事最好沉淀成运维手册因为它有一定的操作风险不适合临时发挥。5. Running 却不可用探针、启动时序与流量接入这一类最消耗排查精力因为从状态上看一切正常Pod 是 Running容器没重启日志也没报错但请求就是打不进来或者进来就 502。问题往往出在就绪的判断逻辑上。5.1 readiness 和 liveness 的职责边界这两个探针的语义差异我见过太多团队搞混readinessProbe 失败Pod 被从 Service 的 Endpoints 里摘掉不再接收流量但容器不重启。它表达的是我现在不能干活先别给我派活。livenessProbe 失败kubelet 直接杀掉容器并重启。它表达的是我卡死了救我一下。把两者用反的后果很严重。我曾经接手过一个服务把依赖检查数据库连通性写进了 liveness。结果数据库做了一次短时维护所有副本同时探针失败、同时重启重启后依赖还没恢复又失败又重启滚成了一片雪崩。依赖检查应该放在 readiness让流量绕开就行了不该触发重启。我的原则很简单liveness 只检查进程自身是否还能响应绝不检查外部依赖。5.2 启动慢的服务为什么必须用 startupProbe没有 startupProbe 的时候liveness 从容器一启动就开始计时。对于一个要跑 90 秒初始化的服务如果 liveness 的容忍窗口只有 30 秒那它会在初始化途中被反复杀掉永远起不来。这时候你会看到一个非常迷惑的现象日志每次都是打印到同一个位置就断了。startupProbe 的作用就是给启动阶段单独开一个窗口。在它成功之前liveness 和 readiness 都不生效。等它成功了才开始正常的健康检查节奏。5.3 探针参数怎么算容忍窗口 periodSeconds × failureThreshold再加上initialDelaySeconds。这个计算经常被忽略我拿一个实际的配置举例startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 10 failureThreshold: 12 timeoutSeconds: 3 livenessProbe: httpGet: path: /healthz port: 8080 periodSeconds: 20 failureThreshold: 3 timeoutSeconds: 3 readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 5 failureThreshold: 2这里的含义是启动阶段最多容忍 120 秒10 乘 12启动成功后liveness 最多容忍 60 秒readiness 最多容忍 10 秒。有个细节必须提醒timeoutSeconds要小于periodSeconds否则探针会在上一轮还没结束时就开始下一轮行为变得不可预期。另外探针的请求本身也会消耗资源在高负载时把periodSeconds设得太小会给服务增加不必要的压力甚至反过来导致探针超时。还有一个隐蔽的坑探针走的是 kubelet 所在的网络路径如果你的服务端口只监听了127.0.0.1而不是0.0.0.0探针会一直失败但你在容器里curl localhost又是通的。这种问题排查起来极其耗时因为现象和配置看起来都对。6. 网络与 DNSPod 内部视角的连通性验证网络问题排查的关键是站对位置。在集群外部 ping、在节点上 curl得到的结论都不能直接代表 Pod 内部的视图。要判断 Pod 的网络通不通就从 Pod 内部出发。6.1 从 Pod 内部出发的四条命令如果镜像里有工具我一般按这个顺序走# 1. 确认本机 IP 和路由 ip addr show ip route # 2. 确认 DNS 配置 cat /etc/resolv.conf # 3. 解析目标服务名 nslookup service-name.namespace.svc.cluster.local # 4. 确认目标端口可达 nc -zv target-ip port如果是精简镜像没有这些工具就用kubectl debug起一个带工具的临时容器用--target共享网络命名空间这样排查的就是同一个网络视图结论才可信。这一点很重要我见过不少人在一个独立的临时 Pod 里测网络然后把结论套到出问题的 Pod 上两者根本不在同一个命名空间、也不在同一批网络策略的覆盖范围内。6.2 DNS 解析慢和解析失败是两回事这两种情况的处理方式完全不同。解析失败报NXDOMAIN或SERVFAIL先确认服务名和命名空间拼写再确认目标 Service 是否真的存在且 Endpoints 非空。注意Service 存在但没有任何 Ready 的 PodDNS 会返回记录但连接必然失败这是两码事。解析慢这是最常见也最容易被归错因的问题。/etc/resolv.conf默认的ndots是 5意味着任何名字只要点数小于 5都会先被当作相对域名去逐个 search 域后缀尝试。一次对外部域名的解析可能要发出多轮查询才命中。在 DNS 压力大的集群里这会明显拖慢首次连接。优化方式有两种我一般优先用第一种spec: dnsConfig: options: - name: ndots value: 2或者直接用 FQDN 访问以点号结尾跳过 search 域展开。改ndots需要注意副作用如果你的应用在同一命名空间内大量使用短服务名访问ndots设得太小反而会增加解析失败的概率因为短名字不会被自动补全成集群域名了。这个参数没有通用最优值得看你的访问模式。6.3 网络策略与端口映射如果连接被拒绝而不是超时通常说明网络路径是通的但没有进程在监听。先确认容器内的监听地址0.0.0.0还是127.0.0.1再确认containerPort和实际端口一致。containerPort本身只是声明性的写错了不会阻止启动但会让依赖它的配置变得不可靠。如果是超时优先怀疑网络策略。现代集群里网络策略默认拒绝的场景很常见新加的服务如果没有对应的 ingress 规则表现就是连接一直挂着。判断方法很直接临时创建一个允许全部的策略验证一下如果通了就说明是策略问题然后按最小权限逐步收紧而不是把全放开当成长期方案。7. 我自己的排查清单与几个值得记住的教训下面这份清单是我现在开新集群或者带新人时都会给的一份按顺序走基本能覆盖八成问题get pod -o wide确认状态和所在节点describe看 Events、Conditions、Last State对比同节点、同 Deployment 其他副本是否正常logs -p看上一次退出前的输出确认配置引用ConfigMap/Secret/PVC是否存在且内容正确确认探针参数与启动耗时是否匹配从 Pod 内部验证 DNS 与端口连通性检查最近是否有发布、配置变更、节点事件关于教训有三个印象最深。第一个是退出码 137 的误判。有一次看到 137直接判定 OOM把 limit 从 2G 调到 4G结果还是 137。后来才发现是 liveness 探针配的timeoutSeconds1而服务在高峰期 GC 一次就要 1.5 秒探针超时被杀。调大 limit 完全无效改探针超时立刻解决。从那以后我看 137 一定先确认reason字段。第二个是镜像更新了但没生效。用固定 tag 加imagePullPolicy: IfNotPresent推了同名 tag节点上已有缓存于是永远不会去拉新的。这个组合看起来毫无问题实际是个稳定的坑。要么用不可变 tag如 commit hash要么明确设成Always。第三个是事件窗口过期。前面提过默认事件只保留一小时。我现在会在排查模板里固定加上一步如果是隔夜问题先去事件持久化系统里捞别在get events上浪费时间怀疑人生。还有一个习惯我想单独说每次排查完把根因和判断依据写进记录里。不是为了交差是因为 Pod 异常的根因分布是高度长尾的——同一个团队一年下来真正高频的就那么七八种剩下的都是偶发。把偶发的那部分记下来下次遇到类似症状能省掉大半天。我自己的记录已经攒了上百条其中至少十几次是直接照着旧记录复现出答案的。最后分享一个小技巧如果你在排查一个不确定原因的问题别急着改配置。先把当前的 Pod 完整 describe 输出和 YAML 存下来再动手。改完之后如果没解决你至少还有一个对照基准改完之后解决了你也得知道是哪一处改动起的作用。我见过太多人一口气改了五处配置问题好了但根本不知道原因下次照样抓瞎。