首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一次 ImagePullBackOff 排障实录:当私有 Harbor 证书过期、containerd 不信任、节点各自为政

一次 ImagePullBackOff 排障实录:当私有 Harbor 证书过期、containerd 不信任、节点各自为政

原创
作者头像
编程小妖女
修改2025-09-14 14:35:43
修改2025-09-14 14:35:43
3780
举报
文章被收录于专栏:后端开发后端开发

在一套使用 kubeadm 部署的生产集群里,我遭遇过一类看似常见却非常“缠人”的镜像拉取失败:ImagePullBackOff

这个错误折腾得我够呛,所以把排查过程记录下来,方便以后再遇到类似问题,直接返回头来查看。


故障环境

  • 操作系统:Ubuntu 22.04 LTS(企业内核通道)
  • 容器运行时:containerd 1.7.x(通过 systemd 管理)
  • 集群:kubeadm 初始化的 Kubernetes 1.29.x,CoreDNS 默认为 1.10+
  • 容器镜像仓库:自建 Harbor(自签名 CA),同时透传外网 registry-1.docker.io
  • CNI:Calico
  • 拉取工具:crictlctr,用于在节点上直接复现实验

现象与真实错误信息

业务侧反馈“新发版本的 Pod 起不来”。kubectl get pods 显示 ImagePullBackOffkubectl describe podEvents 里,有的节点报 x509 相关错误,有的纯超时。

在一台出问题节点上抓到的真实错误消息如下:

代码语言:sh
复制
Warning  Failed     kubelet   Failed to pull image `harbor.intra.example.com/proj/app:1.2.3`: 
rpc error: code = Unknown desc = failed to pull and unpack image `harbor.intra.example.com/proj/app:1.2.3`: 
failed to resolve reference `harbor.intra.example.com/proj/app:1.2.3`: 
failed to do request: Head https://harbor.intra.example.com/v2/proj/app/manifests/1.2.3: 
x509: certificate signed by unknown authority

这个错误和 Kubernetes 社区中相同类型问题的报错基本一致(crictl pull 的一条典型输出见这里)(GitHub)。

图片1

为了确认不是镜像名写错或仓库 404,我在节点上直接复现:

代码语言:bash
复制
# 以 root 在问题节点执行
crictl pull harbor.intra.example.com/proj/app:1.2.3
# 同样命中 x509 unknown authority

这一步的动机很直接:把问题从 Kubernetes 面板拉回到节点与运行时层面,避免被 Pod 重试策略掩盖细节。


额外抓取:containerd 的调用栈

要证明真的是拉取路径上的 TLS 校验问题,我给 containerd 发了 SIGUSR1,导出当时的 goroutine 栈(这一招可用于 Go 守护进程,触发位置与 dockerd 类似,发送信号后栈会被写进日志)(Docker Documentation):

代码语言:bash
复制
# 导出 containerd 栈到 journal
kill -SIGUSR1 $(pidof containerd)
journalctl -u containerd --since "1 min ago" | less

在另外一个 containerd 拉取异常的公开案例里,也能看到“栈里 goroutine 停在 I/O 等待”的描述(issue 中明确提示用 SIGUSR1 抓栈,并给出 goroutine 34 ... [IO wait] 的栈段落)(GitHub)。

我的现场日志里同样能看到拉取路径卡在 remotes/docker 解析与请求阶段的栈帧,这一证据与 x509 报错可以相互印证。


排查路径

  • 观察 kubectl describe podEvents,确认不是镜像名打错或没有权限(例如 pull access denied 这类会走另一个分支),而是 TLS 校验失败、或长时间无法连通。社群文章也建议把 describe 作为入口,这点很受用。
  • 在节点上用 crictl pullctr images pull 直接复现,既能避开 Pod 层面的重试,也能拿到更原始的错误串(Baeldung on Kotlin)。
  • 检查 containerd 的 registry 配置:是否启用了 hosts 目录模式;是否为目标域名创建了 hosts.toml;是否放置、指向了正确的 CA 文件。官方文档对 hosts.toml 的字段有解释(servercapabilitiesskip_verifyca 等)(Fossies)。
  • 如果是 K3s / RKE2 / k0s 之类的发行版,registry 的配置文件路径可能不同,要按各自文档写 registries.yaml 或 drop-in,然后重启服务生效(K3s)。
  • 通过 journalctl -u containerd 对齐时间窗口,结合 SIGUSR1 抓到的栈,确认拉取请求确实在 TLS 校验阶段失败或网络 I/O 超时(journalctl 的过滤姿势可以参考这几篇,用 --since-u 过滤)(BetterStack)。

我的修复方案(附可直接落地的配置示例)

我的修复目标是:让所有节点对 Harbor 的自签名 CA 达成一致信任,并避免临时 skip_verify 带来的合规风险。以下提供两条路径:一条是 containerd 官方 hosts.toml 路径;另一条是 K3s / RKE2 用户更常用的 registries.yaml

路径 A:containerd hosts 目录模式

  1. 确保 containerd 读取 certs.d 目录(config.toml 中的开关),官方建议的写法如下(注意这里的 config_path 指向目录):(Gardener)
代码语言:toml
复制
# /etc/containerd/config.toml
version = 2

[plugins.'io.containerd.grpc.v1.cri'.registry]
  config_path = '/etc/containerd/certs.d'
  1. 为目标域名创建目录与 hosts.toml,并放入 CA:
代码语言:bash
复制
sudo mkdir -p /etc/containerd/certs.d/harbor.intra.example.com
sudo cp /root/ca/harbor-ca.crt /etc/containerd/certs.d/harbor.intra.example.com/harbor-ca.crt
代码语言:toml
复制
# /etc/containerd/certs.d/harbor.intra.example.com/hosts.toml
server = 'https://harbor.intra.example.com'

[host.'https://harbor.intra.example.com']
  capabilities = ['pull', 'resolve', 'push']
  ca = ['/etc/containerd/certs.d/harbor.intra.example.com/harbor-ca.crt']
  skip_verify = false

上面的字段与容器运行时文档一致,其中 skip_verify 只在临时应急时可考虑开启,长期应通过 CA 信任来解决。关于 hosts.toml 的字段含义与 hosts 目录模式,可以参考 upstream 文档与社区答复(Fossies)。

  1. 重启 containerd。多数情况下需要重启服务才能生效(是否支持热加载与版本有关,实践上稳妥的做法是重启)(Stack Overflow):
代码语言:bash
复制
sudo systemctl restart containerd

路径 B:适用于 K3s / RKE2registries.yaml

如果你的发行版内置了更高层的封装(例如 K3s),可以直接写:(K3s)

代码语言:yaml
复制
# /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - 'https://harbor.intra.example.com'
configs:
  'harbor.intra.example.com':
    tls:
      ca_file: /etc/containerd/certs.d/harbor.intra.example.com/harbor-ca.crt
    auth:
      username: your-username
      password: your-password

保存后重启 k3s 服务即可使配置下发到 containerd:(K3s)

代码语言:bash
复制
sudo systemctl restart k3s

用可运行的最小化清单佐证修复效果

准备一个极简 Deployment,使用私有仓库镜像;如需认证,搭配 imagePullSecrets。下面全部为合法 YAML,且不依赖英文双引号。

代码语言:bash
复制
# 如需认证,先创建拉取凭据
kubectl create secret docker-registry regcred \
  --docker-server=harbor.intra.example.com \
  --docker-username='your-username' \
  --docker-password='your-password' \
  -n demo
代码语言:yaml
复制
# demo-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: whoami
          image: harbor.intra.example.com/proj/whoami:1.0.0
          ports:
            - containerPort: 80

应用并观察:

代码语言:bash
复制
kubectl apply -f demo-ns.yaml
kubectl -n demo get pods -w
kubectl -n demo describe pod deploy/whoami

修复成功后,describeEvents 会显示 PulledCreated,而非 ImagePullBackOff。这一步的思路与常见排障文章一致。


反思:为什么同样的 Secret、同样的 Pod,只有部分节点报错?

这就是这类问题“复杂”的地方:镜像拉取最终发生在节点运行时

Kubernetes 中的 imagePullSecrets 解决的是认证,而是否信任服务器证书由节点的 containerd 与宿主信任库决定。

只要不同节点对 CA 处理不一致,就会出现“有的节点能拉,有的节点报 x509”的情况。社区里关于 x509: certificate signed by unknown authority 的案例,也都有“把 CA 放到每个节点特定目录并重启运行时”的共识(DevOps Stack Exchange)。


与代理、证书、平台架构相关的延伸思考

  • 代理劫持 TLS:公司代理插入证书,导致服务端证书链与期望不符,常见提示是 certificate signed by unknown authorityx509 其它变体。要么让 containerd 信任代理的根证书,要么为目标域名直连绕过代理。
  • 证书 SAN/CN 不匹配:仓库访问域名与证书颁发的 Subject Alternative Name 不一致,x509 同样会失败。
  • 平台架构不匹配:如果错误换成了 no matching manifest for linux/amd64 in the manifest list entries,那就不是证书问题,而是镜像没有你这台节点的架构清单;可以用 --platform 拉取或重建多架构镜像(这类报错也会把 Pod 打到 ImagePullBackOff,但事件内容不同)。
  • 是否必须重启:不同版本对动态加载支持不同,许多文档与经验贴都建议修改后直接重启 containerd,更稳妥(Stack Overflow)。
  • SIGUSR1 抓栈Go 守护进程(如 dockerd)约定俗成地支持用信号导出栈;containerd 也在多个 issue 与工程实践中用相同手法抓过栈,有助于确认是否卡在 I/O(示例与方法参考)(Docker Documentation)。

我最终采用的解决方案

  • 所有工作节点统一启用 hosts 目录模式,/etc/containerd/certs.d/harbor.intra.example.com/hosts.toml 指向最新 harbor-ca.crt禁用 skip_verify。配置语义与官方文档一致(Fossies)。
  • K3sRKE2 环境编写 registries.yaml 下发方案,避免每台机手动改 config.toml,并在运维手册中明确“证书更新需同步重启服务”(K3s)。
  • 针对“有代理的办公网段”,把代理根证书也纳入节点信任库,确保 Harbor 访问不被透明代理破坏。
  • 上线前在一台新节点做“拉取健康检查”——通过 crictl pull harbor/... 验证证书信任与带宽连通,写进交付流水线。

避坑总结

  • ImagePullBackOff 是表象,即时查看 describe 里的 Events 才能读到根因(超时、鉴权、证书、DNS、平台架构等路径不同,处理方式完全不一样)。
  • 认证与信任分离imagePullSecrets 只负责账号口令,TLS 信任要交给节点运行时。当你看到 x509,就去看 containerdcerts.d 与宿主信任库。
  • 所有节点一致:别在单机上验证通过就宣告胜利。镜像拉取发生在调度到的那台机上,任何一台没配好都会导致偶发性 ImagePullBackOff
  • 少用 skip_verify:这是应急手段。长期要么配 CA,要么改用可信证书。社区答复强调了 hosts.tomlskip_verify 行为,但也有版本差异与坑(例如部分版本对某些情景忽略 skip_verify)(Stack Overflow)。
  • 会抓栈:当报错信息不足以判断卡在哪里时,给 containerdSIGUSR1,配合 journalctl -u containerd 检索时间窗口,可以定位是否卡在解析与 I/O。相关方法与实践可参考这些资料(Docker Documentation)。

参考与延伸阅读

  • crictl pull 典型的 x509 报错示例(与现场一致)(GitHub)
  • 使用 kubectl describe 作为入口定位 ImagePullBackOff 的思路与图示
  • containerd hosts.toml 与 hosts 目录模式的官方说明与社区答复(Fossies)
  • K3s 私有仓库配置 registries.yaml(含 TLS 与认证样例)(K3s)
  • 在守护进程中用 SIGUSR1 导出堆栈的方法与 containerd 的相关 issue 说明(Docker Documentation)
  • 另一类 ImagePullBackOff 场景截图与说明(便于对照你自己的 Events

如果大家也在公司内网里用自建 Harbor,强烈建议把“节点信任仓库 CA 的一致性检查”加入日常巡检脚本,并在证书续期流水线上触发全节点的配置刷新与运行时重启。这不是 Kubernetes 的“基础语法问题”,而是节点运行时配置一致性与证书生命周期管理的工程问题。只要这个工程问题处理好了,ImagePullBackOff 这类事故的概率会显著下降。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 故障环境
  • 现象与真实错误信息
  • 额外抓取:containerd 的调用栈
  • 排查路径
  • 我的修复方案(附可直接落地的配置示例)
    • 路径 A:containerd hosts 目录模式
    • 路径 B:适用于 K3s / RKE2 的 registries.yaml
  • 用可运行的最小化清单佐证修复效果
  • 反思:为什么同样的 Secret、同样的 Pod,只有部分节点报错?
  • 与代理、证书、平台架构相关的延伸思考
  • 我最终采用的解决方案
  • 避坑总结
  • 参考与延伸阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档