Skip to content

gRPC 与 Protobuf ​

微服务之间的调用,正在从「REST + JSON」转向「gRPC + Protobuf」。 这篇讲清楚它到底好在哪里,以及怎么用。

一、为什么需要 gRPC ​

REST + JSONgRPC + 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 ​

  1. 多路复用:一个 TCP 连接上并发多个请求;
  2. 双向流:HTTP/2 本身支持双向流,gRPC 的四种调用方式直接借用;
  3. 头部压缩: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})

七、工程要点 ​

  1. Deadline 必须设:gRPC 的默认超时是「无限」,一次下游慢会拖垮全链。
  2. 拦截器统一横切:鉴权、traceId 透传、重试、限流都放拦截器,别散在业务里。
  3. 服务端优雅关闭:先停止接收新请求,再 drain 进行中的。
  4. 版本演进:新加字段别改旧编号;破坏性变更开新 service 或新 rpc 方法。
  5. 监控:gRPC 自带状态码,用拦截器把「方法名、状态码、耗时」打到 Prometheus。

八、速查 ​

问题答案
为什么比 JSON 快二进制、字段编号代替字段名、变长编码
对外用 REST 还是 gRPC对外 REST,服务内部 gRPC
proto 字段编号能改吗不能,只增不减
为什么必须设 deadlinegRPC 默认不超时
gRPC 基于什么协议HTTP/2
浏览器怎么用grpc-web 或包装成 REST
流式有几种一元 + 服务端流 + 客户端流 + 双向流