如果你的团队正在为 docker pull 慢、镜像仓库不稳定或者磁盘莫名被塞满头疼,可以先看看这几家的配置和技术说明:onidel(全球云服务器,NVMe SSD 存储,适合自建镜像仓库的落地盘)、vollcloud(香港 CMIN2 大带宽线路,拉镜像和回源都更利索)
我们组的镜像仓库是去年用 Harbor 搭的,起初一切正常,直到某次发版前十分钟,一个 800MB 的镜像拉了 15 分钟还没拉完。运维同事第一反应是「网络抽风」,重启了 daemon、清了 DNS 缓存,全都没用。后来我花了一整个下午,从 DNS 解析一路查到 registry mirror 的回源链路,再翻到 Harbor 自己的 GC 任务日志,才发现四个问题叠在一起,每一个单独看都不致命,凑一起就成了灾难。
这篇就把这四个坑和对应的排查命令完整记下来,方便下次直接照着查。

坑一:mirror 配了,但 Docker 根本没走这条路
registry mirror 的原理不复杂:Docker 先问镜像加速地址要不要这个镜像,加速地址回一个 302,把你重定向到真正的源站;加速地址说没有,才回落到官方源。问题出在两处。
第一处是配置写在了 daemon.json 里,但那个文件里有 JSON 语法错误。Docker 对这个文件的容错是「静默丢弃整个文件」,不报错、不打日志,于是你以为 mirror 生效了,其实 docker info 里 registry mirrors 一栏是空的。验证方式很简单:
docker info | grep -A3 'Registry Mirrors'
如果输出里没有你的加速地址,说明配置根本没加载进去。用 journalctl -u docker 看启动日志,JSON 解析失败的报错只在这一行里,错过就再也找不到了。
第二处更隐蔽:加速地址返回 302 时,重定向的目标域名解析不了。国内机器解析 registry-1.docker.io 经常拿到被污染的 IP,表现就是「连上了但握手卡住」,curl 一直挂着直到超时。这时候加速地址应该自己完成对源站的拉取,然后把层数据以 blob 的形式直接喂给客户端,而不是甩一个 302 让客户端自己回去连源站。

坑二:DNS 解析本身就在拖后腿
镜像拉取是一个典型的「小请求、大流量」场景:manifest 那个请求只有几 KB,但里面的每个 layer 动辄几百 MB。整套链路是长连接 + 分块传输,DNS 解析被放大成了体感上的主要延迟来源。
我们的现象是同一个镜像,在配置了内网 DNS 的机器上要 90 秒,在用公共 DNS 的开发机上只要 22 秒。查下来是内网 DNS 对镜像仓库域名做了递归转发,转发目标在境外,丢包重传把首字节时间拉到了 2 秒多,一个 layer 几十个 chunk,累积起来就是好几倍差距。
处理方式不复杂:在 Harbor 前面挂一层自己的域名解析,或者直接把 Harbor 的访问域名写进 /etc/hosts;如果一定要走 DNS,就在解析链路上把 TTL 压到 60 秒以内,并给仓库域名配多组 IP 做轮询。改完记得 systemctl restart docker,daemon 的 DNS 结果是有缓存的,不重启不生效。
坑三:磁盘被 GC 塞满,push 变 push 不进去
Harbor 默认会定期跑 garbage collection,把已经没有被任何 tag 引用的 blob 清掉。这个动作本身是对的,但默认的 --dry-run=false 加上「删除时只标记不释放」的默认行为,会让磁盘占用在 GC 跑完之后依然停在高位因为真正的空间释放要等到 registry 重新 compact 存储驱动。
我们那次磁盘只剩 3GB,push 一个 1.2GB 的镜像直接失败,日志里是 no space left on device。查 harbor 的日志才看到,GC 上周就报过一轮存储卷满了但被 skip 掉了。把 GC 调成 --dry-run=false --delete-untagged=true,手动触发一次,再进 Harbor 的维护页跑一遍 compact,磁盘占用从 88% 降到 31%。
如果你的仓库要放 CI 推上来的多架构镜像(一个 tag 对应 amd64、arm64 两套层),空间消耗要按两倍算,别按单架构估。

坑四:push 上去的镜像,pull 出来居然不一样
这个坑不慢,但比慢更让人难受。典型场景是 CI 重新构建了一个同 tag 的镜像,push 上去之后,不同机器 pull 到的层内容对不上因为 Harbor 对同一个 tag 默认允许覆盖,而有些机器的本地缓存里还留着旧版本的 layer digest。
解决办法有两个层面。仓库侧开启 tag 不可变(immutable tag),让同 tag 的第二次 push 直接被拒,避免出现「谁最后推谁赢」的混乱;客户端侧则要清楚 docker pull 的层缓存策略,它按 digest 而不是 tag 判断复用,tag 变了但 digest 没变就会继续用旧的层,这时候要 docker system prune 加 docker builder prune 清一遍。
顺带说一句,我在这件事上踩过一次更蠢的坑:有人在 Harbor 上开了「信任所有证书」,因为他们的 Harbor 用了自签证书。结果是内网里的一台机器被中间人塞了一个伪镜像,谁也没发现。现在大家都按指纹校验来配。
如果不想自己踩这一遍
自建 Harbor 不是所有人的最优解。如果你的团队规模不大、镜像量有限,用带大带宽和就近线路的云服务器拉官方源,再配一个轻量的 registry 做缓存层,往往比自己维护一整套 Harbor 更省心。关键是两点:一是拉源那台机器的带宽和线路要够快,镜像传输是纯流量生意;二是缓存节点的存储别吝啬,NVMe 这类盘在大量小 layer 的随机读写上比机械盘快得不是一点半点。
当然,无论用哪种方案,前面四个排查思路是通用的:先确认 mirror 真的加载了,再看 DNS 解析耗时,然后查存储和 GC 状态,最后核对 digest 一致性。绝大多数「拉镜像慢」的问题,答案都藏在这四步里的前两步。

最后补一句题外话:上面所有命令我都建议先在测试环境跑一遍再上生产,尤其是 GC 和 compact 这类操作,它们是破坏性的,而 Harbor 的界面默认不会给你二次确认弹窗。


喜欢
顶
难过
囧
围观
无聊



