☰
Kubernetes镜像拉取提速:从90秒到8秒的containerd与Harbor优化实践
2026/10/23 1:23:19 网站建设 项目流程

1. 从 90 秒到 8 秒:一次被镜像拉取拖垮的部署体验

如果你也在维护 Kubernetes 集群,大概率遇到过这种场景:代码明明只改了一行,kubectl apply之后却要盯着ContainerCreating状态干等一分多钟。我最近接手的一个模拟项目就踩了这个坑——Pod 从创建到 Running 稳定在 90 秒左右,其中绝大部分时间耗在镜像拉取上。把 containerd 的镜像仓库从默认的公共源切换成本地自建的 Harbor 之后,同样的部署流程直接压到 8 秒上下,提速超过十倍。

这篇文章不聊虚的,我会把整个排查链路、containerd 的配置改动、Harbor 的落地细节,以及切换过程中踩到的几个坑完整讲一遍。适合正在被镜像拉取速度困扰的 K8s 运维、后端开发,以及刚接触容器运行时配置的同学。哪怕你用的是 containerd 之外的运行时,思路也是通用的——核心矛盾永远是"镜像从哪里来、走多远的路、以多快的速度到节点"。

先说结论:镜像拉取慢,八成不是网络带宽不够,而是"路径太长"。公共镜像仓库的节点可能跨地域、跨运营商,每一层 blob 都要经过公网多次往返,叠加 TLS 握手、鉴权、并发限流,单次拉取轻松几十秒。本地 Harbor 把这段路径缩短到内网一跳,速度自然就上来了。下面我把这件事拆开讲透。

2. 为什么 90 秒的等待不是网络慢,而是路径长

2.1 镜像拉取到底在拉什么

很多人以为"拉镜像"就是下载一个文件,其实远不止。一个容器镜像由manifest(清单)、config(配置)和若干layer(层)组成。manifest 描述了镜像有哪些层、每层的 digest 是什么;config 记录了环境变量、启动命令等元数据;layer 才是真正的文件系统内容,而且是分层复用的。

拉取一个镜像的完整流程大致是:先请求 manifest,解析出 layer 列表,然后逐个(或并发)拉取 layer,每拉一层都要校验 digest,最后组装成可运行的 rootfs。这里面每一步都是一次网络往返。如果镜像有 8 层,光 layer 的请求就是 8 次起步,再加上 manifest 和 config,十几次往返跑不掉。

提示:镜像层数越多,拉取时的往返次数越多。这也是为什么"合并 RUN 指令减少层数"是 Dockerfile 优化的经典建议。

2.2 公共仓库的隐性成本

公共镜像仓库为了服务全球用户,通常做了 CDN 分发和限流保护。对单个节点来说,这意味着几件事:DNS 解析可能返回一个物理距离很远的节点;TLS 握手要跨公网完成;鉴权 token 需要额外请求;并发拉取层数被限制。这些成本单看都不大,叠加起来就很可观。

我实测过同一个镜像从公共源拉取,冷启动(本地无缓存)耗时 70 到 90 秒不等,波动很大。波动本身就说明问题——不是稳定的带宽瓶颈,而是路径上某个环节在抖动。而内网 Harbor 拉同一个镜像,稳定在 5 到 8 秒,几乎没有波动。

2.3 90 秒的时间都花在哪了

为了搞清楚时间去向,我在节点上用crictl手动拉镜像并观察耗时,同时看了 containerd 的日志。大致分布是这样的:

阶段公共源耗时(约)本地 Harbor 耗时(约)
DNS 解析 + TLS 握手3-8 秒0.1 秒以内
鉴权 token 获取2-5 秒可省略(内网信任)
manifest + config 拉取2-4 秒0.2 秒
layer 下载(主体)60-75 秒5-7 秒
解压与组装5-8 秒1-2 秒

可以看到,layer 下载是大头,但握手和鉴权这些"固定开销"在公网环境下被放大了好几倍。本地 Harbor 把这些固定开销几乎清零,layer 又走内网千兆甚至万兆带宽,整体就压下来了。

2.4 一个容易被忽略的点:并发与限流

containerd 默认会并发拉取多个 layer,但公共仓库往往对单 IP 的并发连接数有限制。一旦触发限流,containerd 会退避重试,这个退避是带指数增长的,几轮下来就是几十秒。日志里会看到类似retrying或too many requests的字样。这也是为什么有时候"换个时间段拉就快了"——不是网络好了,是限流松了。

3. 把 containerd 指向本地 Harbor 的完整配置

3.1 先确认你的运行时是 containerd 还是 dockershim

这一步很关键,配置方式完全不同。K8s 1.24 之后默认移除了 dockershim,绝大多数集群现在跑的都是 containerd。确认方法:

# 在节点上执行,看容器运行时 crictl info | grep -i runtimeType # 或者 systemctl status containerd

如果输出里有io.containerd.runtime.v1.linux或io.containerd.runc.v2,那就是 containerd。如果还在用 Docker 作为运行时(老集群),配置要改/etc/docker/daemon.json,本文以 containerd 为主。

3.2 containerd 的镜像仓库配置结构

containerd 从 1.4 开始推荐用config_path方式管理仓库配置,而不是把所有东西塞进主配置。主配置/etc/containerd/config.toml里通常有这么一段:

[plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d"

这个config_path指向一个目录,目录下按"仓库地址"建子目录,每个子目录里放一个hosts.toml。这种设计的好处是:每个仓库可以独立配置镜像加速、TLS、鉴权,互不干扰。

3.3 为 Harbor 写一份 hosts.toml

假设你的 Harbor 地址是harbor.internal.example(实际请替换成你自己的内网域名或 IP),在/etc/containerd/certs.d/下建目录:

mkdir -p /etc/containerd/certs.d/harbor.internal.example

然后写hosts.toml:

server = "https://harbor.internal.example" [host."https://harbor.internal.example"] capabilities = ["pull", "resolve"] skip_verify = false # 如果 Harbor 用的是自签证书,把 CA 放这里 ca = "/etc/containerd/certs.d/harbor.internal.example/ca.crt"

这里有几个点要解释清楚。server是默认回退地址;capabilities声明这个 host 支持哪些操作,pull和resolve是最常用的;skip_verify = false表示要校验 TLS 证书,生产环境强烈建议保持 false,用ca字段指定自签 CA,而不是图省事跳过校验。

注意:skip_verify = true虽然能快速跑通,但等于放弃了中间人攻击防护,内网也不建议长期这么干。

3.4 让配置生效并验证

改完配置要重启 containerd:

systemctl restart containerd systemctl status containerd

然后手动拉一个镜像验证:

crictl pull harbor.internal.example/library/nginx:1.25

如果能看到镜像 ID 输出且耗时很短,说明配置生效了。再用crictl images确认镜像已经在本地。

3.5 关于镜像地址重写的一个实用技巧

实际项目里,镜像地址往往写死在 Deployment 的image字段里,比如nginx:1.25。你不可能把所有 YAML 都改成 Harbor 地址。这时候可以用 containerd 的mirror 机制:把公共仓库地址映射到 Harbor。

在hosts.toml里可以这样写,让docker.io的请求走 Harbor:

server = "https://harbor.internal.example" [host."https://harbor.internal.example"] capabilities = ["pull", "resolve"]

配合 Harbor 的代理缓存(proxy cache)功能,Harbor 会自动去上游拉取并缓存,节点侧完全无感。这是我最推荐的方案——既不用改业务 YAML,又能享受内网速度。

4. Harbor 侧要做的几件事,少一件都跑不顺

4.1 项目与代理缓存的创建

Harbor 里要先建一个项目,比如叫library或proxy。如果要走代理缓存,创建项目时勾选"代理缓存",填上游仓库地址(如https://registry-1.docker.io)。这样当节点请求一个 Harbor 里不存在的镜像时,Harbor 会自己去上游拉,拉完缓存下来,下次直接命中。

代理缓存的价值在于"第一次慢、后面快"。对于团队里频繁使用的公共基础镜像,第一次由某个人触发拉取后,后续所有人都走内网缓存,整体收益非常大。

4.2 证书这块必须提前想清楚

Harbor 默认用自签证书,节点侧要么导入 CA,要么就得跳过校验。我的做法是:把 Harbor 的 CA 证书分发到每个节点的/etc/containerd/certs.d/<harbor>/ca.crt,然后在hosts.toml里引用。这样既安全又不用改系统信任库。

如果 Harbor 前面挂了负载均衡或反向代理,证书的 CN/SAN 要包含你实际访问用的域名,否则校验会失败。这个坑我踩过——证书里只有 IP,访问时用域名,直接报x509: certificate is valid for ...。

4.3 存储后端别用默认的本地盘

Harbor 默认把镜像存在本地文件系统。小规模够用,但一旦镜像多了、并发拉取上来了,本地盘 IO 会成为瓶颈。生产环境建议把存储后端换成对象存储或独立的高性能盘。我见过一个案例:Harbor 和一堆其他服务挤在同一块盘上,拉取时磁盘 IO 打满,速度反而比公网还慢。

4.4 资源配额与 GC

Harbor 的垃圾回收(GC)要定期跑,否则删掉的镜像层不会真正释放空间。但 GC 期间 Harbor 会进入只读模式,正在拉取的请求可能失败。所以 GC 要安排在业务低峰期,并且提前通知。另外,给 Harbor 的容器分配足够的 CPU 和内存,别让它和别的服务抢资源。

5. 切换过程中我踩过的四个坑

5.1 坑一:改了配置没重启,白等半小时

containerd 的config_path配置是启动时读取的,改完hosts.toml必须重启 containerd 才生效。我第一次改完直接去拉镜像,发现还是走公网,排查了半天才发现没重启。后来养成习惯:改完配置先systemctl restart containerd,再验证。

5.2 坑二:镜像地址大小写和路径不匹配

Harbor 的项目名和仓库路径是大小写敏感的。library/nginx和Library/nginx是两个不同的路径。有一次同事把项目名写成了大写,节点拉取一直 404,日志里报manifest unknown,查了好久才发现是大小写问题。

5.3 坑三:代理缓存首次拉取超时

代理缓存第一次拉取时,Harbor 要去上游下载,如果上游慢,Harbor 侧会等待,节点侧看到的就是"卡住"。这时候要调大 Harbor 的超时配置,或者提前用docker pull把常用镜像预热到 Harbor 里。预热是个好习惯,尤其是大版本发布前。

5.4 坑四:节点时间不同步导致 TLS 失败

TLS 握手对时间敏感,节点时间偏差太大(超过几分钟)会导致证书校验失败。内网环境如果没配 NTP,很容易出现这个问题。排查时看 containerd 日志里的certificate相关报错,然后timedatectl检查时间同步状态。

6. 提速之外,本地仓库带来的额外收益

6.1 部署稳定性肉眼可见地提升

公网拉取受运营商、时段、限流影响,波动大。内网 Harbor 的延迟和带宽都可预期,Pod 启动时间从"看运气"变成"稳定 8 秒"。这对 CI/CD 流水线尤其重要——流水线最怕的就是不可预期的等待。

6.2 带宽成本和安全边界

所有节点不再直接访问公网仓库,出口带宽压力骤降。同时,镜像统一从 Harbor 分发,相当于多了一道管控点:可以扫描镜像漏洞、限制哪些镜像能被拉取、审计拉取记录。安全团队通常会很喜欢这个变化。

6.3 离线环境的可行性

内网 Harbor 让整个集群可以在没有公网出口的环境里运行。对于隔离网络场景,这是刚需。只要把需要的镜像提前同步进 Harbor,节点侧完全不需要外网。

7. 几个让速度再快一点的调优细节

7.1 开启镜像层去重与预热

Harbor 支持镜像层去重,多个镜像共享的层只存一份。配合定期预热常用镜像,能进一步减少首次拉取时间。预热可以用脚本批量docker pull再docker push到 Harbor,或者用 Harbor 的复制功能从上游同步。

7.2 调整 containerd 的并发参数

containerd 的max_concurrent_downloads控制并发拉取的层数,默认值偏保守。内网带宽充足时可以适当调大,但别调太大,否则 Harbor 侧压力大。我一般设成 5 到 8 之间,实测比较均衡。

7.3 节点侧保留镜像缓存

确保节点的imagePullPolicy设置合理。对于固定 tag 的镜像,用IfNotPresent可以避免每次启动都去仓库检查。但要注意:如果 tag 会变(比如latest),IfNotPresent可能导致拉到旧镜像,这种场景还是用Always配合明确的 digest 更稳妥。

7.4 用 digest 而不是 tag 锁定版本

生产环境建议用镜像 digest 而不是 tag 来引用镜像。digest 是内容哈希,唯一且不可变,能避免"同一个 tag 在不同时间拉到不同内容"的问题,也让缓存命中更可靠。

8. 写在最后的一点个人体会

这套方案落地之后,我最大的感受是:很多性能问题不是"加资源"能解决的,而是"改路径"能解决的。90 秒到 8 秒,带宽没变、节点没换,只是把镜像来源从公网挪到了内网。方向对了,收益是数量级的。

如果你现在还在被镜像拉取慢困扰,建议先别急着扩容带宽,花半小时用crictl手动拉一次镜像,看看时间到底花在哪。大概率你会发现,瓶颈不在带宽,而在路径。把 Harbor 搭起来、containerd 配好,剩下的就是享受 8 秒部署的顺畅了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询