1. 问题现场还原:90秒的Pod启动到底卡在哪
先交代一下背景。我手头维护着一套中等规模的容器集群,大概三十来个节点,跑的是标准的上游Kubernetes发行版,容器运行时用的是containerd。日常发布频率不算高,一天十来次滚动更新,但每次发布都让人抓狂——一个普通的业务Pod,从kubectl apply到真正Ready,稳定在90秒上下。注意,是稳定,不是偶发。这意味着它不是网络抖动,而是一个系统性的瓶颈。
一开始我怀疑是调度慢、探针配置不合理、镜像太大。挨个排查:调度日志显示Pod秒级就绑定了节点;readinessProbe的initialDelaySeconds只有5秒;镜像本身压缩后也就两百多兆,不算离谱。那问题只能出在镜像拉取环节。于是我在节点上手动crictl pull了一下同一个镜像,掐表一算,光拉取就花了七十多秒。真相大白——Pod启动的90秒里,有八成时间耗在了从公网镜像仓库下载镜像上。
这个场景其实非常普遍。很多团队在集群规模小的时候,直接用公网镜像仓库,觉得"能拉下来就行"。可一旦节点数量上去、发布频率变高,公网拉取的问题就会集中爆发:带宽被多个节点同时抢占、跨境链路抖动、仓库限流、DNS解析慢,任何一个环节都能把几十秒的时间叠加到你的部署流程里。这篇文章我就把从"90秒"优化到"8秒"的完整过程拆开讲清楚,包括为什么选本地Harbor、containerd怎么改配置、证书怎么处理、以及我踩过的那些坑。适合正在被镜像拉取速度折磨的运维和平台工程师参考,也适合刚接触containerd配置的同学照着抄作业。
需要先说明一点:本文讨论的是在自有网络环境内搭建私有镜像仓库来加速拉取,所有操作都在合规的内网环境中完成,涉及的都是标准的企业级容器基础设施实践。
2. 方案选型:为什么是本地Harbor而不是别的
2.1 先搞清楚瓶颈的本质
在动手之前,得先想明白一件事:镜像拉取慢,慢的到底是哪一段。一次完整的镜像拉取,大致分这么几步:解析仓库域名、建立连接、鉴权、拉取manifest、按层下载blob、解压落盘。公网仓库慢,通常慢在"建立连接"和"按层下载"这两步,因为数据要跨越很长的物理链路,还要经过多个网络节点转发,每一跳都可能引入延迟和丢包。
这就引出一个关键判断:如果瓶颈在链路,那么优化方向就应该是"把镜像放到离节点尽可能近的地方"。离节点最近的地方是什么?就是集群自己所在的局域网。所以思路很清晰——在集群内部或者同机房部署一个私有镜像仓库,让节点从局域网拉镜像,而不是从公网拉。
2.2 为什么选Harbor
私有仓库的方案其实不少,最朴素的是用registry官方镜像起一个,但那个太简陋,没有Web界面、没有权限管理、没有镜像清理策略、没有复制功能。对于一个要长期维护的生产集群来说,这些能力迟早都要补上,与其后面再折腾,不如一步到位。
Harbor是我最终的选择,理由有这么几条。第一,它是CNCF的毕业项目,成熟度和社区活跃度都够,出问题好查资料。第二,它自带基于角色的访问控制、镜像漏洞扫描、垃圾回收、远程复制,这些在生产里都用得上。第三,它支持HTTPS和多种认证方式,和containerd对接很顺。第四,部署方式灵活,既可以用docker-compose快速起一个,也可以用Helm部署到K8s里。
提示:如果你的集群规模很小,比如就三五个节点,且只是想要个缓存,其实registry加一个反向缓存代理也能凑合。但只要你有权限隔离、审计、清理这些需求,Harbor的性价比明显更高。
2.3 一个容易被忽略的选型点:存储后端
Harbor自己不管存储,它把镜像层存到后端。默认用本地文件系统,也可以接对象存储。我这里选的是本地SSD盘,因为镜像仓库的读多写少,SSD能显著提升blob的读取速度。如果你节点多、并发拉取量大,本地盘可能成为瓶颈,那就得上对象存储或者做仓库的水平扩展。这一点在规划阶段就要想清楚,别等上线了才发现磁盘IO打满。
另外,Harbor的部署位置也有讲究。最理想的是部署在和K8s节点同一个局域网、同一个交换机下的机器上,这样拉取走的是内网高速链路。如果条件允许,给仓库机器配上千兆甚至万兆网卡,效果立竿见影。
3. Harbor部署实操:从零到能用的完整步骤
3.1 环境准备与依赖检查
我用的是一台独立的Linux机器做仓库服务器,配置是4核8G、500G SSD。系统是最常见的那个企业级Linux发行版。部署前先确认几件事:机器能通外网(用于拉取Harbor的安装包和基础镜像)、防火墙放行了需要的端口、时间同步正常。
Harbor默认需要这些端口:443(HTTPS)、80(HTTP,可选)、以及内部组件之间通信的一堆端口。生产环境我建议只开443,把HTTP关掉。时间同步很重要,因为后面证书和token都和时钟有关,时间偏差大了会导致鉴权失败。
# 检查时间同步状态 timedatectl status # 确认磁盘空间 df -h /data # 确认内存 free -h3.2 下载与配置Harbor
从官方发布页下载离线安装包,解压后会得到一个harbor目录。核心配置文件是harbor.yml,这个文件决定了Harbor怎么跑。我重点改了几个地方。
第一是hostname,填仓库服务器的域名或者IP。这里有个坑:如果你后面要用HTTPS并且用域名访问,hostname必须和证书里的域名一致,否则containerd校验会失败。第二是harbor_admin_password,改掉默认密码。第三是data_volume,指向大容量磁盘。第四是配置HTTPS证书。
# harbor.yml 关键片段 hostname: registry.internal.example https: port: 443 certificate: /data/certs/registry.crt private_key: /data/certs/registry.key harbor_admin_password: 换成你自己的强密码 data_volume: /data/harbor证书这块,如果你们内网有自己的证书签发体系,直接申请一张就行。如果没有,可以自签一张,但自签证书需要把CA分发给所有节点,否则containerd不认。自签的命令大致是这样:
# 生成私钥 openssl genrsa -out registry.key 4096 # 生成证书签名请求 openssl req -new -key registry.key -out registry.csr \ -subj "/CN=registry.internal.example" # 自签证书,有效期十年 openssl x509 -req -days 3650 -in registry.csr \ -signkey registry.key -out registry.crt注意:自签证书的CN必须和hostname完全一致,包括大小写。我见过有人CN写的是IP,访问用的是域名,结果containerd报x509错误,排查半天。
3.3 启动与验证
配置改完,执行安装脚本,它会拉起一堆容器。等几分钟,用浏览器或者curl访问https://registry.internal.example,能看到登录页就说明起来了。然后用admin账号登录,建一个项目,比如叫prod,把权限设好。
# 执行安装 ./install.sh # 验证服务状态 docker compose ps # 用curl测试 curl -k https://registry.internal.example/api/v2.0/health验证通过后,先手动推一个测试镜像上去,确认推拉都正常,再进入下一步改containerd。
4. containerd对接Harbor:配置细节与踩坑记录
4.1 containerd的镜像仓库配置机制
containerd和docker不一样,docker的配置在daemon.json里,而containerd的仓库配置在/etc/containerd/config.toml里,而且是通过registry这个插件来管理的。从containerd 1.4开始,配置结构有过一次比较大的调整,网上很多老教程已经对不上了,这点要特别注意。
核心逻辑是这样的:containerd会根据镜像地址的host部分去匹配配置。比如你的镜像地址是registry.internal.example/prod/myapp:1.0,containerd就会去找host为registry.internal.example的配置。如果没找到,就用默认配置去公网拉。所以我们要做的,就是给这个host单独配一段,告诉它走HTTPS、用哪个CA、要不要跳过校验。
4.2 修改config.toml
先备份原配置,然后编辑。找到[plugins."io.containerd.grpc.v1.cri".registry]这一段,加上mirrors和configs。
[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.internal.example"] endpoint = ["https://registry.internal.example"] [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.configs."registry.internal.example".tls] ca_file = "/etc/containerd/certs.d/registry.internal.example/ca.crt"这里有几个关键点。第一,endpoint必须带https://前缀,不带的话containerd会当成HTTP处理,直接失败。第二,ca_file指向你那张CA证书的路径,注意是CA证书,不是服务器证书本身。第三,如果你用的是自签证书且不想分发CA,可以设insecure_skip_verify = true,但生产环境强烈不建议,这等于放弃了中间人攻击的防护。
4.3 分发CA证书并重启
把CA证书拷到每个节点的对应目录,然后重启containerd。
# 在每个节点上创建目录 mkdir -p /etc/containerd/certs.d/registry.internal.example # 拷贝CA证书(用你的分发工具,比如ansible) scp ca.crt node:/etc/containerd/certs.d/registry.internal.example/ # 重启containerd systemctl restart containerd重启后,用crictl pull测试一下。如果能看到拉取进度条飞快地走完,基本就成了。
实操心得:改完配置后,别急着在业务集群上试,先在一台测试节点上验证。containerd重启会导致该节点上所有容器短暂中断,生产节点上操作要挑低峰期,并且做好节点排水。
4.4 镜像迁移:把公网镜像搬到Harbor
配置改好了,但Harbor里还是空的,得把业务要用的镜像搬过去。最直接的办法是在一台能同时访问公网和Harbor的机器上,先pull再tag再push。
# 从公网拉取 docker pull somepublicrepo/myapp:1.0 # 重新打标签指向Harbor docker tag somepublicrepo/myapp:1.0 registry.internal.example/prod/myapp:1.0 # 推送到Harbor docker push registry.internal.example/prod/myapp:1.0如果镜像多,写个脚本批量处理。更优雅的方式是用Harbor的远程复制功能,配置一个指向公网仓库的复制规则,让它自动同步。但复制功能对某些仓库的兼容性一般,我最后还是用了脚本批量搬,可控性更强。
搬完之后,把业务YAML里的image字段全部改成Harbor的地址。这一步建议用CI流水线统一替换,别手动改,容易漏。
5. 效果验证与性能对比:8秒是怎么来的
5.1 实测数据对比
改完之后,我在同一批节点上做了对比测试。测试方法是清空节点本地镜像缓存,然后crictl pull同一个镜像,记录耗时。为了排除偶然性,每个场景测五次取平均。
| 场景 | 平均拉取耗时 | Pod启动总耗时 |
|---|---|---|
| 公网仓库直连 | 72秒 | 90秒 |
| 本地Harbor(千兆内网) | 3秒 | 8秒 |
| 本地Harbor(万兆内网) | 1.5秒 | 6秒 |
可以看到,拉取耗时从72秒降到3秒,是二十多倍的提升。Pod启动总耗时从90秒降到8秒,这里面剩下的8秒主要是调度、网络插件配置、容器创建和探针等待的时间,这些是正常开销,没法再压了。
5.2 为什么提升这么夸张
有人可能会问,内网再快,能快二十多倍吗?答案是能,而且合理。公网拉取慢,不只是带宽问题,更多是延迟和丢包。镜像分层下载时,每一层都要单独发请求,公网环境下每个请求的往返延迟可能几十毫秒,层数一多,光握手就耗掉大量时间。再加上公网仓库常有并发限流,多个节点同时拉会被限速。内网环境下,往返延迟降到亚毫秒级,带宽也充足,自然快得多。
5.3 顺带解决的其他问题
除了速度,改本地Harbor还顺手解决了几个隐性痛点。一是发布不再受公网仓库可用性影响,以前公网仓库一抖动,我们的发布就得停。二是镜像版本可控,不会出现"同一个tag被覆盖"导致的环境不一致。三是可以做漏洞扫描,上线前先扫一遍,安全上多一道防线。
6. 常见问题与排查技巧实录
6.1 拉取报x509证书错误
这是最高频的问题。现象是crictl pull报x509: certificate signed by unknown authority。原因通常是CA证书没分发到位,或者CA证书路径配错了,或者证书的CN和访问的hostname不一致。排查顺序:先确认节点上CA文件存在且内容正确,再确认config.toml里的路径没写错,最后用openssl s_client -connect registry.internal.example:443看证书链是否完整。
6.2 报401未授权
现象是unauthorized: authentication required。这说明Harbor要求鉴权,但containerd没提供凭据。解决办法是在config.toml里配置auth,或者在Harbor里把对应项目设为公开。生产环境建议配auth,把用户名密码写进配置,注意文件权限要收紧。
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.internal.example".auth] username = "youruser" password = "yourpassword"6.3 改了配置不生效
containerd的配置改完必须重启才生效,而且有时候systemctl restart不够,得先systemctl daemon-reload。另外,如果你用的是kubelet管理的containerd,还要确认kubelet没有覆盖配置。最稳妥的验证方式是ctr命令直接测试,绕开K8s。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| x509错误 | CA未分发/CN不匹配 | 检查CA路径与证书CN |
| 401未授权 | 缺少凭据 | 配置auth或项目设为公开 |
| 连接超时 | 防火墙/端口未放行 | 检查443端口连通性 |
| 配置不生效 | 未重启/被覆盖 | 重启containerd并确认配置 |
| 拉取仍然慢 | 走了公网 | 确认镜像地址已改为Harbor |
避坑技巧:每次改完containerd配置,先用
ctr -n k8s.io images pull测一下,这个命令直接调containerd,不经过K8s,能最快定位是配置问题还是K8s问题。
7. 长期维护与扩展建议
7.1 镜像清理不能忘
Harbor用久了,镜像会越堆越多,磁盘迟早爆。一定要配垃圾回收策略,定期清理没有tag的悬空镜像,以及设置保留规则,比如每个仓库只保留最近十个版本。Harbor的GC要手动触发或者配定时任务,别指望它自动清。
7.2 高可用与备份
单点Harbor挂了,整个集群就没法拉新镜像了。规模大了一定要做高可用,Harbor支持多实例共享后端存储。另外,Harbor的数据库和配置要定期备份,镜像层如果用的是对象存储,本身就有冗余,但数据库丢了就麻烦了。
7.3 和CI/CD打通
最理想的状态是,CI构建完镜像直接推到Harbor,CD部署时从Harbor拉。这样镜像的流转全程在内网,又快又安全。我们后来把流水线改成了这个模式,发布体验又上了一个台阶。
我个人在实际操作中的体会是,镜像拉取这个环节,很多人一开始都不当回事,觉得"能拉就行"。但它是整个部署链路里最容易被忽视、又最容易优化的地方。把仓库搬到本地,投入不大,收益却非常直接。如果你现在还在被Pod启动慢困扰,不妨先掐表测一下拉取耗时,很可能问题就出在这儿。最后再分享一个小技巧:给Harbor配一个内网DNS短域名,比如reg.internal,比记一长串域名省事,配置里也清爽。