Q23: 如何实现 RPC 调用?
核心结论
RPC 的本质不是“像调用本地函数一样优雅”,而是“把一次远程请求包装成一套可治理的调用协议”。
真正的 RPC 设计至少要解决:
- 怎么定位目标
- 怎么序列化参数
- 怎么关联请求与响应
- 怎么处理超时、失败、重试、幂等
- 怎么观测调用质量
如果只停留在“远程函数调用”的表面,工程上很快就会出问题。
一、RPC 实际上做了什么
一次 RPC 调用通常包含这几个步骤:
- 调用方构造请求
- 把方法名、目标对象、参数序列化
- 通过网络发到目标节点
- 目标节点反序列化并分发到对应处理函数
- 执行后返回结果或错误
- 调用方根据请求 ID 找回等待上下文
所以 RPC 不只是一个函数调用接口,而是一整套通信约定。
二、RPC 最核心的几个组成部分
1. 服务发现或目标寻址
调用方必须知道请求要发到哪里。
常见方式包括:
- 固定节点地址
- 路由表
- 网关转发
- 注册中心
- 基于实体 ID 或分片键路由
在游戏服务里,很多 RPC 不是“找某个服务名”,而是“找负责这个玩家、场景、房间、Cell 的节点”。
2. 协议与序列化
RPC 需要定义请求包结构,通常至少包含:
request_idmethodtargettimeoutpayload
序列化可以用:
- Protobuf
- FlatBuffers
- MessagePack
- 自定义二进制协议
这里重点不是“哪种最先进”,而是团队能否稳定演进和调试。
3. 请求上下文管理
调用方发出去以后,要把回包和原请求关联起来。
因此需要维护:
- 待完成请求表
- 超时定时器
- 取消或失效逻辑
如果这层没做好,超时、重试、节点切换时会很乱。
4. 错误模型
RPC 失败不是只有一种失败。
至少要区分:
- 网络不可达
- 目标不存在
- 业务拒绝
- 调用超时
- 节点繁忙
- 结果未知
“结果未知”尤其重要,因为超时不一定代表没执行,可能只是结果没回来。
三、同步 RPC 和异步 RPC 怎么选
1. 同步 RPC
优点是调用代码直观,适合:
- 初始化流程
- 管理后台
- 低频工具链
缺点也很明显:
- 容易阻塞线程
- 容易把调用链拉长
- 上游稍慢就层层堆积
2. 异步 RPC
异步更适合游戏服务主链路,因为它更容易:
- 控制线程占用
- 做超时和取消
- 适配事件驱动模型
代价是:
- 代码结构更复杂
- 错误处理更容易遗漏
在在线游戏里,大多数核心链路最终都会偏向异步或消息驱动,而不是大面积同步等待。
四、游戏服务里的 RPC 和通用微服务 RPC 有什么不同
游戏服务的 RPC 往往更强调:
- 实体或分区路由
- 短消息高频调用
- 和状态机、AOI、房间、会话强关联
- 对尾延迟非常敏感
它和后台业务服务不完全一样。很多时候“方法调用”只是表面形式,底层更像一套定向消息机制。
例如一个玩家使用技能,真正链路可能是:
- 网关收包
- 路由到玩家所在逻辑节点
- 逻辑节点再调用场景节点
- 场景节点触发周边广播或结算
这条链路里最重要的不是函数接口写得像不像本地,而是每跳是否清楚权责和超时语义。
五、实现 RPC 时最容易忽略的点
1. 超时不等于失败回滚
如果请求超时,调用方只能确认“结果没在时限内返回”,不能直接推断“目标一定没执行”。
这会直接影响:
- 是否允许重试
- 是否需要幂等
- 是否需要查询最终状态
2. 重试必须看操作语义
不是所有 RPC 都能自动重试。
适合重试的通常是:
- 只读查询
- 幂等写入
- 明确支持去重的操作
不适合无脑重试的包括:
- 扣费
- 发奖
- 迁移切主
- 多阶段状态推进
3. 观察性必须内建
RPC 一旦跨进程,问题排查会很依赖:
- 请求 ID
- 调用链日志
- 超时分布
- 错误码
- 节点维度统计
没有这些,线上问题几乎不可查。
六、一个实用的 RPC 包结构
很多自定义 RPC 最终都会收敛到类似结构:
- 包头:魔数、版本、长度、flags
- 路由字段:服务名、节点 ID、实体 ID、分区键
- 请求字段:
request_id、method_id - 元信息:超时、trace_id、重试标志
- 负载:序列化参数
返回包再包含:
request_idstatuserror_codepayload
结构不复杂,但一定要预留版本演进空间。
七、在游戏服务里更稳妥的实践
很多时候,比“到处写 RPC”更好的做法是分层:
- 查询类请求可用 RPC
- 高价值状态推进用消息驱动或单线程实体邮箱
- 广播或事件传播用事件总线
- 强一致链路尽量减少跨节点同步调用
因为跨节点同步等待越多,越容易形成长调用链和连锁超时。
八、常见误区
1. RPC 就是远程函数调用语法糖
不对。真正困难的部分是超时、路由、错误模型、重试语义和可观测性。
2. 所有服务交互都适合 RPC
不对。大量扇出、异步广播、事件传播场景往往更适合消息队列或事件机制。
3. 超时后自动重试最稳
不对。很多写操作在超时后处于“结果未知”状态,重试可能制造双写。
参考资料
- Birrell and Nelson, Implementing Remote Procedure Calls
- gRPC 设计文档
- 各类在线游戏服务实体路由与异步调用实践资料
