2. gRPC 基础回顾:Protocol Buffers、服务定义、一元与流式 RPC

在深入分布式事务压测与契约测试之前,必须牢固掌握 gRPC 的核心基础。gRPC 的高性能、强类型与多语言互操作性,均源于其底层技术栈:Protocol Buffers(protobuf)作为接口定义语言(IDL)与序列化协议,以及基于 HTTP/2 的四种通信模式。本节将系统梳理这些基础构件,为后续的契约测试设计奠定形式化基础。

2.1 Protocol Buffers:契约的基石

Protocol Buffers 是 gRPC 的“契约语言”。它通过 .proto 文件定义数据结构与服务接口,并生成各语言的高效序列化代码。与 JSON/XML 相比,protobuf 具有以下关键优势:

特性 Protocol Buffers JSON / XML
序列化体积 小(二进制,无冗余键名) 大(文本,键名重复)
解析速度 快(二进制直接映射) 慢(文本解析开销)
类型安全 强类型(编译期校验) 弱类型(运行时校验)
向后兼容 内置(字段编号 + 可选/默认) 需手动处理
契约定义 单一 .proto 文件 无统一契约

一个典型的 .proto 文件结构如下:

syntax = "proto3";

package transaction.v1;

// 定义订单状态枚举
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_FAILED = 3;
}

// 定义请求消息
message CreateOrderRequest {
  string order_id = 1;
  string user_id = 2;
  double amount = 3;
  repeated string item_ids = 4;
}

// 定义响应消息
message CreateOrderResponse {
  string transaction_id = 1;
  OrderStatus status = 2;
  string message = 3;
}
契约测试要点:在压测场景中,.proto 文件就是服务提供者与消费者之间的“法律文件”。契约测试的核心任务之一,就是验证客户端发送的请求与服务器返回的响应,是否严格符合 .proto 中定义的字段编号、类型、可选性以及枚举值范围。

2.2 服务定义:从消息到 RPC 方法

在 .proto 文件中,使用 service 关键字将消息组合成 RPC 方法。每个方法指定输入消息类型与输出消息类型,gRPC 编译器据此生成客户端 Stub 与服务器骨架。

service OrderService {
  // 一元 RPC:客户端发送一个请求,服务器返回一个响应
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);

  // 服务器端流式 RPC:客户端发送一个请求,服务器返回一个流
  rpc ListOrders(ListOrdersRequest) returns (stream Order);

  // 客户端端流式 RPC:客户端发送一个流,服务器返回一个响应
  rpc BatchCreateOrder(stream CreateOrderRequest) returns (BatchCreateOrderResponse);

  // 双向流式 RPC:双方同时发送和接收流
  rpc ProcessOrderStream(stream OrderEvent) returns (stream OrderResult);
}

服务定义中的 rpc 关键字后紧跟方法名,括号内为输入/输出类型。关键字 stream 标记流式通信。这种声明方式使得服务契约在代码生成阶段即可被静态检查,避免了运行时接口不匹配的风险。

2.3 一元 RPC 与流式 RPC 对比

gRPC 基于 HTTP/2 提供了四种通信模式。理解它们的差异,对于设计压测场景与契约测试用例至关重要。

模式 请求 响应 典型场景 压测关注点
一元 RPC 1 条消息 1 条消息 创建订单、查询用户 延迟、吞吐量、错误率
服务器端流式 1 条消息 多条消息(流) 批量查询、日志推送 流完成时间、消息间隔
客户端端流式 多条消息(流) 1 条消息 批量上传、数据聚合 流发送速率、合并延迟
双向流式 多条消息(流) 多条消息(流) 实时聊天、事件驱动 全双工吞吐、背压表现
契约测试陷阱:流式 RPC 的契约不仅包含消息结构,还包含流结束信号(gRPC 状态码)与消息顺序。例如,服务器端流必须在发送完所有消息后发送 Status.OK;双向流中,双方关闭流的时机必须符合业务语义。契约测试必须覆盖这些流生命周期行为,而不仅仅是单条消息的格式。

2.4 四种 RPC 模式的通信流程

下图展示了四种 RPC 模式在 HTTP/2 流上的交互过程,帮助理解不同模式下的消息边界与流控制。

四种 gRPC RPC 模式通信流程 客户端 服务器 请求 (1 msg) 响应 (1 msg) 一元 RPC 客户端 服务器 请求 (1 msg) 响应流 (n msgs) 服务器端流式 客户端 服务器 请求流 (n msgs) 响应 (1 msg) 客户端端流式 客户端 服务器 请求流 (n msgs) 响应流 (n msgs) 双向流式 说明: 箭头方向表示消息流向。一元 RPC 是基础;流式 RPC 通过 HTTP/2 流复用连接,实现异步、批量或实时通信。

2.5 gRPC 调用生命周期与状态码

每次 gRPC 调用(无论一元还是流式)都遵循严格的生命周期,最终以状态码结束。契约测试必须验证这些状态码的正确性。

  1. 连接建立:客户端与服务器通过 HTTP/2 建立连接。
  2. 元数据交换:可选的初始元数据(如认证令牌)在调用开始时发送。
  3. 消息传输:根据 RPC 模式,传输一条或多条 protobuf 消息。
  4. 状态与尾部元数据:服务器发送 gRPC 状态码(OK、INVALID_ARGUMENT、UNAVAILABLE 等)以及可选的尾部元数据。
  5. 流关闭:双方关闭 HTTP/2 流。
压测场景库映射:在分布式事务压测中,一元 RPC 常用于事务提交与回滚操作;流式 RPC 常用于事件溯源与状态同步。契约测试需要为每种模式编写独立的断言逻辑,例如:验证服务器端流是否在预期时间内发送了正确数量的消息,以及最终状态码是否为 OK。

2.6 小结:基础回顾与契约测试的衔接

本节的回顾为后续章节奠定了三个关键认知:

  • 契约即代码:.proto 文件是服务间通信的单一事实来源,契约测试围绕它展开。
  • 模式决定测试策略:一元 RPC 的契约测试关注请求/响应对;流式 RPC 的契约测试还需关注流生命周期、消息顺序与背压行为。
  • 状态码是契约的一部分:gRPC 状态码(如 UNAVAILABLE 表示服务不可用)在压测中直接对应故障注入场景,契约测试必须覆盖正常与异常状态码。