Skip to main content

databend-meta 的 musl 与 GNU 构建实测:为兼容老系统,要让掉多少性能?

· 17 min read
wubx
Data AI

Databend 推荐用容器镜像部署。信创环境常常是例外:不少系统版本偏老,glibc 版本不够,容器也不一定能用,通常只能部署静态链接的 musl 二进制。静态包不依赖系统的 glibc 版本,兼容性更好。

兼容性解决了,性能会不会受影响?musl 和 glibc 在系统调用封装、线程和锁等基础实现上有差异,databend-meta 又是一个处理大量小请求的 Raft 服务。我把同一份代码分别编译成 musl 静态版和 GNU 动态版,用同样的负载压测,量化一下兼容的代价。

结论:

  1. 高并发时 musl 明显更慢。 512 和 1024 并发下,GNU 的吞吐中位数分别比 musl 高 15.59% 和 16.46%;换算成 musl 的视角,吞吐低 13%–14%。
  2. 低并发时差距不大。 16 到 256 并发,GNU 的吞吐高 0.75%–6.68%。
  3. 尾延迟各有高低。 多数档位 GNU 的 p99 更低,其中 1024 并发低 32%;但 256 和 1536 并发时 musl 的 p99 更低。
  4. 全部 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.6mold 2.41.0
二进制中记录的 GCC 版本9.4.016.1.1
内存分配器jemallocjemalloc

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、10243
325
15363

同一并发档按 musl → GNU → GNU → musl → musl → GNU 的顺序交替执行,32 并发多出的两轮继续交替。这样可以减小机器状态随时间变化对比较结果的影响,但不能完全消除。

三、测试结果​

吞吐取各轮 QPS 的中位数,p99 取各轮 p99 的中位数。所有档位的错误率都是 0%。

并发musl QPSGNU QPSGNU 吞吐差异musl p99GNU p99
161,826.61,922.1+5.23%12.94 ms11.64 ms
321,907.21,921.5+0.75%48.80 ms48.43 ms
643,853.93,949.7+2.49%27.12 ms25.93 ms
1286,950.37,031.2+1.16%34.03 ms32.19 ms
25611,162.311,907.5+6.68%52.93 ms88.73 ms
51215,176.917,543.3+15.59%101.11 ms92.22 ms
102417,511.020,394.0+16.46%250.57 ms170.40 ms
153617,209.519,111.6+11.05%533.41 ms930.37 ms

吞吐:差距在 512 并发后拉开​

除 32 并发外,每一档 GNU 的每一轮吞吐都高于 musl 的任何一轮。不过 256 并发及以下,差距都在 7% 以内;到 512 和 1024 并发,差距扩大到 15% 以上:

并发musl 三轮 QPSGNU 三轮 QPS
51215,135.6 / 15,513.5 / 15,176.918,189.2 / 17,543.3 / 16,727.0
102417,511.0 / 17,513.7 / 17,200.420,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)
25648.09 / 52.93 / 67.6745.57 / 103.61 / 88.73
1536447.30 / 987.78 / 533.41200.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 最大延迟
2561.07 s1.32 s
5122.34 s2.10 s
10242.12 s2.33 s
15362.51 s3.11 s

这些请求的占比不到 1%,所以 p99 反映不出来。业务对长尾敏感时,需要单独关注最大延迟或 p99.9。

1536 并发:吞吐不再增长​

从 512 到 1024 并发,并发翻倍,吞吐只增加 15%–16%,增速已经明显放缓,于是补测了 1536 并发:

构建1024 QPS1536 QPS吞吐变化1024 p991536 p99
musl17,511.017,209.5-1.72%250.57 ms533.41 ms
GNU20,394.019,111.6-6.29%170.40 ms930.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
16458%–503% / 501%–511%115%–117% / 123%–124%0.7–0.9 / 0.7
32443%–489% / 447%–483%131%–141% / 146%–153%1.8–9.1 / 1.6–2.8
64582%–611% / 630%–637%193%–203% / 235%–237%1.6–1.8 / 1.5–1.7
128584%–590% / 658%–660%221%–227% / 280%–282%1.8–2.4 / 2.1–2.6
256533%–548% / 585%–601%231%–243% / 284%–292%2.4–3.2 / 3.4–8.1
512547%–553% / 559%–585%247%–252% / 296%–313%8.2–10.6 / 5.8–14.3
1024571%–586% / 543%–583%270%–273% / 301%–323%12.5–15.0 / 13.7–26.5
1536525%–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 效率差异和尾延迟差异的具体原因。

五、怎么选​

部署方式的选择顺序:

  1. 能用容器就用容器。 这是官方推荐的部署方式,镜像自带运行环境,不受宿主机 glibc 版本影响。
  2. 系统 glibc 版本足够时,用 GNU 版。 本次编译的 GNU 版需要 glibc 2.34 及以上。在本次测试中,它的高并发写入吞吐比 musl 版高 15%–16%。
  3. 信创等老系统,用 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%。 这个比例不能直接外推到所有场景,有兴趣你也可以在你的环境中测试一下。

附录:测试环境​

硬件与系统​

项目配置
CPUIntel Core i9-12900K,16 核 24 线程,单 NUMA 节点,未绑核
内存125 GiB,未启用 Swap
磁盘NVMe SSD,测试数据与 Raft 日志都在这块盘上
操作系统Arch Linux,内核 7.0.14-arch1-1,x86_64
glibc2.43(GNU 版运行时使用)
文件描述符上限655350

软件版本​

项目版本
Databendv1.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。