kubectl get pods打出来一排CrashLoopBackOff或ImagePullBackOff,RESTARTS从3涨到30,服务一直起不来。这两个状态都不是真正的报错,只是kubelet重试失败之后的等待动作,线索藏在别的地方。
一个已经把镜像拉下来、跑起来又崩了;另一个连镜像都没拉到。分开看,排查路径完全不同。
先看状态字段,别急着翻日志
第一行要敲的不是logs,是describe:
kubectl describe pod <pod名> -n <命名空间>
输出里盯三块:Events时间线、Containers下面的Last State、还有最底部的Warning原因。Last State里那行Terminated带着Exit Code,它决定后面往哪个方向查。
集群里同时有多个Pod出问题时,把事件按时间排一遍更快:
kubectl get events -n <命名空间> --sort-by=.lastTimestamp
CrashLoopBackOff:跑起来又死
程序自己报错退出占大头,退出码通常是1或2。这时候上一次的日志才是关键,因为当前这一轮可能还没来得及输出任何东西:
kubectl logs <pod名> --previous -n <命名空间> kubectl logs <pod名> -c <容器名> --previous
多容器Pod不加 -c会报a container name must be specified,这个提示见过好多次,容易误以为日志丢了。
退出码比日志更快定方向
137是内存超限被杀,limits.memory给小了,也可能是节点本身内存紧张。139是段错误,容器里缺库或者二进制与架构对不上时常见。143是收到SIGTERM,滚动更新时的正常终止。127是命令找不到,Dockerfile里的CMD写错,或者基础镜像里压根没那个命令。
还有一种是退出码0却在不停重启:容器里没有常驻前台进程,主进程跑完就退。CMD里写了个启动脚本、脚本里用后台方式拉起服务,就是这个结果。
内存那条查起来最直接,看实际用量和限制差多少:
kubectl top pod <pod名> -n <命名空间>
kubectl get pod <pod名> -o jsonpath='{.spec.containers[*].resources}'
把活的应用杀掉的往往是探针
第二类是应用本来没事,被livenessProbe判死。启动慢的应用尤其容易中招:initialDelaySeconds给了10秒,实际启动要40秒,于是每次都在启动中途被重启一遍,RESTARTS一路涨,日志里只有半截初始化输出。
真正能容忍的时间不是periodSeconds,而是initialDelaySeconds加上failureThreshold乘periodSeconds。把这三个数算出来再对比实际启动耗时,比盯着某一个参数调要有效。
慢启动的场景应该用startupProbe,1.20之后的集群都有这个字段。它在startupProbe成功之前不会让liveness介入,启动多久都行。readinessProbe失败只会把Pod从Service后端摘掉,不触发重启,所以readiness配错的表现是流量打不进去,而不是不停重启。
配置没建好的几种表现
ConfigMap或Secret整块不存在时,状态是CreateContainerConfigError,不进CrashLoopBackOff,一眼能看出来。麻烦的是引用了不存在的key:Pod起得来,应用读到空值,启动时连不上数据库直接exit 1,看起来跟代码有问题一样。
kubectl get pod <pod名> -o jsonpath='{.spec.containers[*].env}' -n <命名空间>
遇到过一次是Secret里换了新的key名字,Deployment里的secretKeyRef没跟着改,变量值是空的,报出来的错却是连接超时。
ImagePullBackOff:镜像没拉下来
切到镜像这一边。describe的Events里那行Failed to pull image后面跟着具体原因,常见的是四种。
1、tag不存在或者名字拼错,提示manifest unknown或者repository does not exist。把镜像名整段拿到能拉镜像的机器上试一次,比在YAML里反复核对快。
2、私有仓库没凭证,提示unauthorized或者pull access denied。建一个docker-registry类型的secret,然后挂到ServiceAccount上,比在每个Pod里写一遍省事:
kubectl create secret docker-registry regcred \
--docker-server=<仓库地址> --docker-username=<账号> --docker-password=<口令>
kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":"regcred"}]}'
3、节点访问不到仓库。在Pod被调度到的那个节点上手动拉一次最直接,containerd环境用crictl:
crictl config runtime-endpoint unix:///run/containerd/containerd.sock crictl pull <镜像名>
4、节点磁盘满了,提示no space left on device。df -h看一下 /var/lib/containerd所在分区,再用crictl rmi --prune清掉没在用的镜像。
国内环境还多一种:拉registry.k8s.io上的镜像超时。把镜像地址换成境内镜像仓库的前缀,或者在节点上配containerd的mirror规则,指望改DNS解决不太现实。
exec format error躲在循环重启后面
这个坑踩过两次。arm64的节点拉了amd64的镜像,kubectl这边看不到任何拉取失败,Pod正常进入CrashLoopBackOff,describe也干净,只有日志里一行exec user process caused: exec format error。两台机器CPU架构不同、同一个tag拉下来跑不起来,就是这个原因。
多架构镜像(manifest list)会自动按节点架构匹配,单架构镜像只能换对应架构的tag,或者给节点打架构标签后用nodeSelector分流。
latest标签的隐性问题
imagePullPolicy的默认规则是:tag写成latest或者省略tag时按Always,写了具体tag时按IfNotPresent。用latest的Pod每次重启都要重新拉,仓库抖一下就起不来;反过来,用固定tag时仓库里同名tag换了内容,节点上有旧镜像就永远不会更新。要锁死只能写成 镜像名@sha256:... 这种digest形式。
版本偏差也会带来莫名其妙的报错
kubectl与集群版本偏差超过一个小版本时,命令能跑但字段解释会对不上,报错看着像配置问题。维护中的版本是Kubernetes 1.34.11、1.35.9、1.36.4与1.37.0,1.34在2026年10月27日 停止维护,1.35到2027年2月28日。集群侧配套的是containerd 1.7.29与2.0.4,kubectl 1.34、1.35、1.36、1.37,这几套组合在2026年 的发行版里都还在支持范围内。
状态字段本身不复杂,麻烦在于它把真正的报错藏在了上一个容器的日志和Events里。先看describe、再看 --previous日志,比上来就改YAML快得多。
数据截至2026年10月。