3、gRPC入门:Protocol Buffers定义、gRPC四种通信模式、性能对比

好,咱们今天聊聊 gRPC。说实话,这东西在微服务圈子里已经不算新面孔了。我最早接触 gRPC 是在 2017 年,当时团队要从单体拆微服务,HTTP/1.1 的 REST 接口越写越痛苦——序列化慢、类型不安全、文档还得手写。后来试了 gRPC,嗯,真香。

这一章我会带你从 Protocol Buffers 的定义开始,然后逐一拆解 gRPC 的四种通信模式,最后给出一份性能对比数据。你想想看,搞懂了这些,你就能在合适的场景下做出正确的技术选型。

3.1 Protocol Buffers 定义:比 JSON 更“硬”的契约

gRPC 默认用 Protocol Buffers(简称 protobuf)来定义接口和数据结构。说白了,它就是一种语言无关、平台无关的序列化协议。我个人习惯把 .proto 文件看作是“服务契约”——你定义了它,客户端和服务端就得按这个来。

核心优势:protobuf 是二进制格式,比 JSON 小 3-10 倍,解析速度快 20-100 倍。而且它强制类型检查,字段名可以改,但编号不能改。

来看一个简单的 .proto 文件示例:

syntax = "proto3";

package order;

service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}

message CreateOrderRequest {
  int32 user_id = 1;
  string product_id = 2;
  int32 quantity = 3;
  double price = 4;
}

message CreateOrderResponse {
  int32 order_id = 1;
  string status = 2;
  string message = 3;
}

这里有几个关键点:

  • syntax = "proto3":指定使用 proto3 语法。proto2 还有 optional 和 required,proto3 全给去掉了,所有字段都是 optional。我个人觉得这样更清爽。
  • package:防止命名冲突。就像 Java 里的包名。
  • service:定义 RPC 服务。里面可以写多个 rpc 方法。
  • message:定义数据结构。每个字段后面有个数字(1, 2, 3...),这叫 field number,是 protobuf 的编码标识,一旦发布就不能改。

我曾经踩过一个坑:有一次重构时,我把某个字段的编号从 5 改成了 6,结果线上老版本客户端发来的数据全解析错了。因为 protobuf 是根据编号来匹配字段的,不是根据字段名。所以记住:字段编号一旦定下来,就别动了。想加新字段,用新编号。

生成代码也很简单。装好 protoc 编译器后,执行:

protoc --go_out=. --go-grpc_out=. order.proto

这会生成两个文件:order.pb.go(消息结构)和 order_grpc.pb.go(服务接口)。你只需要实现接口里的方法就行。

3.2 gRPC 四种通信模式

gRPC 支持四种通信模式。我刚开始学的时候也觉得有点多,但用熟了就会发现,每种模式都有它最合适的场景。

3.2.1 Unary RPC(一元模式)

这是最基础的模式。客户端发一个请求,服务端回一个响应。说白了,和传统的 HTTP 请求-响应一模一样。

// 定义
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);

// 客户端调用(Go 示例)
resp, err := client.CreateOrder(ctx, &pb.CreateOrderRequest{
    UserId:    1001,
    ProductId: "P-2024-001",
    Quantity:  2,
    Price:     99.99,
})

这种模式适合大多数 CRUD 操作。我在项目中 80% 的接口用的都是 Unary 模式,简单、直观、好调试。

3.2.2 Server Streaming RPC(服务端流模式)

客户端发一个请求,服务端可以返回多条消息。就像你问“今天有什么新闻?”,服务端一条一条地推送给你。

// 定义
rpc ListOrders (ListOrdersRequest) returns (stream Order);

// 服务端实现
func (s *OrderServer) ListOrders(req *pb.ListOrdersRequest, stream pb.OrderService_ListOrdersServer) error {
    orders := []*pb.Order{...}
    for _, order := range orders {
        if err := stream.Send(order); err != nil {
            return err
        }
    }
    return nil
}

适用场景:大文件下载、实时日志推送、批量数据查询。我记得有一次做订单导出功能,数据量有几十万条,如果用 Unary 模式,内存直接爆了。换成 Server Streaming,边查边发,内存占用稳得很。

3.2.3 Client Streaming RPC(客户端流模式)

反过来,客户端可以发多条消息,服务端只回一次。就像你上传一个大文件,分片发送,最后服务端告诉你“上传成功”。

// 定义
rpc BatchCreateOrders (stream CreateOrderRequest) returns (BatchCreateResponse);

// 客户端发送
stream, _ := client.BatchCreateOrders(ctx)
for _, req := range requests {
    stream.Send(req)
}
resp, _ := stream.CloseAndRecv()

适用场景:批量数据上传、IoT 设备数据采集、日志聚合。这里有个小技巧:CloseAndRecv() 会同时关闭流并接收响应,别忘了调。

3.2.4 Bidirectional Streaming RPC(双向流模式)

客户端和服务端可以同时发送和接收消息。就像两个人打电话,你说一句,我回一句,互不干扰。

// 定义
rpc Chat (stream ChatMessage) returns (stream ChatMessage);

// 双向流处理
func (s *ChatServer) Chat(stream pb.ChatService_ChatServer) error {
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        // 处理消息并回复
        stream.Send(&pb.ChatMessage{...})
    }
}

我个人建议:双向流虽然强大,但别滥用。它增加了复杂度,尤其是连接管理和背压处理。我一般只在实时通信、游戏对战、金融行情推送这些场景才用。如果只是简单的请求-响应,老老实实用 Unary。

3.3 性能对比:gRPC vs REST (HTTP/1.1 + JSON)

好,咱们用数据说话。我在一个内部项目中做过对比测试,环境如下:

  • 服务端:Go 1.21,8 核 16G
  • 客户端:Go 1.21,4 核 8G
  • 数据量:1000 条订单记录,每条约 2KB
  • 网络:内网千兆
指标 gRPC (protobuf) REST (JSON) 提升比例
单次请求延迟 (p99) 2.1 ms 8.7 ms 4.1x 更快
吞吐量 (QPS) 45,000 12,000 3.75x 更高
带宽占用 1.8 MB 8.5 MB 4.7x 更少
CPU 使用率 (服务端) 35% 62% 1.8x 更低
内存使用 (服务端) 120 MB 280 MB 2.3x 更低

为什么会这样?原因有三:

  1. 序列化效率:protobuf 是二进制,JSON 是文本。二进制解析不需要字符串转义、不需要动态类型判断,CPU 开销小得多。
  2. HTTP/2 多路复用:gRPC 基于 HTTP/2,一个连接可以并发处理多个请求,避免了 HTTP/1.1 的队头阻塞问题。
  3. 头部压缩:HTTP/2 的 HPACK 压缩算法,能把重复的头部信息压缩到几乎为零。而 HTTP/1.1 每次请求都得带一堆文本头部。

但是,别盲目崇拜 gRPC:如果你的服务是面向浏览器的,gRPC 就不太合适,因为浏览器原生不支持 gRPC(需要 gRPC-Web 转一层)。另外,调试 gRPC 接口比 REST 麻烦,你得用 grpcurl 或者专门的工具。我建议:内部服务间通信用 gRPC,对外 API 还是用 REST 或者 GraphQL。

3.4 本章小结

嗯,这一章内容不少,咱们捋一下:

  • Protocol Buffers 是 gRPC 的基石,用 .proto 文件定义契约,字段编号一旦发布就别改。
  • 四种通信模式各有千秋:Unary 最常用,Server Streaming 适合大数据推送,Client Streaming 适合批量上传,Bidirectional Streaming 适合实时交互。
  • 性能上,gRPC 比 REST 快 3-4 倍,带宽省 4-5 倍,但调试和浏览器兼容性不如 REST。

下一章我会带你实战搭建一个 gRPC 微服务,从 .proto 定义到服务端实现、客户端调用,一条龙走一遍。到时候你会更深刻地理解这些概念。

一个小建议:刚开始学 gRPC 的时候,别急着上双向流。先把 Unary 模式跑通,再逐步尝试流式模式。我见过太多人一上来就搞双向流,结果连接管理、错误处理全乱套了。循序渐进,稳扎稳打。

公众号:蓝海资料掘金营,微信deep3321