HTTP/JSON 到底慢在哪
微服务之间默认用 HTTP + JSON 通信,简单直观,但规模一上来就有三个问题:JSON 文本体积大、每次请求都要完整解析字段、基于 HTTP/1.1 一问一答连接开销高。一次调用可能只是查个库存,却要传上百字节的字段名、引号和逗号,还要在调用方和服务方反复做 JSON 编解码。
gRPC 是 Google 开源的高性能 RPC 框架,建立在 HTTP/2 之上:用二进制的 Protocol Buffers 序列化,体积比 JSON 小很多、解析快;同一条 HTTP/2 连接上能并发多路复用多个请求,还天然支持双向流。服务内部高并发调用、内部编排,gRPC 通常比 JSON over HTTP 快一个量级。
- IDL用 .proto 文件定义服务与消息,两端据此生成代码。
- 四种模式一元、服务端流、客户端流、双向流。
- HTTP/2多路复用让并发请求挤在一条连接上。
- 超时与重试调用方必须显式设超时,避免级联雪崩。
Protocol Buffers 与 IDL
gRPC 的核心是先写一个 .proto 文件:用 IDL(接口定义语言)声明有哪些服务、每个方法叫什么、请求和响应消息包含哪些字段。然后用 protoc 工具,根据这份契约自动生成 Java、Go、Python 等各语言的桩代码——调用方和服务方拿的是同一份强类型定义,字段名、类型对齐,不用再手写 DTO 也不用靠文档对齐。
syntax = "proto3";
service OrderService {
rpc GetOrder (GetOrderReq) returns (Order) {}
}
message GetOrderReq { int64 order_id = 1; }
message Order {
int64 id = 1;
string status = 2;
double amount = 3;
}
字段后面的数字(=1、=2)是字段编号,序列化时用编号代替字段名,体积小、且改名不影响线上兼容。新增字段用新编号,旧代码读到不认识的编号会自动跳过,这就是 proto3 前向兼容的基础。
四种通信模式
gRPC 不只是普通的“请求响应”,它借 HTTP/2 的流能力支持四种模式:
- 一元调用:最常用,发一个请求收一个响应,和普通接口一样。
- 服务端流:客户端发一个请求,服务端持续回传一串消息(如行情推送、日志订阅)。
- 客户端流:客户端连续发一串消息,最后服务端回一个汇总(如批量上传)。
- 双向流:两条流同时打开,双方随时互发消息(如实时聊天室、双向同步)。
选哪种看业务:查单、扣库存这类一次性操作用一元;需要“边生产边处理”的场景(如日志采集、实时对账)就用流模式,避免反复建连。
服务端与客户端代码
生成桩代码后,服务端只要实现 .proto 里定义的接口方法,客户端拿到一个 Stub 就能像调本地函数一样调用。下面是一段 Go 风格的服务端实现示意:
// 服务端:实现 OrderServiceServer 接口
type orderServer struct {
pb.UnimplementedOrderServiceServer
}
func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderReq) (*pb.Order, error) {
return &pb.Order{Id: req.OrderId, Status: "PAID", Amount: 99.5}, nil
}
// 客户端:建立连接后直接调用
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewOrderServiceClient(conn)
resp, _ := client.GetOrder(ctx, &pb.GetOrderReq{OrderId: 1001})
注意连接是长连接,建一次连、反复复用,而不是每次调用重新握手。这也是 gRPC 高并发下省资源的关键。
拦截器、超时与错误处理
生产环境绝不能裸调。三件事必须做:
- 超时:每个调用都要带 context 超时,下游卡住时调用方及时放弃,避免线程被占满引发级联雪崩。
- 拦截器:用 interceptor 统一加日志、鉴权、链路追踪、指标埋点,不用每个方法手写。
- 错误模型:gRPC 用 status code(如 NotFound、Unavailable、DeadlineExceeded)表达错误,比 HTTP 状态码更细,客户端可据此决定是否重试。
重试要克制:只对幂等、且明确是网络抖动(Unavailable)的调用重试,否则重复扣款这类非幂等操作会出大问题。
gRPC 与 REST 的取舍
gRPC 性能好,但不是所有场景都该用。内部服务之间(订单、库存、支付互相调用)用 gRPC,体积小、延迟低、契约强;对外暴露给前端、第三方、浏览器的接口仍用 REST/JSON——因为浏览器原生不支持 gRPC 的二进制流,调试也不如 JSON 直观。
常见做法是:前面一层 API 网关对外提供 REST,内部转成 gRPC 调到各微服务,既保留对前端友好的 JSON,又拿到内部高效通信的红利。网关与微服务的关系,可参考本站《API 网关与微服务治理实战》。
版本演进与灰度
proto 文件会随业务演进演进。原则:字段只增不删、用新编号;老字段废弃就保留、标为 reserved,防止别人误用同一个编号造成数据错位。接口本身演进时,建议新开一个 v2 服务,通过网关或服务发现按比例切流,把灰度流量导到新版,验证稳定后再全量替换老版。
跨语言时尤其要注意:Java、Go、Python 各自生成的桩要和同一份 .proto 版本对齐,升级时先更新契约、再同步各端生成代码,别让两端拿到的字段编号对不上。
实战清单
落地清单:
- 服务内部高并发调用用 gRPC,对外仍用 REST。
- 先写 .proto 契约,再由 protoc 生成各语言桩代码。
- 字段编号只增不删,废弃字段标 reserved。
- 每调必带超时,拦截器统一做日志、鉴权、埋点。
- 流式场景用服务端流或双向流,替代反复建连。
- 重试只对幂等的网络错误,别盲目重试。
gRPC 的价值在于用一份契约和 HTTP/2,把“内部服务之间的调用”从笨重的文本传输,变成高效、强类型、可流式的长连接通信。把 IDL、超时、错误模型这三件事用对,微服务的内部网络开销会明显下降。