搞K8s的人迟早都会遇到这么一遭:集群跑得好好的,突然某个Pod卡在CrashLoopBackOff,或者节点莫名其妙NotReady,再狠一点,删除一个命名空间结果Terminating挂在那边三天三夜。这些问题在官方文档里不一定查得到完整答案,但生产环境可不会等你慢慢翻文档。
这篇内容就是把我自己在K8s运维过程中碰到的各类特殊状态,按照Pod、节点、控制面三个层面做了系统梳理,把每个状态的成因、排查路径和处理方式都记录下来。适合刚接触K8s、正在搭建或维护集群的运维和开发同学,也适合遇到问题不知道怎么下手的初学者拿来当排查手册用。
1. 先搞懂特殊状态从哪来:观察入口和分类思路
K8s里的状态不是凭空冒出来的,它们由不同的控制器和组件持续上报、汇总,最终通过API Server暴露出来。所以要排查状态问题,第一步不是瞎翻日志,而是搞清楚这个状态是从哪里产生的,对应的观察入口是什么。
1.1 Pod状态的特殊面孔
Pod是K8s里最基本的调度单元,它的状态主要由kubelet上报。正常情况下你看到的Pod状态无非是Pending、Running、Succeeded、Failed这几种,但生产环境里跑久了,各种"中间状态"和"异常状态"就会冒出来:
- Pending:Pod还没有被调度到节点上,可能是资源不足、节点亲和性不满足、PVC无法挂载等原因。
- CrashLoopBackOff:容器启动后反复崩溃,kubelet不断重启容器,但每次启动没多久就退出。
- ImagePullBackOff:镜像拉取失败,可能是镜像不存在、tag写错、私有仓库认证失败、网络不通等。
- CreateContainerError:容器创建过程中出错,比如配置了不存在的volume、privileged权限不足等。
- Terminating:Pod正在终止但卡住了,可能是有finalizer没清理完、volume还在挂载、底层容器进程没退出。
- OOMKilled:容器内存使用超过limit,被内核OOM killer干掉。
- Unknown:kubelet失联,API Server无法获取Pod的最新状态。
- Init:Error / Init:CrashLoopBackOff:Init容器执行失败,导致整个Pod无法进入正常启动流程。
这些状态有一个共同点:它们都不是"一锤子买卖"的结果,而是K8s各种控制器持续协调、重试之后形成的稳态。比如CrashLoopBackOff就是kubelet重启容器多次失败后进入的"冷却"状态,目的是避免疯狂重启打爆系统资源。
1.2 Node与控制面的特殊状态
除了Pod,节点和控制面组件同样有各自的特殊状态。节点的状态通过kubectl get node能直接看到,常见的有:
- Ready:正常状态,kubelet心跳正常。
- NotReady:kubelet在给定时间内没有上报心跳,可能节点宕机、网络分区、kubelet进程挂了。
- MemoryPressure / DiskPressure / PIDPressure:节点资源压力过大,触发了eviction manager的保护动作。
控制面这边,API Server、etcd、scheduler、controller-manager的状态就相对隐蔽一些,需要单独去查组件的健康情况。etcd作为存储后端,出现leader频繁切换、存储空间膨胀、读延迟飙高这类问题,往往比Pod异常更致命。
我的建议是:排查任何状态异常之前,先回答三个问题——是谁上报的状态?(kubelet?controller-manager?)、状态持续了多久?(刚发生还是已经很久?)、状态变化前发生了什么操作?(发版?配置变更?节点重启?)。这三个问题能帮你把排查范围从整个集群缩小到很小的面。
2. 高频Pod异常状态逐个拆解:成因与排查思路
Pod异常是日常运维里遇到最多的,也是新手上路最容易一头雾水的地方。下面挑几个最常碰到的状态,结合真实排障场景来讲。
2.1 CrashLoopBackOff:退出码会告诉你答案
CrashLoopBackOff几乎每个K8s运维都见过,它的表现是:
$ kubectl get pods NAME READY STATUS RESTARTS AGE nginx-85b98978db-6jf7d 0/1 CrashLoopBackOff 5 (65s ago) 4m12s注意看RESTARTS列,数值一直在涨。这个状态的背后机制是:kubelet每次启动容器,容器一旦退出,kubelet会按照指数退避的节奏(10s、20s、40s、80s……最多5分钟)去重启它。这样做的目的是防止容器无限快速重启耗尽节点资源。
遇到CrashLoopBackOff,第一步是拿退出码:
$ kubectl logs nginx-85b98978db-6jf7d --previous $ kubectl describe pod nginx-85b98978db-6jf7d | tail -20退出码是最直接的线索,常见的几个:
| 退出码 | 含义 | 常见的坑 |
|---|---|---|
| 0 | 正常退出 | 启动后没有长驻进程,比如把Nginx跑到前台又忘了加daemon off |
| 1 | 通用错误 | 应用启动时报错、配置文件解析失败、端口被占 |
| 127 | 命令找不到 | 镜像里没有对应的可执行文件或entrypoint路径写错 |
| 137 | SIGKILL | 内存超限被OOM杀掉,或者有人手动kill |
| 139 | SIGSEGV | 应用本身有段错误,通常是代码bug或依赖库不兼容 |
| 143 | SIGTERM | 收到终止信号正常退出,可能被preStop钩子或滚动更新触发 |
我遇到过一个蛮典型的案例:一个Java应用偶尔启动就挂,退出码137。一开始以为是内存配额不够,调了resources.limits.memory还是照样崩。后来查了dmesg | grep -i oom发现是节点本身内存不足触发了系统级OOM,而不是容器level的OOM。这说明排查OOM问题,不能只看Pod的limit,还得看节点上的整体内存水位。
CrashLoopBackOff排障的完整思路:
- 看退出码和最近一次日志,判断是启动即崩还是运行中崩。
- 如果是新发版之后才出现,优先怀疑代码变更、依赖版本、环境变量注入。
- 如果日志没有输出或者输出了也没用,考虑用
kubectl exec手动进去跑命令复现。 - 检查镜像启动命令和entrypoint,确认有没有前台进程长期运行。
- 确认探针配置是否合理,有时候是liveness探针误杀,容器明明正常却被K8s重启。
2.2 ImagePullBackOff与ErrImagePull:先分清是网络问题还是配置问题
ImagePullBackOff的排查路径相对固定,大部分情况都是这几个原因之一:
- 镜像仓库地址写错,比如把
nginx:1.25写成nginx:1.25-alpine且这个tag不存在。 - 私有仓库认证失败,Pod没有配置imagePullSecrets。
- 镜像仓库网络不通,尤其是集群节点无法访问外网或者内网镜像源。
- 节点上的容器运行时(docker/containerd)缓存损坏。
排查命令:
$ kubectl describe pod xxx | grep -A 20 EventsEvents里会有明确的报错信息,比如Failed to pull image "xxx:tag": rpc error: code = Unknown desc = failed to pull and unpack image...。
一个实际踩过的坑:某个环境集群是离线内网部署,镜像源在另一套内网仓库里。结果发版时有人直接把Docker Hub的镜像tag拿来用,Pod一直ImagePullBackOff。排查了半天发现不是网络不通,而是应用镜像根本没有推到内网仓库。这类问题在排查顺序上应该先确认镜像是否存在于目标仓库,再去查网络和认证。
还有一点要注意,镜像拉取失败后kubelet会有退避机制,不会立即重试。有时候你刚把镜像推到仓库里,Pod还挂着ImagePullBackOff,需要手动删掉Pod让它重建,或者等它自然退避完成后的下一次重试。
2.3 Pod卡在Terminating:finalizer与挂载卸载之谜
删除Pod时卡Terminating,是另一个高频特殊状态。正常情况下kubectl delete pod xxx几秒钟就结束,但如果底层引擎跟K8s之间的协调出了问题,卡上几个小时甚至几天都有可能。
Terminating卡住最常见的原因有三个:
第一个是finalizer阻塞。K8s里很多资源都有finalizer机制,比如namespace、PVC、StatefulSet管理的Pod。finalizer的本意是让控制器在资源真正被删掉之前执行清理逻辑。如果某个控制器自身挂了或者逻辑有bug,finalizer永远执行不完,资源就一直卡在Terminating。这时候可以用:
$ kubectl get pod xxx -o json | jq '.metadata.finalizers' $ kubectl patch pod xxx -p '{"metadata":{"finalizers":[]}}' --type=merge注意:直接清掉finalizer属于强制手段,相当于告诉K8s"别等清理逻辑了,直接删"。如果底层有资源没清理干净,后面可能留下孤儿资源。生产环境要谨慎,先在测试集群验证再上。
第二个是volume挂载卸载不干净。如果Pod里有云盘、NFS这类持久化存储,kubelet需要先调用CSI插件去卸载卷,卸载完成才能删掉Pod。CSI插件异常或者存储服务端卡住的时候,Pod就会一直卡在Terminating。查附件卷的状态:
$ kubectl describe pod xxx | grep -i volume $ kubectl get pvc xxx -o yaml第三个是容器进程没有被正常清理。某些情况下容器已经停了,但宿主机上对应的进程还没有被完全回收,kubelet等容器完全退出才肯把Pod标记为删除完成。这种问题比较底层,通常要上节点去看containerd的容器列表。
生产环境如果确认Pod已经死透了但Terminating卡住,最保险的做法是用kubectl delete pod xxx --force --grace-period=0强制删除。但这里有个重要提醒:强制删除会让K8s跳过优雅终止流程,如果你的应用依赖preStop钩子做优雅下线(比如注销注册中心、排空连接),强制删除可能会引发短时间内的流量异常。
2.4 OOMKilled与内存配额设置
OOMKilled这个状态本身不算稀奇,它的完整状态名是OOMKilled,容器会被重启,但区别在于它是被内核杀死而不是正常退出:
$ kubectl get pods NAME READY STATUS RESTARTS AGE api-server-6cd8d56f66 0/1 OOMKilled 3 86s根本原因是容器的内存使用超过了resources.limits.memory。K8s在给容器配置了内存limit之后,会通过cgroup限制这个容器最多能用多少内存,一旦超过就会被内核OOM killer选中杀掉。
核心问题往往不是"被杀了"这一个结果,而是"为什么会超过limit"这个根因。常见的原因包括:
- 应用本身有内存泄漏,GC或者引用释放不及时,长时间运行后涨破limit。
- 业务高峰期内存需求超出预估值,比如线上流量翻倍导致POD内存峰值陡增。
- JVM类应用没有感知容器limit,堆内存配置过大,导致总内存超出container限制。
- 排查时第一件事就是看节点上有无内存压力的告警、
kubectl top pod的实时值以及容器退出前的日志。
一个实际经验:排查Java应用OOM时,不要只看kubectl logs,还要看GC日志和heap dump。很多内存相关的问题在业务日志里看不出什么痕迹,但在GC日志里能看到明显的频繁FullGC和堆内存上涨趋势。
如果你的服务确实存在内存峰值波动,可以考虑调整limit和request的配比,比如request设置为稳定基线的80%,limit设置到基线的1.5~2倍。这样既能满足峰值需求,又不会让K8s在调度时把节点资源算得太满。
3. 节点与控制面的特殊状态:集群层面的故障
Pod异常再严重,影响的也只是单条业务链路。节点NotReady、控制面异常这类问题,才是会让整个集群一起抖动的"大事件"。
3.1 NotReady节点的排查与恢复
节点状态变成NotReady的瞬间,集群里所有跑在这个节点上的Pod都会面临两种命运:被重新调度到别的可用节点,或者因为节点失联而无法迁移,只能干等。理想情况下,控制器会把Pod标记为Unknown并将其删除重建到其他节点上,但这个过程中可能会出现大量Pod同时重建的问题。
节点为什么NotReady?排查方向集中在几个地方:
- kubelet进程是否存活。上节点执行
systemctl status kubelet,没起来就看日志journalctl -u kubelet -f。这是最常见的NotReady原因——kubelet不知道什么时候挂了或者被内存挤爆了。 - 节点资源耗尽。节点内存、磁盘、inode或PID耗尽时,kubelet的心跳上报可能无法及时完成,或者node condition被标记为压力状态。
- 网络分区。kubelet到API Server之间的网络不通,心跳无法上报。TCP连接池被占满、DNS解析失败都会引发。
- 容器运行时故障。dockerd或containerd崩溃,kubelet无法调用容器运行时接口,也会触发NotReady。
排查命令参考:
$ ssh node01 $ systemctl status kubelet $ journalctl -u kubelet --since "10 minutes ago" --no-pager $ df -h $ free -h $ top $ systemctl status containerd曾遇到一个有意思的情况:某节点每隔几天就NotReady一次,查kubelet日志发现是因为磁盘空间满了导致kubelet无法上报心跳,而磁盘满的原因是容器日志没有配置轮转。后来在节点上配置了logrotate并给docker/containerd配置了日志大小上限,问题才根治。
3.2 API Server与etcd的状态判断
控制面的核心是API Server和etcd。API Server是集群里所有组件交互的中枢,etcd则负责存所有状态。这两个组件出问题时,往往是全局性的——你连kubectl get pods都可能报错。
API Server状态排查常用两个维度:响应速度和错误日志。在master节点上可以直接请求健康检查接口,比如:
$ curl -k https://127.0.0.1:6443/healthz ok如果返回的不是ok,说明API Server自身有问题,多半要查静态Pod的日志:
$ kubectl -n kube-system logs kube-apiserver-<node> --tail=200etcd这边需要关注的指标有三块:leader是否稳定、存储空间是否够、读写延迟是否正常。etcd出问题最常见的一个表现是集群频繁出现"etcd server stopped"或者leader change。另一个常见状况是etcd存储空间满了,默认的压缩策略如果不及时生效,整个集群就会变成只读状态。 排查etcd健康情况:
$ ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key endpoint health --cluster如果确认etcd存储膨胀,可以通过调整--auto-compaction-retention(比如保留最近24小时的历史)和手动compact来治理。这个操作一定要先备份,我之前见过有人直接对etcd做defrag导致线上集群短暂不可用,这种操作对性能影响很大,必须在低峰期处理。
3.3 驱逐与重调度:资源压力下的自愈机制
当节点出现MemoryPressure或DiskPressure时,kubelet的eviction manager会开始驱逐Pod来释放资源。被驱逐的Pod会在其他节点上重新调度,但如果这些Pod没有设置合理的资源请求(request),或者集群剩余容量不足,就会出现"驱逐之后调度失败"的连锁反应:
$ kubectl get events --sort-by=.lastTimestamp | grep Evictedeviction manager是按优先级挑选驱逐对象的。它会优先看Pod的QoS等级:BestEffort的Pod最容易被驱逐,Burstable其次,Guaranteed的Pod最安全。一个常见的生产经验是:核心业务尽量设置cpu和内存的request等于limit(即Guaranteed),非核心业务可以适当放宽,但也要想清楚最坏情况下被驱逐的后果。
另一个很多人忽略的点是PodDisruptionBudget(PDB)。PDB能限制自愿驱逐场景下允许中断的Pod数量,但注意:节点资源压力触发的驱逐属于"非自愿驱逐",PDB不生效。所以PDB保护不了资源耗尽场景,只能保护节点维护、滚动更新这类主动操作。设计高可用架构时,这两者要分开考虑,不能混为一谈。
4. 排查工具箱:从命令到思路的实战沉淀
状态排查最忌讳东一榔头西一棒子,命令没有章法,信息收集不全就开始猜测。下面是我自己沉淀的一套排查路径和实用命令。
4.1 十分钟定位问题的命令组合
先看集群整体面貌:
$ kubectl get nodes $ kubectl get nodes -o wide $ kubectl get pods -A -o wide | grep -v Running这里第一眼要回答的问题是:异常范围有多大?只有一两个Pod异常,还是多个节点上的Pod集体异常?这个判断直接决定后续排查方向。
再深入看单个资源:
$ kubectl describe pod <pod-name> $ kubectl logs <pod-name> --previous $ kubectl get events --field-selector involvedObject.name=<pod-name> $ kubectl top pod <pod-name> $ kubectl top node <node-name>describe的信息量最大,里面包含了事件、状态、容器ID、镜像、标签、调度记录,很多问题看一遍describe就能浮现出关键线索。
如果问题在节点层面:
$ ssh <node-name> $ journalctl -u kubelet --since "30 minutes ago" --no-pager $ df -h && df -i $ free -m && cat /sys/fs/cgroup/memory/memory.pressure_level 2>/dev/null || true $ docker ps / crictl ps这套命令组合基本覆盖了绝大多数常规排障场景。用熟了之后,一般十分钟内能定位到问题所在的层级。
4.2 一个ImagePullBackOff的完整复盘
简单讲一个从现象到根因的完整过程,帮助你把上面的命令串起来。
某次上线,新Deployment的Pod状态是ImagePullBackOff。我先kubectl get pods -o wide确认只有这个Deployment的Pod异常,其他服务都正常,排除了大规模网络故障的可能。接着kubectl describe pod看到Events里写着:
Failed to pull image "internal-registry:5000/app/web:latest": failed to resolve reference "internal-registry:5000/app/web:latest": not found镜像仓库里有这个repo,但taglatest不存在。去仓库确认后发现,CI流水线这次构建出来的是v2.3.1,没有打latest标签。把Deployment的镜像tag改成v2.3.1后,一切恢复。
这类问题的根因很简单粗暴——tag打错了,但它暴露了一个运维规范问题:任何环境发版前要确认镜像tag真实存在于目标仓库,否则极易出现这种低级故障。我在后面章节会专门展开讲这类配置规范的预防措施。
4.3 排查思路的优先级排序
排查状态问题的思路是有优先级的,不要跳着来:
第一优先:看事件(Events)。K8s的Events会告诉你"发生了什么",不用去猜。kubectl get events -A --sort-by=.lastTimestamp | tail -50往往能直接给出答案。
第二优先:看日志(Logs)。Events告诉现象,日志告诉应用层面的原因。记得看--previous,因为容器重启后日志会被覆盖。
第三优先:看资源视图(top/describe)。从资源配额、实时使用量的角度排除资源型问题。
第四优先:看系统层。宿主机内核日志、kubelet日志、容器运行时日志。这一步通常是前几步都没有结论时才需要做。
很多同学上来就kubectl logs,日志还没看明白又去百度搜报错,查半天没结果。按照这个优先级走,排查效率会高很多。
4.4 kubectl常用命令备忘
个人觉得用得最多的一条是组合了状态过滤和字段输出的命令:
$ kubectl get pods -A -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,STATUS:.status.phase,PODIP:.status.podIP | awk '$3=="Running" && $2!="node01"'另外kubectl -o wide和-o yaml配合jq能完成很多快速分析,比如批量看所有Pod的status条件:
$ kubectl get pods -A -o json | jq '.items[] | {name: .metadata.name, conditions: .status.conditions}'kubectl本身的命令覆盖不了的功能,比如批量操作、复杂筛选,我基本都用jq补足。这也是排查效率的关键——不是把kubectl get打出来就完事,而是学会怎么把输出变成有用的判断依据。
5. 尽量让特殊状态不发生:配置层面的预防意识
状态发生后的排查能力是基本功,但从运维角度看,真正的高手是想办法减少异常状态发生的概率。K8s提供了很多机制来降低故障面,关键是会不会用。
5.1 探针、资源限额与优雅终止的搭配策略
探针(Probe)是K8s判断应用是否健康的窗口。livenessProbe决定什么时候重启容器,readinessProbe决定Pod是否加入Endpoints接受流量,startupProbe用于缓慢启动的应用避免被liveness误杀。
配置探针的一个常见误区是把它们当成摆设。实际生产中见过不少服务readinessProbe永远返回200,结果后端DB挂了之后流量还是往这个Pod打,用户侧超时,K8s却认为服务“健康”。探针的路径应该指向应用真实的健康检查接口,而不是简单的首页,并设置合理的initialDelaySeconds和periodSeconds。
资源限额(requests/limits)的意义不只是防止单个Pod占满节点,它还是调度器做容量规划的依据。requests设置过高会浪费调度空间,过低则可能在繁忙时把节点资源打爆引发驱逐。比较稳妥的经验是:用压测或历史监控数据观察稳定基线,requests取稳定值的75%到90%,limits可以在requests基础上留一定余量,但要意识到过高的limits会让节点的"可调度资源"看起来很多,实际峰值却可能产生资源争抢。
优雅终止(terminationGracePeriodSeconds)很久没被部分同学重视,但它影响的是滚动更新时的流量中断时长。默认值是30秒,如果你的应用收到SIGTERM后需要更长时间处理存量请求(比如等待RPC调用返回、清理连接池),就把它调大。我之前有个网关服务就是因为处理存量请求超过30秒,每次滚动更新都有少量请求失败,把值调到90秒后问题消失。
5.2 可观测性与告警:从"被动救火"到"提前预警"
K8s里除了kubectl的手动排查,更科学的方式是建立可观测性体系,让异常状态在用户发现之前就被系统识别出来。
至少要做到三个层面的可观测:
- Metrics层:节点的CPU/内存/磁盘、Pod的CPU/内存使用率、API Server的延迟和错误率。用Prometheus采集,配合Alertmanager做告警。
- Logs层:业务日志、kubelet日志、容器运行时日志,链路追踪和时间线串联对定位问题帮助巨大。
- Events层:K8s事件集中收集。很多异常状态(比如ImagePullBackOff、CrashLoopBackOff、节点NotReady)在彻底恶化之前都会有事件告警,把这些事件纳入告警通路,比只盯着Metrics更早发现问题。
告警规则不要一味追求多,要有针对性。我建议至少配这么几条:
- 节点NotReady持续超过1分钟。
- Pod处于CrashLoopBackOff或ImagePullBackOff超过10分钟。
- 节点内存使用率持续超过85%。
- 事件里出现Evicted或OOMKilled的Pod。
- API Server错误率超过5%或者P99延迟超过1秒。
有了这些基线,遇到问题的时候你手里才有足够多的历史数据做对比,判断这是突发的还是积累的,是代码变更导致的还是环境变化导致的。
5.3 变更前的自检习惯
最后分享一个虽然不起眼但特别实用的习惯:每次上线前,把变更清单过一遍。我指的不只是代码,而是整个K8s资源定义:
- 镜像tag是否存在,仓库能不能访问。
- ConfigMap和Secret里有没有引用不存在的key。
- 探针路径和端口对不对。
- 资源限额是否合理(尤其是发布到已有节点时)。
- PDB是否存在,滚动更新的maxSurge/maxUnavailable是否合理。
- PVC是否已创建,StorageClass是否正确。
这些如果在上线前提前过一遍,很多特殊状态在生产环境里根本不会有机会露脸。我在团队里推行了发布Checklist,效果非常明显,线上因为配置问题导致的异常状态大幅减少。
写在最后
回到最开始说的:K8s的特殊状态,本质上不是一个"需要背诵答案"的知识点,而是你理解集群内部机制的窗口。每一个异常状态背后,都是一次控制器协调、组件通信或资源争抢的结果。多遇几次、多排查几次,你会慢慢建立起"看到状态就能推测大概原因"的直觉。
我的个人体会是,运维K8s集群最有成就感的时刻,不是所有东西都正常的时候,而是出了故障之后能快速定位、快速修复,并且事后能总结出预防措施的时候。很多状态,你踩过一次坑、记下一次完整的排查过程,后面再遇到就是条件反射,根本不需要翻文档。
最后分享一个小技巧:在排查任何特殊状态之前,先截个屏或记录一下当前集群的kubectl get nodes和kubectl get pods -A输出。这两条命令的输出是排障过程中的"现场快照",它能帮你事后复盘,也能让你在跟同事协作时快速把信息对齐。这个习惯我保持了很久,很多疑难问题都是在复盘快照时发现的规律。