RPC 简介
7 分钟阅读
简要概述
gRPC 的优势
核心应用场景
-
高性能分布式系统
- 低延迟 & 高吞吐:基于 HTTP/2 协议的多路复用和二进制编码,显著减少网络开销,适合微服务间高频通信。
- 强扩展性:支持每秒数万级 RPC 调用,满足高并发需求(如电商大促、游戏服务器)。
-
移动端与云端通信
- 弱网优化:二进制传输比 REST/JSON 更省带宽,提升移动端响应速度。
- 跨平台兼容:自动生成 iOS/Android 客户端 SDK 代码,确保多端行为一致。
-
定制化协议设计
- 精准高效:通过 Protocol Buffers 定义严格的接口和数据结构,避免手动解析错误。
- 语言无关:一套
.proto
文件生成 Java/Python/Go 等多语言代码,解决异构系统协作问题。
-
可扩展的中间件支持
- 分层架构:原生支持或通过拦截器(Interceptor)扩展以下功能:
- 认证:集成 JWT、OAuth2 等。
- 负载均衡:内置客户端负载均衡(如 gRPC-LB)。
- 可观测性:无缝对接 Prometheus、OpenTelemetry 实现监控和日志。
- 分层架构:原生支持或通过拦截器(Interceptor)扩展以下功能:
对比 REST 的优势
维度 | gRPC | REST/JSON |
---|---|---|
传输效率 | 二进制编码,体积小、解析快 | 文本传输,冗余度高 |
协议灵活性 | 支持流式通信(单/双向流) | 通常限于请求-响应模式 |
开发体验 | 自动生成强类型代码,减少手写错误 | 需手动处理序列化/反序列化 |
何时不推荐使用?
- 浏览器直接调用:需依赖 grpc-web、grpc-gateway 转译(部分功能受限)。
- 简单读操作为主:如公开 API,REST 的易调试性可能更优。
如何建立连接
在 grpc 中,客户端与服务端可以是异构语言编写,比如服务端使用 go,客户端可分别是 nodejs、python 等语言编写。
服务端均使用 Protocol Buffers 语言定义,但客户端依赖服务端提供 stub(存根,编译的时候根据客户端目标语言生成,类似 sdk),才可以发起连接并建立请求解析服务端响应。
以下是服务端与多个客户端之间示例流程图:
Protocol Buffers
在 grpc 系统中是使用 protocol buffer 进行序列化的(对比 http 协议下常用的 json 数据结构)。
protocol buffers 存在两种版本,proto2 与 proto3,一般现在使用均建议使用版本3。
RPC 服务类型
单次 RPC (Unary RPC)
客户端发送单个请求并接收单个响应,也就是“一问一答”。
当客户端调用存根方法时:
-
RPC调用通知
- 服务端立即收到调用通知,包含:
✓ 客户端元数据(metadata)
✓ 调用的方法名
✓ 可选的超时截止时间(deadline)
- 服务端立即收到调用通知,包含:
-
服务端响应流程
- 初始元数据阶段:
服务端可选择立即发送初始元数据(必须在响应体之前发送),或等待客户端请求消息。具体顺序由业务逻辑决定。 - 请求处理阶段:
服务端获取客户端请求消息后,执行业务逻辑并生成响应。
- 初始元数据阶段:
-
服务端响应返回
- 响应包含:
✓ 响应体(业务数据)
✓ 状态详情(状态码 + 可选状态信息)
✓ 可选的尾部元数据(trailing metadata) - 若状态为OK,客户端将成功接收响应,完成本次调用。
- 响应包含:
示例流程图
服务端流式 RPC (Server streaming RPC)
与单次 RPC(Unary RPC)类似,不同之处在于:服务端在响应客户端请求时,会返回一个消息流。
核心流程
- 客户端发送单个请求(与单次 RPC 相同)。
- 服务端返回消息流:
- 服务端可以持续发送多个消息(而非单个响应)。
- 消息流传输完成后,服务端会附带状态信息(状态码 + 可选的状态消息)和尾部元数据(trailing metadata),以标志本次 RPC 处理完成。
- 客户端接收完整消息流:
- 客户端会持续接收服务端返回的消息,直到获取所有数据后,本次 RPC 调用才正式完成。
对比单次 RPC(Unary RPC)
特性 | 单次 RPC | 服务端流式 RPC |
---|---|---|
请求方式 | 单次请求 | 单次请求 |
响应方式 | 单次响应 | 消息流(多次响应) |
适用场景 | 简单查询 | 大数据分批传输、实时推送 |
典型应用
- 数据分块传输(如大文件下载)
- 实时数据推送(如股票行情、日志流)
- 长任务分阶段返回(如机器学习模型推理)
示例流程图
客户端流式 RPC(Client streaming RPC)
与单次 RPC(Unary RPC)类似,但区别在于:客户端会向服务端发送一个消息流(而非单个请求),而服务端最终返回单个响应(附带状态信息和可选的尾部元数据)。
核心流程
- 客户端发送消息流:
- 客户端通过持久化连接持续发送多个请求消息(如分批上传数据)。
- 服务端处理并返回响应:
- 服务端可以边接收边处理,但通常会在接收完所有客户端消息后,再返回最终响应(包含状态码、状态消息和尾部元数据)。
- 调用完成:
- 客户端收到服务端的最终响应后,本次 RPC 调用结束。
对比单次 RPC(Unary RPC)
特性 | 单次 RPC | 客户端流式 RPC |
---|---|---|
请求方式 | 单次请求 | 消息流(多次请求) |
响应方式 | 单次响应 | 单次响应 |
适用场景 | 简单操作 | 数据分批上传、长请求 |
典型应用
- 大数据上传(如日志批量采集)
- 聚合计算(如客户端发送多个数据点,服务端汇总后返回结果)
- 长请求处理(如流式语音识别,客户端持续发送音频片段)
示例流程图
双向流式 RPC(Bidirectional streaming RPC)
由客户端发起调用,服务端接收客户端元数据、方法名和截止时间后,可选择立即返回初始元数据,或等待客户端开始发送消息流。
核心特点
-
独立双工通信
- 客户端和服务端可同时发送消息流,两者完全独立,没有固定顺序约束。
- 支持灵活交互模式,例如:
- 顺序处理:服务端收齐所有客户端消息后再响应。
- 实时交互:类似“乒乓模式”(请求-响应-再请求-再响应)。
-
动态控制流
- 服务端可在连接期间任意时机发送初始元数据或消息。
- 客户端和服务端可自主决定读写时序,无需严格同步。
对比其他 RPC 模式
特性 | 单次 RPC | 服务端流式 | 客户端流式 | 双向流式 |
---|---|---|---|---|
请求流 | 单次 | 单次 | 消息流 | 消息流 |
响应流 | 单次 | 消息流 | 单次 | 消息流 |
交互自由度 | 低 | 中 | 中 | 高 |
典型应用场景
- 实时对话系统(如聊天应用,双方随时发送消息)
- 双向数据同步(如游戏状态同步)
- 复杂协作流程(如客户端与服务端分阶段交换计算中间结果)
示例流程图
超时与截止时间
连接时长限制
在建立连接时,允许客户端为每次 RPC 调用设置截止时间(Deadline)或超时时长(Timeout)。若请求未在指定时间内完成,RPC 将自动终止并返回 DEADLINE_EXCEEDED
错误,当然服务端也可主动检查当前请求是否已超时或剩余可用时间还剩多少。
超时核心机制
客户端设置
- 截止时间(Deadline):指定一个绝对时间点(如
2025-05-16 15:26:38
)。 - 超时时长(Timeout):指定相对时间长度(如
5秒
)。
不同编程语言的 API 可能仅支持以上其中一种形式,如下两种语言的实现差异:
# python 示例(使用相对 timeout)
response = stub.SayHello(hello_pb2.HelloRequest(name='grpc-kit'), timeout=5)
// go 示例(使用绝对 deadline)
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
defer cancel()
response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "grpc-kit"})
服务端处理
- 服务端可通过 API 查询:
- 当前 RPC 是否已超时;
- 剩余可用的处理时间(用于动态调整任务优先级)。
- 网络延迟不计入超时,RPC 的截止时间涵盖:
- 请求传输;
- 服务端处理;
- 响应返回的全流程。
- 在 “客户端x1->服务端y1/客户端x2->服务端y2“ 之间的级联传递:
- 若服务端作为其他 RPC 服务的客户端,需手动传递或重新设置截止时间,避免上游超时未触发下游终止;
- 通过 metadata 将 deadline 传递给下游服务,保持调用链超时一致性。
- 底层实现中,gRPC 最终都会将 timeout 转换为 deadline 进行计算
在使用 go 语言实现的服务端,可以通过 context.Context
检查超时状态:
func (m *Microservice) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
if ctx.Err() == context.DeadlineExceeded {
// 清理已进行的操作
return nil, status.Error(codes.DeadlineExceeded, "client deadline exceeded")
}
// 获取剩余时间
deadline, ok := ctx.Deadline()
remaining := time.Until(deadline)
// 根据剩余时间调整处理逻辑
}
典型应用场景
一般在以下场景下使用:
- 快速失败(Fail Fast):避免长时间阻塞的调用拖慢系统。
- 资源优化:服务端通过剩余时间决定是否执行耗时操作。
建议通过实际压测确定最佳超时值,通常建议:
- 服务间调用:500ms-5s;
- 用户直接请求:1-30s;
- 后台作业:可适当延长但必须显式设置上限。
最后提醒 deadline 不是精确的时间保证,实际触发时间可能会有数百毫秒的误差,不能依赖其实现精确的定时功能。
请求的终止机制
四类终止情况
在 RPC 调用可能因以下情况终止:
-
超时终止(Deadline Exceeded)
- 客户端设置的截止时间(Deadline)或超时(Timeout)到期,RPC 自动终止,并返回
DEADLINE_EXCEEDED
错误。 - 适用场景:防止长时间阻塞,确保系统响应能力。
- 客户端设置的截止时间(Deadline)或超时(Timeout)到期,RPC 自动终止,并返回
-
显式取消(Cancellation)
- 客户端或服务端可主动取消正在进行的 RPC,终止未完成的请求。
- 适用场景:用户取消操作、服务端资源不足等。
-
错误终止(Error Termination)
- 若 RPC 执行过程中发生错误(如网络中断、服务不可用),会自动终止并返回对应的错误状态码(如
UNAVAILABLE
)。
- 若 RPC 执行过程中发生错误(如网络中断、服务不可用),会自动终止并返回对应的错误状态码(如
-
正常完成(Normal Completion)
- RPC 成功执行并返回预期结果后,连接正常关闭。
关键区别:
终止类型 | 触发方式 | 典型错误码 |
---|---|---|
超时终止 | 客户端设置的时间到期 | DEADLINE_EXCEEDED |
显式取消 | 手动调用取消方法 | CANCELLED |
错误终止 | 网络/服务端异常 | UNAVAILABLE 、ABORTED |
正常完成 | 请求成功处理完毕 | OK (无错误) |
显式取消
在请求建立完成后,客户端或服务端均可随时取消正在进行的 RPC,取消操作会立即终止连接,后续所有相关处理均被中断。
存在以下核心特性:
- 即时生效
- 取消请求一旦触发,RPC 会立刻停止,不再执行任何后续逻辑(包括网络传输和服务端处理)。
- 客户端和服务端会收到
CANCELLED
状态码(错误码1
)。
- 双向能力
- 客户端取消:例如用户主动取消请求或前端页面关闭。
- 服务端取消:例如服务端资源不足或检测到无效请求时主动终止。
- 资源释放
- 取消后,双方应释放已占用的连接、线程等资源,避免内存泄漏。
典型应用场景:
- 客户端侧:用户中断长时间等待的请求。
- 服务端侧:
- 检测到恶意请求或参数错误时主动取消。
- 负载过高时终止低优先级 RPC。
与超时终止的区别:
行为 | 取消(Cancellation) | 超时(Deadline) |
---|---|---|
触发方 | 客户端或服务端手动调用 | 客户端设置的计时器自动触发 |
错误码 | CANCELLED (1) |
DEADLINE_EXCEEDED (4) |
控制粒度 | 精确到单次 RPC 调用 | 依赖全局 Deadline 设置 |
最佳实践
-
客户端:
- 提供显式的取消按钮或超时逻辑。
- 捕获
CANCELLED
错误并记录日志(非必要不重试)。 - 错误恢复策略:客户端应处理
DEADLINE_EXCEEDED
和CANCELLED
,考虑重试或降级逻辑。
-
服务端:
- 监听取消信号,及时回滚事务或清理临时数据。
- 避免在取消后继续写入响应流。
- 合理设置 Deadline:避免过长(浪费资源)或过短(频繁超时)。
- 资源清理:监听取消信号,及时释放占用的资源。
Metadata
设计解析
- 通信信封:类似 HTTP Headers,但专为 gRPC 优化设计
- 双向流动:
graph LR Client-->|Request Metadata|Server Server-->|Response Metadata|Client
- 生命周期:仅存在于单个 RPC 调用上下文
- 传输载体:底层通过 HTTP/2 HEADERS 帧传输
键命名规范:
关键规则 | 合法示例 | 非法示例 |
---|---|---|
禁止 grpc- 前缀 |
app-version |
grpc-token |
允许字符:a-z ,0-9 ,- ,_ ,. |
x-b3-traceid |
user@info |
二进制键必须-bin 结尾 |
auth-token-bin |
image_data |
大小写不敏感 | CLIENT-ID 与client-id 等价 |
- |
性能关键点:
- 大小限制:建议单个元数据不超过4KB(受HTTP/2规范限制)
- 压缩策略:对于重复键值建议使用HPACK压缩
- 缓存机制:高频使用的元数据应复用Metadata对象
- 二进制优化:二进制数据建议使用Base64编码(某些语言实现需要)
跨语言实现
语言特性注意事项
语言 | 关键特性 |
---|---|
Python | 使用元组列表表示,二进制值需转换为bytes |
Go | 通过context传递,区分header/trailer |
Java | 需要ASCII_STRING_MARSHALLER处理非ASCII字符 |
C++ | 使用MetadataMap容器类,支持直接内存访问 |
Node.js | 通过Metadata类管理,二进制值需Buffer类型 |
- 客户端发送元数据
# Python示例
from grpc import Metadata
metadata = Metadata(
('client-id', 'python-app'),
('auth-token-bin', b'\x89PNG\r\n\x1a\n')
)
response = stub.UnaryCall(request, metadata=metadata)
// Go示例
md := metadata.Pairs(
"client-id", "go-service",
"auth-token-bin", string([]byte{0x89, 0x50, 0x4E, 0x47}),
)
ctx := metadata.NewOutgoingContext(context.Background(), md)
resp, err := client.SomeMethod(ctx, req)
- 服务端读取元数据
// Java服务端
public void someMethod(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
Metadata headers = MetadataUtils.headersFrom(request);
String clientId = headers.get(Metadata.Key.of("client-id", Metadata.ASCII_STRING_MARSHALLER));
// 添加响应元数据
Metadata trailingHeaders = new Metadata();
trailingHeaders.put(Metadata.Key.of("server-load", Metadata.ASCII_STRING_MARSHALLER), "0.75");
responseObserver.onNext(response);
responseObserver.onCompleted(trailingHeaders);
}
流式RPC元数据交互
// 客户端流式发送元数据
func (s *server) ClientStreaming(stream pb.Service_ClientStreamingServer) error {
// 读取初始元数据
md, _ := metadata.FromIncomingContext(stream.Context())
// 流处理中发送响应元数据
header := metadata.Pairs("stream-status", "processing")
stream.SendHeader(header)
// 流结束时发送尾部元数据
trailer := metadata.Pairs("processed-count", "1234")
stream.SetTrailer(trailer)
return nil
}
调试与监控
可通过 Wireshark 抓包分析,选择条目右击 “Decode As…",对 “Current” 更改为 “HTTP2” 类型。
HEADERS 帧示例:
:path: /default.api.oneops.netdev.v1.OneopsNetdev/CreateDeviceNetconfStream
:authority: 127.0.0.1:10081
:method: POST
:scheme: http
content-type: application/grpc
te: trailers
grpc-accept-encoding: identity, deflate, gzip
grpc-timeout: 4990m
user-agent: grpc-python-asyncio/1.71.0 grpc-c/46.0.0 (osx; chttp2)
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
应用场景
场景类型 | 元数据示例 | 说明 |
---|---|---|
认证鉴权 | authorization: Bearer xxxx |
携带JWT/OAuth2 token |
链路追踪 | x-b3-traceid: 463ac35c9f6413ad |
分布式追踪上下文 |
流量控制 | canary-flag: true |
灰度发布标识 |
内容协商 | content-encoding: snappy |
数据压缩格式 |
调试信息 | debug-mode: verbose |
开启服务端详细日志 |
二进制传输 | image-data-bin: <binary> |
传输小尺寸二进制载荷 |
最佳实践清单
-
命名规范:
- 使用kebab-case命名风格(如
api-version
) - 业务自定义键添加项目前缀(如
myapp-feature-flag
)
- 使用kebab-case命名风格(如
-
生命周期管理:
Request Metadata → 服务端处理 → Response Metadata ↓ Trailing Metadata
-
使用上安全实践:
# 不安全示例(明文传输敏感信息) metadata = Metadata(('password', 'qwerty123')) # 安全实践 metadata = Metadata( ('authorization', f'Bearer {jwt_token}'), ('x-encrypted-data-bin', encrypt(payload)) )
-
错误处理模式:
func validateMetadata(md metadata.MD) error { if md.Get("authorization") == nil { return status.Error(codes.Unauthenticated, "missing auth") } // 验证二进制数据 if token := md.Get("signature-bin"); len(token) != 64 { return status.Error(codes.InvalidArgument, "invalid signature") } return nil }
关键结论:
Metadata 机制是 gRPC 实现可观察性和上下文传递的核心设计,其灵活性与性能需要开发者通过以下平衡:
- 信息密度 vs 传输效率
- 开发便利性 vs 类型安全
- 业务扩展性 vs 协议稳定性
正确使用 Metadata 可以使分布式系统获得以下能力:
[上下文传递] → [认证鉴权] → [链路追踪] → [流量控制]
↓ ↓ ↓ ↓
事务关联 安全边界 性能分析 系统弹性
Channels
设计理念
Channel 是 gRPC 客户端的战略资源,其正确使用直接影响:
系统可靠性 ←→ 资源利用率 ←→ 运维复杂度
开发者需要重点把握:
- 生命周期管理:创建、重用、关闭的规范
- 状态感知:实时监控连接健康度
- 配置调优:根据业务场景调整参数
- 跨语言一致性:理解不同实现的细微差异
通过合理设计 Channel 使用策略,可实现:
[高性能连接池] → [智能负载均衡] → [弹性容错机制]
↓ ↓ ↓
高吞吐量 流量最优分配 自动故障恢复
面向客户端应用,它有以下特点:
- 客户端入口点:所有 RPC 调用的起点
- 资源管理器:统一管理连接池、负载均衡策略、压缩设置等
- 虚拟连接抽象:一个 Channel 代表到目标主机的逻辑连接
生命周期
状态机模型
CREATED → CONNECTING → READY → TRANSIENT_FAILURE → (重试)→ READY
↓ ↓
IDLE SHUTDOWN
状态转换触发条件
状态 | 触发场景 | 典型持续时间 |
---|---|---|
CONNECTING | 首次建立连接 | 网络RTT时间 |
READY | 成功建立至少一个连接 | 长期保持 |
TRANSIENT_FAILURE | 连接中断但可恢复(如临时网络故障) | 指数退避重试期间 |
IDLE | 无活动RPC超过keepalive时间 | 直到新请求触发重连 |
SHUTDOWN | 显式关闭Channel | 永久状态 |
跨语言差异对比
语言 | Channel创建 | 状态查询API | 关闭行为 |
---|---|---|---|
Go | grpc.Dial("host:port", opts...) |
conn.GetState() |
必须显式Close() |
Python | grpc.insecure_channel(...) |
channel._channel.check_connectivity_state() |
自动GC回收 |
Java | ManagedChannelBuilder |
getState(true) |
shutdown() + awaitTermination() |
C++ | CreateChannel() |
GetState() |
引用计数控制 |
Node.js | new Client('host:port', creds) |
无直接API | 自动关闭 |
- 性能相关
// Go示例
conn, err := grpc.Dial(
"service.example.com:443",
grpc.WithDefaultServiceConfig(`{
"loadBalancingConfig": [{"round_robin":{}}],
"methodConfig": [{
"name": [{"service": "echo.Echo"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}]
}`),
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
- 安全相关
# Python安全配置示例
channel = grpc.secure_channel(
'api.example.com:50051',
grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(root_certificates),
grpc.access_token_call_credentials('Bearer ' + token)
),
options=(('grpc.primary_user_agent', 'myapp/1.0'),)
最佳实践指南
- Channel 复用策略
+----------------+ +-----------------+
| 客户端进程 | | 服务端集群 |
| | | |
| [Channel A] |---LB---->| Instance1:50051 |
| (单例) | | Instance2:50051 |
| | | Instance3:50051 |
+----------------+ +-----------------+
- 每个服务端点维护一个Channel单例
- 避免为每个RPC创建新Channel(高开销)
- 多线程环境下保证线程安全
- 优雅关闭模式
// Java正确关闭示例
ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:50051")
.usePlaintext()
.build();
// 使用channel...
channel.shutdown(); // 启动关闭流程
try {
if (!channel.awaitTermination(5, TimeUnit.SECONDS)) {
channel.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
channel.shutdownNow();
}
- 状态监控集成
// Go状态监控示例
go func() {
for {
state := conn.GetState()
metrics.RecordChannelState(state.String())
if !conn.WaitForStateChange(ctx, state) {
break
}
}
}()
- 性能优化要点
参数 | 推荐值 | 影响维度 |
---|---|---|
keepalive_time | 60s | 连接保活频率 |
keepalive_timeout | 20s | 保活响应等待 |
max_concurrent_streams | 100 | HTTP/2流并发数 |
initial_window_size | 1MB | 流控窗口初始值 |
max_connection_age | 30min | 连接最大生命周期 |
- 避免每次新建 Channel
# 错误示例(Python)
def make_request(request):
with grpc.insecure_channel('localhost:50051') as channel: # 频繁创建销毁
stub = service_pb2_grpc.MyServiceStub(channel)
return stub.Call(request)
- 忽略 TRANSIENT_FAILURE 状态
// 错误处理方式
conn, _ := grpc.Dial(...)
for {
if resp, err := client.Call(ctx, req); err != nil {
time.Sleep(1 * time.Second) // 未检测连接状态
continue
}
break
}
- 客户端负载均衡配置
[Channel]
|
+-----------+-----------+
| |
DNS-based ServiceConfig-based
(简单轮询) (高级策略)
| |
grpclb xDS配置
| |
+-------+-------+ +-------+-------+
| | | |
后端实例1 后端实例2 CDN节点1 CDN节点2