databend-meta 的 musl 与 GNU 构建实测:为兼容老系统,要让掉多少性能?
Databend 推荐用容器镜像部署。信创环境常常是例外:不少系统版本偏老,glibc 版本不够,容器也不一定能用,通常只能部署静态链接的 musl 二进制。静态包不依赖系统的 glibc 版本,兼容性更好。
兼容性解决了,性能会不会受影响?musl 和 glibc 在系统调用封装、线程和锁等基础实现上有差异,databend-meta 又是一个处理大量小请求的 Raft 服务。我把同一份代码分别编译成 musl 静态版和 GNU 动态版,用同样的负载压测,量化一下兼容的代价。
结论:
- 高并发时 musl 明显更慢。 512 和 1024 并发下,GNU 的吞吐中位数分别比 musl 高 15.59% 和 16.46%;换算成 musl 的视角,吞吐低 13%–14%。
- 低并发时差距不大。 16 到 256 并发,GNU 的吞吐高 0.75%–6.68%。
- 尾延迟各有高低。 多数档位 GNU 的 p99 更低,其中 1024 并发低 32%;但 256 和 1536 并发时 musl 的 p99 更低。
- 全部 52 轮、2,900 多万次请求,客户端没有收到任何错误。
一句话结论:在本次测试条件下,信创环境使用 musl 包,高并发写入可按吞吐低约 14% 来规划容量;能用容器,或系统 glibc 版本足够时,不必选择 musl。
一、测试对象
被测程序是 databend-meta。两份二进制来自同一个 commit,版本都是 v1.2.956-8eb21ffae8:
| 项目 | musl 版 | GNU 版 |
|---|---|---|
| 链接方式 | 静态链接(static PIE) | 动态链接 glibc |
| 动态加载器 | 无 | /lib64/ld-linux-x86-64.so.2 |
| 链接器 | LLD 22.1.6 | mold 2.41.0 |
| 二进制中记录的 GCC 版本 | 9.4.0 | 16.1.1 |
| 内存分配器 | jemalloc | jemalloc |
databend-meta 默认使用 jemalloc 作为全局内存分配器,两份二进制都是如此,所以差异不来自 musl 自带的 malloc。不过 jemalloc 底层的线程和锁仍然依赖各自的 libc。但两者的链接器和 C 工具链版本不同,这次比较的是两份可以实际部署的产物,不能把全部差距都算到 libc 头上。要单独量化 libc 的影响,需要统一链接器和工具链后再测。
压测客户端两边共用同一个 databend-metabench,避免客户端差异影响结果。
二、测试方法
压测使用 Databend 仓库自带的 scripts/benchmark/meta/meta-cluster-bench.py。脚本会生成节点配置,启动集群并指定 Leader,然后执行压测、汇总报告、停止节点。
单轮命令如下:
python3 scripts/benchmark/meta/meta-cluster-bench.py \
--node-bin /path/to/build/bin \
--node-label gnu \
--node-count 3 \
--work-dir "$RUN/cluster" \
--port-base 29101 \
--workload-rpc upsert_kv \
--workload-clients 512 \
--workload-client-pool-size 512 \
--workload-ops 1000000 \
--workload-run-secs 60 \
--repeat 1
测试条件:
| 项目 | 设置 |
|---|---|
| 机器 | Intel i9-12900K,16 核 24 线程,125 GiB 内存,NVMe SSD |
| 集群 | 同一台机器上的 3 个节点,走完整 Raft 复制 |
| 负载 | upsert_kv,key 按 prefix-client-i 递增,持续写入新 key |
| 数据 | 每轮使用新的工作目录,从空数据开始 |
| 时长 | 每轮 60 秒,到时停止发起新请求 |
| 快照 | snapshot_logs_since_last 设为 10 亿,基本不触发周期性快照 |
压测客户端是闭环模型:每个 worker 等上一个请求返回后才发下一个。这种方式能测出给定并发下的吞吐和延迟,但不会模拟固定到达率下请求持续堆积的情况。
每种构建的测试轮次:
| 并发 | 每种构建的轮次 |
|---|---|
| 16、64、128、256、512、1024 | 3 |
| 32 | 5 |
| 1536 | 3 |
同一并发档按 musl → GNU → GNU → musl → musl → GNU 的顺序交替执行,32 并发多出的两轮继续交替。这样可以减小机器状态随时间变化对比较结果的影响,但不能完全消除。
三、测试结果
吞吐取各轮 QPS 的中位数,p99 取各轮 p99 的中位数。所有档位的错误率都是 0%。
| 并发 | musl QPS | GNU QPS | GNU 吞吐差异 | musl p99 | GNU p99 |
|---|---|---|---|---|---|
| 16 | 1,826.6 | 1,922.1 | +5.23% | 12.94 ms | 11.64 ms |
| 32 | 1,907.2 | 1,921.5 | +0.75% | 48.80 ms | 48.43 ms |
| 64 | 3,853.9 | 3,949.7 | +2.49% | 27.12 ms | 25.93 ms |
| 128 | 6,950.3 | 7,031.2 | +1.16% | 34.03 ms | 32.19 ms |
| 256 | 11,162.3 | 11,907.5 | +6.68% | 52.93 ms | 88.73 ms |
| 512 | 15,176.9 | 17,543.3 | +15.59% | 101.11 ms | 92.22 ms |
| 1024 | 17,511.0 | 20,394.0 | +16.46% | 250.57 ms | 170.40 ms |
| 1536 | 17,209.5 | 19,111.6 | +11.05% | 533.41 ms | 930.37 ms |
吞吐:差距在 512 并发后拉开
除 32 并发外,每一档 GNU 的每一轮吞吐都高于 musl 的任何一轮。不过 256 并发及以下,差距都在 7% 以内;到 512 和 1024 并发,差距扩大到 15% 以上:
| 并发 | musl 三轮 QPS | GNU 三轮 QPS |
|---|---|---|
| 512 | 15,135.6 / 15,513.5 / 15,176.9 | 18,189.2 / 17,543.3 / 16,727.0 |
| 1024 | 17,511.0 / 17,513.7 / 17,200.4 | 20,876.4 / 20,394.0 / 19,888.9 |
512 并发时,GNU 最慢的一轮是 16,727.0,仍比 musl 最快的一轮 15,513.5 高 7.8%。1024 并发时,GNU 最慢的一轮比 musl 最快的一轮高 13.6%。
32 并发是唯一一档两种构建的结果有重叠:musl 五轮在 1,693.1–1,937.7 之间,GNU 在 1,817.7–1,940.6 之间,差距在波动范围内。
尾延迟:吞吐高不代表 p99 低
每种构建只跑三轮,选哪一轮作为代表,p99 的结论可能完全相反。以 256 和 1536 并发为例:
| 并发 | musl 三轮 p99(ms) | GNU 三轮 p99(ms) |
|---|---|---|
| 256 | 48.09 / 52.93 / 67.67 | 45.57 / 103.61 / 88.73 |
| 1536 | 447.30 / 987.78 / 533.41 | 200.25 / 1,022.80 / 930.37 |
如果取 256 并发 QPS 为中位数的那一轮,GNU 的 p99 是 45.57 ms,musl 是 67.67 ms,GNU 看起来更好。但 GNU 另外两轮分别是 103.61 ms 和 88.73 ms,musl 三轮都在 68 ms 以内。为避免挑选轮次影响结论,本文统一使用三轮 p99 的中位数。
按这个口径:
- 1024 并发,GNU 的 p99 比 musl 低 32.00%。
- 16、64、128、512 并发,GNU 低 4%–10%;32 并发两者基本相同。
- 256 并发,GNU 高 67.64%;1536 并发,GNU 高 74.42%。
两种构建都出现了秒级的单请求延迟:
| 并发 | musl 最大延迟 | GNU 最大延迟 |
|---|---|---|
| 256 | 1.07 s | 1.32 s |
| 512 | 2.34 s | 2.10 s |
| 1024 | 2.12 s | 2.33 s |
| 1536 | 2.51 s | 3.11 s |
这些请求的占比不到 1%,所以 p99 反映不出来。业务对长尾敏感时,需要单独关注最大延迟或 p99.9。
1536 并发:吞吐不再增长
从 512 到 1024 并发,并发翻倍,吞吐只增加 15%–16%,增速已经明显放缓,于是补测了 1536 并发:
| 构建 | 1024 QPS | 1536 QPS | 吞吐变化 | 1024 p99 | 1536 p99 |
|---|---|---|---|---|---|
| musl | 17,511.0 | 17,209.5 | -1.72% | 250.57 ms | 533.41 ms |
| GNU | 20,394.0 | 19,111.6 | -6.29% | 170.40 ms | 930.37 ms |
吞吐略有下降,p99 则明显升高:musl 约为 1024 并发时的 2 倍,GNU 约为 5.5 倍。对这台机器和这套配置来说,1536 并发已经超出了能有效利用的范围。
1536 并发和 1024 并发不是在同一时间段测的,页缓存和 SSD 状态都可能变化,吞吐的小幅下降不能完全归因于并发数。但从 p99 的增幅看,继续提高并发没有带来收益。
四、资源观测
每轮同时采集了 vmstat、iostat、pidstat。下表使用每轮去掉开头 20 秒和最后 5 秒后的中间段,列出每种构建各轮的最小值到最大值。进程 CPU 中 100% 表示一个逻辑核:
| 并发 | 3 个节点 CPU 合计 musl / GNU | 客户端 CPU musl / GNU | 磁盘写等待(ms) musl / GNU |
|---|---|---|---|
| 16 | 458%–503% / 501%–511% | 115%–117% / 123%–124% | 0.7–0.9 / 0.7 |
| 32 | 443%–489% / 447%–483% | 131%–141% / 146%–153% | 1.8–9.1 / 1.6–2.8 |
| 64 | 582%–611% / 630%–637% | 193%–203% / 235%–237% | 1.6–1.8 / 1.5–1.7 |
| 128 | 584%–590% / 658%–660% | 221%–227% / 280%–282% | 1.8–2.4 / 2.1–2.6 |
| 256 | 533%–548% / 585%–601% | 231%–243% / 284%–292% | 2.4–3.2 / 3.4–8.1 |
| 512 | 547%–553% / 559%–585% | 247%–252% / 296%–313% | 8.2–10.6 / 5.8–14.3 |
| 1024 | 571%–586% / 543%–583% | 270%–273% / 301%–323% | 12.5–15.0 / 13.7–26.5 |
| 1536 | 525%–558% / 536%–551% | 246%–266% / 303%–312% | 35.7–41.3 / 35.7–35.9 |
从这些数据可以看出:
- 整机 CPU 没有用满。 各档整机 CPU 使用率在 26%–42%。但单线程的提交路径、锁竞争仍可能成为瓶颈,整机平均值无法反映这类问题。
- 连接 GNU 版时,客户端 CPU 每一档都更高。 客户端是同一个二进制,但连接 GNU 版服务端时 CPU 高约 6%–25%。在 32、64、128 这些吞吐只差 0.75%–2.5% 的档位也是如此,所以不能只用“请求更多”来解释,原因还没有查明。512 并发及以上,客户端占用 2.5–3.2 个逻辑核,会与服务端争用 CPU。
- 服务端 CPU 的变化与并发有关。 64 和 128 并发时,GNU 版 3 个节点的 CPU 比 musl 版高 6%–12%,吞吐只高 1%–2.5%。到 512 并发及以上,情况反过来:按区间中点粗略估算,GNU 每处理一次请求消耗的服务端 CPU,在 512 和 1536 并发约少 9%–10%,在 1024 并发约少 16%。
- 磁盘写等待只能部分对应尾延迟。 256 并发时,GNU 的写等待(3.4–8.1 ms)高于 musl(2.4–3.2 ms),GNU 的 p99 也更高。但 1536 并发时两者写等待接近,musl 的范围甚至更高,GNU 的 p99 却明显更高。这些是块设备层的统计,没有与单个慢请求对齐。
这次没有做 perf 采样、Raft 提交链路的分段计时,也没有做 CPU 绑核实验,因此还无法确定两种构建 CPU 效率差异和尾延迟差异的具体原因。
五、怎么选
部署方式的选择顺序:
- 能用容器就用容器。 这是官方推荐的部署方式,镜像自带运行环境,不受宿主机 glibc 版本影响。
- 系统 glibc 版本足够时,用 GNU 版。 本次编译的 GNU 版需要 glibc 2.34 及以上。在本次测试中,它的高并发写入吞吐比 musl 版高 15%–16%。
- 信创等老系统,用 musl 版。 按本次测试,高并发写入吞吐可按低 13%–14% 估算,容量规划时应使用 musl 版的实测数据。
对 p99 要求严格的场景,两种构建都需要单独验证。GNU 在 256 和 1536 并发时 p99 更高,两种构建也都有秒级长尾。在这种情况下,更值得优先做的是找到长尾的来源,而不是只靠更换构建。
六、适用范围
这次测试有以下限制,引用结果时需要一并考虑:
- 3 个节点和压测客户端在同一台机器上,没有跨机网络延迟,客户端也会占用服务端可用的 CPU。
- 负载只有递增 key 的小 KV 写入,没有覆盖表元数据操作、读写混合、热点更新或大 value。
- 每轮从空数据开始,只运行 60 秒,无法反映长时间运行、大量数据下的稳定状态。
- 快照阈值调到了 10 亿,没有包含周期性快照的开销。
- 每档只有 3 轮或 5 轮,没有计算置信区间。
- 零错误只表示客户端没有收到失败响应,没有验证重启恢复、Leader 切换和断电后的数据持久性。
因此,本文的结论限定在这台机器、这套配置和这类负载下:高并发写入时,musl 版 databend-meta 的吞吐比 GNU 版低 13%–14%。 这个比例不能直接外推到所有场景,有兴趣你也可以在你的环境中测试一下。
附录:测试环境
硬件与系统
| 项目 | 配置 |
|---|---|
| CPU | Intel Core i9-12900K,16 核 24 线程,单 NUMA 节点,未绑核 |
| 内存 | 125 GiB,未启用 Swap |
| 磁盘 | NVMe SSD,测试数据与 Raft 日志都在这块盘上 |
| 操作系统 | Arch Linux,内核 7.0.14-arch1-1,x86_64 |
| glibc | 2.43(GNU 版运行时使用) |
| 文件描述符上限 | 655350 |
软件版本
| 项目 | 版本 |
|---|---|
| Databend | v1.2.956,commit 8eb21ffae8 |
| Rust 工具链 | nightly-2026-08-28(rustc 1.100.0-nightly) |
| 压测脚本 | 仓库内 scripts/benchmark/meta/meta-cluster-bench.py |
| 压测客户端 | 仓库内 databend-metabench,两种构建共用同一份 |
编译配置
两份构建来自同一个 commit,该 commit 中 Cargo.toml 的 release profile 如下:
[profile.release]
opt-level = "s"
lto = "thin"
codegen-units = 1
debug = "line-tables-only"
incremental = false
overflow-checks = false
两者的链接器不同:musl 版为 LLD 22.1.6,GNU 版为 mold 2.41.0。
Raft 配置
测试脚本为每个节点固定以下参数,两种构建一致:
install_snapshot_timeout = 60000
max_applied_log_to_keep = 10240
snapshot_db_block_keys = 8000
snapshot_db_block_cache_size = 1073741824
snapshot_logs_since_last = 1000000000
节点日志级别为 INFO,压测客户端日志级别为 warn。
