主链路与外围链路差异
Category
networking / IPC。节点定位:主链路(同步、确定性)与外围链路(事件化、可补偿)的判别与纪律。
Definition
消息系统最容易被做坏的地方,不是在外围工具链,而是在主链路上被过度滥用。很多团队看到 MQ 能削峰、能解耦,就想把主流程也一起事件化,结果把原本明确的交互过程改造成了一个“最终可能成功、但谁也说不清何时成功”的黑箱。
所以讨论消息系统,必须先把主链路和外围链路分开。
什么叫主链路
主链路通常指那些直接决定当前玩家体验、当前实例状态和当前交互结果的路径,例如:
- 登录进入游戏
- 匹配入局
- 房间内关键操作
- 副本创建与进入
- 战斗结束的权威结算
这些链路通常具备几个共同特征:
- 对延迟敏感
- 需要明确结果
- 失败必须及时反馈
- 往往存在强顺序和强状态依赖
这意味着主链路更看重确定性,而不是最大吞吐。
什么叫外围链路
外围链路通常是围绕主事实展开的后续处理,例如:
- 战绩归档
- 排行刷新
- 成就更新
- 邮件和推送
- 运营报表
- 埋点和审计
这些链路通常允许晚一点完成,允许异步重试,也更适合多消费者并行处理。它们真正追求的是解耦和扩展性,而不是当前交互里的最低时延。
为什么主链路不能简单照搬 MQ 思维
如果把主链路过度消息化,常见问题会立刻出现:
- 玩家请求已经返回,但系统内部还在排队,不知道最终结果
- 某个关键消费者积压,主流程语义开始漂移
- 顺序稍一错乱,运行时状态就不可解释
- 排障时只能看到“消息发出去了”,却不知道玩家为什么现在还没进房
也就是说,主链路最怕的不是耦合,而是失去确定性。
Problem
外围链路为什么反而常常适合事件化
外围链路更适合消息驱动,是因为它们天然具备这些条件:
- 主事实通常已经产生
- 多个下游会基于同一事实做不同处理
- 下游失败不该直接阻断主链路
- 允许削峰、重试和延迟消费
例如一场战斗已经正式结算,战绩、排行、成就和报表都可以围绕“结算结果已确认”这个事实异步展开。这样既不会让主结算链路承担所有外围成本,也能避免各外围系统反向耦合主流程。
一个更稳妥的拆法
实际工程里,更稳妥的方式通常不是“主链路全同步”或“全异步”,而是分层:
- 主链路上的关键决定和权威状态同步完成
- 主事实一旦确定,再通过事件广播给外围系统
- 外围系统各自按自己的节奏消费、重试和补偿
这类分层的核心是,先定义“哪一步算业务已经成立”,再定义“哪些后续处理可以脱离当前交互时刻”。
最容易做坏的几个点
用消息队列承载实时控制流
如果房间输入、实时同步、关键控制权切换这类事情要穿过 Broker,再好的队列也救不了不适配的链路。
主事实还没落稳,就先广播外围事件
如果上游自己都还没确认最终结果,就先把事件发给一堆消费者,后面一旦回滚,外围系统会全部陷入补偿地狱。
外围系统反向卡住主流程
最常见的形式是,排行、报表、通知这些本该异步处理的东西,被设计成了主链路成功的前置条件。这样主链路和外围链路就又重新绑死了。
一个简单判断标准
判断一条链路更偏主链路还是外围链路,可以先问三件事:
- 玩家是否必须立刻知道结果
- 当前运行时状态是否要立即收敛
- 下游失败是否允许不阻断当前主流程
前两项越强,越偏主链路;后一项越强,越适合事件化外围处理。
常见误区
最常见的错误,是把消息驱动当成架构先进性的象征,连主链路也想统一改造成异步系统。第二类错误,是没有定义清楚“主事实何时成立”,导致主链路和外围链路在失败时一起悬空。第三类错误,是外围系统虽然名义上异步,实际上却仍然决定主流程是否成功,最终既没解耦,也没降低风险。
实战案例:一次「打完了不出结算」的外围反卡主链路排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「外围系统反向卡住主流程」「一个更稳妥的拆法」「一个简单判断标准」「常见误区」用到深水区:主链路最怕的不是耦合,而是失去确定性——这是结算链路的悬空排查,也是「外围名义上异步,实际上决定主流程是否成功」的完整展开。
背景与现象:一款竞技项目,结算链路原本 P99 320 毫秒。新版本上线「结算即推战报」:结算事务里串行加了两步——同步刷新排行、同步发送战报通知。上线 3 天后的晚高峰(叠加运营推送活动),结算 P99 飙到 4.8 秒——涨到约 15 倍,超时率从 0.2% 升到 6.1%;客服收到「打完了不出结算」工单 1700 单。战斗本身毫无异常,慢的是「打完之后」那一段。
第一步:现象与口径——先把「不出结算」变成可拆的时延账
口径统一:结算时延 = 战斗终局判定 → 结算确认返回客户端。三笔账分开记:
- 战斗侧:终局判定 P99 80 毫秒,与版本前持平——战斗没变慢
- 结算侧:结算确认 P99 4.8 秒——悬空点在这里
- 存储侧:结算事务提交 P95 35 毫秒、无锁等待——数据库没拖
战斗快、事务快、唯独「确认」慢——悬空点不在计算也不在存储,在结算事务之外的同步外部调用上。
第二步:分层归因——从战斗到外部调用的悬空账本
战斗层 终局判定 P99 80ms、判罚重算 0 起——排除战斗逻辑
存储层 结算事务提交 P95 35ms、无锁等待——排除数据库
外部账 结算确认里串行两次外部同步调用:
排行刷新 P99 1.2s + 战报通知 P99 3.4s,通知服务被运营推送打满、重试堆积
时间账 80ms + 35ms + 1.2s + 3.4s ≈ 4.7s ≈ 结算 P99 4.8s——账能对上账本收束:两次外部同步调用合计 4.6 秒,占结算时延约 96%——主流程在替外围排队。
第三步:根因——外围被做成了主流程成功的前置条件
对照本页「最容易做坏的几个点」,这是「外围系统反向卡住主流程」的标准形态:排行、通知这些本该异步处理的东西,被设计成了结算成功的前置条件。用「一个简单判断标准」的三问核对战报推送:玩家是否必须立刻知道结果(不必,晚几分钟无感);运行时状态是否要立即收敛(不必,战报不影响对局);下游失败是否允许不阻断主流程(必须允许,通知服务不该绑架结算)——三问全指向该异步化,却全被同步串行。「主事实何时成立」也没有定义过:结算事务一提交,胜负与奖励其实已成立,排行与战报只是围绕主事实的后续处理——误区第三类「外围名义上异步,实际上决定主流程是否成功」逐字应验。
第四步:处置与回填——主事实落稳即返回,外围转事件自愈
修法:结算事务提交(胜负落库 + 奖励入账)即返回客户端——主事实落稳;排行刷新、战报通知改为订阅「结算成功」事件,各自消费、重试、补偿,通知失败进补偿队列,不回滚结算;排行延迟改为分钟级可见(产品确认可接受)。复核:结算 P99 4.8 秒 → 350 毫秒,超时率 6.1% → 0.3%,1700 单工单逐单闭环(悬空对局全部补发结算与战报)。
回填清单:
| 落点 | 动作 | 回填处 |
|---|---|---|
| 主事实口径 | 「主事实何时成立」写进链路文档:事务提交即成立,外围一律不前置 | 一个更稳妥的拆法 |
| 前置依赖审计 | 新功能进结算链路必答三问,禁止同步调用排行/通知类外围 | 外围系统反向卡住主流程 |
| 事件化改造 | 主事实事件广播 + 外围自订阅,失败重试与补偿不回滚主事务 | 外围链路为什么反而常常适合事件化 |
| 悬空监控 | 「请求已返回但结果未落」的悬空率进大盘,主链路看板与战斗侧分离 | 为什么主链路不能简单照搬 MQ 思维 |
案例的三个教训
- 主链路的敌人不是耦合,是失去确定性——同步串入外围,等于把最不稳的下游装进最怕抖的链路。
- 「主事实何时成立」要先于架构讨论——事实没定义清楚,失败时主链路和外围一起悬空。
- 判断标准三问是新功能进链路的门票——玩家不必立刻知道的事,就不该让打完的人等它。
Used By
结算、支付、奖励发放链路评审;「打完了不出结算」一类事故的归因(见页内实战案例)。
Related
上游:IPC 边界(01)与通信模式(02)。下游:幂等与顺序(05)、外部依赖治理(server 树稳定性)。关系链:主/外围判别 → 事件化边界 → 反向卡死防护。
Reference
本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。