【K8S 运维实战】07-Pod生命周期与故障排查

Pod 生命周期与故障排查全景

一句话定位:Pod 状态机 + Exit Code + 故障决策树,一图打通所有"为什么不 Ready"。

写在前面

我见过太多新人对着一个 CrashLoopBackOff 的 Pod 干瞪眼:重启策略设了 Always,它就一直重启;kubectl describe 看了一堆 Event,看不出所以然;kubectl logs 是空的,因为容器根本没起来。最后只能 Google "CrashLoopBackOff 怎么办",照着试一遍。

其实 Pod 的故障排查是有套路的。K8s 把容器生命周期拆成了非常清晰的状态机:Pending → Running → Succeeded/Failed,加上 ContainerState(waiting/running/terminated)和 RestartPolicy 的组合,几乎所有故障都能归到几类里。你只要会读这几个字段,80% 的问题一眼就能定位方向。

这篇把 Pod 生命周期从状态机讲起,覆盖 Init Container、Sidecar(1.28+ stable)、Exit Code 释义、常见故障决策树、优雅终止。最后给一份能直接打印贴墙上的"故障排查决策树"和 Exit Code 速查表。

核心问题

  • Pod 一直 Pending,到底是调度问题还是镜像问题?
  • CrashLoopBackOff 和 ImagePullBackOff 怎么区分、怎么定位?
  • Exit Code 137 是什么意思?是 OOM 还是被人 kill 的?
  • Init Container 卡住、Sidecar 不就绪怎么排查?
  • 怎么让 Pod 优雅终止,不掉流量、不丢数据?

一、原理剖析

1.1 Pod 状态机:Phase 与 ContainerState

Pod 的状态有两层:外层 Phase(粗粒度,5 种),内层每个容器的 ContainerState(细粒度,3 种)。

复制代码
Pod Phase (status.phase)
┌───────────┬──────────────────────────────────────────────┐
│ Pending   │ apiserver 收到对象,但还没跑起来(调度/拉镜像)│
│ Running   │ 至少一个容器还在跑(或正在启/停)            │
│ Succeeded │ 所有容器正常退出且不重启(仅 RestartPolicy=Never/OnFailure)│
│ Failed    │ 所有容器都退出且至少一个失败                 │
│ Unknown   │ 通不到 kubelet,通常是节点失联               │
└───────────┴──────────────────────────────────────────────┘

每个容器的 ContainerState (status.containerStatuses[].state)
┌────────────────────────────────────────────────────────┐
│ waiting    { reason, message }  等待启动(拉镜像/初始化)│
│ running    { startedAt }         正在运行               │
│ terminated { reason, exitCode, signal, startedAt, finishedAt }  已退出│
└────────────────────────────────────────────────────────┘

关键认知 :排错时不要只看 phase。一个 phase=Running 的 Pod,可能某个容器正在 waiting(ImagePullBackOff),只是因为另一个容器还活着,所以整体 phase 还是 Running。真正的故障信息在 containerStatuses[].state 里。

1.2 RestartPolicy 与退出的关系

RestartPolicy 决定容器退出后 kubelet 怎么处理:

复制代码
┌──────────────┬─────────────────────────────────────────────────────┐
│ Always       │ 无论成功失败都重启(Deployment/StatefulSet/DaemonSet 默认)│
│ OnFailure    │ 仅失败时重启(CronJob 的 Job 模板常用)              │
│ Never       │ 不重启(Job 跑完就完;排错时临时设为 Never 方便看现场)│
└──────────────┴─────────────────────────────────────────────────────┘

注意 RestartPolicy 的重启是指数退避 :CrashLoopBackOff 不是 K8s 主动杀的,而是 kubelet 在重试间隔上做了退避------第一次 10s,第二次 20s,第三次 40s,最大 5 分钟。所以你看到 CrashLoopBackOff 时,它其实在"等下一次重启"。
#mermaid-svg-MezkwUoVBpwCnuP3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MezkwUoVBpwCnuP3 .error-icon{fill:#552222;}#mermaid-svg-MezkwUoVBpwCnuP3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MezkwUoVBpwCnuP3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MezkwUoVBpwCnuP3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 .marker.cross{stroke:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MezkwUoVBpwCnuP3 p{margin:0;}#mermaid-svg-MezkwUoVBpwCnuP3 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-MezkwUoVBpwCnuP3 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-MezkwUoVBpwCnuP3 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-MezkwUoVBpwCnuP3 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-MezkwUoVBpwCnuP3 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-MezkwUoVBpwCnuP3 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-MezkwUoVBpwCnuP3 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MezkwUoVBpwCnuP3 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-MezkwUoVBpwCnuP3 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-MezkwUoVBpwCnuP3 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-MezkwUoVBpwCnuP3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MezkwUoVBpwCnuP3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MezkwUoVBpwCnuP3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MezkwUoVBpwCnuP3 .edgeLabel .label text{fill:#333;}#mermaid-svg-MezkwUoVBpwCnuP3 .label div .edgeLabel{color:#333;}#mermaid-svg-MezkwUoVBpwCnuP3 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-MezkwUoVBpwCnuP3 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-MezkwUoVBpwCnuP3 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-MezkwUoVBpwCnuP3 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MezkwUoVBpwCnuP3 .cluster-label,#mermaid-svg-MezkwUoVBpwCnuP3 .nodeLabel{color:#131300;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-MezkwUoVBpwCnuP3 .note-edge{stroke-dasharray:5;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-note text{fill:black;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagram .edgeLabel{color:red;}#mermaid-svg-MezkwUoVBpwCnuP3 #dependencyStart,#mermaid-svg-MezkwUoVBpwCnuP3 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-MezkwUoVBpwCnuP3 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MezkwUoVBpwCnuP3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} API 创建
调度成功 + 镜像就绪
等待调度/拉镜像
容器崩溃,kubelet 按 policy 重启
容器退出 0 且 policy!=Always
容器非0退出且 policy=Never
反复崩溃,退避等待
退避结束再试
Pending
Running
Succeeded
Failed
CrashLoopBackOff

1.3 Init Container 与 Sidecar(1.28+)

Pod 可以有 init container(在主容器之前跑,必须全部成功才进主容器)和 sidecar container(1.28 GA 的原生 sidecar,restartPolicy: Always 的 init container)。

复制代码
Pod 启动时序(有 init + sidecar + 主容器)
时间轴 ─────────────────────────────────────────────────>
    
    [init container A]      必须退出 0
            ↓
    [init container B]      必须退出 0
            ↓
    [sidecar container] ──────── 持续运行(Always)
    [main container] ──────┘  并行启动
    
    主容器退出 → sidecar 被 SIGTERM → Pod 终止

Sidecar 的坑(1.28+ 新特性) :老版本里 init container 跑完就退,sidecar 靠 hack 实现(比如 sleep infinity)。1.28 之后用 initContainers + restartPolicy: Always 表示原生 sidecar,它会和主容器并行跑。但要注意:sidecar 没就绪会阻止主容器启动(默认行为),所以 sidecar 的 readinessProbe 要设好。

1.4 Exit Code 释义

Linux 进程退出码 0-255,K8s 直接透传容器的 exit code。高频的几个:

Exit Code 含义 常见原因
0 正常退出 任务完成(Job)
1 通用错误 应用代码抛异常未捕获
2 shell 误用 命令语法错(脚本类容器)
126 命令不可执行 权限不对或不是可执行文件
127 命令未找到 镜像里没这个二进制(entrypoint 写错)
128 无效退出参数 exit 后没跟数字
137 SIGKILL(128+9) OOMKilled 或被 kill -9
139 SIGSEGV(128+11) 段错误,程序 bug
143 SIGTERM(128+15) 正常终止信号收到
1-255 应用自定义 看应用文档

137 是最常被问的。它分两种情况:

  • OOMKilled :内核 cgroup 内存超限杀的,会在 status.containerStatuses[].lastState.terminated.reason 里写 OOMKilled
  • 外部 SIGKILL :比如 kubectl delete pod --force 或 Docker daemon 重启。reason 不会是 OOMKilled。

区分方法看 describe:

bash 复制代码
kubectl describe pod <pod> | grep -A5 "Last State"
# 如果是 OOMKilled:
#   Reason:      OOMKilled
#   Exit Code:   137
# 如果是外部 kill:
#   Reason:      (空或 Error)
#   Exit Code:   137

二、实战操作

2.1 环境准备

准备一个故障演示用的 Pod 集合,方便复现:

yaml 复制代码
# fault-pods.yaml ------ 故障演示 Pod 集合
---
apiVersion: v1
kind: Namespace
metadata:
  name: fault-demo
---
# 1. CrashLoopBackOff:启动即崩溃
apiVersion: v1
kind: Pod
metadata:
  name: pod-crashloop
  namespace: fault-demo
spec:
  restartPolicy: Always
  containers:
  - name: app
    image: busybox:1.36
    command: ["/bin/sh", "-c", "echo starting; exit 1"]
---
# 2. ImagePullBackOff:镜像不存在
apiVersion: v1
kind: Pod
metadata:
  name: pod-imagepull
  namespace: fault-demo
spec:
  restartPolicy: Always
  containers:
  - name: app
    image: nginx:not-exist-tag-12345
---
# 3. OOMKilled:内存超限
apiVersion: v1
kind: Pod
metadata:
  name: pod-oom
  namespace: fault-demo
spec:
  restartPolicy: Always
  containers:
  - name: app
    image: polinux/stress:1.0.4
    resources:
      limits:
        memory: "64Mi"
    command: ["stress", "--vm", "1", "--vm-bytes", "128M", "--vm-hang", "1"]
---
# 4. Init 卡住:init container 失败
apiVersion: v1
kind: Pod
metadata:
  name: pod-init-fail
  namespace: fault-demo
spec:
  restartPolicy: Always
  initContainers:
  - name: init-db
    image: busybox:1.36
    command: ["/bin/sh", "-c", "echo waiting db; exit 1"]
  containers:
  - name: app
    image: nginx:1.27
---
# 5. 就绪失败:readinessProbe 不过
apiVersion: v1
kind: Pod
metadata:
  name: pod-notready
  namespace: fault-demo
spec:
  restartPolicy: Always
  containers:
  - name: app
    image: nginx:1.27
    readinessProbe:
      httpGet:
        path: /health
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 5
bash 复制代码
kubectl apply -f fault-pods.yaml
# 等待 30 秒,让故障状态充分暴露
sleep 30
kubectl get pods -n fault-demo -o wide

2.2 故障排查决策树(mermaid)

把下面的图打印出来贴墙上,遇到故障按图索骥:
#mermaid-svg-c8L4cjkD8jYjMntn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c8L4cjkD8jYjMntn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c8L4cjkD8jYjMntn .error-icon{fill:#552222;}#mermaid-svg-c8L4cjkD8jYjMntn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c8L4cjkD8jYjMntn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c8L4cjkD8jYjMntn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c8L4cjkD8jYjMntn .marker.cross{stroke:#333333;}#mermaid-svg-c8L4cjkD8jYjMntn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c8L4cjkD8jYjMntn p{margin:0;}#mermaid-svg-c8L4cjkD8jYjMntn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c8L4cjkD8jYjMntn .cluster-label text{fill:#333;}#mermaid-svg-c8L4cjkD8jYjMntn .cluster-label span{color:#333;}#mermaid-svg-c8L4cjkD8jYjMntn .cluster-label span p{background-color:transparent;}#mermaid-svg-c8L4cjkD8jYjMntn .label text,#mermaid-svg-c8L4cjkD8jYjMntn span{fill:#333;color:#333;}#mermaid-svg-c8L4cjkD8jYjMntn .node rect,#mermaid-svg-c8L4cjkD8jYjMntn .node circle,#mermaid-svg-c8L4cjkD8jYjMntn .node ellipse,#mermaid-svg-c8L4cjkD8jYjMntn .node polygon,#mermaid-svg-c8L4cjkD8jYjMntn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c8L4cjkD8jYjMntn .rough-node .label text,#mermaid-svg-c8L4cjkD8jYjMntn .node .label text,#mermaid-svg-c8L4cjkD8jYjMntn .image-shape .label,#mermaid-svg-c8L4cjkD8jYjMntn .icon-shape .label{text-anchor:middle;}#mermaid-svg-c8L4cjkD8jYjMntn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c8L4cjkD8jYjMntn .rough-node .label,#mermaid-svg-c8L4cjkD8jYjMntn .node .label,#mermaid-svg-c8L4cjkD8jYjMntn .image-shape .label,#mermaid-svg-c8L4cjkD8jYjMntn .icon-shape .label{text-align:center;}#mermaid-svg-c8L4cjkD8jYjMntn .node.clickable{cursor:pointer;}#mermaid-svg-c8L4cjkD8jYjMntn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c8L4cjkD8jYjMntn .arrowheadPath{fill:#333333;}#mermaid-svg-c8L4cjkD8jYjMntn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c8L4cjkD8jYjMntn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c8L4cjkD8jYjMntn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8L4cjkD8jYjMntn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c8L4cjkD8jYjMntn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8L4cjkD8jYjMntn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c8L4cjkD8jYjMntn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c8L4cjkD8jYjMntn .cluster text{fill:#333;}#mermaid-svg-c8L4cjkD8jYjMntn .cluster span{color:#333;}#mermaid-svg-c8L4cjkD8jYjMntn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c8L4cjkD8jYjMntn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c8L4cjkD8jYjMntn rect.text{fill:none;stroke-width:0;}#mermaid-svg-c8L4cjkD8jYjMntn .icon-shape,#mermaid-svg-c8L4cjkD8jYjMntn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8L4cjkD8jYjMntn .icon-shape p,#mermaid-svg-c8L4cjkD8jYjMntn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c8L4cjkD8jYjMntn .icon-shape .label rect,#mermaid-svg-c8L4cjkD8jYjMntn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8L4cjkD8jYjMntn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c8L4cjkD8jYjMntn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c8L4cjkD8jYjMntn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Pending
Running 但 Ready=0
Failed
CrashLoopBackOff
FailedScheduling
ImagePullBackOff
readinessProbe failed
livenessProbe failed
exitCode=0
exitCode=137 OOMKilled
exitCode=1
exitCode=127


Pod 状态异常
status.phase?
调度/镜像问题
就绪/健康检查问题
容器已退出
反复崩溃
describe Events
资源不足/taint/亲和性
镜像名/仓库/凭证
describe containers
探针路径/端口错或应用没就绪
应用卡死,被重启
看 lastState.terminated
正常退出,policy=Never
加 limits 或查内存泄漏
应用异常,看 logs
entrypoint/镜像问题
logs --previous 看崩溃前现场
日志有内容?
按日志排查应用
查 ExitCode 和 lastState

2.3 各类故障的定位命令

针对每类故障,这是定位的标准动作:

bash 复制代码
# === 1. Pending ===
# 先看 Events,FailedScheduling 的 message 是关键
kubectl describe pod <pod> -n fault-demo | grep -A5 Events
# 典型 message:
#   "0/3 nodes are available: 3 Insufficient cpu"
#   "3 node(s) had untolerated taint {node-role.kubernetes.io/master:}"
#   "3 node(s) didn't match Pod's node affinity"

# === 2. ImagePullBackOff ===
kubectl describe pod pod-imagepull -n fault-demo | grep -A3 "State:"
# 看 waiting.message,会写明拉取失败原因:
#   "Failed to pull image ... manifest unknown"
#   "unauthorized: authentication required"  → 缺 imagePullSecrets
# 检查 secret:
kubectl get secret -n fault-demo
kubectl describe pod pod-imagepull -n fault-demo | grep -i secret

# === 3. CrashLoopBackOff ===
# 必须看上一次的日志!
kubectl logs pod-crashloop -n fault-demo --previous
# 如果日志是空的,说明应用没来得及输出就崩了,看 exit code 和 reason:
kubectl get pod pod-crashloop -n fault-demo -o jsonpath=\
'{range .status.containerStatuses[*]}{.lastState}{"\n"}{end}'

# === 4. OOMKilled ===
kubectl describe pod pod-oom -n fault-demo | grep -A6 "Last State"
# 输出示例:
#   Reason:      OOMKilled
#   Exit Code:   137
# 解决:调大 limits.memory,或排查内存泄漏
# 监控:看 container_memory_working_set_bytes(不是 container_memory_usage_bytes)
kubectl top pod pod-oom -n fault-demo --containers

# === 5. Init 失败 ===
kubectl describe pod pod-init-fail -n fault-demo | grep -A20 "Init Containers"
# 看 init container 的 state
kubectl logs pod-init-fail -n fault-demo -c init-db --previous
# 关键:init container 失败会一直重启(Always)或退出(Never)
# Sidecar(1.28+)用 restartPolicy: Always 的 init container,要单独识别

# === 6. 就绪失败 ===
kubectl describe pod pod-notready -n fault-demo | grep -A10 "Readiness"
# 看 "Readiness probe failed: HTTP probe failed with statuscode: 404"
# 解决:修正探针路径,或确认应用真的就绪

2.4 优雅终止与 preStop

Pod 被删时的终止流程,是另一个高频踩坑点:

复制代码
kubectl delete pod ──────────────────────────────────────────────>
    
    1. apiserver 把 Pod 的 deletionTimestamp 打上
    2. kubelet 收到删除事件,开始优雅终止:
       a. 触发 preStop hook(如果有)
       b. 同时 kube-proxy 摘除 endpoint(有延迟!)
       c. 给容器发 SIGTERM
       d. 等 terminationGracePeriodSeconds(默认 30s)
       e. 超时还没退出 → SIGKILL
    3. Pod 从 etcd 删除

关键坑 :preStopSIGTERM同时触发的(不是 preStop 跑完才发 SIGTERM)。preStop 经常用来"等一会儿",给 kube-proxy 摘流量的时间:

yaml 复制代码
spec:
  terminationGracePeriodSeconds: 60   # 总等待时间
  containers:
  - name: app
    # ...
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 15 && nginx -s quit"]
    # sleep 15 等 kube-proxy 摘流量,再优雅退出 nginx

这是 Ingress 流量不丢的标准配置:preStop sleep + 长 terminationGracePeriodSeconds

2.5 一键排查脚本

把故障定位打包成一个函数:

bash 复制代码
# /usr/local/bin/pod-triage
#!/bin/bash
# 用法: pod-triage <pod-name> [namespace]
POD=$1
NS=${2:-default}

echo "========== PHASE & READY =========="
kubectl get pod "$POD" -n "$NS" -o custom-columns=\
NAME:.metadata.name,PHASE:.status.phase,\
READY:.status.containerStatuses[0].ready,\
RESTARTS:.status.containerStatuses[0].restartCount

echo "========== CONTAINER STATES =========="
kubectl get pod "$POD" -n "$NS" -o jsonpath=\
'{range .status.containerStatuses[*]}Container: {.name}{"\n"}State: {.state}{"\n"}LastState: {.lastState}{"\n"}{"\n"}{end}'

echo "========== EVENTS (last 10) =========="
kubectl get events -n "$NS" --field-selector involvedObject.name="$POD" \
  --sort-by=.lastTimestamp | tail -10

echo "========== PREVIOUS LOGS (last 50) =========="
kubectl logs "$POD" -n "$NS" --previous --tail=50 2>/dev/null \
  || echo "(no previous container or no logs)"

echo "========== NODE =========="
NODE=$(kubectl get pod "$POD" -n "$NS" -o jsonpath='{.spec.nodeName}')
echo "Scheduled on: $NODE"
kubectl describe node "$NODE" | grep -A5 "Conditions:"
bash 复制代码
chmod +x /usr/local/bin/pod-triage
pod-triage pod-crashloop fault-demo

三、踩坑与排查

踩坑 1:Pod 一直 Terminating,删不掉

现象 :kubectl delete pod <pod> 卡住,等了几分钟还在 Terminating

原因:

  • 容器内有进程没响应 SIGTERM(比如 Java 进程没注册 shutdown hook);
  • finalizer 没移除(比如 PVC、PodDisruptionBudget 相关的 finalizer);
  • kubelet 跟节点失联(节点 NotReady)。

解决:

bash 复制代码
# 1. 看是不是有 finalizer 卡住
kubectl get pod <pod> -n <ns> -o jsonpath='{.metadata.finalizers}'
# 如果有,且确认可以强删:
kubectl patch pod <pod> -n <ns> -p '{"metadata":{"finalizers":[]}}' --type=merge

# 2. 看 kubelet 是否失联
kubectl get node $(kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.nodeName}')

# 3. 应用层问题:加 preStop sleep + 处理 SIGTERM

踩坑 2:exitCode=137 但不是 OOM

现象:Pod 反复 exit 137,describe 里 reason 不是 OOMKilled,但应用没异常。

原因:可能被外部 SIGKILL。常见来源:

  • 节点内存压力大,kubelet 主动驱逐(Reason: Evicted,会在 Pod 的 status 里写);
  • 容器运行时(docker/containerd)重启;
  • kubectl delete pod --force --grace-period=0

解决:

bash 复制代码
# 看是不是被驱逐
kubectl get pod <pod> -o jsonpath='{.status.reason}'
# Evicted 的话,是节点压力问题,看节点:
kubectl describe node <node> | grep -A5 "MemoryPressure\|DiskPressure"

# 看 kubelet 日志确认
journalctl -u kubelet --since "10 min ago" | grep <pod>

踩坑 3:Init Container "卡住"不报错也不退出

现象 :Pod 一直 Init:0/1,kubectl logs -c <init> 没输出,也没失败。

原因 :init container 在等一个外部依赖(比如等数据库就绪),没设超时。restartPolicy: Always 时它失败会重试,但"卡住不退出"时 K8s 不会主动杀。

解决:

yaml 复制代码
# init container 一定要有超时和退出码
initContainers:
- name: wait-db
  image: busybox:1.36
  command:
  - /bin/sh
  - -c
  - |
    for i in $(seq 1 60); do
      if nc -z db-svc 5432 2>/dev/null; then
        echo "db ready"; exit 0
      fi
      echo "waiting db... $i"; sleep 5
    done
    echo "db not ready, give up"; exit 1
  # 60 * 5 = 300s 超时

踩坑 4:Sidecar(1.28+)不就绪,主容器起不来

现象 :升级到 1.30 后,把原来的 sidecar 改成 initContainers + restartPolicy: Always,结果主容器一直不启动。

原因 :1.28+ 的原生 sidecar,它的 readinessProbe 不过会阻止主容器启动(这是设计如此,为了保证 sidecar 就绪主容器才跑)。如果你的 sidecar 探针配错,主容器永远等。

解决:

yaml 复制代码
initContainers:
- name: envoy
  image: envoyproxy/envoy:v1.30
  restartPolicy: Always
  readinessProbe:
    httpGet:
      path: /ready
      port: 9901
    # 一定要确认 envoy 的 admin 端口和路径对
  # ...
# 主容器在 sidecar Ready 后才启动

踩坑 5:Job 完成了但状态是 Failed

现象 :Job 跑完 exit 0,但 Pod 状态显示 Failed

原因 :restartPolicybackoffLimit 的组合问题。Job 模板如果用了 restartPolicy: Always(继承自 Deployment),Job 会一直重启不结束。Job 必须用 NeverOnFailure

解决:

yaml 复制代码
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
spec:
  completions: 1
  backoffLimit: 3
  template:
    spec:
      restartPolicy: OnFailure   # Job 只能是 Never 或 OnFailure
      containers:
      - name: app
        image: my-job:1.0

四、最佳实践

状态判读

  1. 永远看 containerStatuses[].state,别只看 phase:phase 是粗粒度的,真正故障在容器状态里。
  2. restartCount 是健康度的温度计:重启超过 3 次就该介入了,别等 CrashLoopBackOff。
  3. Exit Code 137 先查 OOMKilled 再考虑外部 kill :describe 的 Last State 会写清楚。

探针配置

  1. readinessProbe 和 livenessProbe 要分开:readiness 决定流量,liveness 决定重启。两者路径/阈值不能一样。
  2. livenessProbe 的 initialDelaySeconds 要够长:应用启动慢(Java)的话,设 60s+,否则会被误杀。
  3. 生产禁用 livenessProbe 的 TCP 探针:TCP 探针只能验证端口在监听,不能验证应用真的健康(线程死锁但端口还在)。用 HTTP。

优雅终止

  1. 生产 Pod 一定配 preStop + 长 gracePeriod:默认 30s 对大部分应用不够,建议 60s。
  2. preStop 里 sleep 15s 是给 kube-proxy 摘流量用的:不是给应用用的,应用收到 SIGTERM 后自己做清理。
  3. 应用要捕获 SIGTERM:Java/Go/Python 都要注册 shutdown hook,否则 SIGTERM 直接变 SIGKILL 丢数据。

故障演练

  1. 定期做 Pod 故障演练:用 chaos-mesh 或手动 kill Pod,验证 preStop/探针/超时都生效。
  2. CI 里加 Pod 健康检查 :部署后 kubectl wait --for=condition=Ready,不 Ready 就回滚。
  3. 告警要带 Exit Code 和 Reason:别只告警"Pod 重启",带上 exitCode/reason 才能快速分类。

五、小结

Pod 故障排查的本质是状态机定位:phase 决定大类,containerStatuses 决定具体原因,Exit Code 决定根因。你只要会读这三个字段,就能把"Pod 不正常"这种模糊的告警,精确到"OOMKilled,加内存"或"镜像拉不下来,改 tag"这种可执行的结论。

记住排错的三步走:describe 看 Events 和 Last State → logs --previous 看崩溃前现场 → exec 验证运行时状态。这三步走完,90% 的 Pod 故障都能定位到方向。剩下 10% 是节点级问题(NotReady、kubelet 失联),那是另一个战场。

下一篇讲工作负载,我们会把 Pod 放到 Deployment/StatefulSet 里看------为什么滚动更新会卡住、为什么 StatefulSet 的 Pod 不能随便删。

思考题

  1. 一个 Pod phase=RunningReady=0/1,restartCount=0,可能的原因有哪些?和 restartCount=5 时的排查思路有什么不同?
  2. 写一个 init container,等 Redis 就绪(最多等 5 分钟),超时就让 Pod 失败。
  3. terminationGracePeriodSeconds=30,preStop 里 sleep 20,应用收到 SIGTERM 后清理要 15s。这个配置能保证优雅终止吗?为什么?

延伸阅读

相关推荐
OpenAnolis小助手1 小时前
龙蜥社区智算联盟发布《智算集群基础设施运维通用技术要求》(附下载链接)
运维·人工智能·智算运维·龙蜥智算联盟
.柒宇.1 小时前
ELK日志分析系统实战指南:Elasticsearch搭建与入门(8.18.8版本)
运维·elk·elasticsearch·logback·日志系统
小刘数字化2 小时前
AR数字孪生:重塑工业运维的下一代交互范式
运维·ar
翰德恩咨询2 小时前
华为DSTE咨询洞察:如何规划人才支撑战略落地
运维·服务器·华为
敖行客 Allthinker15 小时前
Parallels Ubuntu虚拟机项目如何让手机访问?完整解决方案
linux·运维·ubuntu
BullSmall16 小时前
Anolis OS 8.10 完整安装 Docker CE(生产可用,解决 podman 冲突)
docker·容器·podman
元Y亨H16 小时前
生产环境监控与故障应急处理(5-5-5 标准)指南
运维
heimeiyingwang17 小时前
【架构实战】可观测性三支柱:日志、指标、链路的融合
elasticsearch·架构·kubernetes
观山岳五楼17 小时前
Ubuntu 24 怎么使用Ubuntu 20 的镜像源
linux·运维·ubuntu