从 HTTP 到 gRPC:一次内部服务迁移的实战记录
把一组 Node.js 微服务从 REST 迁到 gRPC,性能提升明显,但踩过的坑比预期多得多。这是一份记录。
迁了一组微服务从 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不能 nestedmap
第一版迁移完,下游服务收到 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 还少。业务错误码塞哪里?
业界两种做法:
- 塞到 trailer metadata 里(
x-business-error-code) - 用
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 是规模上来之后的工具,不是银弹。