
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 的核心功能:
- 集群内镜像共享机制: Spegel 允许 Kubernetes 集群中的每个节点充当本地镜像仓库,当某个节点首次拉取镜像后,其他节点可直接从该节点获取镜像,无需重复访问外部仓库 。这种点对点(P2P)共享机制显著减少了跨网络拉取的开销,提升了工作负载的启动速度。
- 无状态设计与高兼容性:作为无状态服务,Spegel 不依赖持久化存储,仅通过节点间通信实现镜像分发。它兼容主流的容器运行时(如 Containerd)。
- 灵活镜像源配置:支持配置公共或私有镜像仓库的镜像共享。
Spegel 的工作原理
这里借用官方的一张图来说明 Spegel 的工作原理。
Speqel 有三个组件:注册表(Registry)、路由和发现组件以及广告机制。
- Spegel 不依赖中心化的镜像存储,而是利用集群中每个节点的本地存储作为临时缓存。当某个节点首次从外部仓库(如 Docker Hub)拉取镜像时,该镜像会缓存在本地,记录在注册表中。
- 节点通过分布式哈希表(DHT,Spegel 使用 Kademlia 分布式哈希表的 Go 实现 广播注册表,并记录哪些节点缓存了特定镜像。当新节点请求镜像时,可以快速定位到可用节点进行拉取 。优先从集群内的节点拉取,如果没有则从外部仓库拉取。
- 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
。一旦开启,在所有的节点上都会开启两个端口 6443
和 5001
。6443
作为本地 OCI 库的端口,5001
做节点间点对点广播可用镜像的端口。
可以通过环境变量
K3S_P2P_PORT
设置5001
以外的端口。所有节点上的端口设置必须一致,不支持也不推荐在设置后再修改。
接下来,我们演示下在 K3S 集群上部署 Spegel,实现集群内镜像共享。
演示
演示需要至少两台虚拟机:1 台 server 节点(master),1 台 agent 节点(member1)。
1. 配置 Containerd 注册表镜像
在 K3s 上配置 Containerd 注册表镜像,通过 /etc/rancher/k3s/registries.yaml
文件配置。需要在所有的节点上配置。
在两个节点上都执行以下命令,所有节点都会优先从本地和其他节点拉取 docker.io
和 registry.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