3. gRPC 服务定义:service 定义与四种 RPC 类型
gRPC 的核心通信模型基于 Protocol Buffers(protobuf) 的服务定义。在 .proto 文件中,开发者通过 service 关键字声明一个 RPC 服务,并在其中定义若干 RPC 方法。每个方法可以指定四种不同的数据流模式之一,这决定了客户端与服务端之间如何交换消息。
3.1 service 定义基础
一个典型的 gRPC 服务定义包含:包声明、消息类型定义、以及服务接口声明。以下是一个用于压测场景的示例 benchmark.proto:
syntax = "proto3";
package benchmark;
// 请求消息:携带负载大小和唯一标识
message LoadRequest {
int32 payload_size = 1; // 负载字节数
string request_id = 2; // 请求唯一 ID
bytes payload = 3; // 实际负载数据
}
// 响应消息:返回处理结果和耗时
message LoadResponse {
string request_id = 1; // 回显请求 ID
int64 server_timestamp = 2; // 服务端处理时间戳
bool success = 3; // 处理是否成功
}
// 压测服务定义:包含四种 RPC 模式
service BenchmarkService {
// 一元 RPC
rpc UnaryCall(LoadRequest) returns (LoadResponse);
// 服务端流式 RPC
rpc ServerStreamingCall(LoadRequest) returns (stream LoadResponse);
// 客户端流式 RPC
rpc ClientStreamingCall(stream LoadRequest) returns (LoadResponse);
// 双向流式 RPC
rpc BidirectionalStreamingCall(stream LoadRequest) returns (stream LoadResponse);
}
压测设计要点:在压测场景中,
LoadRequest 中的 payload 字段用于模拟不同大小的数据传输负载,request_id 用于追踪每个请求的完整生命周期,便于后续分析延迟和吞吐量。
3.2 四种 RPC 类型详解
gRPC 基于 HTTP/2 的流式传输能力,提供了四种 RPC 通信模式。下表从数据流方向、适用场景和压测关注点三个维度进行对比:
| RPC 类型 | 数据流方向 | 典型场景 | 压测关注点 |
|---|---|---|---|
| 一元 RPC | 客户端发送 1 个请求 → 服务端返回 1 个响应 | 简单查询、认证、配置获取 | 请求/响应延迟、吞吐量(QPS) |
| 服务端流式 RPC | 客户端发送 1 个请求 → 服务端返回多个响应 | 日志推送、数据订阅、大文件分片下载 | 流式吞吐量、背压机制、内存占用 |
| 客户端流式 RPC | 客户端发送多个请求 → 服务端返回 1 个响应 | 批量数据上传、传感器数据聚合 | 批量写入速率、服务端聚合效率 |
| 双向流式 RPC | 客户端和服务端可同时发送多个消息 | 实时聊天、协同编辑、实时数据管道 | 全双工吞吐量、消息顺序性、并发控制 |
3.3 四种 RPC 模式的通信流程
下面的 SVG 图展示了四种 RPC 模式在客户端与服务端之间的消息交换流程,帮助理解不同流式模式下的数据流动方向:
3.4 流式 RPC 的底层机制
理解流式 RPC 的底层机制对于压测设计至关重要。以下 SVG 图展示了 gRPC 在 HTTP/2 流之上如何实现四种 RPC 模式的消息帧传输:
3.5 四种 RPC 的代码骨架对比
在 Python 的 grpc 库中,四种 RPC 模式的服务端实现具有不同的迭代器/生成器模式。以下代码片段展示了服务端处理四种 RPC 的核心逻辑:
import grpc
from concurrent import futures
import benchmark_pb2
import benchmark_pb2_grpc
class BenchmarkServiceServicer(benchmark_pb2_grpc.BenchmarkServiceServicer):
# 1. 一元 RPC:接收一个请求,返回一个响应
def UnaryCall(self, request, context):
response = benchmark_pb2.LoadResponse(
request_id=request.request_id,
server_timestamp=time.time_ns(),
success=True
)
return response
# 2. 服务端流式 RPC:接收一个请求,通过 yield 返回多个响应
def ServerStreamingCall(self, request, context):
for i in range(10): # 模拟发送 10 个响应
response = benchmark_pb2.LoadResponse(
request_id=f"{request.request_id}-{i}",
server_timestamp=time.time_ns(),
success=True
)
yield response
# 3. 客户端流式 RPC:通过 for 循环接收多个请求,返回一个响应
def ClientStreamingCall(self, request_iterator, context):
total_requests = 0
for request in request_iterator:
total_requests += 1
# 处理每个请求...
response = benchmark_pb2.LoadResponse(
request_id=f"batch-{total_requests}",
server_timestamp=time.time_ns(),
success=True
)
return response
# 4. 双向流式 RPC:同时接收和发送流
def BidirectionalStreamingCall(self, request_iterator, context):
for request in request_iterator:
response = benchmark_pb2.LoadResponse(
request_id=request.request_id,
server_timestamp=time.time_ns(),
success=True
)
yield response
压测陷阱提示:在压测流式 RPC 时,务必注意 背压(backpressure) 机制。如果客户端发送速率超过服务端处理能力,gRPC 的流控机制会阻塞发送方。在压测脚本中,应监控
grpc.FlowControlException 或设置合理的 grpc.Compression 选项,避免因流控导致压测结果失真。
3.6 四种 RPC 的压测指标差异
不同的 RPC 模式在压测时需要关注不同的性能指标。下表总结了四种模式的核心压测指标:
| RPC 类型 | 核心指标 | 次要指标 | 典型瓶颈 |
|---|---|---|---|
| 一元 RPC | QPS、P99 延迟 | 错误率、连接数 | CPU 序列化/反序列化 |
| 服务端流式 | 消息吞吐量(msg/s) | 流持续时间、内存占用 | 网络带宽、GC 暂停 |
| 客户端流式 | 批量写入速率(req/s) | 服务端聚合延迟 | 服务端缓冲区大小 |
| 双向流式 | 全双工吞吐量 | 消息顺序性、并发数 | HTTP/2 流并发限制 |
3.7 本章小结
gRPC 的四种 RPC 模式为不同数据传输场景提供了灵活的通信原语。在压测实践中,需要根据 RPC 模式的特点设计对应的压测策略:一元 RPC 关注请求/响应延迟分布,流式 RPC 关注吞吐量和背压行为。理解 service 定义和四种 RPC 的底层机制,是构建高效压测脚本的基础。