发布卡住、Pod 一直起不来,
kubectl get pod里明晃晃一行ImagePullBackOff——这是 K8s 里最常见、也最容易误判的故障之一。它不一定是网络问题,也不一定是镜像不存在。多数人一上来就docker pull试一下,结果本地能拉、集群拉不了,反而更懵。
正确的做法是按层拆:先看错误原文,再看认证,再看网络与证书,再看仓库,最后看节点自身。五步走完,90% 的情况能直接定位到具体那一层。
第 1 步:看现象,先把错误分成四类
ImagePullBackOff只是结果,真正的原因写在 Events 里。先把原文捞出来:
kubectl describe pod myapp-7d9f-abcde|grep-A20Events你会看到类似Failed to pull image "registry.example.com/app:v1.2.3": ...的句子,后面的尾巴才是关键。按尾巴分四类:
- 认证类:
unauthorized、denied、401 Unauthorized、pull access denied - 不存在类:
manifest unknown、not found、no matching manifest for linux/arm64 - 网络类:
TLS handshake timeout、i/o timeout、dial tcp ... connect: connection refused、x509: certificate signed by unknown authority - 节点类:
no space left on device、too many open files
分完类,后面的排查路径就定了,不要混着查。
顺手区分两个状态:ErrImagePull是第一次拉取失败,ImagePullBackOff是 kubelet 在退避重试中。退避时间从 10 秒开始翻倍,最长 5 分钟——这解释了一个常见现象:你改了配置,Pod 却要等好几分钟才有反应。
想在 K8s 外面单独验证,就在出错的那个节点上直接拉一次:
crictl pull registry.example.com/app:v1.2.3第 2 步:看认证,Secret 是不是真的生效
认证类报错,先确认 Pod 到底有没有拿到凭证:
kubectl get pod myapp-7d9f-abcde-ojsonpath='{.spec.imagePullSecrets}'返回空,就是没挂。最常见的四个坑:
- Secret 建在别的 namespace。Secret 是 namespace 级资源,
prod里建的regcred,test的 Pod 用不了。 - Secret 类型不对。必须是
kubernetes.io/dockerconfigjson,用kubectl create secret generic建出来的config.json不会被识别。 - 凭证本身过期。机器人账号密码轮换过、token 过期,Secret 里还是旧的。
- 账号权限不含该镜像。Harbor 的项目机器人账号只授权了某个 project,跨 project 拉取照样 401。
把 Secret 解出来核对一眼,比猜快得多:
kubectl get secret regcred-nprod-ojsonpath='{.data.\.dockerconfigjson}'|base64-d注意 auth 字段是base64(user:password)的二次编码,解开后看用户名是否是robot$xxx这种完整形式——少写robot$前缀是最隐蔽的 401 之一。
一个省事的做法:把imagePullSecrets挂到 namespace 的 default ServiceAccount 上,之后该 namespace 所有 Pod 自动带上,不用每个 Deployment 都写一遍。
第 3 步:看网络与证书,节点能不能连到仓库
网络类报错,别在 Pod 里 ping,直接在节点上验:
curl-vhttps://registry.example.com/v2/- 返回
401 Unauthorized—— 通了,网络没问题,回去查第 2 步。 - 卡住不动 /
Connection timed out—— 节点出方向 443 被拦(安全组、公司出口策略),或者 DNS 解析不到。 x509: certificate signed by unknown authority—— 内网自签 CA,节点没信任。
自签 CA 要放到运行时认的目录,放完必须重启运行时:
# Dockercpinternal-ca.crt /etc/docker/certs.d/registry.example.com/ca.crt systemctl restartdocker# containerdmkdir-p/etc/containerd/certs.d/registry.example.comcpinternal-ca.crt /etc/containerd/certs.d/registry.example.com/ca.crt systemctl restart containerd只放文件不重启,等于没放——这是自签仓库排查里重复率最高的失误。
节点要走代理才能出网的话,代理必须写进 daemon 的环境,shell 里export是无效的:
# /etc/systemd/system/docker.service.d/proxy.conf[Service]Environment="HTTP_PROXY=http://proxy:3128"Environment="HTTPS_PROXY=http://proxy:3128"Environment="NO_PROXY=localhost,127.0.0.1,registry.internal"containerd 同理,写进config.toml的[plugins."io.containerd.grpc.v1.cri".registry.mirrors]后重启。
第 4 步:看仓库,镜像到底存不存在
manifest unknown和not found这两类,八成不是权限也不是网络,而是名字写错了:
dockermanifest inspect registry.example.com/app:v1.2.3|head-30重点核对三处:
- tag 拼错或已被覆盖。CI 里 tag 规则改过、
v1.2.3实际推成了1.2.3,差一个字符就manifest unknown。 - 短名被补全成 docker.io。写
app:v1而不写registry.example.com/app:v1,K8s 会去 Docker Hub 找docker.io/library/app:v1。 - 多架构镜像缺当前架构。
no matching manifest for linux/arm64表示这镜像只推了 amd64,节点是 arm 就拉不到。
另外两个仓库侧因素:
429 Too Many Requests:Docker Hub 匿名拉取有速率限制,多节点同时拉很容易撞上。配 registry mirror,或用带账号的 Secret 提高配额。- 私有仓库存储满 / 项目配额打满:Harbor 报的是 5xx 或
unknown blob,要去仓库端看磁盘和 GC 记录。
第 5 步:看节点自身,磁盘与运行时状态
节点类报错最直白,也最容易被"网络问题"的惯性思维盖过去:
df-h/var/lib/docker /var/lib/containerddf-i/var/lib/containerdno space left on device是数据盘满了,df -i是 inode 满了——后者在小文件多的环境(大量 layer 解压)很常见,df -h看着还有空间却一样失败。
清完空间还不够,要确认 kubelet 的镜像回收在正常工作。imageGCHighThresholdPercent默认 85,超过就开始清最久未用的镜像。如果节点上堆积了上千个历史 tag,GC 跟不上拉取速度,就会反复触发失败。
最后看运行时状态是否健康:systemctl status containerd、journalctl -u containerd -n 100。运行时半死不活时,报错会伪装成TLS handshake timeout,实际是本地 socket 卡住了,重启运行时立刻恢复。
一图总结
- 第一步看原文:Events 里把错误分四类(认证/不存在/网络/节点)
- 第二步看认证:Secret 的 namespace、类型、凭证、权限四查
- 第三步看链路:节点上 curl /v2/,自签 CA 放对目录并重启运行时
- 第四步看仓库:tag 拼写、短名补全、多架构、限流与配额
- 第五步看节点:磁盘与 inode、镜像 GC、运行时健康
避坑清单
imagePullSecrets是 namespace 级,不会自动跨 namespace,挂到 default ServiceAccount 更省心。- Secret 内容改了,Pod 不会立刻恢复——kubelet 还在退避等待,删 Pod 重建比干等快。
:latest标签的默认imagePullPolicy是Always,"配置明明没动却一直拉取失败"多半是它。- 自签 CA 放完必须重启 docker / containerd,只放文件不重启等于没配。
- daemon 的代理要写 systemd drop-in 的
Environment=,shell 里 export 对后台服务无效。 no matching manifest是架构不匹配,不是网络问题,别在网络上浪费时间。- 生产别用
latest,用 digest 固定镜像;镜像名写全 registry 前缀,短名会被补成docker.io/library。
小结
ImagePullBackOff之所以难查,是因为四种完全不同的原因共用一个状态名。把错误原文分成四类,然后按"认证 → 链路 → 仓库 → 节点"的顺序逐层排除,每一步都有可以当场执行的验证命令,就不会在错误的方向上越走越远。