k8s集群内部分Docker仓库故障,导致k8s dockerd进程镜像拉取资源耗尽,影响全局镜像拉取

作者:小橙子🍊 发布时间: 2026-02-10 阅读量:1 评论数:0

Kubernetes 镜像拉取故障排查:当 Pod Pending 遇上 Registry 连接耗尽

一次生产环境镜像仓库迁移后,Pod 长时间 Pending 的故障排查实录,从现象到底层原理的深度分析。


背景

我们生产环境有两套 Nexus 环境,Docker 镜像制品也使用 Nexus。老环境运行已久,Nexus 非常不稳定,故障 BUG 特别多。在新环境迁移工作中,我们已经转向 Harbor。

某天 Nexus 突然完全故障,所有请求 404(Nexus 本身经常出 BUG,虚机层面磁盘故障等问题造成数据错乱)。于是我们放弃老的 Nexus,全部切换到新的 Harbor。


故障过程复盘

第一阶段:切换完成后的异常

在 2-3 天的排查调整后,基本切换完成,业务没有反馈仓库等问题了。但在中午变更时发现:

异常现象

  • 老环境 Pod 发布后,一直处于 Pending 状态
  • 可能等待 10 多分钟才能启动成功
  • 观察 Pod event、deployment 甚至 replica sets,都没有任何错误事件

根据多年经验,至少 replica sets 会有故障事件,这次什么都没有,非常不科学。

第二阶段:深入排查

回到 Pod 本身,仔细观察 event 事件:

关键发现

  • 虽然没有报错,但事件信息有异常
  • 只有拉取镜像的事件,没有拉取成功的事件
  • 正常情况下一定会有拉取成功的事件

登录节点服务器,手动执行 docker pull,可以正常拉取,没有问题。而且服务器拉取镜像后,Pod 就正常启动了。

第三阶段:扩大影响范围

一时间没有头绪。该服务器连接公网,只有一台,怀疑某些网络问题,先观察。

到了晚上业务变更高峰期,这个问题仍然存在,并且部署在哪个节点都存在同样问题。好在这是老环境,变更系统不多。

临时措施

  • 选一些节点手动拉取镜像
  • 固定 Pod 调度,避免手动拉取镜像后重新调度到没有镜像的节点

特殊现象

  • 故障节点即使手动拉取完镜像后,Pod 也不启动
  • 需要重启 Pod(或等待 10-20 分钟)才会启动

因为是镜像拉取相关问题,怀疑是镜像仓库故障导致的。


环境架构

我们有两套 Docker 仓库:

仓库 域名 类型 位置 状态
新仓库 registry-domain-new Harbor 内蒙数据中心 正常
老仓库 registry-domain-old Nexus 杭州数据中心 故障

业务服务新旧集群都使用 registry-domain-new(新的 Harbor),一直没有任何问题。老环境在杭州数据中心,我们怀疑是专线或网络故障导致从杭州到内蒙的异常。


排查过程

排查组件日志

第二天早上,联系运维同事排查镜像仓库问题,但没有任何收获:

  • 手动操作就是 OK 的
  • 新的镜像仓库在新集群也在使用,新集群是正常的

逐步排查相关组件的日志,模拟故障现场:

journalctl -u kubelet -n 200 -f
journalctl -u docker -n 200 -f
tail -f /var/log/messages | grep docker

结果:没有任何不对的日志。

关键突破

回顾现有信息:

  • Pod 没有拉取镜像成功的事件
  • 镜像在节点准备好之后,就可以正常启动 Pod
  • 相关组件没有任何错误日志
  • 镜像仓库是正常的
  • 手动操作和另外一套新集群都正常

怀疑方向:可能是 dockerd 卡住了,资源耗尽或 TCP 连接被占用无法释放?

解决方案

清理集群内的 Corn Job Pod:

  • 这些 Pod 还在使用老仓库
  • 除了这些,还有一些启动不了的 Pod
  • 大概有 300 多个 Pod 处于 Pending 状态

清理之后,终于恢复正常了!

结论

  • 老仓库故障了
  • 大量的 Pod 还从故障的仓库拉取导致资源被耗尽
  • 推测是日志级别不够,没有打印这么底层的详细日志

问题分析回顾

真实情况分析

更可能的情况是:

  1. kubelet 根本没找 docker 拉镜像,而是找了 containerd / cri-dockerd
  2. 手动 docker pull 时,拉的是 docker 的镜像库,与 kubelet 使用的 containerd 完全无关
  3. 所以
    • docker pull 很快 → 说明镜像仓库侧没问题
    • 但 kubelet 一直在等 containerd 拉镜像
    • 这个过程要么卡住(containerd 内部问题、磁盘问题、某层损坏)
    • 或已经拉好但容器创建阶段卡在 snapshotter/filesystem
  4. journalctl -u docker 看不到任何日志,因为 docker 服务根本没参与

Pod 创建流程解析

以 "创建一个 Pod" 为例,看请求如何从 k8s 走到 dockerd 和 runtime:

graph LR A[kubectl apply] --> B[kube-apiserver] B --> C[scheduler] C --> D[kubelet] D --> E[dockershim] E --> F[dockerd] F --> G[containerd/runc] F --> H[registry]

详细流程

  1. 用户提交kubectl apply -f pod.yaml

    • kubectl → kube-apiserver:提交 Pod 对象
  2. 调度阶段:kube-controller-manager / scheduler

    • 将 Pod 调度到某个 Node
    • 写到 etcd,apiserver 记录
  3. 节点执行:目标 Node 上的 kubelet 发现有新 Pod

    • kubelet 通过 CRI 请求 "create sandbox、create container、start container" 等
  4. CRI 转换:kubelet 内部的 dockershim 收到 CRI 调用

    • dockershim 把 CRI 请求转换为 Docker API:
      • POST /containers/create
      • POST /containers/(id)/start
  5. Docker 执行:dockerd 收到请求

    • 若需要拉镜像:调用 registry 拉镜像,写入本地镜像存储
    • 创建容器配置(namespace、cgroup、mount 等)
    • 调用 containerd / runc,按照 OCI 规范实际启动容器进程
  6. 状态上报:容器启动后

    • kubelet 周期性通过 CRI 检查容器状态
    • kubelet 把 Pod 状态上报给 kube-apiserver

调用链路

kubelet
  -> 调用 CRI 接口 (CreateContainer, StartContainer, ListContainers, ...)
    -> dockershim 实现这些接口
      -> 调用 dockerd 的 HTTP API (/containers/create, /containers/start, ...)

技术深度分析

1. 一个节点上 dockerd 是怎么跑的?

常规生产环境(老的 Docker + k8s)

  • 节点上只有一个 dockerd 进程
ps aux | grep dockerd
  • 它通过 systemd 启动,监听一个 Docker API 端口:
    • Unix socket:/var/run/docker.sock
    • 有时还会暴露 TCP:tcp://0.0.0.0:2375(不安全,仅测试环境偶见)

架构设计

从代码和架构设计看:

  • dockerd 设计:单实例、多 goroutine 的 daemon

    • 接收 API 请求
    • 内部有任务调度 / worker 池
    • 调用 containerd / graphdriver 等子模块
  • 多 dockerd 理论可行

    • 需要完全隔离 runtime 数据目录
    • k8s/kubelet 只会连接一个指定的 Docker Endpoint
    • 部署上几乎没人这么干

结论:所有 k8s 上的 Pod 创建 / 镜像拉取 + 手工 docker pull 都是在打同一个 dockerd。


2. CLI 好用,k8s 卡住,代码层面更可能是哪一层的问题?

分层架构

kubelet
  -> dockershim (CRI 实现)
      -> /var/run/docker.sock
          -> dockerd
               -> containerd / graphdriver
                    -> registry A/B

2.1 dockershim 会不会是瓶颈?

dockershim 的职责

  • 实现 CRI 接口:PullImage、CreateContainer、StartContainer、ListContainers 等
  • 每个接口内部基本是:
    • 解析 CRI 请求
    • 组装 Docker API 调用参数
    • 调用 Docker client(HTTP 客户端)通过 /var/run/docker.sock 发请求
    • 把 Docker 的返回转成 CRI 返回

代码特点

  • 没有复杂的"队列 + 限流 + 优先级调度逻辑"
  • 并发模型:
    • kubelet 上层的 worker 池 + goroutine
    • 每次 Pull 请求就起一个协程调 Docker API

结论:如果 dockerd 反应正常,dockershim 不太容易成为瓶颈。

反过来说:手动 docker pull 正常,但 kubelet/dockershim 拉都卡住,且全集群有大量对坏仓库的请求时,更合理的是 dockerd 资源被塞满

2.2 dockerd/containerd 里面的并发限制

这里是问题的关键!

HTTP client / registry 连接池限制
  • Docker 对 registry 使用 HTTP client 池,每个 host 的并发连接数有限
  • 大量针对坏仓库 A 的 pull:
    • 会占满 A 对应的连接
    • 如果底层实现 / DNS / TLS 握手卡住,还会占用大量 goroutine 和 FD
    • 某些版本的 Docker 在"长时间无法建立连接"的情况下,资源回收并不积极
镜像拉取任务队列
  • dockerd 会将 Pull 请求排队
  • containerd 内部也有内容存储、snapshotter 的操作队列
  • 几百个 Pod 同时触发 pull 同一个坏仓库的镜像:
    • 队列充满"必然失败但要等很久"的任务
    • 新的正常请求(去仓库 B)也得排队
    • 占用共享资源(CPU、网络带宽、FD)
    • 表现出:"整体都慢 / 卡住"
文件句柄、goroutine、系统资源
  • 每个拉取任务可能占用多个 FD(socket、文件、pipe)
  • 若节点 ulimit -n 较小或 Docker 的内部 FD 使用不当,很容易被耗光
  • 这时:
    • 新的连接建立失败
    • docker API 响应也可能异常
    • kubelet 看到的是 Pull 超时或一直 pending
现象对比
对比项 CLI docker pull kubelet/dockershim
仓库 正常仓库 B 坏仓库 A
状态 能拉,说明 dockerd 还活着 卡住
资源占用 轻量使用者,偶尔发起 大量请求占满资源

结论:从代码 / 架构视角,问题主要出在 dockerd/containerd 的资源 / 并发限制,而不是 dockershim。


3. kubelet 这一层会不会限制并发?

kubelet 确实有一些对 image pulls 的控制:

  • --serialize-image-pulls:串行拉取镜像(默认多并发)
  • --image-pull-progress-deadline:镜像拉取超时时间
  • kubelet 会为每个 Pod 创建任务,触发 PullImage,如果慢会重试

但两个事实很关键

  1. kubelet 默认允许并发拉取,不做强限流
  2. 就算 kubelet 限制并发,也挡不住"集群多个节点上都在并发拉同一个坏仓库"

结论

  • kubelet 本身不会产生"让 CLI 请求快、CRI 请求慢"的逻辑
  • 全集群大量 CronJob Pod 都去坏仓库拉镜像,每个节点的 dockerd 都被拖住

常见问题解答

Q1:节点上就一个 dockerd 进程服务,只要 API 调用就能执行?

A

  • 是,一个节点上典型就是一个 dockerd
  • 只要能连上 /var/run/docker.sock,任何客户端都可以发请求
  • 但 dockerd 内部会受资源限制影响:
    • 请求"能发送" ≠ 一定"能被快速处理"
    • 被阻塞/排队/超时也都是可能的结果

Q2:既然是同一个 dockerd,那为什么 CLI 正常而 k8s 卡住?

A

  • dockerd 内部资源有限:
    • 被大量来自 kubelet 的坏仓库 A 拉取任务占用
    • CLI 只是偶尔发起一个 B 仓库的拉取,有时还能挤进来
  • 换句话说:
    • k8s 这条路径是被挤爆的重灾区
    • CLI 是轻量使用者,有一定概率幸存

Q3:会是 dockershim 层并发限制了吗?

A

  • 从设计和代码职责上看,dockershim 更像是"一个薄薄的翻译层":
    • 不负责复杂的限流
    • 也不会自己维护大而持久的队列
  • 现象更符合:dockerd/containerd 的请求/任务层资源被坏仓库 A 的拉取拖死

排查方法

如果想从 "代码层面" 更确证,可以用以下工具验证:

1. 看 dockerd 的连接状况

ss -antp | grep docker
ss -antp | grep <坏仓库域名或IP>

看是否有大量半开 /ESTABLISHED 等待中的连接。

2. 看 dockerd 占用的 fd / goroutine

lsof -p $(pidof dockerd) | wc -l
cat /proc/$(pidof dockerd)/limits

如果监控里有 Go runtime metrics(dockerd 启用 pprof/prometheus),可以看到 goroutine 数非常高。

3. 停掉 CronJob + 清 Pending Pod

步骤

  1. 暂停/删除使用坏仓库的 CronJob
  2. 删除所有 Pending 的那些 Pod
  3. 观察几分钟内,其他业务 Pod 拉取是否恢复正常

如果立即恢复,基本就是 "坏仓库拉取任务把 dockerd 拖死" 这个模式。


Pod 事件对比分析

异常情况 - Pod 一直处于 Pending 状态

字段 内容
事件 Pulling image
镜像地址 [registry-domain]/eos/fy-demo:v1.2025-12-24.09-31-42
来源 kubelet itk8s-worker12
出现次数 1
子对象 spec.containers{fy-demo-v1}
最后发生时间 2025-12-24T09:32:28+08:00

问题描述

  • Pod 仅显示 "Pulling image" 事件,随后陷入 Pending 状态
  • 无任何错误信息和日志输出
  • 持续等待数十分钟仍无进展

正常情况 - Pod 成功启动

字段 内容
事件 Started container
来源 kubelet itk8s-worker08
子对象 spec.containers{hichain-api-v1}
最后发生时间 2025-12-24T09:29:51+08:00
字段 内容
事件 Successfully pulled image
镜像地址 [registry-domain]/feiyun-custom-images/wms/h3c-centre-app:2025122401
拉取耗时 3m10.905694503s
来源 kubelet itk8s-worker08
子对象 spec.containers{hichain-api-v1}
最后发生时间 2025-12-24T09:29:50+08:00

启动流程

Pulling image → Successfully pulled image (3分10秒) → Created container → Started container

对比分析

对比项 异常情况 正常情况
镜像拉取状态 一直处于 Pulling,无进展 成功完成拉取
拉取耗时 无限等待 约 3 分 10 秒
后续事件 Created → Started
错误信息
日志输出 正常

总结与建议

核心结论

  • 一个节点只有一个 dockerd,所有 k8s 和 CLI 的拉取都走它
  • 现象从实现/代码职责角度,最合理的根因在 dockerd/containerd 的拉取并发与资源耗尽
  • dockershim 几乎只是"翻译员",真正"堵车"的是 dockerd 内部的任务队列/连接池/资源上限

运维建议

1. 快速封禁坏仓库

# 在 /etc/hosts 中添加
echo "0.0.0.0 registry-domain-old" >> /etc/hosts

# 或使用 iptables
iptables -A OUTPUT -p tcp -d <坏仓库IP> -j REJECT

2. 清理异常 Pod

# 查找使用坏仓库的 Pod
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.containers[].image | contains("registry-domain-old")) | .metadata.namespace + "/" + .metadata.name'

# 批量删除
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.containers[].image | contains("registry-domain-old")) | .metadata.namespace + " " + .metadata.name' | \
  xargs -I {} kubectl delete pod -n {} --force --grace-period=0

3. 给 CronJob 加保险

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: my-cronjob
spec:
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  concurrencyPolicy: Forbid  # 禁止并发
  startingDeadlineSeconds: 300  # 超时跳过

4. kubelet 参数优化

# /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
--serialize-image-pulls=false \
--image-pull-progress-deadline=10m \
--maximum-dead-containers-per-container=2

5. dockerd 配置优化

// /etc/docker/daemon.json
{
  "max-concurrent-downloads": 3,
  "max-concurrent-uploads": 5,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

参考资料


文章发布日期:2025-12-24

评论