2026/10/23 6:40:25

Harbor+Kraken P2P镜像分发:大规模集群拉取加速与部署实践

Harbor+Kraken P2P镜像分发:大规模集群拉取加速与部署实践 1. 为什么单靠一台镜像仓库扛不住大规模拉取我第一次被镜像拉取速度坑到是在一次夜间批量扩容里。三百多台节点同一时间启动全部指向同一套 Harbor 实例拉取几个基础镜像结果前五十台还算顺后面的节点拉取速度从每秒几十兆掉到几百K最后干脆超时重试。容器编排层判定健康检查失败反复重启整个扩容窗口从预计二十分钟拖到了两个多小时。事后复盘问题不在 Harbor 本身它已经跑在一套还不错的存储和带宽上问题在于星型拓扑本身的物理上限——所有流量都要经过同一个出口节点数量一多出口带宽就成了唯一的瓶颈。这就是我后来去做P2P 镜像分发的直接动机。简单说这套方案要干的事只有一件把一个镜像层的字节从一台服务器分发给 N 台机器变成N 台机器互相传、只有少量回源。Harbor继续当镜像的权威存储和访问入口Kraken负责在节点之间织一张传输网络让节点既是消费者也是提供者从而把镜像拉取速度从受限于单点出口转变成受限于整个集群的内部网络总带宽。这套东西适合谁我的判断是三类人一是集群规模已经上百台、并且有明显批量启动或滚动更新的团队二是经常被镜像预热这件事折磨、每次发版前要手动去每台机器上 docker pull 一遍的人三是像我当时一样明明机器配置不差、内部网络也是万兆却始终跑不出应有拉取速度的运维和平台开发。如果你只有三五台机器说实话没必要上这套动静大于收益但一旦跨过百台门槛收益会非常直观。需要先说明一点这篇是系列的第一篇重点放在整体思路、架构分工和从零把环境跑起来包括 Harbor 怎么配、Kraken 各组件怎么排、链路怎么打通。性能调优的细节、分片大小的推算、大规模场景下的踩坑我会放在后面单独展开。文中涉及的参数、端口、配置片段都是基于我自己实际跑过的环境和公开文档整理的常见实践你落地时一定要按自己的网络和存储条件核对一遍不要照抄数字。2. 传统拉取模型到底卡在哪一环2.1 星型分发下的带宽算术题先把账算清楚很多问题就一目了然了。假设一个镜像的压缩后大小是 1GB集群要扩容 200 台节点。星型模型下从 Harbor 出口流出的总字节数是 1GB × 200 200GB。如果 Harbor 所在机器的出口带宽是万兆理论 1.25GB/s实际能跑满 800MB/s 就很不错了那么理想情况下光传数据就要 200GB ÷ 0.8GB/s ≈ 250 秒也就是四分多钟这还没算上 TCP 慢启动、磁盘读取竞争、并发连接数打满之后的排队。而现实往往更糟。200 个并发连接同时打向同一个 registryHarbor 背后的对象存储或者本地文件系统会先扛不住随机读接着是 registry 的 goroutine 调度和 GC 压力再然后是前端反向代理的连接数上限。这三层任何一层先撑不住表现都是拉取速度断崖式下跌 部分请求超时。我当时看到的现象就是如此单个节点单独拉很快两百个一起来就集体卡死。P2P 模型改变的是这个算术题的结构。节点 A 拉到一部分数据后节点 B 可以直接从 A 拿不需要再回 Harbor。理论上只要集群内部网络是通的总吞吐可以从一个出口扩展到多个入口之和。回源流量只发生在最初的种子数据和缓存未命中的部分常见配置下可以压到总量的 5%~15%。2.2 为什么缓存和 CDN 思路解决不了根问题有人会问那我在每个机架放一台缓存节点或者用一层反向代理做缓存是不是就够了我试过这条路结论是能缓解但不能根治而且运维成本不低。缓存节点的核心问题是预热不可控 命中率依赖调度。你今天扩容的机器如果刚好都调度到同一个机架那台机架的缓存节点一样会被打爆而且缓存节点自己也是星型的中心它的出口带宽依然是上限。更麻烦的是缓存节点一旦挂掉整个机架回退到直连 Harbor压力瞬间加倍容易引发连锁反应。P2P 的差别在于它没有中心缓存节点这个概念。每个节点既是客户端也是服务端节点的加入本身就是扩容带宽。1 台节点加入可用上传带宽多一份100 台加入可用上传带宽多一百份。这是拓扑层面的差异不是靠堆机器能补上的。2.3 什么样的场景最吃这套方案不是所有场景都值得上 P2P。我把适合的场景归成三类你可以对号入座场景特征是否适合原因集群节点数 100经常批量扩容非常适合并发拉取量大星型出口必成瓶颈滚动更新频繁每次几十上百个 Pod 同时重建非常适合同一镜像被大量节点同时请求P2P 命中率高镜像体积大单个 2GB 以上适合分片并行传输收益明显多机房、跨地域集群视情况跨机房 P2P 效果取决于机房之间的链路质量节点数少于 20镜像都很小不太必要收益有限组件维护成本反而突出镜像数量极多但每个只拉一次效果一般P2P 的价值在于同一内容被多次请求最后一行特别值得说。P2P 分发本质上是用重复请求换带宽如果每个镜像在全集群只被拉一次那 P2P 没有可复用的对象价值就接近于零。所以上线前一定要先看看自己的镜像复用率这个数据从 Harbor 的访问日志里就能统计出来。3. Harbor 与 Kraken 的职责划分与配合逻辑3.1 Harbor 在这套方案里的定位没有变很多人在做 P2P 改造时会有个误解觉得既然节点之间互相传了那 Harbor 是不是可以弱化甚至省掉。我的答案是Harbor 依然是整套方案的权威源头地位反而更重要只是它承担的压力被重新分配了。在 Kraken 的架构中Harbor 扮演的是最终可信源的角色。Kraken 的 Origin 集群需要从某个上游 registry 拉取真实的镜像数据这个上游就是 Harbor。所有镜像的鉴权、命名空间、标签管理、漏洞扫描、镜像签名这些都还在 Harbor 里做。Kraken 不替代 Harbor 的任何管理能力它只接管字节怎么从 A 传到 B这一段路。所以 Harbor 的配置重点不在性能调优而在能被 Kraken 顺畅地访问。具体来说有这么几条我认为必须确认Harbor 必须对 Kraken 的 Origin 节点开放 registry API也就是标准的/v2/路径如果 Harbor 开了 HTTPS 且用的是内部签发的证书那 Origin 侧必须把对应的 CA 装进去或者明确配置跳过校验生产环境我不建议后者创建一个专门的机器人账号给 Kraken 用只授予 pull 权限不要图省事用 admin。3.2 Kraken 的组件分工用一张表说清Kraken 不是一个单体程序它是好几个组件拼起来的第一次接触很容易搞混谁负责什么。我用一张表把我理解的分工列出来这张表比官方架构图更好记组件职责部署位置少了它会怎样Agent跑在每台业务节点上对外暴露一个 registry 端点供容器运行时访问对内负责分片下载和上传每一台需要拉镜像的宿主机节点无法参与 P2P拉取回退到直连Scheduler接收 Agent 的请求决定这次拉取该从哪些 peer 拿数据可部署一个或多个前置负载均衡Agent 不知道去哪找数据源分发失效Origin Cluster一组回源节点按哈希分片持有镜像数据负责从 Harbor 拉取并缓存独立一组机器没有种子数据P2P 网络无源可依Tracker记录哪个 Agent 拥有哪个 blob的元数据通常依赖一套内存数据库Scheduler 无法得知 peer 的持有情况BackendOrigin 的实际存储后端可以是本地盘也可以是对象存储跟随 OriginOrigin 无处落盘理解这张表的关键在于Tracker 存的是谁有Scheduler 算的是从谁那儿拿Origin 提供的是最初的种子Agent 是执行者和新的种子。数据从 Harbor 出发先落到 Origin再扩散到第一个 Agent然后 Agent 之间互相扩散越拉越快。Origin 的分片设计很巧妙我要单独强调一下。它不是简单地部署 N 个 Origin 做负载均衡而是每个 Origin 只负责整体 blob 空间的一部分通过一致性哈希决定某个 blob 归属哪个 Origin。这样可以避免所有 Origin 都缓存全量数据导致存储浪费也能让缓存命中更集中。代价是如果某个 Origin 挂了它负责的那部分 blob 需要重新回源短期内会有一次性能抖动。3.3 为什么选这套而不是自研或者别的方案选型这块我纠结过一段时间把当时对比的几个方向列出来供你参考。自研一套 P2P 分发听起来很酷但实际动手就会发现坑在细节里分片怎么切、断点续传怎么做、peer 列表怎么维护、节点失效怎么感知、镜像层和 blob 的对应关系怎么建立每一项都是硬骨头。光是某个 peer 传到一半掉线了剩下的分片怎么重新调度这一个问题就够折腾几周的。我的判断是除非你的业务有非常特殊的约束否则自研不划算。另外一类方案是容器运行时自带的加速能力比如某些运行时的懒加载模式。这类方案的优势是不用改架构镜像层按需拉取启动快。但它解决的是启动速度问题不是总带宽问题。当你有两百个节点同时需要完整镜像时懒加载只是把压力延后了总字节数并没有减少。而且懒加载对存储层的随机读要求很高一不小心就从网络瓶颈变成了存储瓶颈。Kraken 的优势在于它是为大规模集群分发这个场景专门设计的分片、P2P、回源集群、调度四个能力是配套的。它和容器的集成方式也比较自然业务侧只需要把 registry 地址指过去基本无感。这是我在几个方案里最后选它的核心原因。3.4 数据是怎么流动的把这条链路走一遍把链路完整走一遍有助于后面排错。假设一台新节点上的容器要拉取demo/app:v1容器运行时向本机的 Agent 发起拉取请求Agent 对外表现为一个 registry 端点。Agent 收到请求后先向 Scheduler 询问我要拉这个 manifest 对应的 blob谁能给我Scheduler 去 Tracker 查询看当前有哪些 Agent 已经持有这些 blob。如果没有任何 Agent 持有就把 Origin 集群的地址返回。Agent 拿到 peer 列表后把这些 blob 切成多个分片并行地从不同来源下载。同一时间可能从两三个 Agent 加上一个 Origin 拿不同分片。下载完成后Agent 在本地拼装成完整 blob校验哈希返回给容器运行时。同时Agent 向 Tracker 注册我现在有这些 blob 了下次别的节点来拉它就成了新的数据源。第 6 步是这套机制能越用越快的关键。第一次拉取可能全部来自 Origin速度提升有限当集群里已经有几十台机器持有这个镜像后来的机器就几乎不需要回源了。所以在批量扩容场景下你会观察到一条很有意思的曲线前几台拉得慢中段速度爬升后段飞快。4. 从零搭一套能跑起来的验证环境4.1 前置条件与资源盘点动手之前我建议先把这几件事确认清楚不然后面要返工。我自己第一次搭的时候就因为域名解析没弄好卡了大半天。项目要求我的实际情况宿主机Linux内核版本不要太老方便验证 overlay 相关能力几台 8C16G 的虚拟机容器运行时支持配置 registry mirror用常见的容器运行时即可Harbor已部署或能部署至少一个可用项目单独一台机器跑 HarborKraken 组件Scheduler、Origin、Tracker 各至少一份三台机器分别承载网络所有组件与节点之间端口互通DNS 或 hosts 可解析内网同一网段无防火墙拦截存储Origin 需要一块足够大的盘放缓存每台 Origin 挂 200G 数据盘存储这块我特别提醒一句Origin 的缓存盘不要和系统盘混用。镜像层写入是比较重的随机写缓存盘满了之后如果不做清理策略会把整个 Origin 拖死。我第一次就是随手用了根分区跑到第二天缓存写满Origin 直接不可用连带所有回源请求全部失败。4.2 Harbor 侧的关键配置Harbor 的部署过程网上材料很多这里只说我踩过的几个必须调整的点。第一创建专用的机器人账号。路径是在项目设置里创建权限只勾选 pull。不要用 admin 账号因为 Kraken Origin 的配置里会明文写上凭证一旦泄露影响面太大。第二确认 registry API 路径可访问。在任意一台 Origin 机器上执行curl -u robot\$kraken:你的令牌 https://harbor.example.internal/v2/_catalog返回 200 且带仓库列表就说明通了。如果返回 401先查账号密码返回证书错误就是 CA 没装对。注意账号里的$在 shell 里要转义我第一次就是因为这个报 401排查了半天才发现是 shell 把变量展开成了空。第三如果 Harbor 前面有反向代理确认client_max_body_size这类限制不影响大层上传虽然 Kraken 主要是读但你在准备测试镜像时可能会被卡。第四准备一个用于验证的测试镜像。我的做法是挑一个实际业务镜像大小在 500MB 到 2GB 之间压测时放大差异更明显。太小的镜像几十兆在 P2P 和直连之间几乎看不出区别测了也白测。4.3 Kraken 各组件的部署顺序顺序很重要反了会互相等依赖。我的习惯是按 Tracker → Origin → Scheduler → Agent 这个顺序来。Tracker 最先起它本质是个元数据存储起来之后才有地方登记信息。部署时给一个独立的数据目录配置文件里主要是监听地址和存储路径注意如果是单点验证可以用简化模式生产环境必须做高可用这个后面细说。Origin 次之它需要知道两件事上游 registry 是谁Harbor缓存落在哪个盘。配置片段大概长这样origin: listen: 0.0.0.0:5000 hostname: origin-1.example.internal cache_dir: /data/kraken-cache registry: address: harbor.example.internal port: 443 scheme: https credentials: username: robot$kraken password: 你的令牌这里有个容易忽略的点hostname必须是其他组件能解析到的地址不能写localhost。我见过有人为了省事全写 localhost结果 Scheduler 返回的 peer 地址全是 127.0.0.1Agent 拿到的分片地址指向自己直接死循环。Scheduler 起来之后主要配置项是连到 Tracker 的地址和返回给 Agent 的 Origin 地址列表。它本身不存数据是个纯计算组件所以可以部署多个前面挂个负载均衡。单点验证阶段一个就够。Agent 最后装装在业务节点上。它的配置最需要小心因为涉及本地监听端口和与 Scheduler 的通信agent: listen: 0.0.0.0:3000 hostname: node-1.example.internal scheduler: address: scheduler.example.internal:8080Agent 的listen端口就是容器运行时要去访问的地址后面配置 registry mirror 时用的是这个。同样hostname不能写 localhost否则 Tracker 登记的持有者信息就是错的别的节点永远连不上它。4.4 把容器运行时指到 Agent 上这一步是整个链路打通的关键也是最容易出问题的地方。思路很简单让容器运行时认为 Agent 就是一个 registry 镜像源所有拉取请求都先经过它。配置内容大概是往容器运行时的配置里加一段 registry mirror{ registry-mirrors: [http://node-1.example.internal:3000] }改完要重启容器运行时。重启之后已有的镜像和容器不会受影响但所有新的拉取都会走 Agent。这一点要特别小心如果你是在一台正在跑业务的机器上改建议先确认这个 mirror 配置不会影响其他仓库的访问镜像源通常会 try mirror 再 fallback但不同运行时行为有差异。还有一个细节我吃过亏mirror 只对镜像名字里不带 registry 域名前缀的情况生效或者说不同运行时的匹配规则不一样。如果你的镜像地址写的是harbor.example.internal/demo/app:v1某些配置下 mirror 不会被触发而是直连。稳妥的做法是先在测试节点上改完配置然后docker pull一个镜像同时去 Agent 的日志里看有没有收到请求确认真的走了 P2P 再推广。4.5 验证链路是否真的通了配置完不要急着压测先用一个镜像走一遍确认每一段都通。我的验证步骤是这样的在一个测试节点上清掉目标镜像确保是全新拉取。执行拉取观察耗时。立刻去 Origin 的缓存目录看应该能看到对应 blob 文件落盘了。去 Tracker 里查应该能看到这个 Agent 已注册持有某些 blob。在第二个节点上拉同一个镜像观察 Agent 日志应该看到它的下载来源里出现了第一个 Agent 的地址而不是只有 Origin。第 5 步是判断 P2P 是否真的生效的分水岭。如果第二个节点还是全部从 Origin 拿说明 peer 发现没生效回去查 Tracker 和 Scheduler 的连接。这一步我建议多花点时间验证因为后面所有性能收益都建立在这条链路上。5. 性能调优的关键参数与观测手段5.1 分片大小这道算术题怎么算分片piece大小是影响 P2P 效率最直接的参数但很多人是随手填个数字。我把我的推算过程写出来你可以按同样方法算自己的。核心矛盾在于分片太小元数据开销和连接管理成本上升每个分片都要单独协商、校验开销占比高分片太大并行度下降一个大分片只能从一个 peer 拿等于退化成单源下载而且单个 peer 掉线造成的损失也大。一个粗略的估算方法是分片大小 ≈ 单层大小 ÷ 期望并行来源数。假设你的典型镜像层是 100MB你希望同时从 8 个来源并行下载那分片取 12MB 左右比较合适。如果分片取 4MB会切成 25 个分片并行度足够但管理开销偏大如果取 32MB只切 3 个分片并行度就浪费了。我实测下来镜像层普遍在几十到几百 MB 的场景分片大小取 4MB 到 16MB 之间比较均衡。这个值的调整必须以你自己的镜像分布为前提如果你的镜像层动辄 1GB 以上可以往大调如果都是几十 MB 的小层往小调。5.2 并发与限速别把节点自己拖垮Agent 的下载并发数是个双刃剑。调高能更快拉完镜像但会占用节点的 CPU 和网络如果节点上还跑着业务可能造成业务抖动。我一般会设一个相对保守的值比如 4 到 8 个并发分片然后在压测中观察节点负载。如果节点负载很低、拉取还是慢说明瓶颈在源侧不在并发如果节点负载已经很高那就该降并发或者加限速。上传侧同样要限。Agent 作为数据源对外提供服务时如果不限速几十个节点同时来拉这台机器的上行带宽会被打满业务网络可能受影响。我的做法是给 Agent 的上传配一个明确的带宽上限取节点总上行带宽的 50%~70%留出余量给业务。还有一个容易忽略的参数是 Origin 缓存淘汰策略。Origin 的盘是有限的镜像一直在更新旧 blob 会堆积。如果不配淘汰早晚写满。常见做法是按访问时间做 LRU 或类似策略同时设置一个容量水位。我配的是达到容量的 85% 开始淘汰避免频繁触发淘汰导致抖动。5.3 用什么指标判断优化有没有生效性能优化最怕感觉快了必须有量化指标。我固定看这么几个指标采集位置参考意义单节点拉取耗时客户端计时最直观的用户体感回源流量占比Origin 出口流量 ÷ 总下载量越低说明 P2P 命中越好平均分片来源数Agent 日志统计反映并发下载是否充分Tracker 峰值 QPSTracker 监控判断元数据服务是否有压力节点网络上行使用率宿主机监控判断是否触顶回源流量占比这个指标我最看重它是衡量 P2P 是否真的起作用的硬指标。如果这个值长期在 80% 以上说明大部分数据还是从 Origin 拿的要么是镜像复用率太低要么是 peer 发现有问题。我在一次测试中就遇到占比一直下不来的情况最后查出来是 Agent 的 hostname 配了一个无法从其他节点访问的地址Tracker 里登记的 peer 全是不可达的Scheduler 只能回退到 Origin。6. 踩坑实录与排查速查表6.1 拉取卡住不动日志里却什么都没有这是我遇到过最玄的问题。现象是客户端拉取到某个百分比就停住Agent 日志里没有报错只有零星的超时。排查下来发现是 MTU 不一致导致的Agent 之间传输用的是大包某些网络路径上 MTU 偏小大包被丢弃但没返回明确的错误表现就是卡住。判断方法很简单用 ping 带不分片标志去测ping -M do -s 1472 node-2.example.internal如果通说明路径 MTU 正常如果报Message too long就是 MTU 问题需要调整网络或者把 Agent 传输的分片大小往下调。6.2 第二个节点拉取速度没有提升这是最常见的问题原因通常有四类我按排查顺序列出来现象可能原因排查动作日志显示来源全是 OriginTracker 里没有该 blob 的持有记录检查第一个节点拉完之后 Agent 是否正确注册Scheduler 返回的 peer 地址不可达Agent 配的 hostname 不对从其他节点 curl 一下这个地址的监听端口有 peer 但连不上防火墙或安全组拦截检查 Agent 监听端口的入站规则连上了但速度慢上传限速设得太低查看 Agent 上传带宽配置我建议的排查顺序是先看日志、再看地址、最后看网络从现象往根因一层层剥别一上来就怀疑 P2P 算法。6.3 Origin 缓存盘写满引发的连锁故障这个坑我踩得比较深值得单独说。当时是测试环境跑了一周某天开始所有拉取都变慢部分请求直接失败。查下来是 Origin 的缓存盘 100% 占用写新 blob 失败回源请求全部挂着超时。更麻烦的是这个故障是渐进的前几天只是偶尔慢很容易被忽略。我的处理方案有三条一是给缓存盘加独立的磁盘监控阈值设在 80%触发就告警二是配好淘汰策略不要依赖手工清理三是把 Origin 的cache_dir和日志目录分开日志写满盘的场景我也见过一次同样致命。6.4 镜像标签更新后节点拉到旧内容这个问题的根源是缓存一致性。镜像 tag 是可以被覆盖的比如v1重新推了一次内容变了但 tag 没变。如果某台 Agent 已经缓存了旧的 blob它在响应其他节点时可能返回旧数据。Kraken 层面有内容寻址机制blob 是按 digest 存的理论上 digest 变了就是不同的对象不会串。但现实中的坑在于如果客户端用的是 tag 而不是 digestScheduler 和 Tracker 在解析时如果缓存了 tag 到 digest 的映射就可能拿到旧的。我的规避做法是生产环境尽量用 digest 部署或者至少保证每次更新都换 tag不要在同一个 tag 上反复覆盖推送。6.5 一份可以直接抄的排查清单最后把我在实际运维中固定执行的一套检查整理成清单出问题时从上往下跑一遍能覆盖八成以上的场景确认 Agent 进程活着listen端口在监听。从另一个节点确认能访问到该 Agent 的 hostname 和端口。查 Tracker 里是否有该 blob 的持有记录记录里的地址是否可达。查 Scheduler 日志看返回的 peer 列表是否符合预期。查 Origin 缓存目录剩余空间是否超过水位线。查回源流量占比判断是全局问题还是单节点问题。用一个全新镜像做对照测试区分是这个镜像的问题还是链路的问题。我个人在处理这类问题时有个习惯先固定一个镜像、两台节点把最小复现环境搭出来再逐步加变量。比起直接在几百台的集群里瞎找小环境里十几分钟就能定位到根因。这套环境我到现在还留着每次升级组件前都先在它上面跑一遍心里踏实很多。这套架构的底座搭起来之后你会发现真正的收益不在配置写完那一刻而在第二次、第三次扩容时那几十秒的差距。后续我会继续把 Origin 的高可用设计、大规模节点下的 Tracker 压力、以及和其他镜像加速手段的配合方式拆开讲那些才是把这套方案从能跑推到敢在生产上跑的关键部分。