基准测试
在同一台小型服务器上测试 Mistgate、3x-ui、Remnawave 和 PasarGuard 的内存、CPU、磁盘、启动时间和订阅负载,并在单服务器上测试 VPN 核心和 Hysteria2 流量。包含方法、版本和限制。
本页内容
摘要#
- 内存占用差异明显。 用户数为 0 到 200 时,Mistgate 面板本身占用 16–20 MiB 私有内存;3x-ui 为 68–74 MiB,PasarGuard 为 262–265 MiB,Remnawave 为 426–454 MiB。在同一服务器上运行 VPN 核心和数据库时,分别为 57 MiB、74 MiB、297 MiB 和 761 MiB。
- 用户变更和重启不会影响隧道。 Hysteria2 隧道运行期间,Mistgate 添加或删除用户、重启面板都不会断开连接。3x-ui 会在用户变更时重启 Xray 核心。
- 暂不对隧道速度作出结论。 使用修正后的测试工具首次完整测试时,机器上的其他负载干扰了结果(同一面板的多次测试结果最多相差 3 倍),因此不发布阶段 B 和阶段 D 中隧道部分的数据,并会在专用 CPU 核心上重新测量(参见结果:单服务器)。此前的数据(四个面板彼此相差不超过 17%)使用了存在测试工具故障的版本,相关问题见修复之后;这些数据已撤回。
- Mistgate 的薄弱环节。 负载隧道中的小请求 p95 为 29 ms(1 vCPU 时为 67 ms),其他面板约为 2 ms;经查,这与测试设置有关(参见修复之后)。修正测试工具后是否仍有差距,将在隧道复测时确认,因此本页暂不对延迟作出结论。未命中缓存的订阅渲染速度较慢(修改版构建在单个 vCPU 上每秒处理 1,630 个请求,p99 为 1.7 s)。性能剖析发现的三项修复已随 v0.1.33 发布,未缓存路径的速度约翻倍;参见修复之后。通过 API 创建 1000 个用户耗时 49 s,而 3x-ui 和 Remnawave 为 24 s,主要原因是管理 API 的速率限制(参见单服务器表格下方的说明)。
- 未测量已发布构建的订阅吞吐量:每个链接每小时最多获取 60 次,因此负载测试只测量了它拒绝请求的速度。
- 测试使用一台桌面计算机上的一个 CPU、一个 WSL2 环境,并重复 3 次:引用任何数据前,请先阅读这些数据不能说明什么。
仅面板表格:测于 2026 年 10 月 9 日和 10 日(Mistgate v0.1.32)。单服务器表格:于 2026 年 10 月 12 日重新测量(Mistgate v0.1.33,使用修正后的测试工具)。除标注 n=1 的单元格外,数据均为 3 次测试的中位数,方括号中为最小值和最大值。所有测试都在一台桌面计算机的 WSL2 环境中运行。脚本、compose 文件和汇总结果位于仓库中的 tools/bench。
测量内容#
按各项目文档所述方式安装四个面板,并使用各自的默认设置:
| 面板 | 版本 | 安装方式 | 数据库 | 测量组中的进程 |
|---|---|---|---|---|
| Mistgate | 仅面板表格使用 v0.1.32,单服务器表格使用 v0.1.33(从标签构建,未使用发布密钥) | 一个静态二进制文件、mistgate setup、mistgate serve |
SQLite | 1 |
| 3x-ui | v3.9.0 | 已发布镜像 ghcr.io/mhsanaei/3x-ui:v3.9.0、项目 compose 文件 |
SQLite | 3:x-ui、其管理的 Xray 核心、fail2ban |
| Remnawave | 面板 3.4.5 | 官方 docker-compose-prod.yml(后端、Postgres 18.4、Valkey 9),固定镜像版本 |
PostgreSQL | 3 个容器中的 22 个进程 |
| PasarGuard | v5.4.1 | 项目默认的 SQLite compose 文件 | SQLite | 1 个(Python) |
两种测试场景:
- 仅面板:小型面板主机,1 vCPU、1 GB RAM、无交换分区、无 VPN 流量。测量空闲内存和 CPU、磁盘、启动时间、通过 API 创建 200 个用户,以及订阅负载(50 个客户端、60 s、200 个用户)。
- 单服务器:面板、VPN 核心(或节点)和面板数据库都运行在同一台 2 vCPU、2 GB RAM 且无交换分区的服务器上,包含 1000 个用户;所有面板均使用 Hysteria2。
结果:仅面板(1 vCPU,1 GB)#
内存指标以私有内存(anon)为主。旁边也列出 memory.current,因为 docker stats 和 systemctl status 会显示该值;不过它包含页缓存,而且缓存计费方式不一致(参见注释)。
| Mistgate | 3x-ui | PasarGuard | Remnawave | |
|---|---|---|---|---|
| 磁盘:镜像或二进制文件(MB) | 62 | 462 | 462 | 1551 |
| 磁盘:200 个用户时的数据目录(MB) | 5.2 | 4.8 | 4.6 | 61 |
| 空闲、0 个用户:anon(MiB) | 16 (16-16) | 68 (68-68) | 262 (262-262) | 426 (425-430) |
| 空闲、0 个用户:memory.current(MiB) | 27 (17-29) | 186 (185-186) | 358 (295-358) | 599 (595-746) |
| 空闲、200 个用户:anon(MiB) | 20 (20-21) | 74 (72-74) | 265 (265-265) | 454 (452-458) |
| 空闲、200 个用户:memory.current(MiB) | 27 (26-29) | 154 (153-155) | 299 (299-303) | 644 (629-649) |
| 空闲、200 个用户:CPU(占一个 vCPU 的百分比) | 0.07 | 0.40 | 0.15 | 3.09 |
| 进程 / 容器 | 1 / 0 | 3 / 1 | 1 / 1 | 22 / 3 |
| 通过 API 创建 200 个用户(s) | 0.3 | 1.9 | 2.3 | 1.2 |
| 冷启动,空数据目录(s) | 0.33 (0.25-0.33) | 2.12 (2.07-2.12) | 11.6 (11.0-11.9) | 14.2 (14.1-15.0) |
| 冷启动,磁盘上有 200 个用户(s) | 0.13 | 1.51 (1.46-1.92) | 6.7 (6.5-7.0) | 12.9 (12.5-13.1) |
| 订阅负载:运行期间的 anon(MiB) | 27 (27-28) | 85 (85-86) | 280 | 870 (856-874) |
| 订阅负载:memory.current 峰值(MiB) | 35 (34-37) | 177 | 325 | 1024 (达到上限) |
表格说明:
- 冷启动时间是从启动命令到面板主端口首次返回 HTTP 200 的时长。3x-ui 的订阅监听器(端口 2096)比主端口稍晚启动;3 次重复测试中,有 2 次在重启后立即检查该端口时被拒绝,因此这两次测试缺少重启后的内存样本。两次测试的其他数据均完整。
- 负载测试期间 Remnawave 达到 1 GB 上限:3 次重复测试中,cgroup 记录了 47,000 到 131,000 个
max事件(分配时必须回收内存),没有发生 OOM kill。下方仅设置配额的单次测试中,有一个 worker 被终止。负载期间约 870 MiB 的私有内存是真实占用,不是缓存。该进程组由 Postgres、Valkey 和三个 Node.js 进程组成。 - 测量组包含 3x-ui 的 Xray 核心(anon 约 9–12 MiB)和 fail2ban(约 12 MiB)。如果只比较面板进程,请从数据中扣除这些占用:x-ui 本身约为 55 MiB。
- PasarGuard 和 Remnawave 的测量组中没有运行 VPN 核心(未连接节点);Mistgate 也没有连接节点(为使订阅内容非空而临时需要的 Agent 已在测量前移除,节点记录仍保留)。
- 每次启动前清除页缓存(
drop_caches,每个面板重复一次)的第二轮测试得到了相同的 anon 数据(用户数为 0 / 200 时,Mistgate 为 16 / 20 MiB,3x-ui 为 62 / 75 MiB,PasarGuard 为 260 / 265 MiB,Remnawave 为 639 / 616 MiB);Mistgate 的memory.current更高(30 / 38 MiB),因为此时二进制文件是在被测 cgroup 中读取的。
订阅负载#
50 个并发客户端,运行 60 s,使用相同的 User-Agent(v2rayN/7.5.0)、200 个链接并启用 keep-alive。每个面板都为每个用户返回一个链接,以 base64 编码,文本正文为 152 到 360 字节。链接格式因面板而异(Mistgate 使用 Hysteria2,3x-ui 使用 VLESS Reality,Remnawave 和 PasarGuard 使用 Shadowsocks),参见文末说明。
| Mistgate(已发布构建) | 3x-ui | PasarGuard | Remnawave | |
|---|---|---|---|---|
| 每秒请求数,状态码 200 | 197 (limiter, see below) | 148 (147-153) | 274 (268-284) | 271 (166-307) |
| p50 / p95 / p99 延迟(ms) | 2.5 / 5.6 / 7.4 | 296 / 511 / 1230 | 166 / 378 / 519 | 113 / 230 / 1765 |
| 非 200 响应 | 99.4 % | 0 | 0 | 0 |
| 平均 CPU(占一个 vCPU 的百分比) | 98 | 41 | 100 | 99 |
按设计,Mistgate 的已发布版本不会处理这类负载。 每个订阅链接每小时最多可获取 60 次;第 61 次及之后的请求会收到 429。每次测试中,恰有 11,799 个请求得到 200 响应(每个链接每小时 60 次,扣除每个链接的一次预热请求和负载测试前的一次检查请求),另有 160 万到 200 万个请求得到 429。429 路径的开销很低(单个 vCPU 每秒可返回 27,000 到 34,000 个响应),因此首行的「每秒请求数」表示拒绝请求的速度,而非处理请求的速度。这不是有效的测试结果。本次测试并未受公共监听器按客户端地址设置的令牌桶限制(每秒 10 个请求,突发容量 60):在 --trusted-proxy 后方,每个链接都通过 X-Forwarded-For 使用自己的客户端地址;关闭每链接限制后,同一个令牌桶才成为瓶颈,限制约为每秒 2,590 个响应(首次空跑测试)。
3x-ui 每秒处理 148 个请求时仅使用一个 vCPU 的 41%:其瓶颈并非 CPU(每个响应后都会关闭连接;未调查原因)。
补充:关闭限制的 Mistgate(修改版构建)#
这不是已发布的构建。这两个构建修改了 v0.1.32 源码中的一两行(差异见 mistgate-bench-variants.diff),以便将请求开销与其他面板比较。不得将其数据作为 Mistgate 的测试结果引用。
limits off, render cache on (nolimit) |
limits off, render cache off (nolimit-nocache) |
|
|---|---|---|
| 更改内容 | 关闭每链接上限和每地址令牌桶 | 同左,并关闭链接的 10 s 渲染缓存 |
| 每秒请求数,状态码 200 | 19,580 (19,331-19,614) | 1,630 (1,612-1,633) |
| p50 / p95 / p99 (ms) | 2.4 / 5.4 / 7.2 | 4.2 / 6.6 / 1,702 |
| 非 200 响应 | 0 | 0 |
| 平均 CPU 占用率 | 100 % | 100 % |
关闭缓存后,每个请求都会读取数据库(包含多个语句,SQLite 会重新解析每条语句);开启缓存时,每个链接最多每 10 s 读取一次。未缓存路径的性能剖析见性能剖析。未缓存路径的 1.7 s p99 是少量请求受到饥饿所致(整个测试期间始终约为每秒 25 个请求),并非暂停;将 GOMAXPROCS=4 后这一问题消失(p99 为 86 ms,吞吐量相同)。
结果:单服务器(2 vCPU,2 GB,无交换分区),Hysteria2,1000 个用户#
各面板在 2 vCPU、2 GB 的资源组中运行的进程:
| 面板 | 组内进程 |
|---|---|
| Mistgate | 面板(原生进程)和 mistgate-node(容器) |
| 3x-ui | 一个容器:x-ui、Xray 核心、fail2ban |
| Remnawave | 后端、Postgres、Valkey 和 remnawave/node(其中包含 Xray 核心) |
| PasarGuard | 面板容器和 pasarguard/node(其中包含 Xray 核心) |
每台服务器都使用自签名证书;客户端不验证证书(所有面板的设置相同)。通信双方都使用 BBR 拥塞控制,未设置带宽限制,也未启用混淆。客户端使用 hyload,这是基于 Hysteria 2 客户端库(core/v2 v2.12.3)构建的小型程序;它在资源组之外的独立 cgroup 中运行,不受 CPU 限制,CPU 占用为可用 16 个 vCPU 中一个的 150–340%,而资源组受配额限制在 200%。每个核心容器(Mistgate 的节点和其他三个面板的 Xray 核心)都将 GOMAXPROCS 设为资源组的 vCPU 数,这与 Go 运行时在相同 vCPU 数的真实服务器上选择的值一致;如果不这样设置,容器会根据主机的 16 个 CPU 设置 GOMAXPROCS(参见修复之后)。此外,使用官方 hysteria v2.13.0 二进制文件确认隧道正常工作(通过隧道在 5 s 内传输 1.75 GB)。
测试阶段:A 为 1000 个用户空闲状态(稳定等待 60 s,测量 120 s);B 为一条隧道上的 1、16 和 64 个并行下载(各 30 s),每 100 ms 通过同一隧道发送一个小型请求(已测量,未公布,见下文);C 为 400 个客户端同时连接,每 2 s 发送一个小型请求(60 s);D 为持续 120 s 的「高负载日」(64 个数据流、来自 1000 个源地址的订阅请求风暴,每秒 300 个请求,并通过管理 API 每秒添加一个用户、删除一个用户);E 为在 60 s 测试运行到 20 s 时重启面板(不重启核心),同时保持 64 个数据流。
重复测试 3 次取中位数,括号中为最小值至最大值。「私有内存」为整个资源组的 anon 内存加内核内存。
表格未包含的内容。 阶段 B(单条隧道,包含 1、16 和 64 个数据流)、阶段 D 中隧道相关的数据(吞吐量、与阶段 B 相比的下降幅度、通过隧道发送的小型请求)以及阶段 E 的吞吐量均有意省略。使用修正后的测试工具连续重复测试三次时,同一面板在 64 个数据流下的吞吐量为 1.5 至 4.2 Gbit/s;较慢的测试恰好对应客户端 CPU 占用仅为 177–185%(而非约 320%)的情况:同一台机器上的其他任务占用了负载生成器的 CPU。这是外部干扰,并非面板特性。因此,在将资源组固定到两个专用物理核心、客户端放到其他核心上重新测量之前,不公布数据平面性能排名。第一轮测试的数字(四个面板之间相差均不超过 17%)是在修正测试工具之前测得的,现已撤回。下文所有结果均与隧道运行速度无关。
| Mistgate | 3x-ui | Remnawave | PasarGuard | |
|---|---|---|---|---|
| A 整个资源组的私有内存(MiB) | 57 (56-58) | 74 (72-77) | 761 (710-772) | 297 (297-299) |
| A:memory.current(MiB) | 64 (62-64) | 81 (79-84) | 841 (790-852) | 312 (312-315) |
| A:CPU 占用率(占一个 vCPU 的百分比,配额为 200%) | 0.26 | 0.68 | 4.34 | 0.39 |
| A:进程数 / 容器数 | 2 / 1 | 3 / 1 | 36 / 4 | 3 / 2 |
| 通过 API 创建 1000 个用户(s) | 49 (33-49) | 24 (19-27) | 24 (23-26) | 45 (44-45) |
| C 400 个客户端:已连接 / 握手失败 | 400 / 0 | 400 / 0 | 400 / 0 | 400 / 0 |
| C:握手 p95(ms)/ 请求 p95(ms) | 4 / 1.3 | 4 / 1.2 | 3 / 1.2 | 6 / 1.6 |
| C:资源组 CPU 占用率(占一个 vCPU 的百分比)/ 私有内存(MiB) | 18 / 113 | 18 / 137 | 21 / 691 | 21 / 361 |
| D 丢弃的数据流 / 隧道重新连接次数 | 0 / 0 | 512 (384-523) / 6 (4-9) | 0 / 0 | 0 / 0 |
| D:订阅请求风暴的处理量(提供 300 个请求/s),仅计 200 响应 | 300 | 94 (92-96) | 250 (213-282) | 51 (49-57) |
| D:订阅 p95(ms) | 1 | 792 (791-816) | 288 (211-308) | 1814 (1522-1954) |
| D:管理 API 添加用户 p95 / 删除用户 p95(ms) | 4 / 3 | 801 / 1138 | 51 / 83 | 2039 / 2216 |
| D:管理 API 列表请求 p95(ms) | 4 | 502 | 79 | 1609 |
| D:私有内存(MiB) | 80 | 129 | 1114 (1060-1130) | 347 |
| E 面板重启:管理 API 恢复响应所需时间(s) | 0.2 | 1.6 (1.5-1.6) | 14.5 (11.1-15.4) | 8.3 (7.4-11.7) |
| E:隧道无流量的时长(共 60 s)/ 丢弃的数据流 | 0 / 0 | 5 (4-8) / 256 (192-256) | 0 / 0 | 8 (5-9) / 128 (128-192) |
| 用户变更时核心是否重启 | no | yes | no | no |
表格解读:
- 内存(A):Mistgate 的面板和节点共占用 57 MiB,其他面板为 74、297 和 761 MiB。Remnawave 共包含 4 个容器中的 36 个进程(后端的三个 Node.js worker、Postgres、Valkey 和节点)。
- 创建用户:测试工具每次通过一个 API 调用创建用户,并等待
429响应结束。Mistgate 管理 API 每秒允许 30 个请求,突发容量为 200,因此创建前 200 个用户只需不到一秒(仅面板测试中为 0.3 s),剩余 800 个用户则受速率限制。49 s 的耗时主要来自速率限制,而非创建用户本身。 - 大量客户端(C):同时建立 400 条隧道,所有面板均无失败,CPU 占用不超过一个 vCPU 的 21%。在这一规模下,各面板差异不明显。唯一明显的差异是 PasarGuard 的握手 p95 为 4–20 ms。
- 高负载场景(D):隧道承载 64 个数据流,同时每秒有 1000 个地址发来 300 个订阅请求,并且每秒通过管理 API 添加一个用户、删除一个用户。3x-ui 在用户变更时重启 Xray 核心(中位数为 512 个数据流被丢弃,单个客户端在 120 s 内重新连接 6 次);其他三个面板则在核心运行期间添加和删除用户,不会断开连接。3x-ui 每秒处理 94 个订阅请求(提供量为 300),每次用户变更耗时 0.8–1.1 s;PasarGuard 每秒处理 51 个,耗时 2.0–2.2 s;Remnawave 每秒处理 250 个,p95 为 288 ms,且阶段 D 的内存占用达到 1114 MiB(三个 Node.js worker 与 Xray 核心共用两个 vCPU)。Mistgate 每秒处理全部 300 个请求,p95 为 1 ms,用户变更耗时 3–4 ms。
- 重启(E):重启 3x-ui 面板容器会同时重启 Xray(设计如此:核心是面板进程的子进程)。重启 PasarGuard 面板容器也会令隧道中断 5–9 s:面板恢复时节点核心会重启(已观察到,未调查原因)。重启 Remnawave 后端不会影响隧道,但管理 API 需要 11–15 s 才恢复响应。Mistgate 面板在 0.2 s 内重启,其节点继续提供服务。
- 阶段 D 中,资源组均达到配额上限(四个面板都占用一个 vCPU 的 183–197%),因此控制平面各项数据比较的是服务器满载时各面板还能为自身任务保留多少资源。
最低配置服务器:1 vCPU,1 GB RAM,无交换分区(一次重复测试)#
采用相同的 A、B、D、E 阶段,资源组限制为 CPUQuota=100% 和 MemoryMax=1G,使用 Mistgate v0.1.33 和修正后的测试工具(核心中设置 GOMAXPROCS=1),于 10 月 12 日运行。每个面板只重复测试一次,因此无法确定数据波动范围。与上表相同,B 和 D 阶段的隧道数据(吞吐量、通过隧道发送的小型请求)不予公布:本次测试也受到机器上其他负载的干扰,而且单次测试无法排除这种干扰。
| Mistgate | 3x-ui | Remnawave | PasarGuard | |
|---|---|---|---|---|
| A:私有内存(MiB) | 73 | 75 | 820 | 302 |
| D:丢弃的数据流 | 0 | 384 | 0 | 0 |
| D:订阅风暴处理量(提供 300 个请求/s)/ 非 200 响应 | 300 / 0 % | 74 / 0 % | 100 / 31.2 % | 39 / 0 % |
| D:订阅 p95(ms) | 6 | 1002 | 496 | 2603 |
| D:管理 API 添加用户 p95(ms) | 9 | 1340 | 97 | 2921 |
| E:管理 API 恢复响应所需时间(s) | 0.3 | 1.5 | 15.2 | 7.3 |
| E:隧道无流量的时长 | 0 | 4 | 2 | 5 |
在 1 GB 服务器上,Remnawave 于阶段 D 耗尽内存:内核五次终止 rw-api worker,一次终止 rw-processor(OOM kill,cgroup 为 bench-box.slice;这是本次重复测试的内核日志记录),31.2% 的订阅请求因连接被拒绝或重置而失败,另有 23 个管理 API 调用失败。空闲时它已占用 820 MiB。本次未为其增加内存。
Mistgate 在这台服务器上的空闲内存为 73 MiB,高于第一轮测试的 54 MiB,也高于其在 2 vCPU 服务器上的 57 MiB。差异来自节点 Agent,它与面板在同一个测量资源组中运行:空闲时间窗口结束时,此处面板进程和 2 vCPU 测试中的面板进程都占用 44 MiB 私有内存;此处 Agent 占用 28 MiB,而 2 vCPU 测试中为 10–12 MiB(未进一步调查)。创建 1000 个用户后的最初几分钟,资源组的内存会增长(2 vCPU 测试中,4 分钟内从 45 MiB 增长到 63 MiB;本次测试中从 61 MiB 增长到 78 MiB;两次增长结束时都已进入阶段 B,隧道中有流量),因此空闲内存数据取决于测量窗口所在的时间点;本次测试稳定等待 30 s,2 vCPU 测试则等待 60 s。面板的内存增长部分来自 Go 堆恢复到工作状态所需的大小:初始化时使用 argon2id 对所有者密码哈希(v0.1.33 中占用 64 MiB,此后为 19 MiB),之后面板会一次性将所有空闲内存归还给操作系统,因此空闲测量窗口开始时的占用低于稳定状态。单独测量面板(1000 个用户和一个节点,持续 15 分钟)时,内存最初为 34–36 MiB,几分钟内升至约 40 MiB,之后不再增长;存活堆维持在 11 MB。同一测量还发现一项实际开销:订阅端点会保留每个已获取订阅的用户最近一次渲染结果,尽管该结果只会复用 10 s;每个用户约占用 1.5 KB 堆内存。v0.1.33 之后,这些数据会在 10 s 后释放:1000 个用户时减少 2.5 MiB,5000 个用户时减少 14 MiB。
性能剖析(仅 Mistgate)#
另行使用 net/http/pprof 构建版本做了一轮测试(通过 go build --overlay 添加,未修改仓库),收集了 25–30 s 的 CPU 性能剖析,以及 heap/allocs/goroutine/mutex/block 性能剖析和 GODEBUG=gctrace=1 数据;测试对象包括空闲面板、订阅高负载(已发布版本,以及关闭限制和缓存的版本),以及单服务器测试中面板和节点的 B64、C、D 阶段。这些测试数据未计入上文。完整分析见 results/profiling-summary.txt;未公布原始性能剖析数据。主要发现如下:
- 429 路径的开销已经很低。 每个被拒绝的请求约消耗 37 µs CPU 时间,其中 49% 用于 socket 系统调用。每个请求分配 3.2 KB 内存,其中 41% 用于标准库解析请求,19% 用于面板自身中间件(
withStart、WithClientIP)生成的两份请求副本,25% 用于响应头映射。拒绝请求期间,anon 内存稳定在 27 MiB(此前为 20 MiB)。每轮测试开始时观察到的 79 MiB 内存来自一次性使用 argon2id 对所有者密码哈希(64 MiB),而非拒绝请求。 - 从缓存渲染订阅消耗 51 µs CPU 时间,不使用缓存则消耗 614 µs(分配 62 KB 和约 900 个对象)。未缓存路径中,SQLite 为每个请求重新解析和规划相同语句的操作占 CPU 时间的一半(
sqlite3_prepare_v2,49.8%);内存分配的 16% 用于对整个 SQL 字符串调用strings.ToUpper,以检查是否以SELECT开头。 - 未缓存路径的 1.7 s p99 是饥饿所致,并非暂停。 整个测试期间,每秒约有 25 个请求等待超过 500 ms。设置
GOMAXPROCS=4后,慢请求消失(p99 为 86 ms,吞吐量相同)。读取连接池有 4 个连接,database/sql会将释放的连接交给随机等待者。 - 节点: 52% 的 CPU 用于 UDP
sendmsg/recvmsg(正在使用 GSO),5% 用于 AES-GCM,19% 用于构建 QUIC 数据包。高频内存分配(37%)和竞争中的互斥锁(QStream.Write)位于apernet/quic-go和 Hysteria 核心中。 - GC: 每秒执行 2–6 次回收;资源组独占 CPU 运行时,最长 stop-the-world 时间为 0.26 ms;在单服务器测试中达到配额上限时,则为 46–49 ms。
修复之后#
根据上述发现做的三项更改已包含在 v0.1.33 中(下文的第四项也已包含):
- 存储读取(
perf(store)):每个连接只预处理一次语句并重复使用;读取请求按到达顺序进入 4 连接池;两个连接池始终保持所有连接打开;SELECT检查不再将整个查询转换为大写。 - 公共请求(
perf(http)):使用一个请求上下文,而非创建两份副本;从共享切片读取固定响应头;每次调用不再创建strings.NewReplacer;Mihomo 订阅使用 gzip writer 池。 - 用户变更和密码(
perf(fleet,auth)):每个入站的节点凭据摘要重用一个缓冲区(1000 个用户时,每次变更的内存分配约减少 5 倍;仍需遍历所有用户一次);同时一次只运行一个 argon2id 哈希,因此并行登录不再各自占用 64 MiB。
10 月 10 日在同一台机器上使用相同脚本重新测量。当天机器的运行速度比主要测试慢 5–25%,因此同时重新测量了 v0.1.32(n=1)作为对照,并以此作为比较基准:
| Mistgate,仅面板(1 vCPU,1 GB) | v0.1.32,主测试 | v0.1.32,对照 | 修复后(n=3) |
|---|---|---|---|
| 未缓存订阅,每秒请求数(修改版构建,关闭限制和缓存) | 1,630 | 1,296 | 3,034 (3,022-3,038) |
| 未缓存订阅,p50 / p95 / p99 | 4.2 ms / 6.6 ms / 1.70 s | 5.1 ms / 8.4 ms / 2.14 s | 2.1 ms / 4.1 ms / 0.92 s |
| 开启渲染缓存时的每秒请求数(修改版构建) | 19,580 | 13,481 | 15,416 (n=1) |
| 已发布构建每秒返回的 429 响应数 | 33,700 | 25,305 | 25,691 (n=1) |
| 200 个用户时的空闲私有内存(MiB) | 20 | 20 | 21 |
| Mistgate,单服务器(2 vCPU,2 GB) | v0.1.32,主测试 | v0.1.32,对照 | 前三项修复后(n=3) | v0.1.33,修正测试工具(n=3,10 月 12 日) |
|---|---|---|---|---|
| D:请求风暴期间的订阅 p95(ms) | 30 | 27 | 20 (20-20) | 1 (1-1) |
| D:管理 API 添加 / 删除用户 p95(ms) | 25 / 16 | 39 / 32 | 32 / 30 | 4 / 3 |
| A:私有内存(MiB) | 57 | 57 | 57 | 57 (56-58) |
前三列使用旧测试工具测得,该工具允许容器内的 Go 根据主机 CPU 数设置 GOMAXPROCS(见下文);最后一列使用修正后的测试工具和第四项修复。因此从 20 ms 降至 1 ms 的变化是两项调整的综合结果,未分别测量。其他三个面板在两轮测试之间变化不大(例如 3x-ui 的订阅风暴 p95 从 969 ms 变为 792 ms,PasarGuard 从 1619 ms 变为 1814 ms),因此测试工具的变化不能解释面板之间的差异。此表中的吞吐量行已删除:该数据使用旧测试工具测得。
未缓存路径的速度约提升一倍,p99 减半,但长尾仍然存在(0.92 s,未达到预期的 0.1 s;见下文)。阶段 D 的订阅风暴 p95 降低了 25–30%。其他数据(429 路径、内存、重启)均在波动范围内保持不变,这符合预期,因为相关修复未涉及这些部分。
剩余的长尾另有原因。为监视可取消的上下文,database/sql 会为每个事务和查询启动一个 goroutine,SQLite 驱动则会为每条语句启动一个 goroutine:每次获取订阅约 25 个。在单 CPU 上,这些 goroutine 会使 Go 调度器的本地运行队列溢出到全局队列;忙碌的处理器每 61 轮才会清空一次全局队列,因此 HTTP 读取 goroutine 若进入该队列,其连接最多会等待一秒。第四项更改(perf(store),读取批次使用独立连接且不启动上下文监视 goroutine)移除了这些 goroutine。同一台机器、同一会话、未缓存版本,重复测试 3 次:
| Mistgate,仅面板(1 vCPU,1 GB) | 更改前的基准(对照) | 应用更改后 |
|---|---|---|
| 未缓存订阅,每秒请求数 | 4,066 | 4,580 |
| p50 / p95 / p99 | 1.6 / 3.1 / 682 ms | 11.0 / 20.8 / 23.5 ms |
| 最慢请求 | 780 ms | 32 ms |
当天机器运行速度更快,因此只比较表格中的两列,不要与上方表格比较。更高的 p50 是服务器满载时 50 个客户端平均分得的处理时间,这些客户端不会暂停(每秒 4,580 个请求时,50 / 4,580 = 10.9 ms);此前少数客户端的停顿会令服务器对其他客户端空闲。
高负载隧道中的小型请求延迟(阶段 B:三次测试中两次 p95 为 29 ms,另一次为 2.3 ms;1 vCPU 时为 67 ms)最终证实来自测试设置,而非节点。单服务器资源组的配额设置在 systemd slice 上,而拥有独立 cgroup 命名空间的容器无法看到该配额,因此节点的 Go 运行时按主机 CPU 数设置 GOMAXPROCS,每 100 ms 周期开始约 80 ms 后便耗尽 2 CPU 配额,随后余下时间都处于停顿状态(运行时跟踪显示,停顿占 19%)。每 100 ms 发送一次探测请求,总会落在周期的同一位置,因此请求要么刚好避开停顿,要么落入停顿,由此产生两种延迟水平。将 GOMAXPROCS 设为与资源组 vCPU 数相同后(这也是运行时在真实服务器上选择的值),节点不再停顿。使用相同二进制文件和会话、64 个数据流,并每 97 ms 发送一次探测请求以错开周期:修正前 p95 为 14.7–18.2 ms,修正后为 3.3–3.6 ms,吞吐量从 4.2 Gbit/s 升至 5.4 Gbit/s。现在,tools/bench 中的测试工具会为每个核心容器(Mistgate 节点及其他面板的 Xray 核心)设置 GOMAXPROCS = vCPU 数。10 月 12 日,使用该测试工具和 v0.1.33 对四个面板重新做了单服务器测试,每项重复三次。上表列出了内存、大量客户端、高负载场景中的控制平面以及重启数据。隧道数据未列出:该轮测试受到机器上其他负载的影响,一个面板在 64 个数据流下的吞吐量在不同重复测试之间从 1.5 Gbit/s 波动到 4.2 Gbit/s;最慢的两次测试中,负载生成器的 CPU 占用仅为 177–185%。该轮测试中,Mistgate 在 64 个数据流下的小型请求 p95 分别为 7.9、5.1 和 53 ms(其他面板的中位数:3x-ui 为 4.2,Remnawave 为 4.3,PasarGuard 为 13),既不是第一轮测试中稳定的 29 ms,也不是可靠结果。第二轮较短的测试(仅阶段 B,每个面板重复一次,每 97 ms 探测一次)显示,Mistgate 在 64 个数据流下为 5.1 ms,其他三个面板为 2.9–3.2 ms;但该轮中 Mistgate 的 1 和 16 个数据流测试均受到干扰(客户端 CPU 占用为 149%,p95 分别为 20 和 16 ms),因此每项只测一次无法得出结论。在「待完成工作」中所列的低干扰环境复测完成前,不对隧道延迟或速度作出结论。
待完成工作:
- 在低干扰机器上对四个面板重新测量阶段 B 和阶段 D 的隧道数据:将资源组固定到两个专用物理核心(
AllowedCPUs=0,1,8,9、CPUQuota=200%、GOMAXPROCS=2),客户端和接收端放在其他核心上,重复三次,每 97 ms 探测一次。这也将替代第一轮测试中使用旧测试工具测得、现已删除的固定 CPU 敏感度表格。
方法#
测试机器#
AMD Ryzen 7 7800X3D(8 核、16 线程)、15.5 GB RAM、Windows 11 主机、WSL2(Ubuntu 24.04.4)、Linux 6.6.87.2-microsoft-standard-WSL2、Docker 29.4.1 和 Compose v5.1.3、systemd cgroup 驱动、cgroup v2。测试期间,同一 WSL 实例中还运行了少量其他空闲容器(负载平均值约为 0.05–0.4;每次测试前记录在原始 JSON 的 background 字段中)。负载生成器和接收端也在同一 WSL 实例中运行,位于受限 cgroup 之外。
资源限制#
- 面板主机:一个 systemd slice
bench-panel.slice,设置CPUQuota=100%、MemoryMax=1G、MemorySwapMax=0,并固定到主机的一个 CPU(AllowedCPUs=15;在每轮测试前通过systemctl set-property运行时设置;负载生成器不属于该 slice,也未固定 CPU)。Docker 容器通过cgroup_parent: bench-panel.slice创建,Mistgate 通过systemd-run --slice=bench-panel.slice启动,因此每个面板的所有进程(包括 Remnawave 的全部容器)共用同一份配额。每次重复测试前都会停止并重新启动该 slice,使memory.peak从零开始计数。 - 单服务器:使用
bench-box.slice,设置CPUQuota=200%、MemoryMax=2G、MemorySwapMax=0;最低配置服务器测试则分别设为 100% 和 1G。每个核心容器(Mistgate 节点及其他面板的 Xray 核心)的GOMAXPROCS均设为 slice 的 vCPU 数,因为容器无法看到 slice 的配额,否则会按主机的 16 个 CPU 设置该值(参见修复之后)。 - 在主要测试开始前做的一次简短对比(仅设置配额与同时设置配额和 CPU 固定)之后,CPU 固定设置一直保留;所有仅面板测试、Mistgate 修改版测试、清除缓存测试和订阅负载性能剖析都使用了该设置,之后才发现这一情况。仅设置配额会允许多线程运行时先在多个主机 CPU 上突发运行,再限流;与仅设置配额相比,固定 CPU 更接近单 vCPU 服务器的情况。但原计划只设置配额,因此另运行了一次不固定 CPU 的测试作为对照:
| 仅面板测试,重复 1 次 | 固定 CPU + 配额(上方测试) | 仅配额 |
|---|---|---|
| Mistgate:200 个用户时的空闲 anon 内存(MiB)/ 每秒 429 响应数 | 20 / 33,700 | 24 / 15,750 (p99 为 77 ms,之前为 7.4) |
| 3x-ui:每秒 200 个响应时的 p50(ms) | 148 / 296 | 162 / 278 |
| PasarGuard:每秒 200 个响应时的 p50(ms) | 274 / 166 | 102 / 467 |
| Remnawave:每秒 200 个响应时的失败请求数 | 271 / 0 | 251 / 75%(一个 worker 被 1 GB 上限终止,oom_kill 为 1) |
| Remnawave:负载期间的 anon 内存(MiB) | 870 | 786 |
空闲内存、磁盘、启动时间以及 3x-ui 的数据不受 CPU 固定设置影响。PasarGuard 和 Remnawave 的负载数据则会受到影响:仅设置配额时,它们的线程会分布在多个主机 CPU 上运行,并受到配额限制。真实的单 vCPU 虚拟机会呈现与固定 CPU 相似的情况。
- 单服务器测试仅设置了配额(
CPUQuota=200%),未固定 CPU。将资源组固定到两个物理核心、客户端放到其他核心上的测试仍待完成(参见修复之后末尾);第一轮测试中使用旧测试工具做的一次固定 CPU 测试已删除。
测量方式#
- 内存和 CPU:每秒采样一次 slice 的 cgroup 文件(
memory.current、memory.stat中的 anon/file/kernel、cpu.stat、pids)。空闲数据:内存取 5 分钟空闲期最后 30 s 的平均值,CPU 取整个空闲时间窗口的平均值。峰值取窗口内最大的 1 Hz 采样值,以及 slice 的内核memory.peak值。 - 磁盘:使用的 Docker 镜像解压后大小总和,或二进制文件大小;数据目录在安装后和创建 200 个用户后的大小。
- 冷启动:从执行启动命令(
docker compose up -d或systemd-run)到主端口首次返回 HTTP 200 的时间(Remnawave 使用会读取数据库的/api/auth/status)。 - 用户:通过各面板文档所述的 API 逐个创建(Mistgate 使用所有者会话和 Connect-JSON,3x-ui 使用
/panel/api/clients/add,Remnawave 使用/api/users,PasarGuard 使用/api/user)。所有面板均使用相同的虚构用户名user1至user200(单服务器测试中为 1000 个用户)。 - 负载:使用
loadgen(Go 标准库),50 个 worker,轮流请求各链接,先预热一轮,持续 60 s;只统计 200 响应的延迟,其他状态码均计为错误。 - 顺序:轮流测试各面板,每个面板重复三次,每次重复测试前都会清除数据。
各面板的设置方式及其与项目文档的差异#
- Mistgate:使用项目可复现构建(
release build)从 v0.1.32 标签(仅面板表格)或 v0.1.33 标签(单服务器表格)构建,未使用发布密钥,因此无法自行更新或安装签名节点包。使用serve --listen 127.0.0.1:18080 --trusted-proxy 127.0.0.1 --agent-listen <bridge address>启动。由于节点处于活动状态前链接不会包含服务器线路,因此需先让一个节点 Agent 连接一次(在隔离网络中的容器内);测量前会移除该容器。 - 3x-ui:使用已发布的镜像,而非 compose 文件中的
build:;端口绑定到 127.0.0.1;使用默认的admin/admin;一个 VLESS Reality 入站。 - Remnawave:使用官方 compose 文件,并将后端镜像版本从
:3固定为3.4.5;每个请求都发送X-Forwarded-For和X-Forwarded-Proto: https(后端会拒绝未经过 TLS 代理的请求);通过控制面板会话创建 API token(x-remnawave-client-type: browser);使用默认协议配置(一个 Shadowsocks 入站)和默认用户组;一个主机。 - PasarGuard:使用项目默认 compose 文件(主机网络),镜像固定为 v5.4.1,设置
UVICORN_HOST=127.0.0.1;使用默认核心配置(一个 Shadowsocks 入站);一个主机和一个用户组;使用pasarguard-cli generate-temp-key生成的临时密钥创建所有者。 - 未调整任何设置。所有面板均未使用 TLS(回环地址上使用普通 HTTP),尽管实际安装时会在前方配置 TLS 代理。
复现#
脚本、compose 文件、修改版构建的差异、负载生成器和汇总结果均位于仓库中的 tools/bench,并附有 README:py/bench.py(仅面板)、py/onebox.py(单服务器)、py/profile_run.py(性能剖析)、py/aggregate*.py、loadgen/、hyload/、compose/、profiling/(pprof overlay)和 results/(表格和每次重复测试的 CSV)。未公布原始 JSON 和 1 Hz 采样数据。测试工具会创建并删除 Docker 镜像和 systemd slice;请先阅读 README,再在测试机器上运行。
这些数据不能说明什么#
- 使用一台桌面计算机的一个 CPU、一个 WSL2 虚拟机,重复三次。仅面板表格和单服务器表格中的控制平面数据波动较小,但更换 CPU、内核或文件系统会改变绝对值。这台机器并非专用测试机:单服务器测试期间还有其他任务运行,因此未公布隧道吞吐量数据(参见该节中的说明)。
- 负载由 50 个客户端构成闭环:它反映的是处理请求的开销,而非面板可服务的用户数量。真实客户端每隔数小时获取一次订阅。
- 各面板处理每个请求时执行的操作不同,提供的链接也不同(Hysteria2、VLESS Reality、Shadowsocks)。Remnawave 和 PasarGuard 使用已保存的密码生成 Shadowsocks 线路;3x-ui 生成 VLESS Reality 线路;Mistgate 则生成带固定证书的 Hysteria2 线路。
- Mistgate 使用用户数据的 10 s 缓存响应订阅请求;其他面板没有这样做(或未说明是否这样做)。上方关闭缓存的修改版展示了缓存的影响。
- 使用的数据库不同:Remnawave 需要 PostgreSQL,其他面板使用 SQLite。PasarGuard 还支持 PostgreSQL 和 MySQL;本次只测量了其默认设置。
- 测量 Mistgate 时,节点记录仍在,但 Agent 已移除;其他面板没有节点。单服务器测试则包含 VPN 核心。
- 页缓存:Mistgate 的二进制文件由另一个 cgroup 复制,因此开始时文件缓存不会计入该资源组;其他面板则在资源组内读取文件。因此本文以 anon 为主要数据。上文介绍了清除缓存后的第二轮测试。
- 未测量内容:除类似环回连接中的 Hysteria2 外的 VPN 数据平面速度(接收端和客户端位于同一台机器,没有网络传输)、其他协议、管理 Web 界面、Telegram 机器人、升级和备份、多节点、高延迟或有丢包的路径、面板前方的 TLS 代理,以及超过一周的运行情况。