sY.zhang.
博客

从 HTTP 到 gRPC:一次内部服务迁移的实战记录

把一组 Node.js 微服务从 REST 迁到 gRPC,性能提升明显,但踩过的坑比预期多得多。这是一份记录。

1 分钟阅读

迁了一组微服务从 REST 到 gRPC,QPS 提升约 35%,p99 延迟下降一半。但过程中踩的坑远比想象多,下面是按时间顺序整理的复盘。

为什么迁

那组服务在内部,原本是 HTTP + JSON:

  • 单接口平均 ~12ms,p99 ~50ms
  • JSON 序列化占 CPU 约 18%
  • 字段契约靠口头约定,下游经常因字段缺失线上崩

gRPC 的优势刚好对症:Protobuf 严格 schema、二进制更小、HTTP/2 多路复用。

坑 1:Protobuf 不是 JSON

迁的时候第一反应是「把 JSON schema 翻译成 proto」。但 Protobuf 的设计哲学跟 JSON 不一样:

  • 字段默认值(0 / "" / false不能区分未设置和显式设为零值,除非用 optional(proto3 才有)
  • oneof 不能重复设置,否则前一个会清空
  • map 不能 nested map

第一版迁移完,下游服务收到 count: 0 时分不清是「没数据」还是「数据为 0」。后来给所有可能为零的字段加了 optional

坑 2:流式调用调试难

REST 用 curl 一行就能测,gRPC 需要 grpcurl:

grpcurl -plaintext -d '{"id": "123"}' \
  localhost:9090 \
  com.example.UserService/GetUser

但流式接口(server streaming)调试就麻烦,得自己写脚本。后来我们写了一个内部 CLI,把所有 gRPC 接口都暴露成行式 JSON 输出,调试体验才算追上 curl。

坑 3:负载均衡器不认 HTTP/2

上线第一周发现 QPS 没提升,反而偶发超时。定位下来:Cloud LB(Nginx ingress)默认是 L7 负载,对 HTTP/2 的多路复用只算一个连接,所有请求被路由到同一个后端。

解决:换成 L4(直连 Pod IP),让客户端自己做负载均衡(gRPC 默认会)。或者开启 keepalive 让连接复用可控。

坑 4:错误码语义

gRPC 的 status code 只有 17 个,比 HTTP 还少。业务错误码塞哪里?

业界两种做法:

  1. 塞到 trailer metadata 里(x-business-error-code
  2. google.rpc.Status 详细错误(带 code / message / details

我们选了 2,因为对 SDK 友好,客户端能拿到结构化错误。

坑 5:浏览器侧的 gRPC-Web

前端调 gRPC 要走 grpc-web,多一层 Envoy 转换。性能不如直接 gRPC,且不支持 streaming。

最后我们的方案:内部服务全部 gRPC,对前端暴露 REST 网关(用 grpc-gateway 自动生成)。

收益

迁移三个月后:

  • QPS 提升 35%
  • p99 从 50ms 降到 22ms
  • CPU 占用降 40%(主要来自序列化成本下降)
  • 下游因字段缺失崩溃的事故:0 起

但代价也不小:调试成本上升、新人上手慢、监控要换一套(Prometheus + grpc 监控指标)。

如果服务量不大、契约不复杂,REST 仍然是最优解。gRPC 是规模上来之后的工具,不是银弹。