ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

dragonfly P2P镜像分发实战:原理、部署与缓存问题排查

dragonfly P2P镜像分发实战:原理、部署与缓存问题排查 先说结论如果你的集群规模过了几十台节点发版时镜像仓库带宽经常被打满拉镜像从十几秒变成几分钟那 dragonfly 就是很值得投入的东西。它把集中式的镜像拉取改造成 P2P 分发节点越多下载越快而不是所有节点一拥而上把 registry 打到跪。这篇文章我会从原理、部署、接入 containerd、参数估算、常见问题几个维度把我实际踩过的坑和验证过的做法完整写出来尤其是镜像缓存相关的几个根因问题照着排查大概率能自己解决。关键词dragonfly、镜像缓存、P2P 分发、containerd 镜像加速、dfdaemon。1. 为什么镜像拉取会成为瓶颈dragonfly 的核心设计思路1.1 容器化浪潮下的镜像分发难题先从一个真实的业务场景说起。很多团队在容器化初期镜像仓库用的是自建的 Harbor开发环境几十台节点的 K8s 集群。平时还好一到发版时间CICD 流水线把新镜像推到 Harbor然后各节点开始滚动更新Kubelet 通知 containerd 拉取新镜像。当集群里同时有几十上百个 Pod 在做滚动更新时containerd 会并发向 Harbor 发起层文件下载请求。这里的核心矛盾在于镜像本质上是一个个 Layer 组成的同一个镜像的 Layer 内容是一样的。几十个节点同时去仓库拉同一个 Layer仓库要重复发送几十倍的流量不仅带宽被占满Harbor 的 IO 和网络连接数也会暴涨。现象就是正常单节点拉镜像 10 秒发版时可能要几分钟甚至直接超时失败、镜像拉取半路断掉、Pod 调度被拖垮。这个问题有三种常见解法一是加大仓库带宽但费用高而且只是把拥堵时间缩短治标不治本二是用 registry 做中心化缓存比如内网部署一个拉取代理把外部镜像缓存一份但节点多了以后这个中心缓存服务器自己又会变成下一个瓶颈三是上 P2P 分发也就是 dragonfly 方案这也是我最终选择的方向。1.2 dragonfly 的 P2P 分发模型dragonfly 是 CNCF 旗下的开源镜像分发加速项目它的设计哲学和 BitTorrent 很像不依赖中心服务器把所有流量扛下来而是让已经持有某个 Layer 分片的节点直接把它传给正在请求的节点。在 dragonfly v2 架构里角色分为四类Manager控制面组件负责集群管理、动态配置下发、预热任务调度。Scheduler调度中心收集各个节点的网络拓扑、负载、分片情况决定一个下载任务由哪些节点来提供数据。Seed Peer种子节点负责从源 Registry 拉取数据并作为 P2P 网络的数据源头。Dfdaemon每个节点上的代理进程拦截容器运行时的镜像拉取请求把它转成 P2P 下载任务。当一个节点上的 containerd 要拉取镜像时请求并不是直接到 Harbor而是先打到本机的 dfdaemon。dfdaemon 会把镜像的 manifest 和每一层 Layer 拆成多个分片然后向 Scheduler 申请有哪些节点可以给我提供分片。Scheduler 根据全局状态返回一批候选 Peerdfgetdfdaemon 内置下载器并行从多个 Peer 拉取分片全部拿到后拼接成完整的 Layer再返回给 containerd。这里关键点在于分片机制一个 Layer 被切成小块不同节点持有不同分片下载任务就能并行调度而不是整个 Layer 从某一个节点串行拉取。这样当一个新节点加入 P2P 网络时它可以从 A 节点拿分片 1从 B 节点拿分片 2同时 C 节点还能从它这里拿已经下好的分片整个网络越用越热。1.3 和普通 Registry Cache 的差别很多人会把 dragonfly 和普通的镜像缓存仓库比如 Harbor Proxy Cache、Nexus 的 Docker Proxy搞混。我用一个类比说明白普通 Registry Cache 是一个传令兵所有节点都需要找它要数据它把源仓库的数据存一份再分发给所有节点。人多了以后传令兵自己就累趴了。dragonfly 是水泼出去的方式只有第一个节点真正去源仓库拉数据拿到后其他节点直接相互传输源仓库几乎不感知大规模拉取。从带宽节省角度看假设有 50 个节点同时拉一个 2GB 的镜像直接拉 Harbor总下载流量 100GBHarbor 需要吐出 100GB。中心化缓存缓存服务器先从 Harbor 拿 2GB再向 50 个节点分别传输 2GB总流量还是 100GB只是把压力从 Harbor 转移到了缓存服务器。dragonflySeed Peer 从 Harbor 拉取 2GB后续 49 个节点从 P2P 网络获取回源流量近似只有 2GB节省约 98% 的源站流量。这是最让我惊艳的地方也正是镜像缓存里真正值钱的部分不是简单地缓存内容而是把缓存分发给大家互相帮忙。2. 部署前必须搞明白的关键细节2.1 版本选择v1 还是 v2如果你在网上搜索 dragonfly可能会看到很多 v1 的旧文章里面提到 supernode、dfget client、启动一个 65001 端口的 dfdaemon然后用--registry参数挂在 Docker registry。这些方案在 2022 年以后的 K8s 环境里已经不推荐了。dragonfly v1 的核心组件叫 supernode是单体调度节点虽然也能用但扩展性、动态配置、TLS 支持都不如 v2。v2 把控制面拆成 Manager Scheduler配置动态下发支持多区域调度、预热 API、指标监控而且和 containerd 的集成更规范。当前新项目直接上 v2 即可不要在上线新环境时还考虑 v1。版本命名上v2 的镜像仓库是dragonflyio/dragonfly-manager、dragonflyio/dragonfly-scheduler、dragonflyio/dragonfly-dfdaemon这些镜像在 Docker Hub 都能找到。我们实际用的是 v2.1.x 系列功能稳定。如果你用的是 v2.0 之前的版本一些配置字段和现在不太一样下文以 v2.1 为主。2.2 v2 架构中每个角色的职责先别急着点安装命令把角色关系搞清楚后面排查问题会轻松很多。Manager类似中心控制台。它管理者所有 Scheduler 和 Seed Peer提供 HTTP API 给 dfdaemon 查询调度器地址也负责下发 dfdaemon 的配置。它不参与数据面传输流量压力忽略不计。Scheduler类似智能调度器。它维护所有 Peer 的状态包括每个 Peer 的 ID、IP、负载、已缓存的分片、网络区域。当一个 dfget 请求到来时Scheduler 决策优先从哪些 Peer 拿分片选择策略一般按就近和负载均衡来。Seed Peer本质是 dfdaemon 的一种特殊角色只是它被标记为种子。当集群里没有任何 Peer 缓存了某个分片时第一个下载请求会打到 Seed PeerSeed Peer 回源到 Harbor 拉取 Layer然后分发给其他节点。Seed Peer 的磁盘上会保留数据作为 P2P 网络的长期缓存。Dfdaemon每台节点上的代理。它监听一个本地地址默认 65001容器运行时把这个地址配置为镜像 Mirror。Dfdaemon 内部包含 dfget 下载模块负责分片下载、上传、缓存管理。理解这个架构之后你应该能想到几个关键点Scheduler 必须能被所有 dfdaemon 访问Seed Peer 必须能访问源 Registrydfdaemon 所在节点之间网络必须能互通。这三个连通性的任何一处断掉都会导致镜像拉取异常。2.3 与容器运行时集成的两种方式dragonfly 并不是改变镜像仓库的内容而是作为一层流量代理接入容器运行时。目前最常见的是 containerd其次是 Docker。对于 containerd需要修改/etc/containerd/config.toml在 CRI registry mirrors 中加上 dfdaemon 监听地址version 2 [plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.docker.io] endpoint [https://127.0.0.1:65001] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.harbor.example.com] endpoint [https://127.0.0.1:65001]这里的逻辑是containerd 在拉取harbor.example.com下的镜像时不是直接访问 Harbor而是访问127.0.0.1:65001也就是 dfdaemon。dfdaemon 收到请求后再决定是从 P2P 网络拿数据还是回源到 Harbor。对于 Docker则配置 daemon.json{ registry-mirrors: [https://127.0.0.1:65001] }需要注意Docker 的registry-mirrors默认只对 Docker Hub 镜像生效。如果是自建 Harbor你需要额外处理通常是把 Harbor 的域名请求也代理到 dfdaemon或者直接用 containerd 环境避免 Docker 的兼容性问题。我强烈建议在 K8s 环境里统一使用 containerd配合 dragonfly 是最顺滑的。3. 从零部署一套可用的 dragonfly 集群3.1 环境假设和准备清单假设我们现在有一套测试环境1 台 Manager1 台 Scheduler1 台 Seed Peer可以复用一台机器模拟。3 台 K8s worker 节点containerd 版本 1.7。内网 Harbor 地址harbor.example.com。所有节点操作系统为 Ubuntu 22.04 LTS内核 5.15。部署前检查三个前提所有节点能访问 Manager、Scheduler 的地址和端口。Seed Peer 所在机器能正常拉取 Harbor 镜像。节点之间 65001 端口dfdaemon 数据面端口能互通。我习惯用 docker compose 在测试环境快速拉起 Manager、Scheduler 和 Seed Peer。生产环境建议用 helm chart 或 manifest 部署成 K8s Service思路一样。3.2 用 docker-compose 部署 Manager、Scheduler 和 Seed Peer下面是一份我实际用过的精简版 docker-compose 配置只保留关键参数version: 3.8 services: manager: image: dragonflyio/dragonfly-manager:v2.1.8 container_name: dragonfly-manager restart: always ports: - 8080:8080 volumes: - /var/lib/dragonfly/manager:/var/lib/dragonfly scheduler: image: dragonflyio/dragonfly-scheduler:v2.1.8 container_name: dragonfly-scheduler restart: always depends_on: - manager ports: - 8002:8002 command: - --manager-addrmanager:8080 volumes: - /var/lib/dragonfly/scheduler:/var/lib/dragonfly seed-peer: image: dragonflyio/dragonfly-dfdaemon:v2.1.8 container_name: dragonfly-seed-peer restart: always network_mode: host depends_on: - manager - scheduler volumes: - /var/lib/dragonfly/seed-peer:/var/lib/dragonfly - /etc/dragonfly:/etc/dragonfly command: - --manager-addrmanager:8080 - --seed-peertrueSeed Peer 使用network_mode: host因为 dfdaemon 需要监听 65001 端口来和其他 Peer 互通用 bridge 模式会浪费 NAT 开销host 模式是最省心的。启动后验证 Manager API 是否正常curl http://127.0.0.1:8080/apis/v1/health如果返回健康状态说明 Manager 起来了。Scheduler 日志里会看到它向 Manager 注册成功。3.3 在每个节点部署 dfdaemon 并接入 containerdK8s 每台 worker 节点都要部署 dfdaemon。生产环境建议用 DaemonSet这里我给出核心的配置流程。首先创建 dfdaemon 的配置文件/etc/dragonfly/dfget.yamlscheduler: manager: addr: http://manager.example.com:8080 seedPeer: enable: true # 如果使用多 scheduler可以配置列表 # addrs: # - http://scheduler.example.com:8002 hosts: - regx: harbor.example.com url: https://harbor.example.com这里的hosts配置含义是当 dfdaemon 收到harbor.example.com的请求时最终回源地址是什么。如果你的私有仓库是 HTTP 协议需要把url改成http://...并且 dfdaemon 配置里也要允许不安全仓库。然后创建 containerd 的 mirror 配置。以 containerd 1.7 为例在/etc/containerd/config.toml中追加[plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.harbor.example.com] endpoint [https://127.0.0.1:65001] [plugins.io.containerd.grpc.v1.cri.registry.configs.harbor.example.com.tls] ca_file /etc/dragonfly/ca.crt这里有个容易踩坑的细节dfdaemon 默认会启用 HTTPS使用它内置的一个自签 CA。containerd 通过https://127.0.0.1:65001访问 dfdaemon 时如果 containerd 不信任这个 CA就会报x509: certificate signed by unknown authority。解决方式是在 containerd 的 registry configs 里给127.0.0.1这个 dst 指定ca_file指向 dfdaemon 生成的 CA 证书。更简单的做法是在 dfdaemon 启动参数里加--insecure让它对外提供 HTTP 服务然后把 containerd endpoint 改成http://127.0.0.1:65001。但生产环境我建议用 HTTPS 受信任 CA避免引入不安全注册表的额外问题。配置好之后重启 containerd 和 dfdaemonsystemctl restart dragonfly-dfdaemon systemctl restart containerd然后用crictl pull测试拉取crictl pull harbor.example.com/library/nginx:1.24如果能看到 dfdaemon 日志里有请求记录说明流量已经被劫持到 dragonfly。3.4 验证镜像拉取是否真的走了 P2P这是我最喜欢的一步也是判断部署是否成功的核心验证。先在集群的一台节点上拉取一个稍大的镜像比如 500MB 以上的让它成为 P2P 网络里的数据提供者。然后在另一台节点上再次拉取同一个镜像观察 dfdaemon 的日志。正常情况应该是第二台节点下载时日志里会出现大量来自其他 Peer IP 的传输记录而不是所有流量都指向 Harbor 或 Seed Peer。你还可以用下面的命令查看 dfget 的实时日志find /var/log/dragonfly -name *.log -mmin -1 | xargs tail -f如果看到类似download from peer 10.0.0.x的信息说明 P2P 生效了。另外一个更直观的方法是通过 Manager 的监控指标。dragonfly 会暴露 Prometheus 指标比如dragonfly_scheduler_traffic、dragonfly_scheduler_peer_task_total等。在 Grafana 里把 Scheduler 的traffic指标按type分组能看到从 peer 获取的流量占了绝大多数回源流量很低这就说明分发模型运转正常。3.5 镜像缓存容量和带宽收益怎么估算如果你要给团队一个合理的资源申请理由下面这套估算逻辑可以直接用。关于缓存目录大小dfdaemon 的/var/lib/dragonfly/data假设团队常用镜像平均大小 1.5GB一次发版要更新 10 个镜像希望本地能够在无回源的情况下覆盖最近两次发布那缓存容量至少是1.5GB × 10 × 2 30GB加上分片冗余、下载中断产生的临时文件建议按 1.5 倍放大即 45GB所以每个节点的 dfdaemon 缓存目录至少给 80GB 比较稳妥。如果镜像平均更大直接等比例放大。关于带宽收益假设集群 100 个节点同时拉取一个 2GB 镜像全部直连 Harbor 时 Harbor 需要输出 200GB100×2GB。使用 dragonfly 后Seed Peer 回源一次 2GB绝大多数节点从 Peer 拿数据回源流量减少 99%。当然实际中每个 Layer 的首次分发还是需要回源但同一 Layer 第二次之后就不会再回源了。对于频繁发版的镜像缓存命中率会非常高。所以结论是dragonfly 在镜像体积越大、节点越多、并发越高时收益越明显。如果只有 3 台节点、镜像只有几十 MB那确实没必要折腾这也是我踩过坑后才得出的判断。4. 镜像缓存问题排查与避坑实录4.1 高频问题速查表先把最常遇到的问题列个表格便于快速定位。现象可能原因快速排查方法拉取镜像报x509: certificate signed by unknown authoritycontainerd 不信任 dfdaemon 自签 CA检查/etc/dragonfly/ca.crt是否写入 containerd config 的 ca_filecontainerd 拉取一直卡住无响应dfdaemon 端口不通或版本不匹配先 curlhttps://127.0.0.1:65001/v2/测试dfdaemon 日志报no available peersScheduler 没注册到 Manager或 Peer 之间网络不通检查 Manager 中 Scheduler 状态测试节点间 65001 端口镜像能拉但速度极慢流量没走 P2P而是每次回源用 tcpdump 抓包看是否有大量回源连接节点重启后缓存不命中dfdaemon 缓存目录丢失或配置指向了临时目录确认持久化 volume 挂载正确预热任务一直失败Seed Peer 无法访问源 Registry在 Seed Peer 容器内手动docker pull或crictl pull验证管理面 API 返回 5xxManager 数据库损坏或磁盘满查看 Manager 容器日志和磁盘空间4.2 证书问题镜像拉取报 x509我第一次部署时被这个问题折磨了大半天。现象是 containerd 拉取镜像时报failed to do request: Get https://127.0.0.1:65001/v2/: x509: certificate signed by unknown authority原因很简单dfdaemon 为了提供 HTTPS 代理自动生成了一套自签 CA。containerd 在访问https://127.0.0.1:65001时会用系统 CA 列表去校验自然校验失败。解决办法有两个第一种正式做法把 dfdaemon 的 CA 证书加到系统信任链。在每台节点上执行cp /etc/dragonfly/ca.crt /usr/local/share/ca-certificates/dragonfly-ca.crt update-ca-certificates systemctl restart containerd但注意containerd 的 CRI plugin 不一定读取系统 CA所以最稳的还是显式在 containerd 配置中加入ca_file。第二种最简单但不够安全让 dfdaemon 以纯 HTTP 模式运行。启动参数加上dragonfly-dfdaemon --insecure同时 containerd 的 endpoint 改成endpoint [http://127.0.0.1:65001]如果是纯内网环境、仓库流量不涉及外网用 HTTP 模式可以快速跑通。但你要清楚这是不可信的安全要求高的环境不要这样做。4.3 调度不到 Peerno available peer另一个高频问题是在刚开始部署时Magic 流量被 dfdaemon 接收了但日志里出现peer task failed: no available peer这个报错意味着 dfget 向 Scheduler 请求分片提供的 Peer 时Scheduler 返回了空列表。可能原因有三个Scheduler 尚未发现自己集群里有任何 Peer 在线。检查 Scheduler 日志确认 seed peer 和 dfdaemon 是否注册成功。刚启动的 Peer 没有数据Scheduler 认为它没有可提供的分片而 Seed Peer 又没有被标记为可用。Peer 之间网络隔离Scheduler 虽然知道 Peer 存在但无法建立数据面连接。排查思路先看 Scheduler 的/apis/v1/schedulers接口或者 Manager 的控制台确认 Peer 在线数量。然后从 dfdaemon 节点 telnet 到 Scheduler 的 8002 端口以及 telnet 到 Seed Peer 的 65001 端口确认网络通。如果确认网络没问题再看一下 dfget.yaml 里的seedPeer.enable是否配置正确。对于普通 worker 节点的 dfdaemon这个值应该保持默认 false只有 Seed Peer 角色才设置为 true。4.4 缓存不命中和回源过大的排查思路如果你发现 dragonfly 部署了但 Harbor 的带宽还是被打满大概率是缓存不命中。先明确 dragonfly 的分发不是永久缓存而是基于 Peer 的临时持有。当一个节点上的 dfdaemon 缓存目录被清理或者节点重启后缓存丢失它就会变成没有数据的空 Peer下载时只能回源。要排查缓存命中率可以看 Scheduler 的指标。重点看dragonfly_scheduler_host_traffic按type拆分会看到hit和miss两类。如果miss占比非常高说明很多下载都在回源。常见原因每次发版都使用全新镜像 tag旧 Layer 永远不会被复用。建议构建镜像时尽量复用基础层Tag 不要每次都全量变。dfdaemon 缓存目录太小频繁被 LRU 淘汰。某个 Region 的 Peer 很少其他 Region 的 Peer 承载不了导致回源。有一种情况要特别注意容器运行时用 containerd 时镜像拉取会分 manifest 和 layer 两个步骤。如果 manifest 没有缓存每次都会先回源拿 manifest但 layer 层还是可以走 P2P。这不是故障但如果连 layer 都回源了就要考虑缓存淘汰策略了。4.5 第一次拉取慢用预热功能补齐短板P2P 分发最大的弱点是冷启动当一个镜像从来没有被任何 Peer 持有过第一个发起请求的节点需要等待 Seed Peer 回源下载完整 Layer这段时间和直接拉仓库没区别甚至更慢。解决方式是镜像预热。dragonfly Manager 提供了预热 API把即将发布的镜像预先推送到 Seed Peer 的缓存中。这样发版时所有节点直接从 Seed Peer 或互相分片拉取第一个节点也不会慢。调用方式很简单curl -X POST http://manager.example.com:8080/apis/v1/jobs \ -H Content-Type: application/json \ -d { type: preheat, args: { type: image, url: harbor.example.com/library/app:v1.2.3 } }预热任务执行后可以通过 Manager API 查询状态。建议在 CICD 流水线里集成一步构建完成后触发预热这能彻底解决冷启动速度慢的问题。当然预热也是要消耗 Seed Peer 磁盘和带宽的不要把所有历史镜像都无脑预热而是预热即将上线的那几个 tag。5. 除了镜像内网软件源缓存也值得一起做5.1 为什么推荐做内网软件源缓存聊完镜像缓存我想提一个和它思路类似的兄弟场景内网软件源缓存。很多团队只做了镜像仓库但代码拉取、依赖包下载还是走公网网络抖动一次构建就失败。其实容器镜像缓存和依赖缓存是同一类问题大并发下中心化资源访问压力大、依赖外部网络不稳定、重复下载浪费带宽。你可以用 Nexus Repository Manager 搭建内网统一的 Maven、npm、PyPI 代理缓存开发机和 CICD 流水线都指向内网 Nexus。第一次拉取时 Nexus 从公网源获取并缓存后续所有构建都走内网速度和稳定性都会有质的提升。同样的思路也适用于系统软件包比如 Ubuntu 的 apt 源、RHEL 的 yum 源都能用内网镜像仓库解决。既然是通用的缓存代理思想和 dragonfly 并不冲突反而互补。5.2 软件源缓存和 dragonfly 的协同思路dragonfly 解决的是镜像文件的 P2P 分发Nexus 解决的是依赖包的中心化缓存它们可以同时存在基础镜像由 dragonfly 分发保证节点间快速同步。应用依赖jar、npm 包由 Nexus 缓存保证构建环境一致。系统包源用内网 apt/yum 镜像保证系统初始化速度快。在架构上我会这样分工容器运行时层用 dragonfly构建工具链层用 Nexus操作系统层用内网软件源。三层都做内网化之后CICD 流水线几乎不依赖公网稳定性构建时长也会大幅下降。搭建 Nexus 并不复杂以 Docker 方式启动后只需要在 Maven settings.xml、npm config 或 pip config 中把 registry 地址指向 Nexus 的仓库组地址。有一点要注意不要把所有公网依赖盲目缓存按团队实际使用的版本白名单开启远程仓库即可否则 Nexus 磁盘很快会被大量无用的 npm 包塞满。最后再说几句我在实际使用 dragonfly 的过程中最深刻的体会是它不是一个装完就完事的工具而是一套需要持续观察和维护的数据面基础设施。部署只是开始真正有价值的是理解它的调度模型、缓存策略、预热机制然后在自己的环境里去验证和调优。如果你准备在生产环境落地有几个建议第一一定先在小规模集群跑透观察回源流量和分发效果第二把预热集成到发版流水线否则首次拉取的收益不明显第三dfdaemon 缓存目录挂载到独立 SSD 磁盘避免和系统盘争用 IO第四定期核对版本dragonfly 迭代很快新版本在调度和稳定性上经常有大改进。最后分享一个小技巧如果某个节点拉取异常别急着动 containerd先把/etc/dragonfly/dfget.yaml中的日志级别调到debug重启 dfdaemon再拉一次镜像信息量会非常大。九成问题在 debug 日志里都能找到根因。
返回列表