先给结论: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: Terminated 和 Reason 更准。
第二步:kubectl logs --previous 比 logs 重要
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 里的 Command 和 Args 对。
对比一下: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 aux、netstat -tulpn、tcpdump -i eth0 port 8080。--profile=sysadmin 会加 CAP_SYS_ADMIN、CAP_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: 0、periodSeconds: 10、timeoutSeconds: 1、failureThreshold: 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 个坑,按恶心程度排序
- 只看
kubectl logs,不看--previous,错过上一次崩溃的最后一句话。 - 忽略 initContainer,主容器躺枪,实际是 migration 或配置下载失败。
- CrashLoopBackOff 时硬
kubectl exec,浪费时间,容器进程根本不在。 - 无脑
--profile=sysadmin,被 PodSecurity 拒绝,或者审计告警炸锅。 --copy-to复制了 ownerReferences,副本被控制器删掉,刚进去就没了。- 临时容器不继承 volumeMounts,误判“数据盘是空的”,其实原容器挂得好好的。
这些坑没有一个是 Kubernetes 的 bug,全是排查顺序和权限边界问题。所以我现在的观点有点反直觉:CrashLoopBackOff 先别急着 debug,先把 exit code、探针、limit 三个数字对齐。临时容器是手术刀,不是锤子;能
describe看出来的事,不要开 sysadmin 进去ps。
最后给一个可复制的排查清单
kubectl get pod -n prod -o wide看节点、重启次数、AGE。kubectl describe pod -n prod看Last State、Reason、Events、Limits、Liveness。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 不背这个锅。