kubectl get pods 里一排 CrashLoopBackOff,重启次数还在不断往上加,这是 Kubernetes 里最典型的「容器起不来」状态。它的字面意思是「崩溃循环 + 退避」:容器启动后不就退出了,Kubernetes 判定它失败,于是按指数退避的间隔(10 秒、20 秒、40 秒……最长 5 分钟)不断重启它,并对外显示 CrashLoopBackOff。
这个状态本身只是一个「现象」,真正的原因藏在容器退出前的日志里、退出码里、describe 的事件里。它可能来自应用自身崩溃、配置错误、依赖服务连不上、探针配置不合理、权限或文件系统问题等。和 Pending 不同,CrashLoopBackOff 说明容器至少启动过,所以排查路径更偏向「看日志、看退出码」。
这篇文章会讲清 CrashLoopBackOff 的重启机制、如何用 kubectl logs 和 describe 拿到关键线索、退出码的解读方法,以及探针、资源、配置、权限这几类高频根因的处置方式,最后给出一套可复制的排查顺序。
一、先理解「退避」机制
Kubernetes 对反复失败的容器采用指数退避(exponential backoff)重启策略,避免无脑狂重启拖垮节点。大致规律:
- 第一次失败,约 10 秒后重启;
- 连续失败,间隔翻倍:20 秒、40 秒、80 秒……;
- 上限通常是 5 分钟;
- 如果容器能稳定运行超过 10 分钟,退避计数会被重置。
所以看到 CrashLoopBackOff 时,容器可能正处于「等下一次重启」的间隙,这时你 kubectl logs 可能拿不到最新日志(因为容器没在跑)。这也是新手困惑的点。
Pod 的重启策略由 restartPolicy 决定,默认 Always:
spec:
restartPolicy: Always
对于 Job 类一次性任务,restartPolicy 常设为 OnFailure 或 Never,行为不同。
二、看日志:分「当前」和「上一次」
日志是第一手证据。但要注意区分容器当前实例和上一次崩溃实例:
# 查看当前容器的日志
kubectl logs -n <命名空间>
# 关键:查看上一次崩溃容器的日志(容器没在跑时必用)
kubectl logs -n <命名空间> --previous
# 多容器 Pod 指定容器
kubectl logs -c <容器名> --previous
# 实时跟随
kubectl logs -f
--previous(可简写 -p)是排查 CrashLoopBackOff 的核心开关。很多时候当前实例还没起来,只有上一次的日志能告诉你它为什么挂。
如果日志一片空白,检查是否:
- 应用日志写到了文件而非 stdout/stderr(K8s 只能抓标准输出);
- 容器在打印日志前就退出了;
- 日志被缓冲没刷出来(部分语言需要开 unbuffered 或 flush)。
三、看事件与退出码
kubectl describe pod -n <命名空间>
重点看三处:
1. Last State 里的 Reason 和 Exit Code;
2. Events 段落里的 Warning;
3. State 是否显示 Waiting: CrashLoopBackOff。
退出码对照(和 Docker 一致,因为底层就是容器):
| 退出码 | 含义 | 常见根因 |
|---|---|---|
| 0 | 正常退出 | 主进程跑完就退(如命令执行完) |
| 1 | 通用错误 | 应用异常、配置错、脚本报错 |
| 2 | shell 用法错误 | 入口脚本参数/语法问题 |
| 126 | 无法执行 | 命令无执行权限 |
| 127 | 命令不存在 | entrypoint 路径或命令名错 |
| 137 | 被 SIGKILL | 内存超限 OOMKilled |
| 139 | 段错误 | 程序崩溃、架构不匹配 |
| 143 | SIGTERM | 被优雅终止 |
获取退出码也可以直接查 JSON:
kubectl get pod -o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
四、高频根因一:应用自身崩溃或配置错误
这是最直接的一类。比如配置文件里的数据库地址写错了,应用启动时连不上就退出:
FATAL: could not connect to database: connection refused
处置方式就是修正配置。Kubernetes 里配置通常来自 ConfigMap 或 Secret:
# 查看 Pod 实际使用的 ConfigMap
kubectl get configmap <名称> -o yaml
# 查看环境变量是否注入正确
kubectl exec -it -- env | grep -i db
常见坑:
- ConfigMap 更新了但 Pod 没重启,仍用旧值(环境变量形式注入的不会热更新);
- Secret 的 base64 编码后多了换行,导致解析失败;
- 挂载的配置文件名和路径与预期不符。
五、高频根因二:探针配置不合理
livenessProbe(存活探针)如果配置过于激进,会在应用还没启动好时就判定它「死了」,然后杀掉重启,形成 CrashLoopBackOff。典型错误:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 2 # 太短
periodSeconds: 5
failureThreshold: 3
一个 Java 应用冷启动可能要 30 到 60 秒,initialDelaySeconds: 2 必然导致被误杀。合理做法是加 startupProbe(启动探针)或者把 initialDelaySeconds 调大:
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10 # 最多给 300 秒启动时间
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
经验值:启动探针的 failureThreshold × periodSeconds 应设成应用冷启动时间的 2 到 3 倍,给足余量。
六、高频根因三:资源限制与 OOM
如果退出码是 137,且 describe 里 Last State 显示 Reason: OOMKilled,就是内存超限。加大内存限制或优化应用内存:
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi"
JVM 应用特别容易踩这个坑,老版本 JDK 不感知 cgroup,按宿主机内存设堆导致超限。用 -XX:MaxRAMPercentage=70.0 显式控制。
七、高频根因四:依赖服务未就绪
微服务启动时常依赖数据库、缓存、消息队列。如果依赖还没好,应用直接退出就会 CrashLoopBackOff。两种思路:
- 加 initContainer 等待依赖:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db-service 5432; do echo waiting; sleep 2; done']
- 应用自身做重试,不要一上来连不上就退出,而是带指数退避重试若干次。生产环境更推荐后者,因为更健壮。
八、高频根因五:权限与文件系统
如果日志里出现 permission denied,可能是:
- securityContext 里的 runAsUser 与镜像内文件属主不符;
- 挂载的卷权限不对(特别是 emptyDir 之外的存储卷);
- 只读根文件系统(readOnlyRootFilesystem: true)导致应用想写临时文件却失败。
排查:
kubectl get pod -o yaml | grep -A10 securityContext
修正 securityContext 或给卷设置合适的 fsGroup:
spec:
securityContext:
fsGroup: 2000
九、一套可复制的排查顺序
# 1. Pod 概况,看重启次数与状态
kubectl get pod -o wide
# 2. 上次崩溃的日志
kubectl logs --previous --tail=200
# 3. 事件与退出码
kubectl describe pod | sed -n '/Events/,$p'
# 4. 若 OOM,查内存配置
kubectl get pod -o jsonpath='{.spec.containers[*].resources}'
# 5. 若配置相关,验证 ConfigMap/Secret
kubectl get configmap <名称> -o yaml
# 6. 临时进容器手动跑命令定位(覆盖 entrypoint)
kubectl run debug --rm -it --image=<同镜像> -- sh
第 6 步非常实用:用一个 sleep 或者 sh 覆盖入口命令,让容器先「活着」,再进去手动执行原启动命令,看真实报错:
kubectl run debug --rm -it --image=myapp:1.0 --command -- sleep 3600
风险提示:用真实镜像跑 Debug 容器时,注意它可能连生产数据库或执行副作用操作。建议在测试命名空间操作,并确认镜像的启动逻辑无破坏性。
十、临时绕过与调试技巧
如果生产环境急需恢复,可以临时放宽探针或命令来抢时间,但要清楚这只是缓解,不是根治:
# 临时改 deployment 的探针,先让 Pod 起来
kubectl edit deployment <名称> -n <命名空间>
风险提示:放宽或删除 livenessProbe 会让故障容器不被自动重启,务必配合监控,且事后尽快恢复正确配置。回滚方式是 kubectl rollout undo deployment/<名称>。
查看 rollout 历史:
kubectl rollout history deployment/<名称>
kubectl rollout undo deployment/<名称>
十一、把「起不来」防在前面
与其每次等 CrashLoopBackOff 出现再排查,不如在设计和上线阶段就把常见坑填掉。
一是让应用自身健壮:启动时依赖连不上不要立刻退出,而是带重试;主进程要前台运行、正确处理 SIGTERM;把关键启动步骤的日志打到 stdout,别写进文件。二是给探针留足空间:用 startupProbe 覆盖冷启动,liveness 只负责「进程卡死」这种真正需要重启的场景,不要把外部依赖的健康检查塞进 liveness,否则数据库抖一下你的 Pod 就集体重启。三是配置与镜像分离:所有会变的东西走 ConfigMap/Secret,镜像保持不可变,这样「换环境就崩」的概率大大降低。四是上线前做一次 dry-run:在测试命名空间用同一份 manifest 跑一遍,确认能正常起来,再推生产。
实践里还有一个经验:把最常见的那几类退出码做成内部排障速查表,贴到团队的 runbook 里。新同学遇到 137、127、143 时照着表走,能省下大量翻文档的时间,也能避免「一看到容器重启就无脑加大资源」这种误操作。
常见问题
Q1:kubectl logs 显示容器不在运行,拿不到日志怎么办?
A:加 --previous 参数看上一次崩溃的日志。若历史实例也被清理了,检查节点上的容器运行时日志,或用同镜像起 Debug 容器复现。
Q2:容器一直重启,但日志看起来是正常的,怎么回事?
A:检查主进程是否「后台化」了。比如 nohup ... & 让前台进程退出,K8s 认为容器结束就重启。容器里必须让主进程前台运行(PID 1),不要 daemon 化。
Q3:退出码 0 为什么也 CrashLoopBackOff?
A:退出码 0 表示正常结束,但如果 restartPolicy: Always,容器正常结束也会被重启,若应用是「跑完即退」的一次性任务,就会循环。改用 Job,或让主进程持续运行。
Q4:改了 ConfigMap 但容器还是用旧值。
A:环境变量方式注入的 ConfigMap 不会热更新,需要重启 Pod;挂载为文件的方式有一定延迟同步,也不保证立即生效。最稳的是触发一次 rollout restart。
Q5:CrashLoopBackOff 和 ImagePullBackOff 有什么区别?
A:CrashLoopBackOff 是容器能启动但反复退出;ImagePullBackOff 是镜像根本拉不下来,容器从未启动。排查方向完全不同,前者看日志,后者看仓库和网络。
总结
- CrashLoopBackOff 是「容器反复失败 + 指数退避重启」的表象,真正原因在日志、退出码和事件里。
kubectl logs --previous和kubectl describe pod是两把核心钥匙,一个看输出,一个看状态和退出码。- 按退出码快速分流:137 想内存,127/126 想命令与权限,0 想「主进程是否后台化了」,1/2 想应用与配置。
- 探针配置过激是高频根因,给足启动时间(用 startupProbe),依赖服务用 initContainer 或应用内重试处理。
- 线上抢时间可临时放宽配置,但记住用
kubectl rollout undo能回滚,且要尽快恢复正确配置并补上监控。
企业QQ咨询




