先分享一个背景:我在团队里负责把一批 Java 微服务从 Compose 迁到 Kubernetes 集群,早期最常看到的 Pod 状态不是 Running,而是 CrashLoopBackOff。第一次看到这个名字时心里就一紧:崩了、循环、退避,三个词放在一起,透着一股“我起不来但我会一直尝试”的无奈感。排查得多了以后,我反而觉得 CrashLoopBackOff 是 K8s 里最有价值的异常状态之一——它把“容器启动失败”这件事明明白白写在了脸上,还附带事件记录和日志,只要顺着链路一步步查,大概率能在十分钟内定位问题。
这篇文章我打算从底层的退避机制讲起,再讲我实际排查时用的命令和顺序,最后盘一盘五类高频根因。我不打算写成纯理论文档,而是尽量还原现场:你会在什么场景下看到这个状态,看到以后先干什么、后干什么,哪些信息最有价值,哪些坑我踩过以后再也不踩了。如果你也正在被 CrashLoopBackOff 折磨,或者想系统化自己的排障流程,这篇文章应该能帮上忙。
1. CrashLoopBackOff 的底层机制:Kubelet、重启策略与退避算法
1.1 从 Pod 状态机说起:CrashLoopBackOff 到底处在哪个环节
Kubernetes 里一个 Pod 从创建到 Running,要经过调度、拉取镜像、创建容器、启动容器这几个阶段,每进入一个阶段后 Pod 会呈现不同的状态字段。Pending 表示已经创建但还没被调度,ContainerCreating 表示镜像在拉取或容器在创建,这两个都是“正在走流程”的中间态。一旦容器真正启动,Kubelet 开始不断上报容器状态,Pod 的状态就会转为一个终态或半终态,比如 Running、Completed,以及我们今天的主角 CrashLoopBackOff。
CrashLoopBackOff 并不是 Kubernetes 官方 API 对象里的正式状态,它是 Kubelet 根据容器运行状态归纳出来的一个 Pod Condition。Kubelet 会持续监控容器是否存活:如果容器进程在启动后很快就退出,Kubelet 就会依据 Pod 的 restartPolicy 决定是否重启容器。如果容器反复出现“启动即退出”,Kubelet 就会进入所谓的 BackOff 退避状态,并在 Pod 的 status 里显示为 CrashLoopBackOff。你可以把它理解为“容器在尝试重启,但每次都没能撑过存活期”。
在 Kubernetes 里,restartPolicy 有 Always、OnFailure 和 Never 三种。默认是 Always,也就是说不管是正常结束还是异常结束,Kubelet 都会尝试重启。如果 restartPolicy 是 Never,容器如果退出,Pod 就会变成 Error 或 Completed,但你不会看到 CrashLoopBackOff。所以看到 CrashLoopBackOff 这个状态时,首先要确认一件事:restartPolicy 是不是被设成了默认的 Always,这决定了后续所有排查动作的方向。
1.2 BackOff 重试机制:为什么不是无限快速重启
Kubernetes 没有选择“容器挂了就立刻重启”,而是引入了一个退避算法。Kubelet 内部用 Exponential Backoff 来计算每次重启之间的等待时间,第一次失败后等 10 秒,第二次 20 秒,第三次 40 秒,后面依次翻倍,直到 300 秒封顶。这个设计很符合实际运维需求:应用如果本身有问题,快速重启只会反复消耗系统资源,而退避期可以给运维人员留出观察和介入的时间。
退避时间的计算并不是从容器第一个退出瞬间开始的,而是基于该容器的重启次数和失败时间窗口。重启计数会通过 Kubelet 上报到 kube-apiserver,你通过kubectl get pod能看到RESTARTS列,这个数字就是判断问题严重程度的重要参考。如果 RESTARTS 一直往上走,比如 3、4、5、6,说明容器确实在反复崩溃;如果长时间停在一个数字,比如卡在 2 后不再增长,可能是容器终于稳定了,也可能是探针把它判活后不再触发重启。
从实践经验来看,一个容器如果持续 CrashLoopBackOff 超过 10 分钟,基本可以排除“瞬时故障”的偶然性,问题大概率出在应用本身、配置或环境上。退避期间 Kubelet 并不会停止拉取日志,已经输出的 stdout/stderr 日志被存到了容器运行时,所以无论容器重启多少次,你都能通过 logs 命令读到上一次运行的输出,这一点对排查非常友好。
1.3 崩溃归因模型:启动即退出的三类常见路径
把大量 CrashLoopBackOff 案例归因之后,我发现容器“启动即退出”基本可以收敛到三类路径。第一类是“进程不存在”:镜像里根本没有设置启动命令,或者启动命令指向的文件不存在、没有执行权限,容器一启动就报 exec 错误。第二类是“进程活着但活不久”:应用本身启动逻辑有异常,比如连不上数据库就直接退出、配置文件缺失导致启动失败、JVM 参数异常,这类进程往往能撑几秒到几十秒,但最终会以非零退出码结束。第三类是“进程被外部条件杀死”:比如容器内资源受限触发 OOMKilled,或者 LivenessProbe 探针探测失败,Kubelet 主动杀掉容器。
区分这三类路径,是排查 CrashLoopBackOff 的第一步。因为对应的日志位置和排查命令不一样:第一类看kubectl logs往往没有任何应用日志,要看 Kubelet 事件;第二类应用日志通常很完整,报错信息里基本就有答案;第三类不仅要看日志,还要看 describe 事件里的 OOMKilled 或探针失败提示。带着这个分类去看问题,比漫无目的地翻日志要高效得多。
2. 日志排查的标准流程:从 describe 到容器日志再到系统事件
2.1 第一步总是kubectl describe:Events 区藏着的真相
很多新手遇到 CrashLoopBackOff 会直接去翻kubectl logs,但我个人的习惯是永远先执行kubectl describe pod <pod-name>。这条命令会返回大量元信息:节点名称、容器镜像、环境变量、挂载卷、Conditions、Events、最近几次失败的类型和原因。尤其是 Events 区域,记录了 Kubelet 对容器做出的每一个关键动作,比如拉取镜像失败、容器启动失败、探针失败、被 OOM 杀死。
举一个真实场景:曾经有一个服务升级镜像后开始 CrashLoopBackOff,我直接看 logs,看到的是 Spring Boot 的启动日志,堆栈里也看不出明显问题。后来切换到 describe,在 Events 里看到一行Readiness probe failed: HTTP probe failed with statuscode: 500。问题一下就清楚了:应用启动完成得比预期慢,Readiness 探针在应用没有准备好时就发起了探测,连续失败超过 failureThreshold 后容器被杀。这种问题在 logs 里几乎看不出破绽,但 describe 里写得明明白白。
另一个实用技巧是:describe 的 Events 里如果出现BackOff事件,后面通常会跟着restarting failed container的说明。这个事件本身不告诉你“为什么失败”,但它会告诉你 Kubelet 在退避计时期间已经停止了重启尝试,你需要在退避窗口内抓紧完成日志读取和现场收集。等退避结束,Kubelet 会再次尝试启动容器,此时很多线索会被新一轮日志覆盖,所以时机很重要。
2.2kubectl logs的正确用法:--previous、--tail、--timestamps、-c
日志命令虽然基础,但用得对不对效果天差地别。默认的kubectl logs <pod>只能拿到当前存活容器的日志,如果容器已经退出重启,当前容器的日志往往是空的。这时候必须加--previous参数,才能看到上一次容器的输出。我见过不少同事在这里栽跟头:明明容器启动失败了,执行 logs 却输出为空,就以为是“容器没有日志”,其实只是少打了一个 --previous。
另外要控制输出量。一个应用如果启动阶段打了很多无意义日志,默认的 logs 命令可能一次性拉几百行,核心报错反而被淹没。我的习惯是先kubectl logs <pod> --tail=100 --timestamps,只看最近 100 行,同时开启时间戳,用时间戳和 describe 里的容器启动时间、退出时间对齐,能判断日志是启动阶段的输出还是运行过程中的报错。多容器场景下还要注意加-c <container-name>,因为 Pod 里如果是 sidecar 模式,多个容器的日志是混在一起的,不指定容器名就拿不到目标容器的日志。
还有一种不太常用但很能救命的情况:当容器崩溃太快,连--previous也拿不到完整日志时,可以临时把 Pod 的启动命令改为睡眠指令,比如command: ["sleep", "infinity"],然后用kubectl exec进去手动启动原进程,观察前台输出。这种诊断方式需要临时修改 Deploy 或 Pod 定义,适用于线上容器无法正常进入、但你想复现问题现场的场景。
2.3 容器内没有日志时的另一个出口:查看容器运行时与 Kubelet 事件
有相当一部分 CrashLoopBackOff 是“容器起不来,日志也完全为空”的。比如启动命令指定的脚本不存在、二进制文件没有执行权限、镜像里缺动态链接库,这些错误发生在容器进程真正启动之前,日志不会输出到 stdout,只存在于容器运行时的事件里。此时看kubectl logs基本没用,要去看 describe 的 Events,常见的failed to create shim、exec: "xxx": executable file not found都指向这个层面。
更细致的检查还可以进入容器运行时的日志目录。如果用的是 containerd,Kubelet 的崩溃事件会记录在系统日志里,执行journalctl -u kubelet -n 200能看到 Kubelet 与容器运行时交互时的报错。当然,这条命令在大多数托管 K8s 平台(如 EKS、ACK)里不可用,你只能通过 describe 间接获取信息。但对于自建集群来说,这条路径能帮你找到很多 API 层看不到的底层细节,比如 CNI 调用失败、运行时 state 异常、卷挂载超时等。
3. 五类高频根因与真实案例复盘
3.1 启动命令与镜像配置问题:Entrypoint、Command、可执行权限
先说一下我见过最典型的一类:镜像构建时没设置任何 CMD 或 ENTRYPOINT,而 Dockerfile 的基础镜像又恰好没有常驻进程,Kubernetes 调度后容器启动即退出,日志里往往只有一句空输出。查下来发现是团队在写 Deployment 的 YAML 时压根没写 command 字段,容器起来后发现没啥可跑的,就正常退出了。这类问题的修复其实很简单,重新在 Deployment 的containers[].command里指定启动命令就好,但如果在复杂的多环境配置里,排查起来会被其他噪音干扰,容易绕弯路。
另一种情况是 command 里写错了路径。比如之前有个服务使用自定义脚本启动,镜像里脚本放在 /app/bin/start.sh,但 YAML 里写的是 /opt/bin/start.sh。执行后 Kubelet 报exec: "/opt/bin/start.sh": stat /opt/bin/start.sh: no such file or directory,容器瞬间退出。这类问题直接表现在事件里,但如果不看 describe,光看日志也会一头雾水。所以我的习惯是:凡是 CrashLoopBackOff 且日志无输出的,第一件事就是在 describe 的事件里搜索exec、entrypoint、command关键字。
还有一类比较隐蔽:镜像里脚本没有加执行权限。Dockerfile 里 COPY 进来的 shell 脚本默认没有 x 权限,如果启动命令是./start.sh而不是bash start.sh,大概率会报 Permission denied。这个错误同样出现在事件层而不是应用日志层。把基础镜像换成 distroless 这类精简镜像时尤其容易踩坑,因为 distroless 镜像里连 shell 都没有,无法用交互方式进入容器调试,排查时要更依赖事件信息。
3.2 依赖缺失与环境变量问题:配置没挂载、DB 没就绪、initContainer 怎么用
业务应用启动失败最常见的问题是外部依赖不满足。举一个非常普遍的案例:Java 服务启动时需要从环境变量里读取数据库连接串,但 Deployment 里没有注入这个环境变量,Spring Boot 启动时初始化 DataSource 失败,直接抛异常退出。这类 CrashLoopBackOff 的日志其实非常友好,堆栈里明确写了CannotGetJdbcConnection一类的信息,看到基本就能判断方向,接下来用kubectl get pod -o yaml检查 env 是否缺失就行。
要提醒的是,日志只能告诉你“数据库连不上”,但不会告诉你“是密码错了”“还是网络不通”“还是数据库侧 IP 白名单没放开”。遇到这类问题要多层排查:先看环境变量键值是否匹配,再看 ConfigMap 或 Secret 是否挂载到了预期的路径,再看应用所在节点是否能连通数据库服务。如果网络隔离基于命名空间或安全组,往往还要做跨层验证。这些步骤没有捷径,只能一步步来。
为了避免主容器启动时反复被依赖问题拖累,更好的做法是使用 initContainer 做前置依赖检查。initContainer 和主容器共享存储,但它只负责做初始化,运行成功后就会退出,主容器才会启动。它的经典用法是循环探测数据库端口,或者等待某个外部服务就绪。有一个注意点:initContainer 如果反复退出,Pod 的状态也会表现为 CrashLoopBackOff,但 describe 里能看到容器层面区分——事件里的Init:CrashLoopBackOff会明确告诉你问题出在 init 阶段,而不是主容器阶段。
3.3 资源限制与容器权限:OOMKilled、探针误杀、目录读写权限
容器跑起来之后又崩,资源问题是最大元凶之一。K8s 中如果容器内存使用量超过了 limits.memory 设定值,容器会被内核 OOM Killer 杀死,退出码是 137,同时 Kubelet 会在事件里记录OOMKilled。很多 Java 服务遇到这个问题,是因为 JVM 默认最大堆内存设置为物理内存的 1/4,而容器的内存限制可能只有 512MB,JVM 刚启动时就算没跑到堆上限,加上 Metaspace、线程栈、堆外内存后也会超限被 kill。
Java 容器场景还要留意一个历史问题:老版本 JDK(8u191 之前)不识别 cgroup 的 CPU 和内存限制,会导致 JVM 默认识别到宿主机全部资源,进而按宿主机规格计算堆大小。升级到新一些的 JDK 版本,打开UseContainerSupport就能让 JVM 读取容器资源限制。虽然这个功能默认开启,但你可能遇到 JDK 版本差异或显式关闭的配置,所以排查 Java 服务崩溃时除了看退出码,也要确认 JVM 拿到的堆大小是否和容器 limit 一致。
权限问题也值得单独提一下。容器内进程如果以非 root 用户运行,而对挂载目录没有写权限,应用启动阶段写日志或写临时文件就会失败,表现就是启动一半就退出。解决方式不只有全局 chmod。在 K8s 里更规范的做法是用 securityContext 里的fsGroup或runAsUser来声明 Pod 内文件所属的用户和组,让挂载卷具备正确的属主和权限。这个知识点非常常用,尤其当遇到 NFS 或 PVC 类型的持久化存储时,权限错误几乎属于必踩的坑。
3.4 探针存活探测失败:应用其实活着,但被 Kubelet 判定“死”了
LivenessProbe 和 ReadinessProbe 是 K8s 探活体系的两大核心。Readiness 探针失败会把 Pod 从 Service 的 Endpoints 里剔除,但不会杀容器;Liveness 探针失败则会直接杀死容器并触发重启,进而引发 CrashLoopBackOff。最常见的坑是:探针配置的 path 不对、端口不对、或者应用启动速度比探针初试间隔慢太多。
举一个我调过的真实案例:一个 Spring Boot 服务的 health 接口在启动过程中需要加载大量本地数据,通常需要 30 秒左右。当初配置 livenessProbe 时initialDelaySeconds设成了 5,periodSeconds 是 10,failureThreshold 是 3,也就是说容器启动后 5 秒开始探测,每 10 秒一次,如果连续 3 次失败就杀。结果就是:应用还在启动中,探针已经开始报失败,第 3 次失败后容器被杀,进入 CrashLoopBackOff,完全不给应用完成加载的机会。修复方式很简单,把 initialDelaySeconds 调到 40,或者增加 failureThreshold,但背后的教训是:探针参数的设置必须以应用真实启动耗时和资源消耗为基准,不能拍脑袋。
排查探针问题时,describe 事件里有一行很有标志性:Liveness probe failed: HTTP probe failed with statuscode: 500。看到这个,先不要怀疑应用本身,先去看探针配置是否符合应用启动节奏。另外,探针成功路径上的日志量也要关注,频繁探测会放大日志量、消耗不必要的 CPU,所以探针间隔不能无脑设得非常小。
3.5 应用逻辑本身的“假崩溃”:批处理容器与单次任务容器的循环退出
不是所有 CrashLoopBackOff 都意味着错误。有一种场景是应用逻辑上“跑完就退”,但 restartPolicy 还是默认的 Always,于是容器每次正常退出后被 Kubelet 拉起来重新跑,跑完又退出,形成无限循环。这类问题最典型的就是 CronJob、DataX 同步任务、迁移脚本这类批处理容器。DataX 容器执行完数据同步后,进程正常结束,退出码为 0,但从 K8s 视角看,任何非零退出都是“异常”,容器退出后必然会被重启。
如果遇到批量任务类的镜像出现 CrashLoopBackOff,第一反应应该是检查任务本身是否已经执行成功。正确的处理方式是把 restartPolicy 改为 OnFailure 或 Never。OnFailure 只在非零退出码时才重启,Never 则完全不重启。对于 CronJob,K8s 本身的机制已经控制了是否重跑,底层 job 容器如果还设了 Always,就会出现明明 job 已完成但容器还在不断重启的诡异现象。
还有一种比较误导人的场景:容器经过若干次重启后,应用终于起来了,但状态显示 CrashLoopBackOff 没变。这是因为 Kubelet 的退避计时还没走完,或者状态字段没有及时刷新。遇到这种“日志显示正常但状态还不更新”的情况,不要急着回滚或重建,先等一下再kubectl get pod观察状态是否自然切换。如果长时间不恢复,再进一步排查退避周期和被拒原因。
4. 排查避坑速查:现象、原因、对应命令
4.1 高频现象对照表:看到哪个事件优先查什么
整理一个我在日常排查时最常用的速查表,每一项都来自真实案例。遇到 CrashLoopBackOff 时,可以按照表格里的对应关系快速找切入点,避免从头开始瞎翻。
| 现象特征 | 可能的根本原因 | 优先排查命令/位置 |
|---|---|---|
| 日志完全为空 | 启动命令不存在、无执行权限、镜像缺文件 | describe 事件、容器运行时日志 |
| 日志有报错但退出很快 | 配置缺失、依赖不可达、数据库连接失败 | kubectl logs --previous --tail=100 |
| 退出码为 137 | 内存超限 OOMKilled | describe Events 里的 OOMKilled、limits 配置 |
| 退出码为 143 | 被 SIGTERM 终止,通常是探针杀容器或手动删除 | describe Events、探针配置、终止时间线 |
| describe 显示 Init:CrashLoopBackOff | initContainer 启动失败 | kubectl logs <pod> -c <init-container-name> |
| 事件里看到 BackOff 重启 | 容器反复失败,进入退避周期 | 按日志方向排查,同时关注退避时间 |
| 应用日志显示正常但容器仍被杀 | Liveness 探针误杀 | 探针参数、failureThreshold、initialDelaySeconds |
| 批量同步任务跑完就退 | restartPolicy 为 Always,正常退出后触发重启 | 调整 restartPolicy 为 OnFailure/Never |
表中“退出码 137”和“退出码 143”在实操中很常见。137 表示容器进程被 SIGKILL 杀死,最常见的是 OOMKilled;143 表示进程收到 SIGTERM,常见于应用优雅停机失败后的强制结束。退出码本身能帮你快速缩小排查范围,但最终定位还是要结合事件和日志一起看。
4.2 排查时容易踩的坑:时间戳、多容器日志、探针间歇性失败
我列几个自己踩过的坑,每一个都花了不少时间才走出来。第一个坑是只看容器当前日志,不看 previous。容器被探针杀掉后会重新拉起,新的容器如果启动失败,日志可能只有短短几行,真正崩溃前的完整堆栈都在 previous 里,所以--previous一定要形成肌肉记忆。第二个坑是忘记看事件时间戳和日志时间戳的对齐关系。容器退出后日志打印时间和 Kubelet 杀容器的时间如果差距过大,说明不是探针立即触发的问题,而可能是应用卡死后超时被杀,两者对排查角度的影响完全不同。
第三个坑是初始化容器与主容器的日志混在一起看。一个 Pod 里如果既有 initContainer 又有普通容器,你直接跑kubectl logs默认拿的是主容器日志,init 失败的报错得用-c init-container-name才能看到。很多同学对 initContainer 不熟悉,看到 Pod 状态 CrashLoopBackOff 就死磕主容器日志,结果怎么查都查不到根因。第四个坑是探针间歇性失败时容易被事件刷屏误以为很严重。偶尔一次探测失败可能只是瞬时抖动,不代表容器真的有问题。如果你发现探针失败事件频繁但应用日志没有异常,先调整探针参数观察,不要盲目回滚版本。
4.3 定位之后的修复路径:临时诊断命令与永久修复策略
定位到根因之后,是直接改 YAML 还是先用临时命令验证,取决于问题的影响范围。如果是测试环境,我通常会直接改 Deployment 并滚动更新,观察新 Pod 是否恢复;但如果是生产环境,我倾向于先做一个临时 Pod 或临时修改 command,比如用kubectl run起一个同镜像但加了sleep infinity的调试容器,在容器内手动执行原启动命令,观察报错是否复现。这种方式不改变现网配置,风险最小。
永久修复时,要注意把临时诊断时的改动还原。曾经有同事为了排查问题,在 Deployment 的 command 里临时覆盖成了 sleep,定位完后忘了改回去,部署结果变成了容器不执行业务逻辑,只是挂着不动。这种低级错误在团队里出现过不止一次。修复后一定要回归验证,确认 Pod 状态稳定、日志正常、探针通过,同时查一遍 git 记录看改动是否完全落库。
另外,针对探针问题、依赖问题这类比较常见的 CrashLoopBackOff 根因,修复时不能只改参数或启动方式,还要把“为什么这么设置”沉淀到文档或注释里。比如探针间隔为什么设 40 秒、initContainer 为什么要做额外的 DNS 检查。这不只是为了后人看代码时能理解,也是为了下次出现类似问题时,你可以直接参考历史决策,而不是把整个排查流程又走一遍。
5. 从排障到预防:几个值得长期养成的习惯
5.1 用好 Hooks 与健康检查:把“跑起来再挂”变成“挂之前有痕迹”
我发现很多 CrashLoopBackOff 问题之所以排查耗时,是因为应用启动阶段的信息没有可靠地输出到容器 stdout/stderr。比如某些 Java 服务把启动日志写进了文件而不是控制台,导致 K8s 的日志体系完全看不到启动过程。要解决这个问题,最直接的做法是通过容器化环境规范让所有应用日志都走 stdout/stderr,同时在应用侧增加启动阻塞钩子,确保关键前置检查失败时能尽早暴露,而不是一路启动到最后才报错。
PreStop Hook 也同样值得配置。容器在收到 SIGTERM 后会执行 preStop 里定义的动作,比如向外部系统注销、把临时数据刷盘。如果没有 preStop,进程直接结束,下次重启时可能因为上次留下了脏状态而起不来。这类问题几乎都能通过健康检查加钩子的组合在启动阶段就拦截,而不是等进入 CrashLoopBackOff 才手忙脚乱。
5.2 统一日志采集与检索:让“翻日志”这件事不再依赖命令行
K8s 节点上的容器是不断变化的,日志会随容器生命周期一起消失。如果只在崩溃当时用 kubectl logs 拉日志,等容器被清理或节点被回收之后,日志很难再找回来。所以在生产环境里,我强烈建议接入统一的日志采集方案,比如通过 DaemonSet 部署 Filebeat 或 Fluentd,把容器 stdout/stderr 日志采集到 ElasticSearch、Loki 或云厂商的日志服务里。这样排查 CrashLoopBackOff 时,可以直接按 Pod 名、容器名、时间范围检索历史日志,甚至能看到一个 Pod 跨多次重启的完整日志链条。
采样和日志格式也需要提前规划。如果每条日志都带时间戳和结构化字段,后续检索会很方便;如果日志只是纯文本堆叠,检索堆栈信息就非常痛苦。我们团队的要求是:所有新服务在接入 K8s 时,都必须在三天内完成 stdout 输出规范和日志平台接入,否则不允许上线。这个规定不是形式主义,是真的能节约大量故障排查时间。
5.3 把排障记录沉淀成 Playbook:同一个坑确保只踩一次
CrashLoopBackOff 排查本身是有模式可循的,因此非常适合沉淀成团队内的排障手册。我建议每次处理完一个真实的线上问题后,都在文档里简单记录:现象是什么、第一次看到时想到了什么方向、最终定位用了哪些命令、修复方式是什么、应当怎样在以后避免。不需要写成长篇大论,三五行加一个命令示例就够了。时间久了,这份记录的价值会超过任何付费知识库。
我个人的体会是,排查 CrashLoopBackOff 最消耗时间的往往不是专业技术的瓶颈,而是对环境中细节的不熟悉:这个服务依赖哪些外部系统、日志路径在哪个位置、启动需要多少秒、探针之前有没有因为改动被调整过参数。这些上下文信息只有通过日常运维和记录才能真正积累下来。当你能对着一个陌生的 CrashLoopBackOff 状态,很快说出“日志在什么位置、依赖有哪些、上次改动过什么”时,排障速度自然就快起来了。
最后再分享一个实战小技巧:在首次拿到 CrashLoopBackOff 的 Pod 时,先截一张完整 describe 输出和 logs 输出的存档,哪怕暂时看不出来问题。因为退避重试过程中,Pod 重启次数越多,最原始的现场信息越容易被覆盖。这个存档动作,可能在你怀疑“这到底是不是同一批 Pod”时,帮你节省一整个下午。