YEN HARVEY
cd ../notes

后端 / 性能

先别急着怪 Rust:一次 debug 模式压测乌龙

8 min read
深夜桌面上的压测终端,开发者盯着请求延迟数据发呆。
深夜桌面上的压测终端,开发者盯着请求延迟数据发呆。

这篇算是一次压测乌龙记录。

我一开始想测一下后端匿名读接口的性能。项目是 Rust + Axum,接口也不是很离谱,所以心理预期其实挺高。结果第一轮数字出来,列表接口直接把我看沉默了。

最夸张的是这个:

/v1/community/anli?page=1&page_size=20
20 并发
RPS: 21.1
avg: 942.1ms
p95: 1011.0ms
p99: 1041.2ms

看到这个数字的时候,第一反应当然是:这是不是也太弱了?Rust 后端就这?

后来冷静下来才发现,这个结论下得太早了。当时我测的是 debug binary,而且请求级日志也没按压测场景收敛。更重要的是,我把几个完全不同性质的接口混在一起看了。

第一轮数字

先看当时的 debug 结果。

/health 这种轻链路接口:

场景成功率RPSavgp95p99
20 并发 10s100%59333.37ms5.97ms9.56ms
100 并发 10s100%543618.40ms31.14ms55.94ms

匿名业务接口,300 请求 / 20 并发。详情页我取了不同类型的帖子 ID,避免只看一种内容形态:

接口成功率RPSavgp95p99
/v1/community/anli/{article_id}100%169116.6ms146.0ms157.5ms
/v1/community/anli/{media_moment_id}100%25876.6ms91.4ms96.9ms
/v1/community/anli/{qa_id}100%32660.1ms114.9ms122.3ms
/v1/community/qa/{qa_id}/answers?page=1&page_size=50100%54935.9ms69.5ms76.7ms
/v1/community/recommendations/anli/{article_id}/anlis100%125815.4ms58.8ms65.5ms
/v1/community/recommendations/anli/{article_id}/authors100%142613.8ms27.6ms33.9ms
/v1/community/anli?page=1&page_size=20100%21942.1ms1011.0ms1041.2ms

这个表其实已经在说话了。

/health 不算差,推荐接口也不差,QA answers 也还行。真正难看的是列表,尤其是 page_size=20。所以问题不是“整个 Rust 服务都慢”,而是某些业务路径很重。

然后发现我还在 debug 模式

更尴尬的是,我当时跑的是:

target/debug/acghub-server

这对 Rust 来说差别很大。debug 构建没有 release 优化,很多路径的开销会被放大。再叠加请求级日志、开发环境配置、本地 DB,拿这个数字去评价最终性能,本来就不公平。

于是我重新 build release,再测同一批接口。

release 之后

同样先看 /health

场景成功率RPSavgp95p99
20 并发 10s100%305080.65ms1.15ms2.18ms
100 并发 10s100%290333.44ms6.36ms8.74ms

这个变化很直观:轻链路从 5k-6k RPS 到接近 3 万 RPS。

匿名业务接口也变了很多。详情页、QA answers 和推荐接口基本都回到了比较合理的区间,但列表页还是明显慢:

接口成功率RPSavgp95p99
/v1/community/anli/{article_id}100%73426.9ms84.9ms88.9ms
/v1/community/anli/{media_moment_id}100%131315.0ms21.2ms24.1ms
/v1/community/anli/{qa_id}100%162912.0ms25.6ms26.8ms
/v1/community/qa/{qa_id}/answers?page=1&page_size=50100%22538.7ms25.1ms27.3ms
/v1/community/recommendations/anli/{article_id}/anlis100%60443.2ms5.3ms6.8ms
/v1/community/recommendations/anli/{article_id}/authors100%44714.4ms10.2ms12.6ms
/v1/community/anli?page=1&page_size=20100%99200.7ms237.6ms241.2ms

这时就比较清楚了:

  • 框架轻链路没问题;
  • /v1/community/anli/{article_id}{media_moment_id}{qa_id} 这几类详情样本都没问题;
  • QA answers 和推荐接口很轻;
  • 列表接口仍然偏慢,哪怕 release 后也只有 99 RPS、p95 237.6ms。这个数字和详情页、QA answers、推荐接口放在一起看,就很扎眼:它不是构建模式能完全解释的问题,而是列表路径自己还有结构性问题。

debug 数字不是废物

虽然 debug 不能当最终性能结论,但它也不是完全没用。

它适合做两件事:

  1. 看接口之间的相对差异;
  2. 看优化前后的相对变化。

比如列表接口在 debug 下很差,这个信号是真的。后面继续追,确实发现列表项装配复用了详情逻辑,导致 N+1 查询。也就是说,debug 数字不能拿来吹上限,但可以用来发现问题形状。

我后来单独写了一篇列表优化的记录。核心就是把每条帖子循环里的查询提到循环外,整页批量预取,再纯内存组装。

这次学到的第一件事:先测 health

现在我觉得压测任何后端,第一步应该先测一个最轻接口,比如 /health

它的意义不是证明业务接口能扛多少,而是给你一个上界参照:

/health 快:框架、运行时、网络、基本中间件大概率不是瓶颈
/health 慢:先别看业务 SQL,基础链路就有问题

这次就是这样。debug 下 /health 已经有 5k-6k RPS,release 后接近 3 万 RPS。说明 Axum/Rust 这层没什么大问题。

业务列表只有 21 RPS,原因就应该继续往业务路径里找,而不是先怀疑语言。

第二件事:不要拿 hello world 和业务接口互相羞辱

这个坑也很常见。

有人会说某某框架 hello world 几十万 RPS,也有人会说自己业务接口几百 RPS 就很不错。两边都没错,但它们说的不是同一个东西。

这次几个接口刚好能形成对照:

类型release 表现说明
/health~30k RPS基础链路,非常轻
related 接口4k-6k RPS返回体小,查询轻
QA answers~2.2k RPS业务读,但数据量小
/v1/community/anli/{article_id} / {media_moment_id} / {qa_id}700-1600 RPS真实业务聚合读
/v1/community/anli?page=1&page_size=20优化前 ~99 RPS整页装配重,暴露 N+1

同一个服务,不同接口能差两个数量级。这很正常。

第三件事:日志级别也会影响压测

生产环境不是不能开 info,但高频请求路径不应该每次都打一堆 info!

比较合理的方式是:

  • 启动、配置、后台任务摘要:info
  • 单请求细节、参数、路径调试:debug
  • 异常但可恢复:warn
  • 明确失败:error

压测时尤其要把请求级 debug/trace 日志关掉。不然你测到的可能是日志 I/O、格式化和终端输出,而不是接口本身。

我这次最开始没有把这些边界分清楚,所以第一眼看到低数字才会很焦虑。

后来我怎么重新看这些数字

现在回头看,第一次压测其实不算失败。它至少暴露了三个事实:

  1. release 和 debug 差距巨大;
  2. 轻链路和业务链路不能混着评价;
  3. 列表接口的慢不是错觉,确实有结构性问题。

如果没有这轮压测,我可能不会那么快去查列表装配,也不会发现那个“单条详情函数被列表循环复用”的 N+1。

所以这篇其实只解决了第一个误判:release 和 debug 要分开看。它没有解决列表本身的问题。真正的问题是:为什么 health、详情页、QA answers 都明显提升了,唯独 anli list 还是慢?这个问题还得继续往列表装配和查询结构里挖。

所以这次不是“压测结果很差”,而是“第一次解读太急”。

我现在会怎么测

下一次我会按这个顺序来:

  1. 确认跑的是 release binary;
  2. 关掉请求级 debug/trace 日志;
  3. 先测 /health,建立基础链路上限;
  4. 再测单条详情,确认业务聚合读的基线;
  5. 再测列表 page_size=1/20,看延迟是否随条数异常放大;
  6. 最后才讨论 Redis、缓存和多实例。

这个顺序能避免很多误判。

最后

这次最有意思的地方不是 release 快了多少,而是我意识到:压测不是跑一个工具然后看 RPS 排名。

压测真正有价值的地方,是把不同层次的问题拆开:

运行时 / 框架 / 中间件
业务查询 / 响应装配
构建模式 / 日志级别
单接口 / 整页链路
平均值 / p95 / p99

一开始看到 21 RPS 的时候,我确实有点怀疑人生。后来把构建模式、日志、接口类型和查询结构拆开看,事情就清楚多了。

Rust 没有魔法。debug 模式也不该背最终性能的锅。真正该做的,是先把测量条件弄干净,再让数据指向代码里的问题。

作者头像
yen@harvey:~$ exit 0