
你选择了“最快”的技术栈,因为你厌倦了总要解释为什么 p99 是别人的问题。

你选了别人称为“快如闪电”的语言,然后眼睁睁看着延迟曲线长出獠牙。
而且你依旧要在每次评审里回答同一个问题:为什么又慢了。
让我们沉迷的基准测试
在选语言之前,我们做了每个严肃团队都会做的事。
同一个接口。同样的载荷。同一台机器。同样的压力模式。
解析一个小 JSON,做一次缓存查询,未命中就访问一次数据库,然后返回一个精简响应。
不是玩具,但足够干净。
Go 纪律严明、可预测。
Rust 像个超级英雄。
Node.js 看起来像是那种“追求交付速度,而不是 CPU 速度”的选择。
我们说服自己:为热点路径选最快工具是负责任的做法。
因为服务要承受高流量,原始性能似乎是最安全的下注。
这种信念一直持续到服务遇到真实流量。
一百万请求的真实世界
在规模化场景中,一个端点从来不只是一个端点。
它是数据库连接池前的队列。
它是你乐观时设置的重试策略。
它是预发布中很便宜、上线后很昂贵的指标流水线。
它是那个变慢的依赖,迫使所有调用者礼貌地等待。
当我们一天内跨过一百万请求后,图表就不再在乎微基准了。
p50 依然好看。
p95 让你产生虚假的信心。
p99 成了每天的争论点。
延迟的形状变了。
不再是平滑的曲线。
而是平静、平静、平静,然后突然出现像被人掐住系统一样的尖刺。
那时我们学到了后端里最烦人的真相:
速度不是语言特性。
速度是你的系统拒绝排队。
我们无法忽视的记分板
我们用同样的压测再跑一次,但这次测的是生产里真正会痛的指标。
不只看中位延迟,还看压力下的尾延迟。
我们还模拟了下游抖动,因为现实系统就是这么表现的。
下面是同一 API、同一流量曲线下的结果:
栈 p50 p95 p99 峰值 RPS 重试突发下错误率
Node 8ms 38ms 170ms 18k 0.3%
Go 6ms 30ms 240ms 22k 0.6%
Rust 4ms 22ms 410ms 26k 1.1%
Rust 赢了“干净场景”的比赛。
Rust 也在系统变得混乱时交出了最丑的尾巴。
Node 不是最快的。
但当周围一切不再平静时,Node 是最稳的。
这才是让我们惊讶的部分。
时间到底花在哪里
当 p99 变坏,我们开始追查慢代码。
几乎没找到。
服务并没有把时间花在做事上。
它把时间花在了等待上。
我们开始追三个比火焰图更诚实的指标:
队列深度。
数据库连接池等待。
重试率。
重试率上升,队列深度紧随其后。
队列深度上升,连接池等待紧随其后。
然后 p99 爆炸,即使处理函数本身效率完美无缺。
如果你盯着延迟图表时感到胃一沉,你已经知道接下来会发生什么。
你修的不是某个请求。
你修的是压力的形状。
“最快的那个” 输掉的时刻
一个下游依赖略微变慢。
不是全量故障,也不是灾难性失败。
只是慢到让一些请求超时。
然后重试开始启动。
重试策略本来是为了保护。
结果成了放大器。
Rust 完全照它被训练的方式工作。
它猛烈地吞噬进入的工作。
它也放大了爆炸半径,因为我们的反压体系还不成熟。
请求堆积。
队列增长。
连接池等待变成真正的延迟来源。
尾延迟从一个数字变成了一种情绪。
痛苦在于,Rust 本身并没有错。
错的是我们的自信。
我们把处理器优化得很好,却把控制系统搭得太薄。
Go 表现更好,但尾部仍有熟悉的模式。
在突发流量下,GC 压力像节拍一样出现在 p99 里。
不总是发生,也不持续,但足以在最糟时刻被注意到。
Node 才是反转。
Node 没赢原始速度。
Node 之所以赢,是因为它让系统更难被不小心烧穿。
它天然的摩擦像一个软限流。
它逼我们更早正视反压。
它让我们选择更小的并发。
它让我们尊重队列深度。
它让服务变得无聊。
在一百万请求的规模下,无聊就是你想要的。
我们反复画的那张图
我们画了很多次,最终它成了每次事故复盘的心智模型。
客户端
|
v
API
|
+--> 解析 JSON
|
+--> 缓存(命中)
| |
| v
| 返回
|
+--> 数据库连接池(未命中)
|
v
下游依赖
|
v
返回
p99 死亡之处:
一旦你接受这张图,语言战争就会安静下来。
因为瓶颈很少在语法上。
瓶颈在等待上。
我希望更早明白的结论
Rust 是手术刀。
Go 是锋利可靠的厨刀。
Node 是一把结实的多用途刀,更多人能安全使用。
选择与你团队诊断压力、塑形流量、无惧发布修复的能力匹配的工具。
因为在一百万请求的规模下,赢家不是“单点最快”的语言。
赢家是能帮你在世界不平静时让 p99 保持平静的语言。
这场性能对比的实践再次说明,系统设计的复杂性远超单一语言层面的比较。对 后端与架构 的深刻理解,往往比追求极限的微秒级性能更为关键。技术选型的讨论,也欢迎在 云栈社区 与更多同行交流碰撞。