参考
性能
测试机是 4 核 20 GiB 的 AMD EPYC 9275F,Debian 13。全部数字按 hub v1.4.1、agent v1.2.1 的发布版二进制实测(release 构建:LTO + opt-level=z + strip,musl 静态链接,与 Docker 镜像里的是同一个)。负载来自一个模拟
agent 程序,与 hub 跑在同一台机器上,按真实 agent 的格式上报。
装机体积
| hub 可执行文件 | 6.8 MiB |
| agent 可执行文件 | 1.8 MiB |
| hub 冷启动到能响应请求(8 个节点、7 天历史的库) | 6.5 ms |
| 30 天历史数据(7 个节点,每台 4 个延迟监控) | 9.2 MiB |
一个二进制加一个 .db 文件就是全部,没有配置文件,也没有额外目录。
内存
| 场景 | RSS |
|---|---|
| hub 空转 | 7.0 MiB |
| hub,200 个节点在线,每 2 秒上报一次 | 10.8 MiB |
| hub,1000 个节点在线,每秒上报一次 | 29.5 MiB |
| hub,1000 个节点每秒上报 + 8 个人同时刷状态页 | 34.6 MiB |
| agent,线上实例常驻 | 2.0 MiB |
内存随在线节点数缓慢增长,每台约 23 KiB,是各自那条连接的收发缓冲。Rust 没有 GC,也没有语言运行时,上报再频繁也不会攒出待回收的垃圾。
密码登录
上面量的是 agent 负载,不含面板的密码登录。密码校验走 argon2,一次要 19 MiB,用完即还给系统,常驻内存不会因为有人登录过而上涨:
| RSS | |
|---|---|
| 登录前 | 7.0 MiB |
| 串行密码登录 20 次,过程中的峰值 | 25.8 MiB |
| 登录完 | 7.1 MiB |
并发登录由闸门挡住:同一时刻只算一次 argon2,其余请求直接返回 429,不排队。8 个请求同时登录,峰值是 25.8 MiB。首次启动生成应急密码也要算一次,所以刚装好的 hub 峰值就已经在 25 MiB 左右。
自己用 glibc 目标构建(cargo build --release 的默认目标)不一样:glibc 不把这块内存还给系统,每次密码登录常驻内存涨一截,串行登录 50 次后停在约 350 MiB,超过单元文件给的 MemoryMax=256M。自己构建请用 musl 目标;发布的二进制和 Docker 镜像都是 musl,没有这个问题。
CPU
干同样的活烧掉多少 CPU 秒:
| 场景 | |
|---|---|
| 200 节点每 2 秒上报,跑 90 秒 | 1.7 秒 |
| 1000 节点每秒上报,跑 60 秒 | 5.5 秒 |
| 上报 + 刷状态页混合,跑 90 秒 | 16.8 秒 |
换算一下,200 个节点在线时占一颗核的 2%,1000 个节点占 9%,再加 8 个人不停刷状态页是 19%。
状态页响应速度
在 1000 个节点每秒上报的同时,8 个并发不停地刷状态页,和浏览器一样要 gzip:
| 每秒处理的请求数 | 820 |
| 中位延迟 | 9.5 ms |
| p99 延迟 | 21 ms |
820 这个数打到了压测客户端自己的上限(解开每个回复),hub 只用了一颗核的 19%,真实上限更高。1000 个节点的节点列表是 1.04 MiB 的 JSON,压缩后约 34 KiB。压缩结果与实时推送共用,每 2 秒只算一次,与请求数、开着的页面数都无关。不要 gzip 时 hub 反而更忙:同样的负载 CPU 用 43.0 秒,花在发送 1 MiB 的明文上。
这些数字是调出来的
光换语言只拿得到体积和内存的优势。下表是那一轮调优时在另一台 3 核机器上、200 个节点每 200 ms 上报的混合负载下量的,与上文不是同一台机器、同一组负载:
| 调优前 | 调优后 | ||
|---|---|---|---|
| 内存 | 43.3 MiB | 18.2 MiB | 降 58% |
| CPU(90 秒) | 99.3 秒 | 33.0 秒 | 降 67% |
| 状态页每秒请求数 | 26 | 723 | 高 28 倍 |
| p99 延迟 | 842 ms | 33 ms | 快 26 倍 |
改动只有四处,都很小:
- WebSocket 读缓冲从 128 KiB 降到 4 KiB。库的默认值是按大文件传输定的,一个上报包只有几百字节,200 个连接就是 25 MiB 白占的内存
- 节点列表不再逐个查流量。原来渲染 200 个节点要查 200 次数据库,现在一次查完
- 状态页的 HTTP 接口复用已有的 2 秒快照缓存。这个缓存本来就在,但只有 WebSocket 那条路走它
- 调了三个 SQLite 参数:8 MiB page cache、WAL 检查点阈值和大小上限