全部文章

Writing

用 Go 重写解析服务:800ms 到 120ms 的取舍

不是所有慢接口都要换个语言重写。先看火焰图,确认瓶颈是 CPU 还是 I/O,再决定投入。

问题:一个「很简单」的接口

中间层服务里有个解析接口,职责是把上游返回的嵌套 JSON 映射成插件需要的扁平结构。逻辑看起来不复杂:深度遍历、字段重命名、按规则匹配。但它的平均响应时间是 800ms,P99 达到 2.4s,单核 CPU 经常打满。

这个接口被插件高频调用,一旦变慢,整个操作链路都会卡住。

先定位,再动手

第一反应是「Node 不适合 CPU 密集任务,换 Go」。但这个结论必须由数据支撑,否则只是换了语言继续慢。

用 --cpu-prof 采样后,火焰图给出了明确答案:

  • 约 46% 时间在正则匹配(每条记录要跑十几个正则)
  • 约 28% 时间在深度遍历与对象拷贝
  • 约 12% 时间在 JSON 序列化
  • I/O 等待几乎可以忽略

结论:这是纯 CPU 瓶颈,且算法本身也有优化空间。于是确定方案:保留 Node 做业务编排,把解析下沉为 Go 服务。

设计:把边界切在 HTTP 上

没有选择 Node 原生插件(NAPI)或 FFI,而是最朴素的独立 HTTP 服务:

type ParseRequest struct {
    Records []map[string]any `json:"records"`
    Rules   []Rule           `json:"rules"`
}

func (h *Handler) Parse(w http.ResponseWriter, r *http.Request) {
    var req ParseRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    out := make([]FlatRecord, 0, len(req.Records))
    for _, record := range req.Records {
        out = append(out, h.flatten(record, req.Rules))
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(out)
}

选独立服务而不是原生模块,理由有三:

  1. 部署独立:Go 服务可以单独扩容,Node 层不必跟着重启。
  2. 故障隔离:解析服务崩溃不会拖垮整个中间层。
  3. 可回滚:Node 侧保留旧实现,按开关切流,出问题一分钟切回。

代价是多一次网络调用(同机房内约 1ms),相对于 680ms 的收益可以忽略。

优化不止于换语言

Go 版本第一版跑出来是 260ms,还不够。继续做了三件事:

1. 预编译正则

原来每次请求都 regexp.MustCompile,改在服务启动时按规则 ID 编译并缓存。这一项直接从 260ms 降到 150ms。

2. 复用缓冲区

用 sync.Pool 复用 bytes.Buffer 与 map,减少 GC 压力:

var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

GC 停顿从平均 18ms 降到 3ms 以内。

3. 限制单请求体积

对超过 5000 条记录的请求做分片,避免单个大请求长时间占用 CPU 导致其他请求排队。这是保护尾延迟的关键,比优化平均值更有意义。

结果

指标 Node 实现 Go 实现
平均响应 800ms 120ms
P99 2.4s 380ms
单机 QPS 约 40 约 320
CPU 使用 单核打满 平稳,可水平扩展

整体降幅约 85%,并且在流量翻倍时不再需要紧急扩容。

什么时候不该这么做

这次改造划算,是因为满足三个条件:

  • 瓶颈确实在 CPU,而不是数据库或第三方接口。
  • 该模块逻辑稳定,不常改;换语言带来的维护成本可以摊薄。
  • 有压测与灰度切流能力,能安全验证。

如果接口慢是因为 N+1 查询或缺少索引,换语言只是把问题搬家。先优化算法和数据访问,再考虑换技术栈。

复盘

这次项目最大的收获不是 Go 本身,而是建立了一套判断流程:采样 → 定位 → 估算收益 → 设计可回滚方案。语言只是最后的执行细节。把这套流程沉淀下来,后面再遇到「这个接口很慢」,团队就不会靠直觉争论了。