Docker Hub 限速时代:Spegel 无状态缓存如何实现离线镜像共享

Docker Hub 限速时代:Spegel 无状态缓存如何实现离线镜像共享

TL;DR

Spegel 是一个非常有意思的项目,可以帮助我们在 Kubernetes 集群中实现镜像共享,提高镜像拉取的速度,减少对外部镜像仓库的依赖。对于一些离线或者内网环境、带宽优化和成本控制、容灾和高可用等场景,尤其是绕过 Docker Hub 镜像拉取限制方面,Spegel 都是一个不错的选择。

背景

Docker Hub 的限制越来越严,从今天的 4 月 1 日起,未经身份验证的用户每小时最多拉取 10 次镜像,并且是基于 IP 地址或 IPv6 子网限制,这意味着在一个局域网中多个用户共用一个公网 IP 的情况下,这个限制会更加严格。如果收到 429 Too Many Requests 响应,说明已经超过了限制,拉取请求被限流。

Docker Hub 多年来一直免费,但是随着用户数量的增加,成本也在增加,为了保证服务的稳定性,Docker Hub 也需要收费,这是一个必然的趋势。毕竟,运营如此全球规模的服务,维护成本是非常高的。天下没有免费的午餐。

但是对于一些个人开发者或者小团队来说,这样的限制可能会影响到他们的日常开发。

如何避免被限流

1. 登录 Docker Hub

最简单的方法,登录 Docker Hub 可以提高拉取镜像的次数,每小时 100 次。这对于个人开发者来说应该是足够的了。

如果是 Kubernetes 用户,可以通过 创建 Docker Hub 的 Secret 来实现。

kubectl create secret docker-registry dockerhub --docker-server=https://index.docker.io/v1/ --docker-username=USER_NAME --docker-password=PASSWORD

2. 使用其他的镜像仓库

Docker Hub 并不是唯一的镜像仓库,还有很多其他的镜像仓库也提供了免费的服务,但是可能不会有 Docker Hub 那么多的镜像。比如:

  • Quay.io:Red Hat 出品的镜像仓库,安全性高,支持私有仓库,但镜像相对较少。
  • GHCR:GitHub Container Registry,GitHub 提供的镜像仓库服务,可以通过 GitHub Actions 来构建镜像并推送到 GHCR。
  • 云厂商产品:比如阿里云、腾讯云、华为云、Azure、GCP、AWS 等,都提供了镜像仓库服务,可以通过这些镜像仓库拉取镜像,适合公有云上的生态。

3. 自建镜像仓库

自建镜像仓库是最好的方法,可以完全控制镜像的拉取和推送,不受限制。比如:

  • Harbor:CNCF 项目,VMware 开源的企业级仓库,支持 RBAC、镜像扫描、审计日志等,功能强大,安全性高。
  • Nexus Repository:Sonatype 出品的仓库,功能强大,支持 Maven、Docker、NPM 等多种仓库。
  • Docker Registry:Docker 官方的镜像仓库,可以自建,但是功能较弱,没有 UI(可与 docker-registry-browser 配合提供可视化),非常适合个人开发和测试环境。

4. 避免使用 latest 标签

使用固定的版本,而非使用 latest tag,可以避免因为 latest tag 指向的镜像发生变化而导致的频繁拉取。

5. 非必要避免使用 Always 拉取策略

这里说的是 Kubernetes 下的 imagePullPolicy: Always,如果不是必要,可以改为 IfNotPresent 甚至是 Never,避免频繁拉取镜像。这也是正确的镜像使用姿势。

6. 使用集群本地缓存

使用集群本地镜像缓存是个折中的方案,既不脱离 Docker Hub,又可以避免频繁拉取镜像被限流。复用已拉取的镜像,可以避免频繁拉取镜像,还可以提高镜像的拉取速度。当 Docker Hub 不可用时(比如网络、服务中断等原因),本地缓存可以提供镜像,保证服务的连续性。

这也是我们今天要介绍的方案,使用 Spegel 无状态镜像缓存绕过 Docker Hub 的新速率限制。

Spegel 无状态镜像缓存

什么是 Spegel

Spegel,瑞典语中的“镜子”,是一个专为 Kubernetes 集群设计的无状态本地 OCI 注册表镜像工具,旨在优化镜像拉取效率并减少对外部镜像仓库的依赖。使用 P2p 协议,支持集群内镜像共享机制,可以在集群内节点之间共享镜像,提高镜像拉取速度。适合离线或内网环境、带宽优化和成本控制、容灾和高可用等场景。

Spegel 的核心功能:

  1. 集群内镜像共享机制: Spegel 允许 Kubernetes 集群中的每个节点充当本地镜像仓库,当某个节点首次拉取镜像后,其他节点可直接从该节点获取镜像,无需重复访问外部仓库 。这种点对点(P2P)共享机制显著减少了跨网络拉取的开销,提升了工作负载的启动速度。
  2. 无状态设计与高兼容性:作为无状态服务,Spegel 不依赖持久化存储,仅通过节点间通信实现镜像分发。它兼容主流的容器运行时(如 Containerd)。
  3. 灵活镜像源配置:支持配置公共或私有镜像仓库的镜像共享。

Spegel 的工作原理

这里借用官方的一张图来说明 Spegel 的工作原理。

Speqel 有三个组件:注册表(Registry)、路由和发现组件以及广告机制。

  1. Spegel 不依赖中心化的镜像存储,而是利用集群中每个节点的本地存储作为临时缓存。当某个节点首次从外部仓库(如 Docker Hub)拉取镜像时,该镜像会缓存在本地,记录在注册表中。
  2. 节点通过分布式哈希表(DHT,Spegel 使用 Kademlia 分布式哈希表的 Go 实现 广播注册表,并记录哪些节点缓存了特定镜像。当新节点请求镜像时,可以快速定位到可用节点进行拉取 。优先从集群内的节点拉取,如果没有则从外部仓库拉取。
  3. Spegel 与容器运行时(目前仅支持 containerd)集成。通过配置 Containerd 注册表镜像 将 Spegel 注册为镜像仓库的“镜像端点”(mirror endpoint),从而拦截默认的镜像拉取请求。

Spegel 的兼容性

Spegel 已在以下 Kubernetes 发行版上进行了兼容性测试。绿色状态表示 Spegel 可立即使用,黄色表示需要额外配置,红色表示 Spegel 无法使用。

Status Distribution
🟢 AKS
🟢 Minikube
🟡 EKS
🟡 K3S and RKE2
🟡 Kind
🟡 Talos
🟡 VKE
🔴 GKE
🔴 DigitalOcean

K3s 与 Spegel

K3s 的内置注册表镜像 (Embedded Registry Mirror)是从 2024 年 1 月发布的实验性功能开始提供的:v1.26.13+k3s1、v1.27.10+k3s1、v1.28.6+k3s1、v1.29.1+k3s1,从 2024 年 12 月发布的版本开始提供 GA 版本:v1.29.12+k3s1、v1.30.8+k3s1、v1.31.4+k3s1。

K3s 内嵌了 Spegel 的功能,可以直接在 K3s 集群中使用 Spegel,无需额外安装部署。

启动 K3s server 时通过参数 --embedded-registry 启动,或者在配置文件中设置 embedded-registry: true。一旦开启,在所有的节点上都会开启两个端口 644350016443 作为本地 OCI 库的端口,5001 做节点间点对点广播可用镜像的端口。

可以通过环境变量 K3S_P2P_PORT 设置 5001 以外的端口。所有节点上的端口设置必须一致,不支持也不推荐在设置后再修改。

接下来,我们演示下在 K3S 集群上部署 Spegel,实现集群内镜像共享。

演示

演示需要至少两台虚拟机:1 台 server 节点(master),1 台 agent 节点(member1)。

1. 配置 Containerd 注册表镜像

在 K3s 上配置 Containerd 注册表镜像,通过 /etc/rancher/k3s/registries.yaml 文件配置。需要在所有的节点上配置。

在两个节点上都执行以下命令,所有节点都会优先从本地和其他节点拉取 docker.ioregistry.k8s.io 的镜像:

sudo mkdir -p /etc/rancher/k3s/
sudo tee /etc/rancher/k3s/registries.yaml <<EOF
mirrors:   
  docker.io:  
  registry.k8s.io:
EOF

2. 创建集群

这里推荐我之前写的 1 分钟快速搭建指南,最新的脚本已经更新到了 k3s-cluster-automation。将脚本下载到本地,执行以下命令:

export HOSTS="13.75.122.224 52.175.36.37"

setupk3s \
    --k3s-version v1.29.12+k3s1 \
    --embedded-registry \
    --mini

你可能会在 K3s 节点上看到如下日志,不过不用担心,这是因为 Kademlia DHT 需要至少 20 个节点才能形成稳定拓扑。

Mar 01 12:04:54 master k3s[21794]: 2025-03-01T12:04:54.302Z WARN dht go-libp2p-kad-dht@v0.25.2/lookup.go:43 network size estimator track peers: expected bucket size number of peers

3. 准备测试镜像

在部署应用时,我们需要一个 Docker Hub 不存在的镜像,提前将其保存到 server 节点上。这里使用 nginx 镜像,将其 tag 成其他的名字:

sudo crictl pull docker.io/library/nginx:1.27.4
sudo ctr -n k8s.io images tag docker.io/library/nginx:1.27.4 docker.io/library/nginx-not-exist:1.27.4

检查镜像是否成功 tag:

# server 节点: master
sudo crictl img list | grep nginx-not-exist
docker.io/library/nginx-not-exist            1.27.4              b52e0b094bc0e       72.2MB

这个镜像只存在于 server 节点,在 agent 节点上并不存在

4. 部署应用

使用该镜像部署一个简单的 Deployment:

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx-not-exist:1.27.4
        ports:
        - containerPort: 80 
EOF

查看 Pod 的状态,发现其被调度到 agent 节点 member1 上,并成功运行:

kubectl get po -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP          NODE      NOMINATED NODE   READINESS GATES
nginx-74ddfd8c7d-8cn4v   1/1     Running   0          11s   10.42.1.2   member1   <none>           <none>

通过 kubectl describe 查看 Pod 的详细信息,可以看到因为是从 server 节点拉取的镜像,拉取非常快:

Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  66s   default-scheduler  Successfully assigned default/nginx-74ddfd8c7d-8cn4v to member1
  Normal  Pulling    66s   kubelet            Pulling image "nginx-not-exist:1.27.4"
  Normal  Pulled     63s   kubelet            Successfully pulled image "nginx-not-exist:1.27.4" in 3.323s (3.323s including waiting)
  Normal  Created    63s   kubelet            Created container nginx
  Normal  Started    63s   kubelet            Started container nginx

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus