Kubernetes Pod 一直 CrashLoopBackOff 怎么排查?我用 kubectl debug 临时容器踩过的 6 个坑

🔑 关键词:Kubernetes,CrashLoopBackOff,kubectl debug,临时容器,exit code 137

📖 摘要:从一次生产环境 CrashLoopBackOff 排查出发,记录 kubectl debug、--previous 日志、livenessProbe、OOMKilled 的具体参数和踩坑顺序,附可复制命令。

先给结论:CrashLoopBackOff 的排查顺序,我改了

我一开始也跟大多数教程一样,先 kubectl logs,结果十次有五次日志是空的,Pod 已经重启到第十次,kubectl exec 根本进不去。后来在 1.26 集群上排一个 Java 服务,才把顺序改成:先看 exit code 和 lastState,再看 Events,再看 livenessProbe,最后才看日志。原因很土:CrashLoopBackOff 不是一种错误,它只是 kubelet 在反复拉起的退避状态。退避时间从 10s 开始,20s、40s、80s、160s,最大 300s。你看到 5 分钟重启一次,不代表问题变轻了,只代表 backoff 到顶了。 命令先跑这三条: kubectl get pod -n prod myapp-7d8f9c6b5-xxxxx -o wide kubectl describe pod -n prod myapp-7d8f9c6b5-xxxxx | sed -n '/Events/,$p' kubectl get pod -n prod myapp-7d8f9c6b5-xxxxx -o jsonpath='{.status.containerStatuses[*].lastState.terminated.exitCode}' 如果最后一条输出 137,不要立刻喊 OOMKilled;137 是 128+9,可能是 OOMKilled,也可能是 livenessProbe 失败后 kubelet 发的 SIGKILL。143 是 128+15,通常是 SIGTERM。1 多半是应用自己退出,139 是 segfault 段错误。看 describe 里的 Last State: TerminatedReason 更准。

图片

第二步:kubectl logs --previouslogs 重要

CrashLoopBackOff 时容器是死了又起,当前容器可能刚起来还没输出,kubectl logs 经常只给你一行启动 banner。要看上一次死掉的容器: kubectl logs -n prod myapp-7d8f9c6b5-xxxxx --previous --timestamps --tail=200 如果 Pod 有多个容器,加 -c app;如果是 initContainer 挂了,加 -c init-migrate。我遇到过最坑的一次,主容器日志完全正常,initContainer 里跑数据库 migration 报 connection refused,但 kubectl logs 默认只看主容器。还有一次日志为空,是因为 Dockerfile 里 ENTRYPOINT 写成了 java -jar app.jar,但 jar 路径在镜像里是 /opt/app/app.jar,容器启动 0.3 秒就退出,根本来不及打日志。这种只能靠 describe 里的 CommandArgs 对。 对比一下:logs 看当前,logs --previous 看上一具尸体,describe 看 kubelet 视角。三个缺一个,排查就会变成猜谜。

图片

第三步:kubectl debug 临时容器怎么用,以及别乱用

当 Pod 还在 CrashLoopBackOff,kubectl exec 基本没戏,因为容器进程已经不在了。Kubernetes 1.25 之后 ephemeral containers 已经 stable,可以用: kubectl debug -it myapp-7d8f9c6b5-xxxxx -n prod --image=nicolaka/netshoot:0.9 --target=app --profile=sysadmin -- bash --target=app 会让临时容器和目标容器共享进程命名空间,进去可以 ps auxnetstat -tulpntcpdump -i eth0 port 8080--profile=sysadmin 会加 CAP_SYS_ADMINCAP_SYS_PTRACE 等权限,能 strace、能看网络。但坑也在这里:如果命名空间开了 PodSecurity restricted,sysadmin profile 会被拒绝,报 ephemeral container is not allowed。生产环境我更常用 --profile=general,或者干脆不加 profile,只做网络和文件系统检查。 另一个坑:临时容器不自动继承原容器的 volumeMounts。你 ls /data 可能是空的,不是应用没写数据,而是临时容器根本没挂那个 PVC。要确认挂载,得自己看 Pod spec 里的 volumeMounts,或者用 --copy-to 创建副本。

图片

第四步:--copy-to 是排查启动类 CrashLoopBackOff 的暗器

如果怀疑是启动参数、镜像 entrypoint、配置文件路径问题,临时容器看不到历史进程,这时用 --copy-to 复制一个 Pod,改掉 command 让它别退出: kubectl debug myapp-7d8f9c6b5-xxxxx -n prod --copy-to=myapp-debug --container=app --image=busybox:1.36 -- sleep 3600 然后 kubectl exec -it myapp-debug -n prod -- sh,进去手工跑原命令。这个办法我用来抓过 Nginx 的 nginx -t 失败,也用来确认过 Java 的 -Dspring.profiles.active=prod 是否被 entrypoint 覆盖。--copy-to 的坑是它会复制原 Pod 的 label、annotation、ownerReferences,可能被 admission webhook 拦截,或者被 Service 选中。我一般会加 --copy-to 后立刻改 label,或者用 kubectl label pod myapp-debug -n prod app=debug --overwrite。 对比:临时容器适合查活着的网络/进程,--copy-to 适合查启动配置,Sidecar 适合长期观测,但 Sidecar 要改 Deployment,重启一次代价更大。

图片

第五步:livenessProbe 和 resources,比日志更常见的元凶

我统计过自己经手的 30 多个 CrashLoopBackOff 工单,真正应用代码抛异常的大概 11 个,剩下 19 个里,探针和资源限制占大头。livenessProbe 默认值很激进:initialDelaySeconds: 0periodSeconds: 10timeoutSeconds: 1failureThreshold: 3。一个 Spring Boot 应用启动要 35 秒,容器起来后 0 秒开始探,10 秒一次,连续 3 次失败,大约 20 到 30 秒就被 kubelet 杀掉。你看到的日志可能只有 Spring 的启动初始化,没有异常。 改法:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 45
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 6

如果是 JVM 容器,内存 limit 给 128Mi 基本是自杀。kubectl top pod -n prod myapp-7d8f9c6b5-xxxxx --containers 看实际 RSS。exit code 137 + Reason: OOMKilled 就加 -XX:MaxRAMPercentage=75.0,别写 -Xmx128m 再配 128Mi limit,堆外内存、Metaspace、线程栈会把容器撑爆。requests 和 limits 也别差 10 倍,调度器会按 requests 放,节点内存碎片会让它死得很难看。

图片

我踩过的 6 个坑,按恶心程度排序

  1. 只看 kubectl logs,不看 --previous,错过上一次崩溃的最后一句话。
  2. 忽略 initContainer,主容器躺枪,实际是 migration 或配置下载失败。
  3. CrashLoopBackOff 时硬 kubectl exec,浪费时间,容器进程根本不在。
  4. 无脑 --profile=sysadmin,被 PodSecurity 拒绝,或者审计告警炸锅。
  5. --copy-to 复制了 ownerReferences,副本被控制器删掉,刚进去就没了。
  6. 临时容器不继承 volumeMounts,误判“数据盘是空的”,其实原容器挂得好好的。 这些坑没有一个是 Kubernetes 的 bug,全是排查顺序和权限边界问题。所以我现在的观点有点反直觉:CrashLoopBackOff 先别急着 debug,先把 exit code、探针、limit 三个数字对齐。临时容器是手术刀,不是锤子;能 describe 看出来的事,不要开 sysadmin 进去 ps

图片

最后给一个可复制的排查清单

  • kubectl get pod -n prod -o wide 看节点、重启次数、AGE。
  • kubectl describe pod -n prodLast StateReasonEventsLimitsLiveness
  • kubectl logs -n prod --previous --timestamps --tail=200 看上一轮日志。
  • kubectl get pod -n prod -o jsonpath='{.status.containerStatuses[*].lastState.terminated}' 拿退出码和原因。
  • kubectl debug -it ... --target=app --profile=general 查网络和进程,注意权限。
  • kubectl debug ... --copy-to=debug -- sleep 3600 查启动命令和配置文件。
  • 如果 137 + OOMKilled,调 limit 和 JVM 参数;如果 143,看 terminationGracePeriodSeconds 和 preStop。 这些步骤不优雅,但比我早期乱 exec 快很多。那次 Java 服务最后根因就是 initialDelaySeconds 没设,加上 limit 192Mi、JVM 默认堆 128Mi,活不过 25 秒。改完探针和 -XX:MaxRAMPercentage=70.0,重启次数从 47 次降到 0。Kubernetes 不背这个锅。
🏷️ 标签: