Skip to content

一致性、恢复与重连 ​

Category ​

server / services 子树。节点定位:一致性拆两类、恢复与一致性一起设计、幂等补偿常备、重连语义提前写清——分布式链路共同承担的现实成本。

Definition ​

服务拆开以后,一致性、恢复和重连不再是某个单点模块的局部问题,而是整条系统链路都必须共同承担的现实成本。单体里,一个事务失败也许只是本地回滚;分布式系统里,更常见的是这种情况:

  • 这边写成功了,那边超时了
  • 玩家会话已经迁移,旧节点还没完全停掉
  • 房间已经结束,结算只写回了一半
  • 客户端重连进来,但绑定关系还没切完

所以这里真正要讨论的,不是分布式一致性的理论大全,而是游戏项目里哪些状态需要多强的一致性,哪些状态可以接受最终一致,以及失败以后靠什么恢复。

Problem ​

单体里一个事务失败也许只是本地回滚;拆开后,同一失败会沿链路放大。一致性、恢复、重连不是单点模块的局部问题——重连语义不提前写清楚,事故现场就是它的首次定义现场。

先把一致性拆成两类 ​

如果不先分层,后面几乎一定会陷入“所有东西都想强一致”的误区。

运行时一致性 ​

这类一致性关注房间、场景、匹配和迁移过程中的临时状态是否自洽,例如:

  • 同一玩家是否只属于一个实例
  • 同一局是否只会结算一次
  • 迁移时新旧节点是否不会同时拥有控制权
  • 重连后会话是否能回到正确运行时上下文

长期资产一致性 ​

这类一致性关注奖励、货币、道具、任务、排行、支付结果等长期状态是否最终可信,例如:

  • 奖励有没有重复发
  • 资产有没有丢失或重复扣减
  • 战绩、排行和结算是否最终收敛
  • 支付成功后发货是否最终完成

这两类问题处理方式通常不同。试图用同一套强事务手段统一解决,通常既重又慢。

为什么恢复能力必须和一致性一起设计 ​

很多系统只讨论“正常路径如何成功”,却不讨论“失败以后怎么办”。这会导致线上一出问题,团队只能靠人工查日志和手工修数。

而真实项目里,这些事情迟早会发生:

  • 节点异常退出
  • 服务间调用超时
  • 控制平面调度失败
  • 客户端断线重连
  • 玩家迁移中断
  • 房间结算中途失败

恢复能力真正要回答的是:失败后系统如何重新进入一个可解释、可继续、可补偿的状态。

游戏系统里最常见的恢复问题 ​

玩家重连 ​

重连不是“重新连一下网关”这么简单。它往往同时涉及:

  • 会话恢复
  • 路由重新定位
  • 房间或场景实例查找
  • 最近状态恢复
  • 已完成但客户端未确认结果的再次确认

如果这些语义没有提前定义,所谓“偶发重连问题”通常只是系统对恢复路径没有正式设计。

节点迁移 ​

房间迁移、场景迁移、热点切流、摘流、扩容,本质上都属于控制权迁移问题。这里最重要的从来不是“把数据复制过去”,而是“新旧节点如何交接控制权,以及失败时回到哪里”。

跨服务链路失败 ​

比如房间结算完成了,但奖励发放超时;支付成功了,但资产回写失败;活动进度更新了,但战绩记录没写进去。这类问题几乎都不能只靠一次调用解决,必须依赖幂等、补偿和恢复语义。

幂等和补偿为什么是常备能力 ​

很多跨服务链路根本不适合依赖一个全局大事务。更现实的做法通常是:

  • 关键操作带唯一业务 ID
  • 各服务基于业务 ID 做幂等处理
  • 失败后进入明确的重试或补偿路径

例如:

  • 同一场战斗结算重复到达,不会重复发奖
  • 同一笔支付回调重复触发,不会重复发货
  • 同一份迁移请求重复执行,不会导致玩家双重绑定

幂等不是附加优化,而是让分布式恢复真正可行的基础设施。

重连语义为什么必须提前写清楚 ​

成熟系统通常会把下面这些问题提前定死,而不是等线上报错再猜:

  • 重连窗口有多长
  • 玩家当前状态以哪个服务为准
  • 断线期间房间或场景是否继续推进
  • 重连后是拉快照、追增量,还是重新入局
  • 已完成但客户端没收到的结果如何再次确认

只要这些语义模糊,后面所有重连 bug 都会显得像随机故障,实际上只是规则没写清楚。

一条更贴近工程实际的恢复链路 ​

mermaid
flowchart TD
    A[跨服务业务开始] --> B[生成唯一业务ID]
    B --> C[服务A完成本地状态更新]
    C --> D[调用服务B或发送事件]
    D --> E{服务B是否处理成功}
    E -->|是| F[记录结果并推进后续]
    E -->|否| G[进入重试或补偿流程]
    G --> H{是否允许幂等重试}
    H -->|是| D
    H -->|否| I[触发补偿 恢复到可解释状态]

这条链路最关键的不是流程图,而是两件事:

  • 每条关键业务必须有唯一标识
  • 每次失败都必须有明确定义的去向,而不是停在半完成状态

恢复能力通常应该落在哪些位置 ​

比较稳妥的系统,通常会提前在这些位置埋恢复落点:

  • 房间和场景的最近状态快照
  • 玩家会话绑定和迁移记录
  • 结算、奖励、订单的唯一业务编号
  • 控制平面的调度操作日志
  • 关键长链路的重试、补偿和审计记录

有了这些设施,故障以后系统至少能朝着“可解释”恢复,而不是完全依赖人脑拼日志。

一致性强度应该怎么分层取舍 ​

并不是所有链路都值得同样强的一致性。更现实的做法通常是:

  • 局内高频状态优先保证运行时语义清楚,必要时接受局部最终收敛
  • 长期资产优先保证幂等、审计和最终正确
  • 外围通知和低价值链路允许延迟,不应该拖垮主路径

这种分层,比试图让所有状态都同步且强一致,更接近长期可维护的工程方案。

常见误区 ​

最常见的错误,是只讨论正常路径如何协作,不讨论失败后如何恢复。第二类错误,是试图用统一强事务解决所有一致性问题,结果复杂度和性能同时失控。第三类错误,是没有唯一业务 ID 和幂等语义,导致重试、回调、重连一来就出现重复副作用。第四类错误,是把重连只当接入层小功能,不把它放回跨服务状态恢复链路里一起设计。

实战案例:一次「结算发了两次奖」的重复副作用排查 ​

案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「幂等和补偿为什么是常备能力」「重连语义为什么必须提前写清楚」「恢复能力通常应该落在哪些位置」用到深水区:正常路径一笔不差,失败路径全是重复——这是跨服务半完成链路的排查,也是「每次失败都必须有明确定义的去向」的完整展开。

背景与现象:某手游赛季结算上线后,玩家投诉重复收奖,月底对账发现重复发奖 217 笔;同月支付回调重复触发重复发货 30 笔。两类问题的共同点:链路只在「一切顺利」时被设计过。

第一步:现象与口径——先数重复副作用从哪条链路来 ​

结算口径:房间结算完成后调用奖励发放超时,结算方按超时重试,发放方其实已经发出——同一笔奖发了两次,且全链路没有一个能识别「这是同一次结算」的编号。

回调口径:支付平台回调重推,发货服务没有按订单号幂等,同一笔订单发了两次货。

重连口径:重连后客户端把「已完成但没收到的结算结果」又确认了一次,触发二次发放——重连语义没有提前写清楚,每个环节都在按自己的理解补偿。

第二步:分层归因——按重复副作用的来源分账 ​

text
重复副作用归因  占比
  无唯一业务 ID 与幂等,重试即重复     58%
  失败无补偿路径,链路停在半完成       27%
  重连语义未定义,已完成结果被再确认    15%

逐项比对:结算重试与回调重推占大头——都是没有唯一业务 ID 和幂等语义,重试、回调、重连一来就出现重复副作用;奖励发放超时后既不重试也不补偿,链路停在半完成——命中「房间结算完成了,但奖励发放超时」这条最常见恢复问题;重连再确认占剩余部分。

第三步:根因——只设计了正常路径,失败没有去向 ​

整条结算链路只讨论了正常路径如何协作:结算成功、发奖成功、客户端展示。失败后该重试还是补偿、由谁重试、怎么保证不重复,全都没有定义。幂等不是附加优化,而是让分布式恢复真正可行的基础设施——缺了它,每一次超时重试都是一次新的重复副作用。

第四步:处置与回填——给每次失败一个明确定义的去向 ​

  1. 关键操作带唯一业务 ID:结算单、订单、迁移请求各生成唯一编号,奖励发放按结算单号幂等,发货按订单号幂等。
  2. 失败进入明确路径:超时不盲目重试,先查发放结果;未发放则带原单号重试,已发放则标记完成并回传。
  3. 重连语义提前定死:重连窗口、状态以哪个服务为准、拉快照还是追增量、已完成未确认的结果如何再确认,逐条写进设计文档。
  4. 恢复落点预埋:结算与订单的唯一业务编号、关键长链路的重试补偿审计记录,落进恢复设施清单。

复核账:重复发奖 217 笔/月 → 0 笔;重复发货 30 笔 → 0 笔;半完成悬空链路清零;超时结算的平均恢复耗时从人工对账 2 天降到自动补偿 5 分钟。

回填清单:

回填项动作回填处
唯一业务 ID 与幂等结算单、订单带唯一编号,发放与发货按编号幂等幂等和补偿为什么是常备能力
失败补偿路径超时先查结果再重试或补偿,链路不停在半完成一条更贴近工程实际的恢复链路
重连语义五问窗口、权威方、推进策略、快照增量、再确认逐条定死重连语义为什么必须提前写清楚
恢复落点业务编号、绑定记录、重试补偿审计预埋进恢复设施恢复能力通常应该落在哪些位置

案例的三个教训 ​

  1. 只设计正常路径,失败就会自己发明路径。结算方按超时重试、发放方按请求发货,两条「正确」的逻辑拼出一次重复发奖。
  2. 幂等是基础设施,不是优化。没有唯一业务 ID,重试、回调、重连每一条都是重复副作用的入口。
  3. 每次失败都必须有明确定义的去向,而不是停在半完成状态。重试、补偿还是放弃,写清楚才不用人肉对账。

Used By ​

分布式一致性方案评审;恢复链路设计;幂等与补偿能力建设。

  • Orleans(commit b1b557d):状态与恢复的抽象出口——持久化 IPersistentState<TState> : IStorage<TState>(src/Orleans.Runtime/Facet/Persistent/IPersistentState.cs:10),提醒系统 IReminderService/IRemindable 承载可恢复的周期任务(src/Orleans.Reminders/),存储提供者目录外置(memory/AdoNet/Azure/Redis/Cassandra)

关系链:两类一致性 → 恢复同设计 → 常见恢复问题 → 幂等补偿 → 重连语义 → 恢复链路 → 落点。上游:services/04(路由与协作的可靠性面)。下游:database 树(事务一致性)、networking 树。

Reference ​

本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。

游戏知识体系