背景

现在有两台服务器,一台是位于中国成都内地的 ESC 实例 A(20M带宽)

另外一台是位于吉隆坡的轻量应用服务器 B (峰值200M带宽)

在这之前,我比较喜欢使用 1Panel 面板可视化管理服务器。1Panel 面板是基于 Docker 的管理面板,它的应用体系基本依靠 Docker Compose。

1Panel 面板官方提供了一个应用商城。该应用商城分为两个部分,一个是官方提供的应用,另外一个部分是可以从本地的目录中同步应用。鉴于官方提供的应用较少且覆盖不完整,我配置了第三方应用商城以及个人的 Docker Compose 编排,每天定时更新。

OMMKr.png

由于新买的服务器在中国成都,受到网络条件制约,目前国内主流的 Docker 镜像源要么收费,要么相对不稳定,因此,我在位于吉隆坡的服务器 B 使用 Docker 官方的 Registry 搭建了简单的 Docker 镜像源,当接到拉取请求时,吉隆坡的服务器先从 DockerHub 拉取镜像到本地,再发送给请求者,镜像本机缓存 24 小时。

OMJQ4.png

准备安装的 astrbot + napcat 的容器编排如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
networks:
1panel-network:
external: true

services:
napcat:
image: mlikiowa/napcat-docker:v4.18.18
container_name: napcat-${CONTAINER_NAME}
restart: always
networks:
- 1panel-network
ports:
- ${PANEL_APP_PORT_NAPCAT}:6099
mac_address: ${NAPCAT_MAC_ADDRESS:-02:42:ac:11:00:02}
env_file:
- ${GLOBAL_ENV_FILE:-/etc/1panel/envs/global.env}
- ${ENV_FILE:-/etc/1panel/envs/default.env}
volumes:
- ${ASTRBOT_ROOT_PATH}/data:/AstrBot/data
- ${ASTRBOT_ROOT_PATH}/ntqq:/app/.config/QQ
environment:
- TZ=Asia/Shanghai
- MODE=astrbot
- NAPCAT_UID=${NAPCAT_UID:-1000}
- NAPCAT_GID=${NAPCAT_GID:-1000}
astrbot:
image: soulter/astrbot:v4.27.2
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
restart: always
networks:
- 1panel-network
ports:
- ${PANEL_APP_PORT_HTTP}:6185
- ${PANEL_APP_PORT_QQ_WH}:6199
- ${PANEL_APP_PORT_QQ_API}:6196
- ${PANEL_APP_PORT_WECOM}:6195
- ${PANEL_APP_PORT_WECHAT}:11451
env_file:
- ${GLOBAL_ENV_FILE:-/etc/1panel/envs/global.env}
- ${ENV_FILE:-/etc/1panel/envs/default.env}
volumes:
- /etc/localtime:/etc/localtime:ro
- ${ASTRBOT_ROOT_PATH}/data:/AstrBot/data
- ${ASTRBOT_ROOT_PATH}/ntqq:/app/.config/QQ
- ${ASTRBOT_ROOT_PATH}/napcat/config:/app/napcat/config
environment:
- TZ=Asia/Shanghai

可以看到,这一个编排中有两个容器

一个容器是mlikiowa/napcat-docker:v4.18.18

另外一个容器是soulter/astrbot:v4.27.2

问题出现

我在中国成都的内地服务器中从 1Panel 的应用商城安装,底层仍然是容器编排,暴露出的信息如下

OMzAQ.png

OMdEJ.png

拉取的实时网络:

OMxzK.png

可以看到实时的网速只有 75KB 每秒左右,很慢

但是拉取其他的镜像,比如 nacos

OMT1Z.png

速度就是正常的 10MB/s。因此应该不是成都服务器的问题(但是我服务器不是只有 20M 吗?服务器购买的时候的带宽,应该是指的出口带宽,入口带宽应该是动态的。但根据阿里云官方文档,出口带宽理论应该与入口带宽保持一致,下限为 20 Mbps)

手动运行 docker compose pull 命令拉取镜像

OMc8c.png

OM03O.png

12.05 MB/s,拉取速度一切正常

会不会是两台服务器之间通信的问题?在此使用 iperf3 进行服务器间测速:

OM2om.png

可见两台服务器之间的连接速度是一切正常的,达到了(甚至超过了)理论峰值速度。因此理论上应该不是两台服务器之间的通信速率问题。

那可能是 1panel 面板的问题。在此之前, 1panel 面板在我的理解中一直是依赖 docker,通过 Docker compose 编排安装的应用。有没有可能它在里面添加了自己的小巧思,修改了部分逻辑呢?
感谢万能的 chatgpt ,直接阅读 1panel 面板的源码和 pr 分析相关的信息,让我这个完全不会 go 语言的也能完成分析

简单来说,在服务器中,docker 可以简单理解成 docker enginedocker cli两部分。其中直接在命令行输入的 docker pull 命令,会唤起 docker clidocker cli再通过 API 向 docker engine 发送拉取指令,由 docker engine 实际拉取。在拉取过程之中,docker engine 会实时向 docker cli 反馈下载进度,并打印在控制台中。

使用 docker compose pull 指令手动拉取目标编排,docker cli 会默认同时拉取两个镜像,两个镜像的拉取之间是并行的。

如果使用 1panel 安装应用,对于一个包含多个镜像的 docker compose 编排,面板会先解析其中包含的镜像,再通过串行的方式逐个拉取。

比如当前编排中两个镜像,mlikiowa/napcat-docker:v4.18.18soulter/astrbot:v4.27.2,面板会先拉取mlikiowa/napcat-docker:v4.18.18 ,在该镜像拉取完成之后再开始拉取soulter/astrbot:v4.27.2

与 AI 求证,并且简单翻阅源码之后,发现面板并非直接简单使用 docker pull 命令拉取,而是直接与 docker engine 通信。更准确的讲,在 PR#7955 之前,Docker 在拉取镜像的时候,是通过 docker cli 运行 docker pull 命令拉取的。但在该修改之后,面板会直接与 docker engine 通信,通过 API 拉取。

这样怀疑范围又小了一部分。在对整体拉取速度进行统计之后,就确定问题出现于~~~~`mlikiowa/napcat-docker:v4.18.18`~~~~ 上。

在进行更加细致的观察后,发现并不是单一容器的问题,而是对于某些镜像的某些特定 layer 存在下载速度过慢的问题而不是局限于整体的容器拉取速度。

于是手动修改配置文件,将 Docker 的同时下载的 Layer 数量改为 1 (在默认情况下,Docker 会同时下载的 Layer 数量为 3)

1
2
3
4
5
6
{
"registry-mirrors": [
"https://docker.***.***"
],
"max-concurrent-downloads": 1
}

此后再在服务器和本机上不同的环境下进行拉取修改测试

全量拉取比较各个 layer 拉取速度后发现是两个镜像的这两个 Layer 拉起速度较慢。

Napcat: 99adaac412a4

Astrbot: 1284b755ec6a

拿 napcat 的该 layer 测试,直接使用 curl 命令,手动拉取源数据并指定下载。

1
2
3
4
curl -s \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
https://docker.***.***/v2/mlikiowa/napcat-docker/manifests/v4.18.18 \
| jq

得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:cbcfebdc2656f7f7ff388ddfe0762b192335572a6e2bdfb576aa4246edb19943",
"size": 1628,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:9478065f1ca882a1a73dffdec958f947f24f8086d18548f27b37b432c21c5ace",
"size": 1628,
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:9202418e2e6946c5bea024374a62a7d79ae63ab947284692c94bf47ec5b0ae08",
"size": 564,
"annotations": {
"vnd.docker.reference.digest": "sha256:cbcfebdc2656f7f7ff388ddfe0762b192335572a6e2bdfb576aa4246edb19943",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:a762cef58e8e48b6064f42f224e447f3110dd164d8fbb0613b49e5324848b7d4",
"size": 564,
"annotations": {
"vnd.docker.reference.digest": "sha256:9478065f1ca882a1a73dffdec958f947f24f8086d18548f27b37b432c21c5ace",
"vnd.docker.reference.type": "attestation-manifest"
},
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}

这是napcat的多架构镜像顶层索引,因为服务器是 X86 架构,因此选用 architecture amd64,也就是

“digest”: “sha256:cbcfebdc2656f7f7ff388ddfe0762b192335572a6e2bdfb576aa4246edb19943”

再获取对应镜像的layer信息

1
2
3
4
curl -s \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
https://docker.***.***/v2/mlikiowa/napcat-docker/manifests/sha256:cbcfebdc2656f7f7ff388ddfe0762b192335572a6e2bdfb576aa4246edb19943 \
| jq

得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:0f76fde5ecb3d738f970e8b6fafff8a151540907fde308221e781b87b0992b94",
"size": 5110
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:6414378b647780fee8fd903ddb9541d134a1947ce092d08bdeb23a54cb3684ac",
"size": 29535688
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:490f82e472ca8db02fef02d020d6cadeb58dea37642c5d42814fa9819fe58eb6",
"size": 271963756
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:7c7b5cd01343fbc3495bf320ebcc9a413a180551d49146885cfbe6e1426d6b21",
"size": 1412
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:47e443e2d71a3cd56adf54144a2b7d13e7791fdb17b965cea974b3f9bbf5791a",
"size": 93
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:81c066f39089ddcace93effcec25665495e3d4d7ca7896d98eea4d9cf4dbf29b",
"size": 28791179
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:99adaac412a4a27d8281d9004e797149f3d6f7cb5e8fb51bc073c31b2361eb49",
"size": 267101944
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:dc00ce595fccf890cd8505403725479661a93194c6ff8350d1841d22629b7870",
"size": 3167
}
]
}

找到对应的 layer

OMXKl.png

使用指令进行手动下载

1
2
3
4
curl -L \
-o /dev/null \
-w $'\nHTTP=%{http_code}\nSIZE=%{size_download} bytes\nSPEED=%{speed_download} bytes/s\nTIME=%{time_total}s\n' \
"https://docker.***.***/v2/mlikiowa/napcat-docker/blobs/sha256:99adaac412a4a27d8281d9004e797149f3d6f7cb5e8fb51bc073c31b2361eb49"

在不同的情况下多次尝试下载

省流:

在成都云服务器上,多次下载速度分别为(单位MBps):11.1、11.5、5、0.1、13、6.2、7、4.5、11.9、11.6、10.3、12.1、1.9

在本机上,多次下载速度分别为(单位MBps):4.85、0.018、0.042、14.88、8.84、0.002、0.006,3.7、11.7、7、6.7

由数据可知,下载速率是随机波动的

那么有没有可能不只是这一个 Layer 下载波动,其他的 Layer 也存在相同的问题呢?现在随机挑选了一个 24MB 的 Layer,多次下载测试。

在成都云服务器上,多次下载速度分别为:11.7、12.3、11.9、4、12.5、12.3、11.9、13.1、3.1、4.8、12.7

在本机上,多次下载速度分别为:5、6.5、5.4、6.5、5.7、5.8、4.9、6.8、4.4、4.1、4.9

整体的下载速度都相对不怎么稳定,时快时慢的。那么如果是直接从 Docker Hub 拉取呢?

在本地环境直接挂梯从 dockerhub 拉取,测试10次速率恒定,都是稳定跑慢速

感觉可能不是拉取问题,而是涉及到 tcp 网络连接,但是当前的精力不足可能得过段时间再继续了,目前服务器中仍然在跑定时脚本收集数据

Docker 缓存

在对比测试的时候,某一次在成都的机器上拉取两个镜像时,出现了没有流量流入,而是秒拉取成功。疑问是可能是 Docker 有潜在的缓存机制

一个 Docker 的镜像有很多个 Layer 组成,运行 docker pull 命令拉取镜像时,会逐个 Layer 下载(官方默认线程为 3,也就是同时下载 3 个 Layer)。已经下载好的 layer 会放在 content store 中,下次如果需要下载相同的layer就可以直接使用本地已有的不需要联网重下

Docker 默认的 content store存放位置在 /var/lib/containerd/io.containerd.content.v1.content

OMvgp.png

作为测试用途,我必须清理掉 content store 的缓存。但是官方似乎并没有给出一个单独的清理方案

github中也有相似的issues。根据实测使用docker image prune -a命令可以在清理掉未被引用的镜像的同时一起清理掉content store缓存(这似乎是一个bug)