先说明一下,这一篇不是给你背排障命令清单,而是想带大家建立一套完整的排障思路。K8s用久了你会发现,故障本身不可怕,可怕的是遇到故障毫无章法地乱试。就拿最常见的CrashLoopBackOff来说,我见过不少同学直接改配置、重启、删Pod重来,折腾一上午,结果根因就是镜像里的启动命令少了个参数。作为这个系列的第十四篇,我把在生产环境里真正用过的定位方法和高频故障案例整理出来,覆盖镜像拉取、容器重启、调度卡死、网络不通、存储挂载、节点异常这几个大类。这篇适合已经熟悉kubectl基础操作、开始独立维护集群的读者,看完能直接拿去用。
1. 排障的核心不是“修得快”,而是先答对“卡在哪一层”
1.1 把故障拆成“定位-归因-修复”三步
K8s的排障和传统服务器排障有个很大的区别:一个Pod能不能正常运行,牵扯到kube-apiserver、调度器、kubelet、容器运行时、网络插件、存储插件好几个组件。很多人一上来就盯着Pod本身看,这是最容易走弯路的地方。我的习惯是先把故障现象拆到“层”:是Pod压根没创建出来,还是创建了但调度不了,还是调度了但镜像拉不下来,还是容器起来了但一直崩溃。这一步不需要太多技术,纯靠kubectl get pods -o wide和describe就能判断。
打个比方,车坏了先看仪表盘亮什么灯,而不是直接打开发动机盖。K8s的“仪表盘”就是Events事件和Pod的状态字段。先确认故障发生在哪一层,再决定下一步查什么,这才是排查的核心思路。定位错了层,后面所有操作都是在浪费时间。
1.2 黄金三件套:describe、logs、get events
排障最常用的就是这三个命令,我都快刻进肌肉记忆了:
kubectl describe pod <pod-name> -n <namespace>:看事件、状态、条件、挂载、探针配置,这是第一手现场。kubectl logs <pod-name> -n <namespace>:看容器内应用日志,能判断是程序问题还是环境问题。kubectl logs <pod-name> --previous -n <namespace>:容器崩溃重启后,用这个参数看上一次生命周期里的日志,这个极其重要,很多人会漏掉。kubectl get events --sort-by='.lastTimestamp' -n <namespace>:按时间排序看整个命名空间的事件流,能还原故障发生前后的上下文。
我排障时会先在终端开两个窗口,一个盯events实时变化,一个执行具体命令。事件里往往直接写着失败原因,比如“Failed to pull image”“Back-off restarting failed container”,顺着线索推进,速度快很多。
1.3 时间线思维:先问“发生了什么变化”
很多K8s故障不是凭空出现的,而是环境里某个条件变了。比如新发了一个版本、节点加了污点、镜像仓库的凭证过期了、某个底层Node磁盘满了。我在排查时有个习惯:先回滚时间线,问自己“这个服务昨天是不是好的”“最近有没有动过什么”。尤其要小心那些“看起来无关”的改动,比如给集群加了个Gateway,结果影响到了现有Service的转发规则。
时间线思维还能帮你判断故障是持续性的还是间歇性的。持续性故障通常在配置或资源上找原因,间歇性故障往往和网络抖动、资源竞争、探针超时这类动态问题相关。方向定对了,后面不会白费力气。
2. Pod起不来的两大高频元凶:ImagePullBackOff与CrashLoopBackOff
2.1 镜像拉不动的完整定位过程
ImagePullBackOff算是K8s里最常见也最好排查的一个故障,状态字面意思就是镜像拉取被拒了。先别急着改镜像,用describe看一下具体报错,根因通常集中在这几种:
- 镜像名或tag打错,比如把
nginx:1.27写成了nginx:1.27多带了个空格,或者tag在仓库里根本不存在。 - 访问私有仓库认证失败,K8s拉私有镜像需要imagePullSecrets,如果不配或者配错了,就会报
Unauthorized或denied。 - 集群网络访问不了镜像仓库,比如公网仓库需要走代理、私有仓库在另一个VPC网段。
有一回我在一个环境里排查Pod全部ImagePullBackOff,describe里明明显示的是Failed to pull image "xxx/xxx:v1.0",手动在节点上crictl pull又是成功的。最后发现是这个节点的容器运行时证书目录里缺了私有仓库的CA证书,而手动pull用的是root的配置。这种“节点上能拉但K8s拉不了”的坑,很多时候就是kubelet和容器的信任链不一致。
处理建议是这样的:先在节点上手动拉一遍镜像,确认到底能不能拉通;再检查imagePullSecrets是否存在且secret里的内容是对的;最后看节点到仓库的网络连通性。定位到具体原因后,改完配置记得把故障Pod删掉让它重建,光改配置是不会自动恢复的。
2.2 CrashLoopBackOff里藏着的两类问题
CrashLoopBackOff比ImagePullBackOff复杂一些,意思是容器启动后崩溃,K8s按退避策略反复重启。退避时间从10秒开始,指数增长到300秒封顶,所以你会看到Pod状态在CrashLoopBackOff和Running之间反复横跳。
第一类问题是应用本身起不来。比如启动命令参数不对、环境变量缺失、配置的数据库连不上。这种直接看kubectl logs就能看到panic或者error日志。但要注意:容器如果崩溃得太快,当前日志可能是空的,这时候kubectl logs --previous就派上用场了,它能看到崩溃前那个容器实例留下的日志。
第二类问题是探针配置太激进。我有次遇到一个Pod每几分钟就重启一次,日志里完全没有报错,最后发现是startupProbe的超时时间太短。应用启动要3秒,探针却只给了1秒,K8s认为容器没起来,反复杀死重建。这个问题不看yaml根本想不到。遇到CrashLoopBackOff,建议把探针配置从头看一遍,确认时间参数是否匹配应用的真实启动耗时。
2.3 一个完整的CrashLoopBackOff排查命令流
贴一套我实际排查时的标准操作,按顺序执行:
# 先看Pod实时状态和重启次数 kubectl get pods -o wide -n <namespace> # 看Events,确认K8s视角的原因 kubectl describe pod <pod-name> -n <namespace> # 看当前容器日志 kubectl logs <pod-name> -n <namespace> # 容器快速崩溃时,看上一个实例的日志 kubectl logs <pod-name> --previous -n <namespace> # 确认探针定义 kubectl get pod <pod-name> -n <namespace> -o yaml | grep -A 20 "probe"如果日志显示是应用在抛异常,那把异常信息处理掉基本就好了。如果日志干干净净但一直在重启,十有八九是探针或者资源限制的问题——比如limits里memory设得太小,容器一启动就因为OOM被杀了。OOM的话可以看kubectl describe pod里的Last State是不是OOMKilled,是的话把resources调大或者优化应用内存。
3. Pod卡在Pending:调度器到底在犹豫什么
3.1 从Events里读出调度器的判断逻辑
Pod状态一直Pending不会自动恢复,因为它压根没有被调度到任何节点上。很多新手一看到Pending就慌,其实调度器的拒绝理由都写在Events里。执行kubectl describe pod,Events部分通常会出现类似:
0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had untolerated taint——资源不够加污点没容忍。0/2 nodes are available: 1 node(s) didn't match node affinity——亲和性规则限制。0/3 nodes are available: 1 node(s) had volume node affinity conflict——存储卷和Pod调度互相矛盾。
说句大实话,K8s调度器非常诚实,它拒绝你的时候会把所有原因列出,我们只需要逐条对照。难的不是理解这些原因,而是很多人压根不看Events,直接凭感觉给Pod加副本数期望它能起来——这毫无意义。
3.2 资源、污点、亲和性三类拦路虎
资源不足是高频原因。注意这里看的是requests而不是limits。调度器按requests来决定把Pod放到哪,如果集群里所有节点的可分配CPU或内存都不满足Pod的requests,就会Pending。kubectl top nodes可以看实时用量,但更准确的判断是看kubectl describe node里的Allocated resources,它会显示每个节点已经被“预留”了多少资源。有时候集群明明大量资源空闲,但Pod还是Pending,就是因为别的Pod把节点上的requests份额占光了。
污点和容忍是另一类常见原因。节点打上了node.kubernetes.io/xxx这类污点,而Pod没声明对应的toleration,调度器就会直接跳过这个节点。还有亲和性,比如nodeSelector指定的标签在节点上不存在,或者podAntiAffinity规则导致Pod不能和同类应用跑在同一节点,都会造成Pending。我遇到过一种哭笑不得的情况:两个不同服务的Pod都配置了podAntiAffinity,结果集群里正好只有两个节点,第三个Pod永远调度不上去。
3.3 一个容易被忽视的底层原因:PVC未绑定
还有一种Pending原因非常隐蔽,就是Deployment挂载了PVC,但PVC本身一直Pending。调度器在做调度决策时需要确认存储卷能挂到目标节点上,如果PVC没有绑定成功,Pod会被卡在Pending,而Events里可能只显示和调度相关的原因,不会直接告诉你“因为PVC没绑好”。
遇到这种问题,先看PVC的状态:
kubectl get pvc -n <namespace>如果PVC卡在Pending,再看StorageClass和底层provisioner是否正常。这部分我会在第五节详细展开。总之,Pod Pending的排查顺序建议是:先Events看调度原因,再查Node资源、污点、亲和性,最后别忘了看PVC。
4. Service不通与DNS异常:网络排障的标准打法
4.1 先确认“从哪里访问不通”再动手
网络故障最忌讳一上来就试各种东西。先确认一点:从哪里访问Service失败的?是从集群外部用NodePort访问不通,还是集群内部Pod访问ClusterIP不通?访问路径不同,排查方向完全不同。
如果是集群内部访问不通,我习惯按这个链路一步步走:
# 看Service定义和选择器 kubectl get svc <service-name> -n <namespace> -o yaml # 看后端Endpoints有没有被正确纳入 kubectl get endpoints <service-name> -n <namespace> # 看EndpointSlice是否正常 kubectl get endpointslices -n <namespace> -l kubernetes.io/service-name=<service-name> # 进入一个Pod里用ClusterIP测试(不要用Service域名,先排除DNS干扰) kubectl exec -it <pod-name> -- curl http://<cluster-ip>:<port>这里有个关键点:如果Endpoints列表是空的,说明Service的selector和后端Pod的label对不上。人眼很难在一堆yaml里一眼看出app: web和app:web这种空格差异,但我至少见过三次这种低级错误。只要Pod上的标签和Service的selector不完全匹配,Endpoints就不会有数据。
4.2 CoreDNS或网络插件出问题时怎么定位
如果Endpoints没问题,直接访问ClusterIP不通,那大概率问题出在网络层。K8s集群内Pod互访依赖什么?在标准架构里是CNI插件加kube-proxy这套组合。常见的故障:
- kube-proxy的iptables/IPVS规则没同步。如果集群用了IPVS模式,检查
ipvsadm -Ln看Service对应的虚拟IP是否存在,不存在就说明kube-proxy出了问题。 - CNI插件给Pod分配的网段异常,比如之前用的网络插件分配的IP和节点路由表冲突。
- 网络策略NetworkPolicy误拦截。有些团队上了网络策略组件,但策略写得过宽或过窄,导致本来该通的流量被拦了。
DNS解析异常也属于网络故障的高发区。现象是Pod里curl域名不通,但直接访问ClusterIP是通的。这时候先确认CoreDNS的Pod是否正常运行,再检查Pod的/etc/resolv.conf有没有被异常覆盖。很多临时的DNS问题都跟dnsPolicy设置有关,比如dnsPolicy: None但配置里的nameserver写得不对,就会整个DNS不可用。
4.3 别忘了检查Service类型和端口名
还有个低级的坑值得单独提醒:Service的type不同,暴露方式也不同。ClusterIP只能在集群内部访问,NodePort才能通过节点IP加端口访问。如果你把数据库服务配成了ClusterIP,然后从外部拿节点IP去连,那必然不通,这不是任何组件故障,是访问方式用错了。
端口名也是容易踩的地方。Service的port和targetPort可以定义名字,像http、grpc这种。如果Service和Pod用的端口名不一致,比如Pod容器里监听8080但Service的targetPort写了80,流量会被转发到容器里不存在的端口上。排查方法很简单,kubectl get endpoints能看到Endpoints里的IP:port,对照一下Pod的实际监听端口就清楚了。
5. 存储与节点层故障:PVC卡Pending、节点NotReady的处理过程
5.1 PVC一直Pending,先查StorageClass再查底层插件
PVC Pending是存储故障里最常见的形态,而且很多时候是“看起来简单但链条很长”的问题。一个PVC要正常使用,需要有StorageClass,StorageClass要引用一个可用的provisioner,provisioner要在集群里正常运行,底层存储还要有足够的资源。这条链上任一环节出问题,PVC都会一直Pending。
排查步骤我从简到繁列出来:
# 1. 看PVC当前状态和事件,先确认卡在哪个环节 kubectl describe pvc <pvc-name> -n <namespace> # 2. 确认有没有匹配的StorageClass kubectl get storageclass # 3. 看StorageClass的provisioner配置 kubectl get storageclass <sc-name> -o yaml比较常见的坑:集群里根本没有默认StorageClass,PVC不指定className就永远没有provisioner来响应它;或者StorageClass指定的provisioner组件没部署好,比如云厂商的存储插件控制器处于异常状态。还有一种是PVC和PV的容量、访问模式对不上。我记得有一次帮某开发者排查,PVC要求ReadWriteMany,可底层存储只支持ReadWriteOnce,调度直接卡住。
如果底层是自建的存储服务,比如用NFS或者对象存储做的CSI插件,那还得检查插件Pod本身健不健康。存储插件的Pod一般跑在kube-system命名空间,别盯着PVC一查到底,跳出来看看CSI控制器和节点驱动的日志,往往更快定位。
5.2 节点NotReady:从kubelet日志一路挖下去
节点状态变成NotReady,属于影响面很大的故障,因为节点上的Pod会被驱逐或者无法重建。节点NotReady的本质通常是kubelet上报心跳失败,或者kubelet认为节点自身不健康。最常见的原因是容器运行时挂了、kubelet进程异常、磁盘空间不够、kubelet和apiserver之间网络异常。
排查先看kubelet服务状态和日志:
# 在NotReady节点上执行 systemctl status kubelet journalctl -u kubelet -f --since "10 minutes ago"kubelet日志里如果出现PLEG is not healthy或者Failed to update node status这类内容,就顺着日志里的错误信息继续查。PLEG相关的报错常见于containerd或者docker卡住,重启容器运行时能临时缓解,但真正的根因多半是节点负载过高或者磁盘IO阻塞。
DiskPressure是另一种经常让人懵的状态。节点不是NotReady,但上面所有Pod都被驱逐,或者新Pod调度不上来。查看节点状态会看见Pressure: DiskPressure=True。这时候df -h和du -sh /var/lib/docker(或者containerd目录)是最直接的排查操作。K8s节点的镜像缓存和日志文件非常容易撑爆磁盘,尤其是日志没配轮转的环境。我处理过一个案例,节点上某个应用每小时产生几十GB日志,把磁盘干到100%,整节点驱逐。根治方案是在容器运行时层面配置日志轮转,同时用cron检查大文件目录。
5.3 节点资源压力驱逐背后的生产环境教训
节点MemoryPressure触发驱逐也是生产环境里的高频事件。Kubelet检测到节点内存即将耗尽,会根据QoS等级驱逐Pod,被驱逐的Pod会变成Failed,如果受管于Deployment还会在别的节点重建。这种故障的根子通常是某个应用的memory limit配置过大或者完全没配limit,导致一个Pod把整台节点的内存吃光。
处理这种问题,先把集群里的资源requests/limits规范起来,尤其是limits.memory必须设置。再配合HPA(水平自动扩缩)在业务流量上涨时自动加Pod,把压力分摊开。我们之前有个服务一次流量高峰就把节点内存吃穿,加了HPA和内存limit之后,再没出现过整节点驱逐的故障。记住一句话:K8s的节点资源隔离不是默认可靠的,你得靠配置策略主动保护。
6. 平时养成这些排查习惯,关键时刻少走几小时弯路
6.1 用事件窗口还原故障上下文
很多K8s故障是瞬时发生的,等你反应过来的时候,现场的Pod早都重启好几轮了。所以我现在排查任何问题,第一步都是查看最近一段时间的事件,而不是去翻当时的yaml。命令是这样:
kubectl get events --sort-by='.lastTimestamp' -n <namespace> --since=2h加上--since=2h能快速定位最近两小时的事件流。观察事件的时间顺序,能还原当时发生了什么:先是镜像拉取失败,还是先有节点压力,还是探针开始报错。事件流比单个命令的输出信息量大得多,但前提是你有按时间查询的习惯。
另外,kubectl describe pod里每个容器的State字段里会有Started At和Finished At,对比一下重启时间点和Events时间,能反推出容器崩溃和哪个系统事件相关。这个小细节排查间歇性故障时特别有用。
6.2 “先复现,再修复”比“凭经验猜”更可靠
排障过程中最忌讳的就是凭经验直接下结论。有一次同事和我说某Service不通肯定是防火墙问题,我过去一看,Pod里的进程压根没监听在那个端口上。直接复现比打嘴仗有用得多:进到Pod里用netstat -tlnp看监听端口,用curl实测连通性,用ps aux确认进程状态。复现成功,根因就跑不掉。
遇到需要抓现场的情况,可以在Pod里临时装网络工具,换一个Debug镜像临时替代业务容器跑同样的命令,这些都是很实用的手段。K8s本身支持kubectl debug,可以临时在目标Pod里注入一个带shell的容器,不动原Pod,这个功能排查网络问题时强烈推荐。
6.3 给关键配置做一份“变更记录”
最后说一个我没有早点学会、后来一直坚持的习惯:给集群的关键配置改动做记录。K8s的故障排查很多时候都是在找“最近变了什么”。如果没有记录,就只能靠翻聊天记录和yaml历史来回忆。我现在会在维护的集群目录里维护一份简单的变更记录,内容包括时间、操作人、改了什么、影响范围。运维多个环境之后会发现,这份记录省下的时间远超记录占用的时间。
排查故障的经验可以积累,踩过的坑也可以写下来,但比记录经验更重要的是把环境里发生了什么都搞清楚。排障不是找茬,是理解系统当前的真实状态,理解了,修复就是顺理成章的事情。
最后分享一个小技巧给正在维护集群的朋友:每次修复完一个故障,别急着收工,把事件时间点、根因、修复动作写进自己的排查笔记里。不是给你看,是给三个月后可能再遇到同类问题的你看的。你会发现,很多看起来复杂的问题,第二次遇到时只需要五分钟就能解决。