Skip to main content

databend-meta 升级:写入从 110 QPS 到 1.7 万 QPS

· 14 min read
wubx
Data AI

上一篇测 databend-meta 的 musl 和 GNU 构建时,我顺手把 1.2.820 也跑了一遍。820 是 2025 年 9 月的 nightly。跑出来的数字让我一度以为脚本写错了:并发从 16 加到 1536,写入吞吐一直在 110 QPS 左右,延迟跟着并发一起涨。同一台机器上,1.2.956 在 1024 并发时是 17,511 QPS。

脚本没问题。我用官方镜像在 820 和 956 之间做了二分,问题出在 1.2.896 之前 MetaWorker 里的一个循环:它一次只处理一个请求。

先说结论:写入并发高的环境,databend-meta 至少要升到 1.2.896,能用新版本就直接用新版本。按兼容性文档,query 还停在 820 的话,也可以先单独升级 meta。

测试方法​

和上一篇一样。用 Databend 仓库里的 scripts/benchmark/meta/meta-cluster-bench.py 在一台机器上起 3 个节点,走完整的 Raft 复制。负载是递增 key 的 upsert_kv,每轮 60 秒,从空数据开始。快照阈值调到 10 亿,基本不触发快照。两个版本用同一份压测客户端 databend-metabench,32 并发每个版本跑 5 轮,其他并发各跑 3 轮。

两个二进制都是 musl 静态构建:

项目1.2.8201.2.956
日期2025-09-252026-10-27
链接方式musl static PIEmusl static PIE
链接器mold 2.40.4LLD 22.1.6
Rust 工具链1.88.0-nightly1.100.0-nightly

测试结果​

QPS 和 p99 都取各轮的中位数。两个版本所有轮次都没有报错。

并发820 QPS956 QPS956 / 820820 p99956 p99
16130.21,826.614 倍192.6 ms12.9 ms
32111.51,907.217 倍420.1 ms48.8 ms
64109.93,853.935 倍707.3 ms27.1 ms
128111.86,950.362 倍1.47 s34.0 ms
256110.111,162.3101 倍2.68 s52.9 ms
512110.415,176.9137 倍5.24 s101.1 ms
1024111.217,511.0157 倍10.48 s250.6 ms
1536113.217,209.5152 倍15.22 s533.4 ms

820 一共跑了 26 轮。除了 16 并发的前两轮是 154.9 和 130.2,其余都在 107 到 118 QPS 之间。并发翻了 96 倍,吞吐没动。

820 卡在哪​

吞吐不随并发变化,最常见的原因是请求在某个地方排队,一次只处理一个。820 的其他数据也都对得上这个解释。

延迟基本就是排队时间。每秒只能处理 110 个请求的话,N 个并发请求平均要等前面 N 个处理完。照这个算,16 并发的 p50 应该是 145 ms,1536 并发是 14 秒;实测是 139 ms 和 13.4 秒。

机器很闲。整机 CPU 不到 10%,3 个节点加起来只用了 1.2 个逻辑核左右。磁盘每秒 225 次 flush,每次 2 ms 左右,利用率 67%。

写入没有合并。拿 256 并发的第 1 轮来看,node1 在 60 秒里处理了 6,800 个请求左右,Raft 日志的 append 和 save_committed 次数跟请求数几乎一样,磁盘 flush 次数大约是请求数的两倍。每个请求都单独写一次日志、单独刷盘。

110 QPS 是怎么算出来的​

请求串行处理时,吞吐就是 1 ÷ 单个请求的耗时。为了把这个耗时拆开,我又对 820 跑了几组 20 秒的实验,分别改并发数、节点数,以及数据目录放在 NVMe 还是内存盘(tmpfs)上:

实验节点数并发存储QPS每个请求耗时
A31NVMe160.46.2 ms
B364NVMe169.55.9 ms
C164NVMe490.52.0 ms
D364内存盘547.01.8 ms
E164内存盘10,846.70.09 ms

表里的“每个请求耗时”是用 1 ÷ QPS 算的。这组实验和正式测试不在同一时段跑,所以 NVMe 上是 160 到 170 QPS,不是 110。

A 和 B 说明并发不起作用:1 个并发和 64 个并发的吞吐差不多。E 组既不刷盘也不复制,一个请求只要 0.09 ms,但 64 并发时 p50 还是 5.76 ms,正好约等于 64 × 0.09 ms,请求照样在排队。

剩下几组用来拆耗时。C 比 E 多出 1.9 ms 左右,这是一次刷盘的时间,单独测这块盘写 4 KiB 再 fsync,p50 是 1.96 ms。D 比 E 多出 1.7 ms 左右,这是三节点复制的开销,这里走的还是本机回环网络。把 Leader 刷盘、Follower 刷盘和复制往返加起来,大约 6 ms,和 A、B 对得上。两次刷盘在流程上不一定完全串行,但三个节点共用一块盘,刷盘本来就要互相排队。

这块盘是消费级 NVMe,没有掉电保护,fsync 的 p99 能到 58 ms。所以单个请求的耗时会在 6 到 9 ms 之间来回变,对应 110 到 170 QPS。

换好一点的硬件也高不了多少。D 组刷盘几乎不花时间,三节点也只有 550 QPS 左右;真正跨机部署,还要再加网络往返。我估计 820 三节点在一般服务器上的写入上限是几百 QPS,这个数没有实测,但不管什么硬件,加并发都不会让它变快。

用官方镜像二分​

历史 nightly 的 Release 页面上已经找不到二进制,好在 Databend 国内镜像仓库 registry.databend.cn/public/databend-meta 还留着历史 nightly 的镜像,可以匿名拉,这次要用的版本都拉到了。我从镜像里取出 databend-meta 和 databend-metactl,每个版本跑一轮 64 并发、20 秒:

版本日期QPSp50p99
1.2.8202025-09-25161.7345 ms3,359 ms
1.2.8802026-02-08172.7356 ms392 ms
1.2.8902026-03-22135.8486 ms523 ms
1.2.8952026-04-12123.6490 ms1,789 ms
1.2.8962026-04-134,749.412 ms22 ms
1.2.8972026-04-194,443.812 ms24 ms
1.2.8992026-04-224,908.012 ms22 ms
1.2.9002026-04-224,614.112 ms23 ms

镜像里是 glibc 动态链接的二进制,测试时长也和正式测试不同,这张表只用来看版本之间有没有跳变,绝对值不要和前面比。895 到 896 差了 38 倍,位置很清楚。

895 和 896 之间只有 3 个提交,其中两个升级了 databend-meta 依赖:

提交内容
#19703升级到 databend-meta v260312.6.0,带入 rotbl 0.2.10,改进快照块读取的并发
#19704升级到 databend-meta v260312.7.0

v260312.7.0 里有一个提交 6bc6f54,标题是 parallelize MetaWorker request processing,和前面看到的现象完全对得上。rotbl 改的是快照数据的读取,而这次测试从空数据开始,也基本不触发快照,所以我认为原因是 6bc6f54。

那个循环​

MetaWorker 从队列里取请求,交给 meta 节点执行。820 里是这么写的:

pub async fn run(mut self) {
while let Some(box_fn) = self.worker_rx.recv().await {
(box_fn)(self.meta_node.clone()).await;
}
// ...
}

取一个请求,await 到它执行完,再取下一个。写请求的“执行完”要走完一整轮 Raft 提交,所以任何时候都只有一个写请求在处理,吞吐被卡死在 1 ÷ 单次提交耗时。

6bc6f54 改成每个请求起一个独立任务,用信号量限制最多 4096 个同时执行(代码有删节):

const MAX_IN_FLIGHT: usize = 4096;

pub async fn run(mut self) {
let semaphore = Arc::new(Semaphore::new(Self::MAX_IN_FLIGHT));

while let Some(box_fn) = self.worker_rx.recv().await {
let permit = semaphore.clone().acquire_owned().await.expect("semaphore closed");
let meta_node = self.meta_node.clone();
RT::spawn(
async move {
(box_fn)(meta_node).await;
drop(permit);
},
Some("meta-request".to_string()),
);
}
// ...
}

一共改了 19 行。多个写请求能同时进入 Raft 以后,日志模块可以把它们合并成一次写入、一次刷盘,刷盘的成本就摊到了多条写入上。

后面的版本在这条路径上还有优化。2026 年 5 月的 #19861 升级依赖后用上了 raft-log 的 WAL group commit,会稍等一下,把更多写入攒到一次刷盘里。仓库里的基准报告显示,2 节点集群上吞吐又提高了 10% 到 13%,1024 并发时平均一次刷盘合并 99 条写入左右。

6bc6f54 的提交说明里还提到,串行时读请求也得排在慢写入后面,改完之后他们测的读请求 p50 从 29 ms 降到了 226 µs。

升级​

写入并发高的环境,至少升到 1.2.896。更建议直接上新版本,group commit 这些后续优化也一起拿到。

兼容性可以查仓库里的 src/meta/ver/compat_docs/。820 的 query 兼容 1.2.677 及以上的 meta;文档里 1.2.896 和最新收录的 1.2.926 到 1.2.929 的 meta,都兼容 1.2.676 及以上的 query,956 自己报告的最低客户端版本也是 1.2.676。所以先只升级 meta、query 暂时不动,版本上是可以的。meta 节点之间,文档里最新的 1.2.929 要求对端不低于 1.2.818,820 满足;956 还没收录进文档,滚动升级前要再确认一下。

820 和 956 的磁盘数据格式都是 V004。生产环境升级前还是要先备份,在测试环境把滚动升级演练一遍。

几点说明​

这次只测了小 KV 写入,没测读请求,也没测真实的元数据操作。3 个节点和压测客户端在同一台机器上,共用一块消费级 NVMe,没有跨机网络,所以 110 QPS 只代表这台机器;“吞吐不随并发增长”这一点和硬件无关。

820 和 956 的正式测试隔了几个小时才跑,不是交替进行的。这台机器上我见过的状态漂移在 1.7 倍左右,同一时段各跑一次 16 并发的冒烟测试也差了 8.6 倍,远小于 14 到 157 倍的差距。两个二进制的链接器和工具链也不一样,但二分结果已经说明,主要的跳变来自 896 的代码改动。

二分时每个版本只跑了一轮 20 秒,只用来找跳变的位置。我也没有单独编译只包含 #19703 的版本来排除 rotbl 的影响,6bc6f54 读请求的那组数据我同样没有复测。

附录:二分探测命令​

拉取指定版本的镜像,取出二进制:

TAG=v1.2.896-nightly
IMG=registry.databend.cn/public/databend-meta:$TAG
BIN=/data/meta-bisect/bin/$TAG

docker pull "$IMG"
mkdir -p "$BIN"
C=$(docker create "$IMG")
docker cp "$C:/usr/bin/databend-meta" "$BIN/"
docker cp "$C:/usr/bin/databend-metactl" "$BIN/"
docker rm "$C"

# 每个版本使用同一份压测客户端
cp /path/to/databend-metabench "$BIN/"

在 Databend 仓库根目录执行一轮探测:

python3 scripts/benchmark/meta/meta-cluster-bench.py \
--node-bin "$BIN" --node-label "$TAG" --node-count 3 \
--work-dir "$(mktemp -d)/cluster" --port-base 29101 \
--workload-rpc upsert_kv \
--workload-clients 64 --workload-client-pool-size 64 \
--workload-ops 1000000 --workload-run-secs 20 --repeat 1

测试机的硬件和系统配置见上一篇。

💬 社区支持
有问题与我们的团队联系:Slack

微信:82565387

wubx 的微信二维码