不知道你有没有过这种体验:线上一个 Pod 状态栏躺着个CrashLoopBackOff,日志翻了一下午也看不出它到底死在哪一步;面试被问到“Pod 的生命周期和状态转换”,能背出 Pending、Running、Succeeded、Failed、Unknown 五个词,但追问一句“一个 Pod 里多个容器,其中一个没启动成功会影响别人吗”,当场卡壳。我早几年玩 Kubernetes 也是这么被绕晕的。这玩意儿看着是张状态机,真落到 kubectl 命令行和线上故障里,全是细节。这篇文章就以 Pod 生命周期为主线,把状态转换、多容器协作、探针机制、优雅终止、排障思路一次讲透,适合正在学 K8s 的开发者,也适合做过一段时间集群运维但老被状态机坑的人。
顺带说一句,搜索引擎里经常有人把“Spring Bean 生命周期”“Android Activity 生命周期”和 Pod 生命周期混在一起搜,这完全是三个层级的东西,别被概念带跑。K8s 的 Pod 生命周期,管的是容器进程组从创建到销毁的完整编排流程,它发生在集群调度器和节点 kubelet 这一层,和代码框架里的生命周期回调不是一回事。
1. 先搞懂一件事:Pod 生命周期管的是“一窝容器”的命运
1.1 为什么 K8s 的最小调度单位是 Pod 而不是容器
很多新手一开始会困惑:K8s 里明明一个 Pod 里可以塞多个容器,为什么不用容器作为调度单位?答案在于“共享”和“协同”。
一个容器本质上只是一个进程,它可以有自己独立的文件系统、网络栈、PID 命名空间。但在真实业务里,一个应用往往不是单个进程能搞定的:你需要一个 nginx 进程对外提供入口,一个日志采集进程在旁边把 stdout 转发到日志系统,可能还要一个边车容器做本地缓存清理或配置热更新。这些进程必须跑在同一台机器上,共享同一个网络 IP 和存储卷,才能高效配合。K8s 的 Pod 就是为这种“同生共死的进程组”设计的。
我把 Pod 类比成“一窝鸟”:Pod 是鸟窝,里面的容器是幼鸟。K8s 调度器不会单独为某一只幼鸟找树杈,它整窝搬。容器之间共享网络命名空间,意味着它们都可以通过 localhost 互相访问,共享同一个 Pod IP、共享挂载的 Volume;同时它们也有独立的东西,比如文件系统层、进程树、资源配额。所以 Pod 是 K8s 的最小调度和资源单位,生命周期也是以 Pod 为单位对外呈现的。
这也解释了一个常见误区:很多人用podman create pod也在本机创建了一个类似 Pod 的分组,但 podman 的 pod 是单机容器工具层面的概念,用来在本机管理一组容器的共享命名空间;K8s 的 Pod 则是由 API Server、Scheduler、kubelet 共同驱动的分布式编排对象。两者都叫 Pod,但生命周期管理方式完全不同,别把它们当成同一套机制。
1.2 PHASE 与容器状态:kubectl 里那串状态到底是谁的状态
用kubectl get pods看状态时,你其实看到了两类信息:PHASE和聚合出来的STATUS列。这两者非常容易被混淆,也是很多人排障半天找不到北的根源。
Pod 的 PHASE 是 K8s 对 Pod 整体生命周期的粗粒度定义,总共五个阶段:
| PHASE | 含义 |
|---|---|
| Pending | Pod 已被 API Server 接受,但还没有完成调度,或正在下载镜像、创建容器 |
| Running | Pod 已绑定到节点,所有容器已创建,至少有一个容器正在运行,或在启动/重启过程中 |
| Succeeded | Pod 中所有容器都已成功终止,并且不会重启 |
| Failed | Pod 中所有容器都已终止,且至少有一个容器以失败方式终止(非 0 退出码或系统终止) |
| Unknown | 无法获取 Pod 状态,一般是因为与 Pod 所在节点通信失败 |
但kubectl get pods的STATUS列经常显示CrashLoopBackOff、Init:0/1、OOMKilled这种花式状态,它并不是标准 PHASE,而是把容器状态和 Pod 条件聚合后展示的“增强状态”。换句话说,PHASE 告诉你 Pod 总体走到了哪一步,容器状态才告诉你具体的容器实例发生了什么。
容器的标准状态有三种:Waiting(等待创建、拉镜像、崩溃退避中)、Running(容器进程活着)、Terminated(容器执行完毕或失败)。每个状态下还有 reason,比如ContainerCreating、ImagePullBackOff、CrashLoopBackOff、Error、Completed、OOMKilled,这些才是排障时真正要盯的细节。
2. 状态机拆解:从 Pending 到 Failed,每一步都在发生什么
2.1 Pending:从 yaml 提交到调度成功
kubectl apply -f pod.yaml提交之后,Pod 对象首先会被写入 etcd,此时它的 PHASE 就是 Pending。Pending 阶段做了两件大事:调度和资源准备。
调度器从 API Server 里 watch 到未调度的 Pod,然后根据节点资源、污点容忍、亲和性、拓扑分布等一系列约束挑选一个节点,把 Pod 的nodeName写回去。接着节点上的 kubelet 开始干活:创建 Pod 的 sandbox、准备存储卷、拉取镜像、启动容器。
如果你发现 Pod 一直卡在 Pending,最值得怀疑的是调度失败。用kubectl describe pod xxx看 Events,通常会看到类似:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 10s default-scheduler 0/6 nodes are available: 2 Insufficient cpu, 2 node(s) didn't match node selector, 2 node(s) had taint {key: value}, that the pod didn't tolerate.这里每一句都是线索:Insufficient cpu说明节点 CPU 不够;didn't match node selector说明你的 nodeSelector 或亲和性找不到匹配节点;had taint ... didn't tolerate说明节点有污点,而 Pod 没有对应容忍。
还有一种调度失败比较隐蔽:高优先级 Pod 抢不到资源时,调度器会尝试抢占低优先级 Pod,但如果找不到任何可被抢占的对象,就会产生一条类似no preemption victims found for incoming pod的事件。它翻译过来就是“这个 Pod 已经很急了,但集群里所有可牺牲的 Pod 都不存在”。遇到这种情况,不要只盯着 Pod 改配置,要往集群容量和 PriorityClass 的方向排查。
2.2 Running:业务代码跑起来之后,生命周期还没结束
Pod 进入 Running 阶段,说明调度完成了,Pod sandbox 和业务容器都已创建成功,并且至少有一个容器处于运行状态。但这里有个大坑:Pod 显示 Running 不代表容器一切健康。
因为在容器反复崩溃重启的过程中,Pod 的 PHASE 依然可以是 Running。K8s 官方定义里,Running 包含“至少有一个容器仍在运行,或正在启动或重启过程中”。也就是说,你看到一个 Pod PHASE 是 Running,但它STATUS列完全可能是CrashLoopBackOff,这太常见了。
Running 阶段真正决定服务质量的是探针(Probe)。就绪探针失败时,容器进程虽然活着,但不会收到 service 转发的流量;存活探针失败时,kubelet 会杀掉容器并按重启策略拉起新容器。所以 Running 只是一个表象,容器内部是否健康、是否具备服务能力,完全由探针和业务自身决定。
2.3 Succeeded / Failed / Unknown:终点也分三种
Pod 走到终点无非三条路:正常退出、失败退出、失联。
Succeeded表示 Pod 内所有容器都以退出码 0 正常结束,且不会重启。这种场景多见于 Job、CronJob 跑批任务:任务执行完,Pod 进入 Completed 状态,不会白白占资源。如果你用 Deployment 跑常驻服务,理论上不应该出现 Succeeded,因为常驻进程不会正常退出,一旦退出就会按重启策略重新拉起。
Failed表示容器全部终止,并且至少有一个容器以非 0 退出码结束或被系统终止。比如restartPolicy: Never的 Pod 里某个容器执行出错,Pod 就不会重启,而是直接 Failed。这是工作负载类型(Job 类)常用的状态。
Unknown则是节点层面出问题才出现的,通常是 kubelet 失联或节点宕机,API Server 短时间内拿不到 Pod 的最新状态。这时候 Pod 可能还在旧节点上活着,但控制面已经“失去联系”了。遇到 Unknown,一般等节点恢复后状态会刷新;如果节点长期不可用,Deployment 等控制器会根据 pod eviction timeout 把 Pod 重建到其他节点。
3. 一个 Pod 里有多个容器时,生命周期怎么协同
3.1 Init 容器:主容器启动前的“守门员”
刚才说 Pod 是“一窝容器”,但这一窝容器不是同时一股脑启动的。某些特殊的容器必须在业务容器启动之前完成初始化,这就是 Init 容器。
Init 容器和普通容器最大的区别在于:它们串行执行,并且全部成功之后才会启动业务容器。只要有一个 Init 容器失败,整个 Pod 就停在 Init 阶段,业务容器根本不会启动。初始化失败的容器会按照 Pod 的restartPolicy决定是重启整个 Pod 还是直接进入失败状态。反复失败的 Init 容器会在状态里显示Init:CrashLoopBackOff,这时候就算你把业务容器镜像换成全宇宙最稳定的版本也没用,问题一定出在初始化逻辑上。
Init 容器适合做什么?等数据库就绪、下载依赖、初始化目录权限、写配置文件、给存储卷准备数据。我比较推荐把轻量的“前置条件检查”放进 Init 容器里,但不要在 Init 容器里跑太重的任务,因为它是串行的,拖的时间越长,整个 Pod 启动越慢。
注意:Init 容器失败后如果触发整个 Pod 重启,已经执行成功的 Init 容器也会重新执行一遍。有一些人在这里踩过坑,以为 Init 容器只在 Pod 创建时跑一次,其实 Pod 重启后所有 Init 容器都会重新来一遍,所以初始化脚本要写成幂等的,不然第二次执行时会出各种奇怪问题。
3.2 普通容器之间:一个容器崩了,另几个会怎样
这是很多人特别关心的一个问题:一个 Pod 里包含多个容器,如果其中一个容器没有成功启动,会不会影响其它容器?热搜词里也出现了这句话,答案很明确:会影响,但不能简单地说“会杀死别的容器”,要看影响在哪个层面。
先从机制上说:普通业务容器之间没有启动顺序保证,谁先起来全看运行时调度。当其中一个容器启动失败或崩溃时,kubelet 会按照restartPolicy单独重启这个失败的容器,而不会立刻杀掉其它正常容器。如果restartPolicy是Always,崩溃的容器会被一遍一遍重启,其它容器在这期间可能还活着、还在工作。
但问题在于 Pod 的整体就绪状态。“Pod Ready”有一个严格定义:Pod 内所有容器都通过就绪探针(没有探针就认为就绪)之后,Pod 才会被标记为 Ready。只要有一个容器一直起不来或一直不 Ready,这个 Pod 就不会被加入 Service 的 Endpoints,也就不会收到任何业务流量。从外部看,服务就是不可用的,这就是“影响”的真相。
如果restartPolicy是Never或OnFailure,影响就更直接了:失败容器不会被重启,等到所有容器都终止后,整个 Pod 会进入Failed阶段,直接被控制器回收。所以你不能指望一个 Pod 里的容器是完全“物理隔离”的,Pod 本质上就是一个故障域:一个容器挂了,整个 Pod 的服务能力都要打折。
3.3 探针:在生命周期内部做“微调节”
探针是 Pod 生命周期里最灵活的调节手段,它决定了一个 Running 的容器到底是“真的能干活”还是“假活”。
K8s 提供了三种探针:
| 探针类型 | 作用 | 失败后果 |
|---|---|---|
| startupProbe | 判断容器应用是否启动完成 | 失败按 restartPolicy 重启容器 |
| livenessProbe | 判断容器是否存活 | 失败会杀掉容器并重启 |
| readinessProbe | 判断容器是否具备服务能力 | 失败只会摘掉流量,不重启容器 |
实际配置时,三者的分工很容易搞混。我的经验是:livenessProbe只检查“进程活没活”,readinessProbe检查“能不能对外提供服务”,startupProbe专门解决那些启动特别慢的应用。
举一个典型的 Java 应用例子:JVM 启动可能要 20-30 秒,但如果你设了每 10 秒探测一次、失败阈值 3 次的 livenessProbe,应用还在预热就被杀了,然后陷入无限重启。这时候加一个 startupProbe,告诉 K8s:“你先给老子 60 秒慢慢起,起来了再开始用 liveness 管我。”
apiVersion: v1 kind: Pod metadata: name: probe-demo spec: restartPolicy: Always containers: - name: app image: myapp:latest startupProbe: exec: command: - sh - -c - "test -f /tmp/startup.lock" periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /livez port: 8080 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 5 timeoutSeconds: 2这段配置的思路是:先用文件锁方式的 startupProbe 判断应用是否完成启动,等它成功后,liveness 才开始接管;readiness 则持续检查服务是否可用,一有问题就自动从流量池里剔除。
4. 状态转换的幕后推手:kubelet、Pause 容器与优雅终止
4.1 从 kubectl apply 到容器运行的全链路
理解 Pod 生命周期,不能只停留在 kubectl 输出上,你得在脑子里有一条完整的调用链:
kubectl apply把 Pod 定义发给 API Server,API Server 校验后写入 etcd。- Scheduler 一直 watch 未调度的 Pod,发现有符合条件的 Pod 后,为它挑选节点,并把
nodeName写回。 - 选中节点上的 kubelet watch 到自己被分配了这个 Pod,开始调用容器运行时的 CRI 接口准备 sandbox。
- 运行时先创建 pause 容器,也就是 infra 容器,它负责持有 Pod 的网络、IPC 等命名空间。
- 然后 kubelet 挂载卷、拉镜像,按顺序创建普通业务容器,把它们加入 pause 容器的命名空间。
- 容器启动后,探针开始工作,Pod 状态最终进入 Running。
排障时可以用crictl pods和crictl ps -a直接看容器运行时的视角,很多 API Server 层面看不到的细节,在 CRI 层会暴露得更清楚。比如 Pod 卡在ContainerCreating,多半是 sandbox 创建失败、卷挂载失败或 CNI 网络配置失败,这时候光看日志不一定有用,得去节点上查 containerd 的日志。
4.2 Pause 容器为什么是 Pod 生命周期的“锚点”
很多人第一次看节点上的容器列表时会被吓一跳:怎么每个 Pod 旁边都挂着一个叫pause的容器?其实它就是整个 Pod 的生命周期锚点。
pause 容器的职责特别单纯:创建一个称之为 infra 的容器,占住网络命名空间、IP 地址和 PID 命名空间,然后一直休眠。其它容器加入这个命名空间,从而做到网络共享和进程共享。可以说,Pod 是“活的”还是“死的”,在底层看的就是 pause 容器还存不存在。
明白了这一点,你就理解了一个现象:业务容器怎么崩、怎么重启,只要 pause 容器还在,Pod 这个对象就还在集群里。反过来,如果 pause 容器被删掉或异常退出,整个 Pod 的 sandbox 就没了,业务容器也会被连带清理。所以你可以把 pause 容器理解成“房子的地基”,业务容器是屋子里的人,地基不塌,房子就可以一直换人住。
这个机制还解释了另一个细节:为什么容器重启时 Pod 的 IP 不会变。因为 IP 被 pause 容器持有了,不管内部容器怎么重建,只要 sandbox 还在,IP 就是稳定的。
4.3 删除 Pod 时的优雅终止流程
Pod 生命周期不仅包括“怎么活着”,还包括“怎么体面地死”。删除 Pod 时,K8s 不会直接在后台一刀切,而是走一套有序的优雅终止流程:
- 你执行
kubectl delete pod,Pod 状态变为 Terminating,立即从 Service 的 Endpoints 里摘除,不再接收新流量。 - kubelet 收到通知后,并行执行所有容器的
preStop钩子,如果没有配置 preStop,则跳过。 - preStop 执行完后,kubelet 向容器主进程发送
SIGTERM,给应用一次清理资源、断开连接的机会。 - 默认宽限期
terminationGracePeriodSeconds是 30 秒,如果宽限期耗尽进程还没退出,kubelet 直接发SIGKILL强制杀死。
这里最容易踩的坑是应用不处理SIGTERM。很多从传统虚拟机迁过来的服务,应用只习惯被强杀,从来没做过信号处理,结果滚动更新时旧 Pod 永远要等到 30 秒宽限期结束才被 SIGKILL,用户侧就会感觉到连接中断、请求超时。
解决办法是两件事一起做:应用里捕获SIGTERM,先停止接收新请求、把内存中未处理完的请求处理完再退出;同时配置preStop钩子,在收到删除指令后主动做一次“下线通知”,比如去注册中心摘掉自己的节点,或者 sleep 几秒等网关更新完转发规则。
apiVersion: v1 kind: Pod metadata: name: graceful-demo spec: terminationGracePeriodSeconds: 60 containers: - name: app image: myapp:latest lifecycle: preStop: exec: command: - /bin/sh - -c - "sleep 5 && curl -s -X POST http://127.0.0.1:8080/offline || true"上面这个 preStop 的意思是:先睡 5 秒,让 Service 的 Endpoints 变更同步完成,再主动调应用的 offline 接口做优雅下线。注意|| true很关键,万一 offline 接口挂了,preStop 也不能让 Pod 卡死,否则会白白耗尽宽限期。
5. 生产环境排障实录:生命周期卡死的典型场景
5.1 Pending 卡死:先查 Events,再查资源请求
我见过太多新手一看到 Pending 就跑去翻应用日志,其实 Pod 都没调度成功,日志根本不存在。正确姿势永远是先kubectl describe pod看 Events。
最常见的原因是节点资源不够。你在 Deployment 里给容器设了requests.cpu: 2、requests.memory: 4Gi,但集群里每台节点的可分配 CPU 只剩 1 核,调度器就算把所有节点遍历一遍也塞不下。这时候kubectl get nodes看到的状态可能是 Ready,但describe node一查就是满的。
处理方式无非几条路:扩容节点、降低 requests、把 Pod 的调度约束放宽、或者把优先级较低的 Pod 停掉。如果你在事件里看到no preemption victims found for incoming pod,说明当前 Pod 优先级不低,但集群里实在找不到可以抢占的对象,这时候最有效的动作是加资源,或者在业务允许的情况下拆分大 Pod。
我还想强调一点:requests 不是 limits。requests 是调度依据,limits 是运行期间的限制。很多人为了省事只写 limits,不写 requests,这会导致调度器无法准确判断资源占用,非常危险。一定记得两个都写,而且 requests 不要拍脑袋写,最好根据压测和监控数据来定。
5.2 CrashLoopBackOff 与 OOMKilled:退出码是突破口
CrashLoopBackOff应该是线上出现频率最高的异常状态之一。看到这个状态时,先把kubectl logs <pod> --previous拉出来看上一次容器的完整日志,因为你当前看到的容器可能刚被重启,日志是空的。
第二个关键信息是退出码。kubectl describe pod的容器状态里会写 exitCode,不同的退出码指向完全不同的排查方向:
| 退出码 | 信号 | 常见原因 |
|---|---|---|
| 0 | 无 | 正常退出,但常驻进程不该退出 |
| 1 | 无 | 应用自身异常,看业务日志 |
| 2 | 无 | 命令行参数错误、应用初始化失败 |
| 127 | 无 | 命令找不到,通常 entrypoint 路径写错 |
| 137 | SIGKILL | 被强杀,最常见是被 OOM Killer 杀掉 |
| 143 | SIGTERM | 收到终止信号,可能是优雅终止逻辑卡住 |
| 255 | 无 | 启动命令或脚本异常退出 |
OOMKilled是退出码 137 里最典型的场景:容器实际内存使用超过了limits.memory,被内核 OOM Killer 一刀毙命。这种问题改代码和加内存 limts 都是路子,关键是要先确认是业务内存泄漏,还是 limits 设得太紧。可以通过kubectl exec进容器看实时内存,也可以给容器加监控看内存曲线,不要一上来就盲目调参。
5.3 Evicted 与节点压力驱逐
如果你看到 Pod 状态是Evicted,先别慌,这不是某个容器崩了,而是节点整体压力太大,kubelet 主动把这个 Pod 驱逐了。
节点层面的驱逐信号包括内存压力、磁盘压力、PID 压力和文件系统 inode 压力。kubelet 按服务质量等级从低到高驱逐 Pod:BestEffort(没设 requests/limits)最先被驱逐,Burstable其次,Guaranteed(requests 等于 limits)最后。所以你会看到,越是不设置资源请求的 Pod,在节点压力大时越容易先死。
防止 Evicted 的有效手段:所有 Pod 都设置合理的 requests 和 limits;节点上的容器镜像做好 GC,日志做轮转,别把节点磁盘写满;对于系统关键服务,尽量用 Guaranteed QoS。
另外,Evicted 的 Pod 被驱逐后会变成 Failed,它的 terminating reason 会写Evicted。这类 Pod 一般不用手工清理,控制器会重新调度新的副本。
5.4 关于并发量的一个常见误区:2c4g 能撑多少并发
热搜词里“2c4g 的 Pod 支持的并发量”是个特别容易误导人的问题。很多人想找一个万能答案,比如“2c4g 能支持 500 QPS”,但现实根本不存在这种通用结论。
并发能力取决于太多因素:业务是 CPU 密集还是 IO 密集、单请求平均耗时是多少、每个请求占用多少内存、语言的运行时模型是什么(Java 的堆和 Go 的 goroutine 差别巨大)、有没有连接池和外部依赖、网络延迟多少。一个纯静态文件服务器和一个做复杂计算的 Java 应用,你给它同样的 2c4g,性能可能差一个数量级。
正确做法是直接压测,观察三个指标:CPU 使用率、内存使用率、请求响应时间。假设你的 SLA 要求 p99 响应时间小于 500ms,那么就在压测中不断增加并发,直到 CPU 到 70% 或内存到 80% 或响应时间突破 SLA,这个临界点所承受的 QPS 才是这个 Pod 的真实容量。之后再根据容量设定 HPA 的阈值和 Pod 的 requests/limits,资源估少了自然会引发 Pending、Evicted、OOMKilled 这些生命周期问题。
6. 生命周期设计建议与排障习惯
6.1 探针、钩子与优雅关闭的配套设计
Pod 生命周期的健康程度,大部分靠的是配置习惯。我在生产环境里的默认配置思路是这样的:
- 每个常驻服务的容器都要配
readinessProbe和livenessProbe,一个管流量,一个管生死。 - 启动慢的应用加
startupProbe,探测间隔和失败阈值放宽,宁愿多等一会也不要误杀。 - livenessProbe 不要依赖外部组件,比如不要拿 MySQL 连通性做存活判断。数据库一抖动,本来只是慢,结果 liveness 把容器全杀了,故障面瞬间扩大。正确做法是让 liveness 只查本地轻量接口,readiness 可以适度包含下游依赖。
- 所有接受流量的 Pod 都应该处理
SIGTERM,配合preStop做下线动作,宽限期建议根据应用实际清理耗时调整,但不要无脑设一个很大的值。
另外还有一个细节:滚动更新时,如果你的应用需要比较长的排空时间,记得把terminationGracePeriodSeconds调大,否则即使你处理了 SIGTERM,宽限期不够也会被 SIGKILL。
6.2 我踩过的坑和日常排障顺序
说几个我真实踩过的坑,都是教科书上不会细讲、但线上必现的典型。
第一个坑:livenessProbe 指向了依赖服务。有一次我把 liveness 配成了查数据库的读写接口,结果数据库主从切换那几十秒,所有 Pod 的 liveness 全失败,kubelet 开始疯狂重启容器,雪崩现场算是亲身体验了。后来我把 liveness 改回查本地/livez,数据库抖动只影响 readiness,Pod 不再被误杀。
第二个坑:Java 应用启动慢,没配 startupProbe。应用还在做 Spring 上下文初始化,liveness 就已经开始探测了,连续失败 3 次就被杀,再重启,又杀,形成一个无解的循环。加了 startupProbe 后世界清净了。
第三个坑:preStop 里写了个不带超时的脚本,结果下线流程卡住,Pod 一直 Terminating 到宽限期结束,滚动更新反而更慢。后来所有钩子脚本都加了超时和第二层|| true兜底。
排障顺序我建议固定下来:
kubectl get pods先看 STATUS 列,是 Pending、CrashLoopBackOff 还是 Evicted。kubectl describe pod <name>重点看 Events 和容器状态里的 reason、exitCode,这一步能解决 80% 的定位问题。- 确认容器真的启动失败后,再用
kubectl logs <name> --previous看上次退出前的日志。 - 如需要看节点层面的资源,
kubectl describe node <node-name>看 requests 和 limits 分配情况。
我个人的体会是,大部分人排障效率低,不是因为不会用 kubectl,而是顺序反了。一上来就翻业务日志,Pod 根本还没跑起来,日志自然看不出问题;先从 Events 和退出码入手,把生命周期卡在哪一环搞清楚,再往下钻,十分钟定位不出来都难。