一、问题背景
我们生产集群跑在 Kubernetes 1.24 上,所有业务镜像都存放在自建的 Harbor 私有镜像仓库。集群通过 imagePullSecret(docker-registry 类型的 Secret)向 Harbor 做认证拉取镜像。某天上午一次常规的版本发布,滚动更新(RollingUpdate)刚触发不久,发布平台就报"就绪实例数不足",监控里该服务的 5xx 错误率开始抬头。最初以为是新版本代码有问题,直到发现大量新 Pod 根本没起来——它们卡在了镜像拉取阶段,整条发布被堵死。
二、故障现象
kubectl get pods -n order 的输出里,本次发布的 Pod 几乎集体处于 ErrImagePull / ImagePullBackOff 状态,旧版本的 Pod 已经有一部分被滚动更新策略 terminate 掉,导致可用副本数骤降。服务网关侧直接体现为部分请求 5xx,发布流水线卡住无法继续。
describe 某个卡住的 Pod,Events 末尾赫然写着:
|
|
节点层面用 crictl ps -a 能看到这些 Pod 的 sandbox 一直起不来,对应容器停留 ContainerCreating。登录其中一个工作节点手动执行 crictl pull harbor.internal.com/order/order-svc:1.8.2,同样返回 unauthorized: authentication required——镜像本身拉不下来,和代码无关。
三、排查过程
第一步,确认镜像是否真的存在。我们直接到 Harbor 的 Web 控制台搜索 order-svc:1.8.2,镜像明明白白躺在项目里,说明不是"镜像没推上来"或 tag 拼错。这说明问题出在"拉取"环节,而不是"构建/推送"环节。
第二步,看拉取失败的具体原因。unauthorized: authentication required 已经把范围收窄到"认证"。既然是私有仓库,Kubernetes 一定是通过某个 imagePullSecret 去认证的。先确认 Pod 关联的 Secret 在不在:
|
|
Secret 存在,名字也对得上。那就要怀疑 Secret 里的凭证本身是不是还有效。
第三步,把 Secret 内容解码出来看。docker-registry 类型的 Secret 把 ~/.docker/config.json 做了 base64 编码存在 .dockerconfigjson 字段里:
|
|
解码后看到里面的 auth 是一串看起来正常的 base64(username:password)。为了验证它是否真的能用,我在一台能通 Harbor 的机器上把它解出来,构造 ~/.docker/config.json 后执行 docker pull harbor.internal.com/order/order-svc:1.8.2——结果依旧 unauthorized。
第四步,去 Harbor 侧核实账号状态。我们 regcred 用的是 Harbor 的一个 robot account(机器人账号)。查 Harbor 后台发现:一个月前运维做了一次账号体系加固,给 robot account 的 token 加上了有效期策略,而这个 token 恰好在这两天到期。Secret 是当初手工 kubectl create secret docker-registry 写进去的,是个静态凭据,仓库侧把 token 一过期,K8s 这边的 Secret 完全不知道,依旧拿着失效的 token 去拉镜像,于是全量 unauthorized。
第五步,反查为什么之前没暴露。原来老版本镜像 :1.8.1 早就被各节点的 containerd 缓存过,旧 Pod 用缓存镜像根本不触发重新拉取;这次发 :1.8.2 是新 tag,节点没有缓存,必须走一次真实鉴权,失效凭证才第一次"现原形"。这也解释了为什么故障"恰好"在发布新版本时爆发。
四、解决方案
止血和恢复很快,三步搞定:
- 重建有效的 Secret。用更新后的 robot account 凭证重新生成:
|
|
-
让 Pod 用上新凭证。Secret 改完,但已经卡住的 Pod 不会自动重试用新 Secret。直接删掉处于
ImagePullBackOff的 Pod,由 ReplicaSet / Deployment 控制器重建;新 Pod 挂载到更新后的 ServiceAccount imagePullSecrets,拉取成功,滚动更新继续推进。 -
确认发布完成。等所有新版本 Pod 进入
Running且就绪,5xx 回落,发布流水线跑通。
注意:如果集群用 Rancher / Argo CD 之类管理 Secret,要在对应平台同步更新,避免被覆盖回旧值。
五、根因分析
表面是"镜像拉取失败",根因是 imagePullSecret 是个静态、无生命周期的凭据,它的有效期完全独立于镜像仓库的账号体系。仓库侧做了 robot token 有效期策略,K8s 侧却没有任何机制感知和同步刷新;再加上节点镜像缓存"帮"老版本掩盖了问题,失效凭证一直没被触发,直到一次全新 tag 的发布才暴露。这属于典型的"凭据漂移 + 缺乏凭证有效期监控"类故障。
六、预防措施
- 集中管理 Secret,杜绝手工漂移:用 external-secrets-operator 或 sealed-secrets 把
imagePullSecret纳入统一密钥管理,由 CI/CD 统一定期刷新,不再手工kubectl create secret。 - robot account 也要纳入轮换与告警:即便用机器人账号,也应监控其 token 过期时间,临期自动告警或自动轮换。
- 发布流水线加"镜像可拉取"预检:部署前先在目标命名空间用一个 test Pod 拉一次镜像(
kubectl run drypull --image=... --restart=Never),拉取失败就阻断发布,把问题卡在流水线里而不是生产环境。 - 对 ErrImagePull 做指标告警:用 kube-state-metrics + 容器运行时的
ErrImagePull/ImagePullBackOff事件配置告警,故障一发生就通知到人。 - 镜像引用尽量用不可变 tag 或 digest,避免
:latest这类可覆盖 tag 带来的版本错乱。
七、总结
ImagePullBackOff 看起来吓人,但 unauthorized: authentication required 这条线索已经把问题钉死在"认证"上。真正的坑不在 Kubernetes,而在"静态 imagePullSecret 与镜像仓库账号体系脱节"——仓库悄悄轮换了凭证,K8s 还在用旧 token。这次借节点镜像缓存的"掩护"潜伏了很久,直到一次新 tag 发布才爆发。把 imagePullSecret 纳入自动化的密钥管理 + 发布前拉取预检,能从根上消除这类"凭据漂移"型故障。