技术博客

行情异常处理:断线、乱序、重复与序列号空洞

第 78 / 141 篇 · 2026-05-19 · secfin · 行情系统可靠性异常处理

行情的传输底座是 UDP 组播:不保证到达、不保证顺序、不防重复。接受这个事实之后,行情系统的可靠性工程就收敛成一个问题的多种形态:靠序列号发现异常,按异常类型选恢复策略。这篇把每条异常路径走一遍。

序列号:唯一的真相来源

每个频道独立维护一个单调递增的序列号(u32 或 u64),每条消息一个号。接收方只维护一个期望值:

$$ \text{expected} = \text{last} + 1 $$

每条消息到达,先比序列号,再谈正文。三种比对结果对应三种异常:

  • seq == expected:正常,处理,前进;
  • seq > expected:出现空洞(丢了或者乱序晚到);
  • seq <= last:重复或迟到太久,丢弃。

u32 序列号会绕回(一天几十亿条的消息量不至于,但多日复位不规范的实现会踩到),比较要用无符号模运算 $(a - b) \bmod 2^{32}$ 判断大小,直接有符号相减在绕回点附近会反。

空洞:补包、等快照、还是认栽

发现 seq > expected,先在乱序窗口里等等。UDP 乱序是常态,做一个长度几十条的环形缓冲,把乱序到达的消息暂存,窗口内的空洞可能被后续到达填满。窗口等不到,升级处理:

补包请求。交易所网关提供重传窗口,接收机可以请求重发缺失区间——但窗口极短(秒级),因为重传要占用洪峰带宽,交易所只愿意垫付很小的量。请求要迅速:检测到空洞立即发,犹豫几秒窗口就关了。

等快照覆盖。补不回来就靠下一张快照。快照是全量状态,序号空洞里的所有变化会被快照的最终状态吸收。代价是这段时间的逐笔数据永久缺失——对盘口展示无所谓(快照一覆盖就是对的),对订单流分析是洞(那几笔成交再也回不来)。所以订单流类下游必须注册"数据缺口事件",缺口时段的分析结果要标脏。

认栽并告警。连续空洞、快照也等不到,说明链路真的断了,进入断线流程。

断线重连:心跳判定与状态重置

断线的判定靠心跳:频道静默超过 $k$ 个心跳周期(通常 $k=3$)即判离线。难点在重连之后——第一件事永远是拿全量快照,而不是从断点续。断线期间本地快照已经和真实市场偏离,继续吃增量只会让错误累积。重连状态机:

断线判定 → 标记本地状态为脏 → 重连 → 请求/接收快照 →
快照应用后清脏标记 → 从快照序号+1 恢复增量

"标记脏"这个中间态很重要:脏状态下的数据如果推给了下游,客户端看到的就是假盘口(尤其价格还在跳,跳的方向还和真实市场相反)。所以行情服务器要把数据质量状态做成一等公民广播出去——下游客户端显示"行情延迟/恢复中"标识,靠的就是这个通道,而不是下游自己猜。

重复与迟到

重复消息靠序号去重:$seq \le last$ 直接丢。但"迟到"的消息要小心分类——落在 (expected, last] 区间外但还在乱序窗口里的,是乱序;越过窗口的迟到消息和重复无法区分,统一丢弃。这意味着乱序窗口大小的选择是可用性权衡:窗口大,能救回的乱序多,但延迟了正常消息的处理(要等窗口期确认不补洞);窗口小,正常消息快,但轻微乱序就变成空洞触发补包风暴。生产上窗口取几十毫秒量级,和专线延迟的抖动方差匹配。

主备切换:序列号衔接的坑

券商通常接主备两路行情(同一交易所的两个不同物理链路或两个接收机)。最坑的场景是主源挂了切备源:两路源的序列号空间相互独立,备源的当前序号和主源的断点没有半点关系。切换瞬间,接收方看到的不是"从 10005 继续"而是"毫无征兆的 802341"——如果按普通空洞逻辑处理,要么触发不可能的补包,要么等一张快照。

正确处理是切换也走"全量快照重置"路径:切源 = 状态置脏 + 快照同步 + 新序号空间重新锚定。两个源的序号空间在配置层就要声明独立,不能共享一个 expected。更深一层的防御是双路并行合并:主备同时收,按序号各维护状态,定期互 diff(《盘口(十档)数据结构》末尾提过),一路断了零延迟切另一路的最新快照。多花的带宽买切换的确定性,这笔账在行情服务商那里是必付的。

把异常做成可观测

所有异常路径都要出指标:空洞频率、补包成功率、乱序窗口命中率、断线次数、切源次数、脏状态时长。这些是行情系统的体检表——空洞频率突然升高往往先于用户投诉暴露专线劣化;切源次数增多是接收机硬件老化的早期信号。《行情系统的容量与降级》讲的过载保护,触发条件里也有一半来自这些指标。

序列号检测与空洞处理决策频道序列号轴57-58 空洞63-69 空洞乱序窗口等待(几十 ms)暂存重排,命中即补洞补包请求(秒级窗口)失败则等快照覆盖持续空洞 = 断线置脏 → 快照重置 → 恢复缺口事件订单流标脏重复/迟到:seq ≤ last 直接丢弃;绕回比较用无符号模运算主备切换 = 新序号空间,必须快照重置,不可当普通空洞