Dragonfly V2 分发集群的镜像
1. Dragonfly 简介
Dragonfly 的相关文档在社区 https://d7y.io/zh/docs/ 已经有详细说明。这里只是简单介绍一下,V2 版本的主要组件:
- Manager,提供 UI 界面、用户管理、集群监控、任务管理等功能
- Scheduler,调度 Peer 之间的流量、提供预热等功能
- Seed Peer,回源节点,用于从源站(Harbor、Docker.io 等)下载数据,也可以作为 Peer 节点
- Peer,提供下载数据的终端节点
其中 Manager、Scheduler 是单独的容器镜像,Seed Peer 和 Peer 是同一个容器镜像。
Dragonfly 支持的镜像预热功能,可以和 Harbor 进行集成,但本文不会涉及。本文主要是介绍我们在支撑 AI 业务时,生产环境下的一些实践。值得注意的是 Dragonfly V2 实际上构建了一个 P2P 分发的网络,不仅可以分发镜像,还可以分发文件,这就打开了想象空间。
2. IDC 机房中的 Dragonfly 集群
我们 AI 模型的推理和训练都是基于 Kubernetes 集群,后端存储采用的是企业版 JuiceFS,在每个 Node 节点都挂载了几个 T 的 SSD 磁盘,用来挂载 JuiceFS 的缓存目录。
因此,Kubernetes 集群中的每个 Node 节点都具备作为 Dragonfly Peer 节点的条件。但 Peer 组网时,我们不希望有额外的负担,包括:
- 跨 VPC 的 NAT 流量
- 公网传输数据
下面是 Dragonflyv2 机房多 VPC 部署拓扑图:
- LB 需要公网 IP,作为 Peer 的接入点
- 一个 VPC 对应一个 Dragonfly 的 Cluster 抽象
- 虽然 IDC 打通了 VPC 之间的网络,但一个 VPC 内的 Peer 才允许组网
- 集群内每个 Node 节点部署一个 Peer
VPC 内,下面这张图给出了详细的高可用方案。
- LB 只需要内网 IP 即可
- 使用云厂的 MySQL 8.0、Redis 6 服务
- 两台 VM 部署 Manager、Scheduler、Seed Peer
- 每个 VM 部署的是一套完整的 Dragonfly 集群,包括 Manager、Scheduler、Seed Peer,不用经过 LB 也能用
- 每个 Node 节点部署 Peer
Dragonfly 构建的 P2P 分发网络,不应该和 PaaS 层耦合太紧密,避免循环依赖。因此,这里采用双 VM 的方案,共享数据存储,保障可用性。在 Kubernetes 集群的 Master 节点上,我们也不会进行加速优化,保障 PaaS 层控制面的简洁和独立。
3. VM 上部署 Dragonfly 控制平面
需要提前安装好 Docker,分别在两台 VM 上进行独立部署。
3.1 安装 docker-compose
- 下载 docker-compose
curl -L https://mirror.ghproxy.com/https://github.com/docker/compose/releases/download/v2.23.3/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
- 添加执行权限
chmod +x /usr/local/bin/docker-compose
- 查看版本
docker-compose -v
3.2 安装 dragonfly
参考 https://d7y.io/zh/docs/getting-started/quick-start/docker-compose/
- 下载 docker-compose 部署文件
cd /data
wget https://mirror.ghproxy.com/https://github.com/dragonflyoss/Dragonfly2/archive/refs/tags/v2.1.28.tar.gz
tar -zxvf v2.1.28.tar.gz
cp -r Dragonfly2-2.1.28/deploy/docker-compose ./
- 清理不需要的文件
rm -rf *2.1.28*
- 生成默认的配置文件
cd docker-compose
由于默认的发布包中,没有配置文件,这里先生成一份配置文件,然后再修改。
export IP=VM_IP
./run.sh
立即终止执行,然后继续修改配置文件。
- 固定镜像版本
sed -i 's/latest/v2.1.28/g' docker-compose.yaml
- 修改存储账号及其他配置
修改 Redis、MySQL 地址和密码
vim template/manager.template.yaml
修改 Redis 密码
vim template/scheduler.template.yaml
在这两个配置文件中,还有一些其他的配置项,可以根据实际情况进行修改。比如,manager 的 addr 指向当前主机的服务、将日志输出到控制台、开启 Metrics 等。
- 修改 seed-peer 的缓存目录
vim docker-compose.yaml
volumes:
- ./cache:/var/cache/dragonfly
- ./data:/var/lib/dragonfly
如果关闭了 seed-peer 作为 peer 节点的功能,可以跳过这一步,同时,VM 的磁盘空间也可以不用太大。
- 启动服务
docker-compose up -d
- 查看服务
docker-compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
manager dragonflyoss/manager:v2.1.28 "/opt/dragonfly/bin/…" manager 14 hours ago Up 14 hours (healthy) 0.0.0.0:8080->8080/tcp, 0.0.0.0:65003->65003/tcp
scheduler dragonflyoss/scheduler:v2.1.28 "/opt/dragonfly/bin/…" scheduler 14 hours ago Up 14 hours (healthy) 0.0.0.0:8002->8002/tcp
seed-peer dragonflyoss/dfdaemon:v2.1.28 "/opt/dragonfly/bin/…" seed-peer 14 hours ago Up 14 hours (healthy) 65001/tcp, 0.0.0.0:65006-65008->65006-65008/tcp
- 打开管理页面看看
访问 http://${VM_IP}:8080
端口可以看到 Dragonfly 的管理界面,如果机器没有公网 IP,可以使用 socat 进行端口转发。找一台有公网 IP 的机器,执行以下命令,将 30000 端口转发到 8080 端口:
export IP=VM_IP
socat TCP-LISTEN:30000,fork TCP:$IP:8080
两台 VM 部署完成,在 Dashboard 中可以看到这样一个集群,两个 Scheduler、两个 Seed Peer。如下图:
4. 在集群部署 Peer 节点
- 创建命名空间
kubectl create ns dragonfly-system
- 创建配置文件
这里需要将 LB 的 IP 地址填入配置文件中,Peer 才能接入到 Dragonfly 集群中。
export MANAGER_IP=LB_IP
有很多参数,可以根据实际情况进行修改,这里提供了一份默认的配置文件。
kubectl apply -f - <<EOF
apiVersion: v1
data:
dfget.yaml: |
aliveTime: 0s
gcInterval: 1m0s
keepStorage: false
workHome: /usr/local/dragonfly
logDir: /var/log/dragonfly
cacheDir: /var/cache/dragonfly
pluginDir: /usr/local/dragonfly/plugins
dataDir: /var/lib/dragonfly
console: true
health:
path: /server/ping
tcpListen:
port: 40901
verbose: true
pprof-port: 18066
metrics: ":8000"
jaeger: ""
scheduler:
manager:
enable: true
netAddrs:
- type: tcp
addr: $MANAGER_IP:65003
refreshInterval: 10m
netAddrs:
scheduleTimeout: 30s
disableAutoBackSource: false
seedPeer:
clusterID: 1
enable: false
type: super
host:
idc: ""
location: ""
download:
calculateDigest: true
downloadGRPC:
security:
insecure: true
tlsVerify: true
unixListen:
socket: ""
peerGRPC:
security:
insecure: true
tcpListen:
port: 65000
perPeerRateLimit: 512Mi
prefetch: false
totalRateLimit: 1024Mi
upload:
rateLimit: 1024Mi
security:
insecure: true
tlsVerify: false
tcpListen:
port: 65002
objectStorage:
enable: false
filter: Expires&Signature&ns
maxReplicas: 3
security:
insecure: true
tlsVerify: true
tcpListen:
port: 65004
storage:
diskGCThreshold: 1000Gi
multiplex: true
strategy: io.d7y.storage.v2.simple
taskExpireTime: 72h
proxy:
defaultFilter: Expires&Signature&ns
defaultTag:
tcpListen:
port: 65001
security:
insecure: true
tlsVerify: false
registryMirror:
dynamic: true
insecure: false
url: https://index.docker.io
proxies:
- regx: blobs/sha256.*
security:
autoIssueCert: false
caCert: ""
certSpec:
dnsNames: null
ipAddresses: null
validityPeriod: 4320h
tlsPolicy: prefer
tlsVerify: false
network:
enableIPv6: false
announcer:
schedulerInterval: 30s
kind: ConfigMap
metadata:
labels:
app: dragonfly
name: dragonfly-dfdaemon
namespace: dragonfly-system
EOF
- 创建 DaemonSet
我们从官方的 Helm Chart 中提取出来的 DaemonSet 文件。需要注意的是,Peer 使用的缓存目录是主机上的 /data/dfget
目录。最好提前清理主机上的 /data/dfget
目录,避免出现权限问题,也不用提前创建,DaemonSet 会自动创建。
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app: dragonfly
name: dragonfly-dfdaemon
namespace: dragonfly-system
spec:
selector:
matchLabels:
app: dragonfly
template:
metadata:
labels:
app: dragonfly
spec:
containers:
- image: dragonflyoss/dfdaemon:v2.1.28
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:65000
name: dfdaemon
ports:
- containerPort: 65001
protocol: TCP
- containerPort: 40901
hostIP: 127.0.0.1
hostPort: 40901
protocol: TCP
- containerPort: 8000
protocol: TCP
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:65000
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources:
limits:
cpu: "2"
memory: 2Gi
securityContext:
capabilities:
add:
- SYS_ADMIN
volumeMounts:
- mountPath: /etc/dragonfly
name: config
- mountPath: /var/cache/dragonfly
name: dfgetcache
- mountPath: /var/lib/dragonfly
name: dfgetdata
hostNetwork: true
hostPID: true
volumes:
- configMap:
defaultMode: 420
name: dragonfly-dfdaemon
name: config
- hostPath:
path: /data/dfget/cache
type: DirectoryOrCreate
name: dfgetcache
- hostPath:
path: /data/dfget/data
type: DirectoryOrCreate
name: dfgetdata
EOF
- 查看负载
kubectl -n dragonfly-system get pod
NAME READY STATUS RESTARTS AGE
dragonfly-dfdaemon-79qkw 1/1 Running 0 14h
dragonfly-dfdaemon-8hhzb 1/1 Running 3 14h
dragonfly-dfdaemon-nnfc5 1/1 Running 0 14h
dragonfly-dfdaemon-w7lff 1/1 Running 0 14h
dragonfly-dfdaemon-wrmzw 1/1 Running 0 14h
5. 在 VM 上部署 Peer 节点
- 创建目录
mkdir -p /data/dfget && cd /data/dfget
- 设置 IP
wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/shaowenchen/hubimage/main/nydus/dfget.template.yaml -O dfget.yaml
export MANAGER_IP=LB_IP
sed -i "s/__MANAGER_IP__/$MANAGER_IP/g" dfget.yaml
- 启动 Peer
nerdctl run -d --name=peer --restart=always \
-p 65000:65000 -p 65001:65001 -p 65002:65002 \
-v $(pwd)/data:/var/lib/dragonfly \
-v $(pwd)/cache:/var/cache/dragonfly \
-v $(pwd)/dfget.yaml:/etc/dragonfly/dfget.yaml:ro \
dragonflyoss/dfdaemon:v2.1.28
6. 使用节点配置
6.1 Docker
Docker 的 Mirror 方式只能加速 Docker.io 的镜像,这里采用 Proxy 的方式,代理全部 Dockerd 的流量。Proxy 与 Mirror 的区别在于,Mirror 挂了,Dockerd 会拉取源站,而 Proxy 挂了,Dockerd 直接拉取失败。
- 添加代理
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:65001"
Environment="HTTPS_PROXY=http://127.0.0.1:65001"
EOF
- 重启 Docker
systemctl daemon-reload
systemctl restart docker
注意,这里如果 /etc/docker/daemon.json
中没有配置 "live-restore": true
,会导致容器全部重启。
- 查看环境变量
systemctl show --property=Environment docker
Environment=HTTP_PROXY=http://127.0.0.1:65001 HTTPS_PROXY=http://127.0.0.1:65001
- 镜像拉取测试
docker pull nginx
此时,Dockerd 的流量会经过 Dragonfly Peer 节点。
6.2 Containerd
参考 https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
在 /etc/containerd/config.toml
中 [plugins."io.containerd.grpc.v1.cri".registry]
项的 config_path = "/etc/containerd/certs.d"
提供了类似于 mirror 的配置方式。
- 配置 Docker.io
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<EOF
server = "https://docker.io"
[host."http://127.0.0.1:65001"]
capabilities = ["pull", "resolve"]
[host."http://127.0.0.1:65001".header]
X-Dragonfly-Registry = ["https://registry-1.docker.io"]
[host."https://registry-1.docker.io"]
capabilities = ["pull", "resolve"]
EOF
- 配置其他、私有镜像仓库
其他镜像仓库的配置可以通过脚本生成,比如:
wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/dragonflyoss/Dragonfly2/main/hack/gen-containerd-hosts.sh
bash gen-containerd-hosts.sh ghcr.io
这里没有用脚本生成 docker.io 的配置是因为,生成的配置文件中,X-Dragonfly-Registry
是 https://docker.io
,而不是 https://registry-1.docker.io
。
使用 X-Dragonfly-Registry = ["https://docker.io"]
会出现如下错误:
unknow type: text/html
以上添加的 mirror,不用重启 Containerd,直接能生效。
- 镜像拉取测试
nerdctl pull nginx
此时,在本地 /data/dfget/data
目录下可以看到 Peer 节点缓存的镜像数据。
7. 集成 Nydus
如果 Nydus 已经配置好,这里其实已经能轻松配置好。
- 给 Nydusd 添加 mirror
vim /etc/nydus/nydusd-config.fusedev.json
{
"device": {
"backend": {
"type": "registry",
"config": {
"mirrors": [
{
"host": "http://127.0.0.1:65001",
"auth_through": false,
"headers": {
"X-Dragonfly-Registry": "https://index.docker.io"
},
"ping_url": "http://127.0.0.1:40901/server/ping"
}
]
}
}
}
}
- 重启 Nydusd
systemctl restart nydus-snapshotter
- 镜像拉取测试
nerdctl pull shaowenchen/demo-ubuntu:latest-nydus
8. 总结
本篇记录了这周在生产环境中,测试并部署 Dragonfly V2 的部分过程,主要内容包括:
- 机房中 Dragonfly 集群的部署拓扑
- 集群和 VM 上 Peer 节点的部署
- Docker、Containerd、Nydus 的集成
说下不足,没有指标监控,在做 Benchmark 时,我们发现 AZ 内和跨 AZ 的 Peer 之间的数据传输都受限,如果想构建高性能的 P2P 分发网络,Peer 与 Peer、Peer 与 Seed Peer 之间的网络是一个重要的考量因素。