Apollo 技术文档Apollo 技术文档
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
  • MMORPG 架构 QA

Q23: 如何实现 RPC 调用?

核心结论

RPC 的本质不是“像调用本地函数一样优雅”,而是“把一次远程请求包装成一套可治理的调用协议”。

真正的 RPC 设计至少要解决:

  • 怎么定位目标
  • 怎么序列化参数
  • 怎么关联请求与响应
  • 怎么处理超时、失败、重试、幂等
  • 怎么观测调用质量

如果只停留在“远程函数调用”的表面,工程上很快就会出问题。

一、RPC 实际上做了什么

一次 RPC 调用通常包含这几个步骤:

  1. 调用方构造请求
  2. 把方法名、目标对象、参数序列化
  3. 通过网络发到目标节点
  4. 目标节点反序列化并分发到对应处理函数
  5. 执行后返回结果或错误
  6. 调用方根据请求 ID 找回等待上下文

所以 RPC 不只是一个函数调用接口,而是一整套通信约定。

二、RPC 最核心的几个组成部分

1. 服务发现或目标寻址

调用方必须知道请求要发到哪里。

常见方式包括:

  • 固定节点地址
  • 路由表
  • 网关转发
  • 注册中心
  • 基于实体 ID 或分片键路由

在游戏服务里,很多 RPC 不是“找某个服务名”,而是“找负责这个玩家、场景、房间、Cell 的节点”。

2. 协议与序列化

RPC 需要定义请求包结构,通常至少包含:

  • request_id
  • method
  • target
  • timeout
  • payload

序列化可以用:

  • 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_id
  • status
  • error_code
  • payload

结构不复杂,但一定要预留版本演进空间。

七、在游戏服务里更稳妥的实践

很多时候,比“到处写 RPC”更好的做法是分层:

  • 查询类请求可用 RPC
  • 高价值状态推进用消息驱动或单线程实体邮箱
  • 广播或事件传播用事件总线
  • 强一致链路尽量减少跨节点同步调用

因为跨节点同步等待越多,越容易形成长调用链和连锁超时。

八、常见误区

1. RPC 就是远程函数调用语法糖

不对。真正困难的部分是超时、路由、错误模型、重试语义和可观测性。

2. 所有服务交互都适合 RPC

不对。大量扇出、异步广播、事件传播场景往往更适合消息队列或事件机制。

3. 超时后自动重试最稳

不对。很多写操作在超时后处于“结果未知”状态,重试可能制造双写。

参考资料

  • Birrell and Nelson, Implementing Remote Procedure Calls
  • gRPC 设计文档
  • 各类在线游戏服务实体路由与异步调用实践资料
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu