问题不在速度,而在限额
CI 每跑一次,FROM python:3.12-slim 就从 Docker Hub 下来一次。十个节点就是十次,一天跑一百次流水线就是一百次。Docker Hub 在数着。
Docker 文档给出的限额是这样的。未登录的客户端 每个 IPv4 地址(或 IPv6 /64 网段)每 6 小时 100 次,用个人账号登录是 200 次,Pro、Team、Business 不限。像公司办公室或集群那样多台机器从 NAT 后面用同一个地址出去时,这 100 次是大家一起分的。
我没有只凭文档相信这些数字,而是读了响应头。Docker 为查看限额专门放了一个 ratelimitpreview/test 仓库,用匿名令牌请求它的 manifest,响应头里就带着剩余次数。
TOK=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s -I -H "Authorization: Bearer $TOK" \
https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimit
ratelimit-limit: 100;w=3600
ratelimit-remaining: 85;w=3600
做了几次实验期间,remaining 从 91 → 88 → 85 递减。响应头里的窗口(w=3600)以秒计写着 3,600,而文档说是 6 小时。无论哪个对,有一点是确定的:这次查看请求本身也算一次。
代理缓存就是守住这个数字的装置。集群里所有节点都去问我的缓存而不是 Docker Hub,同一镜像就只从上游下来 一次。
最小的实验:把 registry:2 当作 pull-through 缓存
Docker 发布的 registry:2 镜像只需一个环境变量就变成只读代理。
docker run -d --name regmirror -p 5001:5000 \
-v $PWD/data:/var/lib/registry \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
registry:2
这时拉取 localhost:5001/library/nginx:1.27-alpine,缓存会把没有的层从 Docker Hub 取来放在磁盘上,再交给客户端。先确认本地守护进程没有任何 nginx 的层(docker images | grep nginx 为 0 行),然后把同一镜像拉三次,每次都删掉本地副本。
| 尝试 | 耗时 | 缓存目录 | 备注 |
|---|---|---|---|
| 第 1 次(经缓存) | 7.81 秒 | 22M(15 个 blob 文件) | 缓存为空,从上游填充 |
| 第 2 次(经缓存) | 3.95 秒 | 22M(无变化) | 直接来自磁盘 |
| 第 3 次(经缓存) | 4.01 秒 | 22M(无变化) | 直接来自磁盘 |
| Docker Hub 直连 | 2.86 秒 | — | 对照 |
docker images 报告的镜像大小是 76.8MB,缓存却是 22M。缓存保存的是压缩后的层 blob,守护进程报告的是解压后的大小。
实验说明了什么
第一次拉取会变慢。 从上游取来、写到磁盘、再转交是两步,所以比直接拉取(2.86 秒)多花了将近三倍。
第二次起也没有比直连更快。 家用线路直连 Docker Hub 是 2.86 秒,缓存填满后经代理约 4 秒。代理容器跑在同一台笔记本上,相当于多了一跳。线路快、客户端只有一台时,代理缓存 不是为速度而设的装置。
得到的是上游请求次数。 第 2、3 次拉取时缓存目录一个字节都没增加。说明层没有再从上游下载。二十个节点的集群,Docker Hub 数到的次数就从二十接近于一。这就是代理缓存的价值。
有一点要老实写下。registry:2 不会把发往上游的请求记进 info 日志,所以我没能数出每次拉取做了几次 manifest 检查。从"磁盘没有增长"这一事实,只能说没有重新下载层。
Nexus:代理、托管,以及组
registry:2 的代理只看一个上游。公司里通常既要看 Docker Hub,又要推送自己做的镜像,还想两处用一个地址。Sonatype Nexus Repository 的 Docker 仓库有三种。
- proxy — 缓存远端仓库。Docker Hub 的话,Remote storage 填
https://registry-1.docker.io,Docker Index 选 "Use Docker Hub"(搜索由index.docker.io负责,所以两者分开填写)。 - hosted — 我推送的地方。
- group — 把多个 proxy 和 hosted 合成 一个 URL。文档把组称为"向用户以只读方式暴露全部仓库的推荐方式",成员按"期望的顺序"加入。往组里加入新仓库后,不改客户端配置也能立刻看到。
客户端侧的配置,只需把一个 Repository Connector 端口(例如 nexus.example.com:8082)注册为守护进程的 mirror。此后 docker pull nginx 按组 → 成员的顺序查找,hosted 里没有就由 proxy 从 Docker Hub 取来并缓存。
Nexus 把 ECR 作为上游 时,认证比较特别。ECR 的令牌有效期是 12 小时,按 Nexus 文档,它在第一次拉取时缓存该令牌,并每六小时刷新一次。把 AWS 凭据交给 Nexus 一次,其余由 Nexus 自己转。
ECR pull-through cache:在 AWS 内部缓存
集群在 AWS 上的话,不必另外运维缓存,ECR 来做这件事。在规则(pull through cache rule)里写上上游和前缀,拉取 <账号>.dkr.ecr.<区域>.amazonaws.com/<前缀>/library/nginx:1.27-alpine 时,ECR 会从上游取来并保存在我账号下的 ECR 仓库里。
文档里确认的规则如下。
- 无需认证的上游是 ECR Public、Kubernetes 容器镜像仓库、Quay 三个。Docker Hub、Azure Container Registry、GitHub 和 GitLab Container Registry、Chainguard 必须有 Secrets Manager 密钥,其他账号的 ECR 用 IAM 角色认证。
- 该密钥的名字必须以
ecr-pullthroughcache/开头,并且与规则处于同一账号和区域。这样 Docker Hub 是以账号拉取的,用的是该账号的限额而不是匿名的 100 次。 - 再次拉取同一标签时,若 过去 24 小时内已与上游核对过,就不再询问上游,直接返回缓存。窗口过期则检查新版本并更新。若有覆盖标签来部署的习惯,可能在长达一天内拿到旧镜像。
- 即使上游更新失败,最后的缓存副本仍会下发。也就是说 Docker Hub 挂了,用已拉取过的镜像仍能部署。
- 给缓存仓库开启标签不可变(immutability)后,同一标签的更新会被阻止。
- 多架构镜像会把 manifest 列表及其中的全部架构都拉下来。只想要一个,就用该架构的 digest 拉取。
- 第一次拉取时 ECR 必须能访问上游,可能需要通往互联网的路由。只有 PrivateLink 端点的 VPC 第一次拉取可能失败,文档建议预先准备路由。之后的拉取不需要。
- AWS Lambda 不支持通过 pull-through cache 规则生成的镜像。
选哪一个
| registry:2 proxy | Nexus(proxy + group) | ECR pull-through cache | |
|---|---|---|---|
| 运维负担 | 一个容器 | 一台服务器加磁盘管理 | 无(托管) |
| 上游 | 一个 | 多个组成一组 | 每条规则一个 |
| 推送自己的镜像 | 不可(只读) | 到 hosted | 到普通 ECR 仓库 |
| 更新检查 | 每次拉取都核对上游 manifest | 可配置 | 每标签每 24 小时一次 |
| 适合 | 家庭实验室、几个节点 | 公司内部,连语言包也放一处 | AWS 内的 EKS、ECS |
在我们的流水线里
LabHub 的构建由 Jenkins 用 kaniko 做镜像推到私有 Harbor,再由 ArgoCD 部署该标签。基础镜像在 Dockerfile 里连 digest 一起固定。
FROM python:3.12-slim@sha256:78387bc3…
前面放了代理缓存,这一行也不变。digest 是内容的哈希,不论经过哪个缓存,字节必须一致才能通过。缓存守住限额,digest 守住内容。两个装置做的是不同的事。
一句话
代理缓存不是让拉取更快的装置,而是 把上游数到的次数减为一 的装置。节点只有几个,registry:2 就够;仓库有多个,用 Nexus 的组做出一个地址;在 AWS 里,就让 ECR 代为缓存。
현재 단락 (1/54)
CI 每跑一次,`FROM python:3.12-slim` 就从 Docker Hub 下来一次。十个节点就是十次,一天跑一百次流水线就是一百次。Docker Hub 在数着。