监控告警显示某个 Deployment 的 Pod RESTARTS 计数一路往上走,业务时断时续,但 kubectl get pods 里 STATUS 却写着奇怪的 CrashLoopBackOff。Pod 频繁重启是 K8s 里最常见的应用层故障,也是最容易被误判的一类——因为容器已经退出了,现场只剩下一次 veteran 记录,看 kubectl logs 又只能看到最新一次启动的日志。本文讲清楚如何正确读取 Pod 的事件与前一次运行的日志,如何通过退出码快速区分 OOMKilled、健康检查失败与应用自身崩溃,以及各类型问题对应的处理动作。
一、先读状态:Pod STATUS 字段的含义
kubectl get pods 输出的 STATUS 列是第一手线索,不同值直接指向不同方向。
| STATUS | 含义 | 优先怀疑方向 |
|---|---|---|
| CrashLoopBackOff | 容器反复启动后反复退出,K8s 正在按退避时间等待重启 | 应用启动失败、OOM、配置缺失 |
| Error | 容器已退出且退出码非 0,重启策略未触发或已达上限 | 应用崩溃且 restartPolicy 为 OnFailure/Never |
| OOMKilled | 容器因超出内存限制被内核 kill | limits.memory 设置过小或应用内存泄漏 |
| ImagePullBackOff / ErrImagePull | 镜像拉取失败 | 镜像名错、tag 不存在、私有仓库认证失败 |
| CreateContainerConfigError | 容器配置的 ConfigMap/Secret 不存在 | 挂载了不存在的 key 或对象 |
| Running 但 READY 为 0/1 | 容器在跑但未通过就绪检查 | readinessProbe 配置不当、依赖未就绪 |
| Evicted | 被 kubelet 因节点资源压力驱逐 | 节点磁盘/内存不足 |
| Init:Error / Init:CrashLoopBackOff | 初始化容器失败 | initContainer 逻辑失败或镜像问题 |
# 1. 看所有 Pod 的状态与重启次数
kubectl get pods -A -o wide
kubectl get pods -n <命名空间> --sort-by=.status.containerStatuses[0].restartCount
# 2. 实时观察状态变化(-w 相当于 watch)
kubectl get pods -n <命名空间> -w
# 3. 直接取出所有 Pod 的重启次数
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.status.containerStatuses[0].restartCount}{"\n"}{end}' | sort -k3 -rn
# 4. 只看某个 Pod 的完整状态树
kubectl get pod -n -o yaml | sed -n '/^status:/,$p'
二、核心命令:describe 里的三段现场
kubectl describe pod 是解决 Pod 重启问题最核心的命令,重点看三段:容器状态(Last State)、事件(Events)、以及容器相互 ID 与镜像。
# 核心命令 kubectl describe pod-n <命名空间> # 只提取容器状态部分(含上一次退出的退出码与原因) kubectl get pod -n -o jsonpath='{range .status.containerStatuses[*]}{"容器: "}{.name}{"\n"}{" 就绪: "}{.ready}{"\n"}{" 重启次数: "}{.restartCount}{"\n"}{" 当前状态: "}{.state.running.startedAt}{.state.waiting.reason}{.state.terminated.reason}{"\n"}{" 上次状态: "}{.lastState.terminated.reason}{"\n"}{" 退出码: "}{.lastState.terminated.exitCode}{"\n"}{" 退出时间: "}{.lastState.terminated.finishedAt}{"\n"}{end}' # 只看 init 容器状态 kubectl get pod -n -o jsonpath='{.status.initContainerStatuses}' # 拉取与该 Pod 相关的事件(Events 字段可能已滚动丢失,用这条兜底) kubectl get events -n --field-selector involvedObject.name= --sort-by=.lastTimestamp # 查看 Deployment 级别的事件(副本级问题在这里) kubectl describe deployment -n
Events 段落里最常见的几类条目及其含义:
Liveness probe failed: HTTP probe failed with statuscode: 500→ 存活探针失败,kubelet 会杀掉容器。Readiness probe failed: Get "http://...": dial tcp ... connection refused→ 就绪探针失败,只摘流量,不会导致重启。Back-off restarting failed container→ 已经失败多次,正在退避。OOMKilled出现在 Last State 的 Reason 里 → 内存超限。Failed to pull image "...": rpc error: code = NotFound→ 镜像或 tag 不存在。Error: container's runAsUser breaks non-root policy→ 安全策略冲突。
三、用退出码快速定位:这是最快的一步
容器退出码是判断故障类型最直接的证据。注意 K8s 记录的退出码含义:
| 退出码 | 含义 | 常见原因 | 处理方向 |
|---|---|---|---|
| 0 | 正常退出 | 命令执行完就结束(如 shell 脚本、一次性任务) | 改容器命令使其常驻,或改用 Job |
| 1 | 应用通用错误 | 代码异常、配置解析失败 | 看 --previous 日志 |
| 2 | 命令用法错误 | 启动脚本参数写错 | 检查 command/args |
| 125 | 容器无法运行 | docker/containerd 层面执行失败 | 检查镜像与运行时 |
| 126 | 命令无法执行 | 权限不足、脚本无可执行位 | chmod +x 或调整 fsGroup |
| 127 | 命令不存在 | 镜像精简后缺少 sh/curl 等 | 换镜像或改用 exec 探针 |
| 137 (128+9) | 被 SIGKILL 杀死 | OOMKilled,或主动 kill | 调大 memory limit 或查泄漏 |
| 139 (128+11) | 段错误 SIGSEGV | 程序内存访问越界 | 升级版本或回退 |
| 143 (128+15) | 被 SIGTERM 终止 | 正常停止流程(滚动更新、缩容) | 属正常现象 |
# 查看"上一次"运行的日志 —— 这是排查重启问题最关键的命令 kubectl logs-n --previous # 带上时间戳,判断崩溃发生的时间点 kubectl logs -n --previous --timestamps # 多容器时必须指定容器名 kubectl logs -n -c <容器名> --previous # 查看 init 容器日志 kubectl logs -n -c init- # 查看当前日志最后若干行 kubectl logs -n --tail=200 -f # Pod 一直起不来时,用它临时打开一个能常驻的容器去看文件系统 kubectl debug -it -n --image=busybox:1.36 --target=<容器名> # 宿主机层面确认 OOM(节点内存不足导致的 Container 级 OOM 在这里能看到) dmesg -T | grep -iE 'oom|killed process' | tail -20 journalctl -k --since "1 hour ago" | grep -i oom
四、OOMKilled:内存超限排查
OOMKilled 的判定很干脆:describe pod 里 Last State 的 Reason 显示 OOMKilled,Exit Code 显示 137。但要区分两种:
- 容器级 OOM:容器用量超过了
resources.limits.memory,cgroup 直接杀掉容器内进程。K8s 会记录 OOMKilled。 - 节点级 OOM:宿主机整体内存耗尽,内核 OOM Killer 挑一个进程杀掉,不一定是超 limit 的那个。
dmesg里能看到Killed process。
# 1. 确认当前配置的资源限制 kubectl get pod-n -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{" requests: "}{.resources.requests}{"\n"}{" limits: "}{.resources.limits}{"\n"}{end}' # 2. 查看实际用量(需要 metrics-server) kubectl top pod -n --containers kubectl top pod -A --sort-by=memory | head -20 # 3. 调整 Deployment 的内存限制 kubectl set resources deployment \ --requests=memory=512Mi,cpu=500m \ --limits=memory=1Gi,cpu=1000m -n # 4. 观察滚动更新是否完成 kubectl rollout status deployment/ -n # 5. 查看该 Pod 的 QoS 等级(决定资源紧张时被驱逐的顺序) kubectl get pod -n -o jsonpath='{.status.qosClass}' # Guaranteed: requests == limits(且每项都设)→ 最难被杀 # Burstable: 部分设置或不相等 # BestEffort: 完全没设 → 最先被杀
判断标准:把 memory limit 设为应用在峰值时 RSS 用量的 1.3-1.5 倍。设得太小会频繁 OOMKilled,设得太大则节点资源超卖、其他 Pod 受害。Java 应用尤其要注意 -Xmx 与容器 limit 的关系:JVM 只认 -Xmx,如果 -Xmx 接近 limit,元空间、线程栈、堆外内存的开销容易把总用量顶到 limit 之上,典型的建议是 -Xmx 不超过 limit 的 75%。
五、健康检查(探针)配置不当
探针是 Pod 频繁重启的第二大原因,而且最容易误伤。必须分清三种探针的作用——很多人把 readiness 配置错了却说"探针导致重启"。
| 探针 | 失败后果 | 是否会重启容器 | 典型用途 |
|---|---|---|---|
| livenessProbe(存活探针) | kill 容器并按 restartPolicy 重启 | 会 | 检测死锁、进程假死 |
| readinessProbe(就绪探针) | 从 Service Endpoint 摘除,停止转发流量 | 不会 | 检测依赖未就绪、预热未完成 |
| startupProbe(启动探针) | 阻塞 liveness/readiness;失败则 kill 重启 | 会 | 慢启动应用(如大型 Java) |
关键参数与默认值(默认值经常是踩坑根源):
initialDelaySeconds:容器启动后多久开始探测,默认 0。慢启动应用必须设大,否则刚起来就被判死。periodSeconds:探测间隔,默认 10 秒。timeoutSeconds:单次探测超时时间,默认 1 秒。接口偶发慢就判失败,非常容易误伤,建议放宽到 3-5 秒。failureThreshold:连续失败多少次才认定为失败,默认 3。successThreshold:失败后连续成功多少次才算恢复,默认 1。
# 错误的典型写法:delay 0 + timeout 1s,慢启动 Java 必然被打死
# livenessProbe:
# httpGet: { path: /health, port: 8080 }
# 推荐写法:给足启动时间,三个探针分工明确
# readinessProbe:
# httpGet: { path: /ready, port: 8080 }
# initialDelaySeconds: 10
# periodSeconds: 5
# timeoutSeconds: 3
# failureThreshold: 3
# livenessProbe:
# httpGet: { path: /healthz, port: 8080 }
# initialDelaySeconds: 60
# periodSeconds: 10
# timeoutSeconds: 5
# failureThreshold: 5
# startupProbe:
# httpGet: { path: /healthz, port: 8080 }
# failureThreshold: 30
# periodSeconds: 10
# 在线 kubectl 修改探针配置
kubectl edit deployment -n
# 手工验证探针路径是否真的可用(进容器里测)
kubectl exec -it -n -- curl -sv http://127.0.0.1:8080/healthz
# 镜像里没有 curl 时用 wget 或 /dev/tcp
kubectl exec -it -n -- wget -qO- http://127.0.0.1:8080/healthz
# 镜像极其精简(distroless)时的替代方案:改用 tcpSocket 或 exec 探针
# exec:
# command: ["cat", "/tmp/healthy"]
还要注意探针接口本身的复杂度。如果 /healthz 里做了数据库查询、缓存连通性检查,一次数据库抖动就会导致全部 Pod 被判死重启,形成雪崩。正确的做法是:liveness 探针只检查进程自身能否响应(极简),把依赖检查放到 readiness 探针里。
5.3 重启策略与退避机制
K8s 的 restartPolicy 默认为 Always,容器退出后会被 kube-controller-manager 不断重启,重启间隔按指数退避:10s → 20s → 40s → 80s → 160s → 300s(上限)。这就是 CrashLoopBackOff 里 "BackOff" 的来源。成功运行一段时间后重置。
# 查看当前重启策略 kubectl get pod-n -o jsonpath='{.spec.restartPolicy}' # Always(默认,Deployment 只能是 Always) / OnFailure / Never # Job/CronJob 通常配置为 OnFailure 或 Never # 临时让 Pod 停下来方便排查(把副本数缩到 0) kubectl scale deployment --replicas=0 -n # 排查完恢复 kubectl scale deployment --replicas=3 -n # 重建所有 Pod(配置已修好的情况下最快恢复手段) kubectl rollout restart deployment -n # 回退到上一个版本(配置改错导致重启的标准止损方式) kubectl rollout undo deployment/ -n kubectl rollout history deployment/ -n
六、标准化排查步骤清单
kubectl get pods看 STATUS 与 RESTARTS,确认是 CrashLoopBackOff、OOMKilled 还是 ImagePullBackOff。kubectl describe pod,重点读 Last State(Reason + Exit Code)和 Events。- 取上一次运行的日志:
kubectl logs,这是唯一能拿到崩溃现场的途径。--previous - 按退出码判断方向:137→内存超限、127→命令不存在、1→应用异常、0→命令执行完就结束。
- 若读 kubelet:确认为 137 后区分容器级 OOM 与节点级 OOM,后者查
dmesg | grep oom和节点free -h。 - 若探针失败:确认是 liveness 还是 readiness(只有 liveness 会重启),检查
initialDelaySeconds与timeoutSeconds。 - 若探针路径无效:进容器
curl手工验证,镜像无 curl 时改 tcpSocket 或 exec。 - 模拟器 CronJob/Job 场景确认 restartPolicy 是否为 OnFailure/Never。
- 确认镜像拉取与配置:ImagePullSecrets、ConfigMap/Secret 是否存在且 key 正确。
- 需要止损时:
kubectl rollout undo回退版本,或scale --replicas=0暂停。 - 修复后验证:
kubectl rollout status观察至少 10 分钟,确认 RESTARTS 不再增长。
小结
Pod 频繁重启的排查有一条铁律:永远先看 kubectl describe pod 的 Last State(尤其是退出码)和 kubectl logs --previous,而不是先看当前日志。当前日志属于已经重启成功的新进程,几乎不含崩溃信息。三大数据因占比大致是:应用自身错误或依赖不可用(约四成)、内存超限 OOMKilled(约三成)、探针配置不当(约两成)。
避坑建议:
- 不要把依赖检查放进 livenessProbe,数据库抖动会导致全部 Pod 同时重启,比故障本身更严重。
- 区分 readiness 和 liveness:readiness 失败只是摘流量,不会重启;看错方向会白排查半天。
timeoutSeconds默认 1 秒太激进,生产环境建议 3-5 秒以上。- 慢启动应用必须用 startupProbe,或者把 liveness 的 initialDelaySeconds 设到足够长。
- 核心业务设置 Guaranteed QoS(requests == limits),降低节点资源紧张时被优先杀掉的概率。
- Java 应用注意
-Xmx与 limits 的比例,建议-Xmx不超过 limit 的 75%,给堆外内存留空间。 - 善用
kubectl rollout undo,改了配置导致重启时,回退比现场 debug 成本低得多。 - 给 RESTARTS 增长配告警,而不是等业务报障:5 分钟内重启次数超过 3 次即触发。
延伸阅读:如果整个节点上的 Pod 一起异常、伴随节点 NotReady,问题已不在 Pod 层,而应转到节点状态、kubelet 与 CNI 网络的排查。
企业QQ咨询




