后端 / 性能
先别急着怪 Rust:一次 debug 模式压测乌龙
这篇算是一次压测乌龙记录。
我一开始想测一下后端匿名读接口的性能。项目是 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 这种轻链路接口:
| 场景 | 成功率 | RPS | avg | p95 | p99 |
|---|---|---|---|---|---|
| 20 并发 10s | 100% | 5933 | 3.37ms | 5.97ms | 9.56ms |
| 100 并发 10s | 100% | 5436 | 18.40ms | 31.14ms | 55.94ms |
匿名业务接口,300 请求 / 20 并发。详情页我取了不同类型的帖子 ID,避免只看一种内容形态:
| 接口 | 成功率 | RPS | avg | p95 | p99 |
|---|---|---|---|---|---|
/v1/community/anli/{article_id} | 100% | 169 | 116.6ms | 146.0ms | 157.5ms |
/v1/community/anli/{media_moment_id} | 100% | 258 | 76.6ms | 91.4ms | 96.9ms |
/v1/community/anli/{qa_id} | 100% | 326 | 60.1ms | 114.9ms | 122.3ms |
/v1/community/qa/{qa_id}/answers?page=1&page_size=50 | 100% | 549 | 35.9ms | 69.5ms | 76.7ms |
/v1/community/recommendations/anli/{article_id}/anlis | 100% | 1258 | 15.4ms | 58.8ms | 65.5ms |
/v1/community/recommendations/anli/{article_id}/authors | 100% | 1426 | 13.8ms | 27.6ms | 33.9ms |
/v1/community/anli?page=1&page_size=20 | 100% | 21 | 942.1ms | 1011.0ms | 1041.2ms |
这个表其实已经在说话了。
/health 不算差,推荐接口也不差,QA answers 也还行。真正难看的是列表,尤其是 page_size=20。所以问题不是“整个 Rust 服务都慢”,而是某些业务路径很重。
然后发现我还在 debug 模式
更尴尬的是,我当时跑的是:
target/debug/acghub-server 这对 Rust 来说差别很大。debug 构建没有 release 优化,很多路径的开销会被放大。再叠加请求级日志、开发环境配置、本地 DB,拿这个数字去评价最终性能,本来就不公平。
于是我重新 build release,再测同一批接口。
release 之后
同样先看 /health。
| 场景 | 成功率 | RPS | avg | p95 | p99 |
|---|---|---|---|---|---|
| 20 并发 10s | 100% | 30508 | 0.65ms | 1.15ms | 2.18ms |
| 100 并发 10s | 100% | 29033 | 3.44ms | 6.36ms | 8.74ms |
这个变化很直观:轻链路从 5k-6k RPS 到接近 3 万 RPS。
匿名业务接口也变了很多。详情页、QA answers 和推荐接口基本都回到了比较合理的区间,但列表页还是明显慢:
| 接口 | 成功率 | RPS | avg | p95 | p99 |
|---|---|---|---|---|---|
/v1/community/anli/{article_id} | 100% | 734 | 26.9ms | 84.9ms | 88.9ms |
/v1/community/anli/{media_moment_id} | 100% | 1313 | 15.0ms | 21.2ms | 24.1ms |
/v1/community/anli/{qa_id} | 100% | 1629 | 12.0ms | 25.6ms | 26.8ms |
/v1/community/qa/{qa_id}/answers?page=1&page_size=50 | 100% | 2253 | 8.7ms | 25.1ms | 27.3ms |
/v1/community/recommendations/anli/{article_id}/anlis | 100% | 6044 | 3.2ms | 5.3ms | 6.8ms |
/v1/community/recommendations/anli/{article_id}/authors | 100% | 4471 | 4.4ms | 10.2ms | 12.6ms |
/v1/community/anli?page=1&page_size=20 | 100% | 99 | 200.7ms | 237.6ms | 241.2ms |
这时就比较清楚了:
- 框架轻链路没问题;
/v1/community/anli/{article_id}、{media_moment_id}、{qa_id}这几类详情样本都没问题;- QA answers 和推荐接口很轻;
- 列表接口仍然偏慢,哪怕 release 后也只有 99 RPS、p95 237.6ms。这个数字和详情页、QA answers、推荐接口放在一起看,就很扎眼:它不是构建模式能完全解释的问题,而是列表路径自己还有结构性问题。
debug 数字不是废物
虽然 debug 不能当最终性能结论,但它也不是完全没用。
它适合做两件事:
- 看接口之间的相对差异;
- 看优化前后的相对变化。
比如列表接口在 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、格式化和终端输出,而不是接口本身。
我这次最开始没有把这些边界分清楚,所以第一眼看到低数字才会很焦虑。
后来我怎么重新看这些数字
现在回头看,第一次压测其实不算失败。它至少暴露了三个事实:
- release 和 debug 差距巨大;
- 轻链路和业务链路不能混着评价;
- 列表接口的慢不是错觉,确实有结构性问题。
如果没有这轮压测,我可能不会那么快去查列表装配,也不会发现那个“单条详情函数被列表循环复用”的 N+1。
所以这篇其实只解决了第一个误判:release 和 debug 要分开看。它没有解决列表本身的问题。真正的问题是:为什么 health、详情页、QA answers 都明显提升了,唯独 anli list 还是慢?这个问题还得继续往列表装配和查询结构里挖。
所以这次不是“压测结果很差”,而是“第一次解读太急”。
我现在会怎么测
下一次我会按这个顺序来:
- 确认跑的是 release binary;
- 关掉请求级 debug/trace 日志;
- 先测
/health,建立基础链路上限; - 再测单条详情,确认业务聚合读的基线;
- 再测列表
page_size=1/20,看延迟是否随条数异常放大; - 最后才讨论 Redis、缓存和多实例。
这个顺序能避免很多误判。
最后
这次最有意思的地方不是 release 快了多少,而是我意识到:压测不是跑一个工具然后看 RPS 排名。
压测真正有价值的地方,是把不同层次的问题拆开:
运行时 / 框架 / 中间件
业务查询 / 响应装配
构建模式 / 日志级别
单接口 / 整页链路
平均值 / p95 / p99 一开始看到 21 RPS 的时候,我确实有点怀疑人生。后来把构建模式、日志、接口类型和查询结构拆开看,事情就清楚多了。
Rust 没有魔法。debug 模式也不该背最终性能的锅。真正该做的,是先把测量条件弄干净,再让数据指向代码里的问题。