gRPC 与 Protobuf
微服务之间的调用,正在从「REST + JSON」转向「gRPC + Protobuf」。 这篇讲清楚它到底好在哪里,以及怎么用。
一、为什么需要 gRPC
| REST + JSON | gRPC + Protobuf | |
|---|---|---|
| 格式 | 文本 JSON | 二进制 Protobuf |
| 体积 | 大(字段名重复、无压缩) | 小 3~10 倍 |
| 解析 | 慢(反射/字符串) | 快(直接读字节) |
| 契约 | 靠文档/约定 | .proto 文件即契约,可生成多语言代码 |
| 流式 | 不支持(要 WebSocket/SSE) | 原生双向流 |
| 生态 | 通用 | 服务间通信主流 |
一句话判断:对外(浏览器、第三方)用 REST;服务内部(微服务之间)用 gRPC。
二、Protobuf:契约与编码
proto 文件
proto
syntax = "proto3";
package order;
message Order {
int64 id = 1; // 字段编号一旦用上就不能改,只增不减
string user_id = 2;
double amount = 3;
Status status = 4;
}
enum Status {
UNKNOWN = 0; // enum 第一个值必须是 0
PENDING = 1;
PAID = 2;
}
service OrderService {
rpc GetOrder(GetOrderReq) returns (Order);
rpc ListOrders(GetOrderReq) returns (stream Order); // 服务端流
}syntax = "proto3";
package order;
message Order {
int64 id = 1; // 字段编号一旦用上就不能改,只增不减
string user_id = 2;
double amount = 3;
Status status = 4;
}
enum Status {
UNKNOWN = 0; // enum 第一个值必须是 0
PENDING = 1;
PAID = 2;
}
service OrderService {
rpc GetOrder(GetOrderReq) returns (Order);
rpc ListOrders(GetOrderReq) returns (stream Order); // 服务端流
}编码为什么小
Protobuf 用字段编号 + 类型 + 值的二进制编码,不存字段名;int32 用变长编码,小数字占 1 字节。
兼容性铁律
| 操作 | 兼容吗 |
|---|---|
| 新增字段 | ✅(老客户端忽略新字段) |
| 删除字段 | ⚠️ 可以,但该编号不能复用(否则老数据会解析成新语义) |
| 改字段类型 | ❌ 不兼容 |
| 改字段名 | ✅(线上传输只看编号) |
三、四种调用方式
proto
rpc Get(Req) returns (Resp); // 一元(unary)
rpc ServerStream(Req) returns (stream Resp); // 服务端流:客户端发一次,服务端持续推
rpc ClientStream(stream Req) returns (Resp); // 客户端流:客户端持续发,服务端最后回一次
rpc BidiStream(stream Req) returns (stream Resp); // 双向流rpc Get(Req) returns (Resp); // 一元(unary)
rpc ServerStream(Req) returns (stream Resp); // 服务端流:客户端发一次,服务端持续推
rpc ClientStream(stream Req) returns (Resp); // 客户端流:客户端持续发,服务端最后回一次
rpc BidiStream(stream Req) returns (stream Resp); // 双向流| 方式 | 典型场景 |
|---|---|
| 一元 | 普通查询 |
| 服务端流 | 订阅行情、日志推送 |
| 客户端流 | 批量上传 |
| 双向流 | 聊天、实时协作 |
四、核心概念
| 概念 | 说明 |
|---|---|
| Channel | 到某个服务的长连接(基于 HTTP/2) |
| Stub | 生成的客户端桩,像本地方法一样调用 |
| Deadline | 每个调用必须设超时(withDeadlineAfter),不设默认永远等 |
| 拦截器(Interceptor) | 相当于中间件:鉴权、日志、追踪、重试 |
| Metadata | 类似 HTTP 头的键值对,传 traceId、token |
| Status Code | 标准错误码(NOT_FOUND、DEADLINE_EXCEEDED 等) |
五、gRPC 为什么基于 HTTP/2
- 多路复用:一个 TCP 连接上并发多个请求;
- 双向流:HTTP/2 本身支持双向流,gRPC 的四种调用方式直接借用;
- 头部压缩:HPACK。
这也是「gRPC 不适合从浏览器直连」的原因之一:浏览器不能完全控制 HTTP/2 帧。浏览器场景用 grpc-web 或把 gRPC 包装成 REST。
六、最小示例
Java(Spring Boot + grpc-spring-boot-starter)
java
// 服务端实现(.proto 生成的接口)
@GrpcService
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderReq request, StreamObserver<Order> resp) {
resp.onNext(Order.newBuilder()
.setId(request.getId())
.setAmount(199.0)
.build());
resp.onCompleted();
}
}
// 客户端调用
var stub = OrderServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(3, TimeUnit.SECONDS); // 必须设超时
Order order = stub.getOrder(GetOrderReq.newBuilder().setId(1).build());// 服务端实现(.proto 生成的接口)
@GrpcService
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderReq request, StreamObserver<Order> resp) {
resp.onNext(Order.newBuilder()
.setId(request.getId())
.setAmount(199.0)
.build());
resp.onCompleted();
}
}
// 客户端调用
var stub = OrderServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(3, TimeUnit.SECONDS); // 必须设超时
Order order = stub.getOrder(GetOrderReq.newBuilder().setId(1).build());Go
go
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := order.NewOrderServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
order, err := client.GetOrder(ctx, &order.GetOrderReq{Id: 1})conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := order.NewOrderServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
order, err := client.GetOrder(ctx, &order.GetOrderReq{Id: 1})七、工程要点
- Deadline 必须设:gRPC 的默认超时是「无限」,一次下游慢会拖垮全链。
- 拦截器统一横切:鉴权、traceId 透传、重试、限流都放拦截器,别散在业务里。
- 服务端优雅关闭:先停止接收新请求,再 drain 进行中的。
- 版本演进:新加字段别改旧编号;破坏性变更开新 service 或新 rpc 方法。
- 监控:gRPC 自带状态码,用拦截器把「方法名、状态码、耗时」打到 Prometheus。
八、速查
| 问题 | 答案 |
|---|---|
| 为什么比 JSON 快 | 二进制、字段编号代替字段名、变长编码 |
| 对外用 REST 还是 gRPC | 对外 REST,服务内部 gRPC |
| proto 字段编号能改吗 | 不能,只增不减 |
| 为什么必须设 deadline | gRPC 默认不超时 |
| gRPC 基于什么协议 | HTTP/2 |
| 浏览器怎么用 | grpc-web 或包装成 REST |
| 流式有几种 | 一元 + 服务端流 + 客户端流 + 双向流 |
Cao Note