最初的判断很简单:这个接口 90% 的时间都花在 JSON 的序列化与反序列化上,而不是业务逻辑。既然要动,就干脆把整条热路径换成一门能精确控制内存的语言。

但我们给自己划了条线:不追求全部重写,只替换真正吃 CPU 的那 300 行。剩下两万多行 Go 代码一行不动——重写的收益必须大于它带来的风险。

火焰图里藏着什么

在动手之前,我们先跑了 48 小时的持续采样。火焰图里最高的那座山不是数据库,也不是网络,而是 encoding/json 里的反射调用:每一次请求都要重新解析一次结构体标签。

如果你不能在优化前量化它,那你也很难在优化之后证明它真的有效。

这轮采样给出了三个明确结论:

  • P99 延迟 112ms,其中 74ms 花在序列化上;
  • 峰值 CPU 有 38% 被垃圾回收吃掉;
  • 数据库耗时只占 9%,根本不是瓶颈。

有了这三条,后面所有决策都不需要拍脑袋。

换掉反射解析

反射的问题在于它每次都要重新读取结构体标签。改成编译期生成解析代码之后,这部分开销直接归零:

use std::time::Instant;

pub fn parse_payload(buf: &[u8]) -> Payload {
    let start = Instant::now();
    let payload = simd_json::from_slice(buf)?;
    // 反射解析换成 SIMD,单次调用 41 微秒 -> 6 微秒
    metrics::observe("parse", start.elapsed());
    Ok(payload)
}

换成 SIMD 版的解析器之后,单次调用耗时从 41 微秒降到 6 微秒。整条链路的 P99 从 112ms 掉到 8ms 左右,峰值 CPU 占用也降了近四成。

那些我们最后放弃的优化

不是所有优化都值得做。下面三件事我们评估过,最后全部放弃:

  1. 把整个服务迁到 Rust —— 收益集中在那 300 行里,迁移剩下的部分只会增加维护成本。
  2. 手写 SIMD 指令 —— 编译器自动向量化已经够用,手写版在老机器上反而更慢。
  3. 用异步运行时重写 IO —— 压测显示瓶颈根本不在 IO,改了也不会更快。

上线之后

灰度两周,错误率没有变化,P99 稳定在 8ms 上下。最大的收获其实不是性能,而是我们终于有了一条可复现的压测基线:以后任何一次重构,都能在合并前就知道它到底是变快了还是变慢了。

代码和压测脚本都已经放到仓库里,欢迎直接跑一遍推翻我的结论。