4. 单笔丢包补录实战

逐笔数据丢包补录,最怕的不是“补不进去”,而是“补错了还不知道”。本节用一个完整的单笔案例,把日志定位、请求构造、结果验证、归档留痕四件事串成一条可复现的链路。案例中的证券代码、时间戳、序列号均为教学示例,用于说明判断逻辑,不代表真实生产数据。

一、案例背景与初始现象

某券商行情接入节点在日终对账时发现:某只沪市股票在 14:32:07 这一秒内,逐笔成交明细的本地序列号出现跳变。上游推送的序列号为 20240612-143207-000381 与 20240612-143207-000383,中间缺少 ...000382。下游策略服务反馈该秒成交笔数比交易所公开汇总少 1 笔,但价格与总量差异极小,属于典型的“单笔丢包”。

这类问题的关键特征是:缺口只有一笔、序列号连续区间内出现单点空洞、前后记录均正常。它既不是整段断流,也不是乱序,因此排查重点应放在“定位这一笔到底丢在哪一环”,而不是盲目重连或全量重拉。

二、从日志定位缺失记录

定位阶段建议按“先定缺口、再定环节、后定证据”的顺序推进。首先在接入层日志中检索缺失序列号的前后邻居,确认缺口确实存在于本地落库环节,而非上游未推送。示例日志片段如下:

2024-06-12 14:32:07.118 INFO  recv seq=20240612-143207-000381 code=600XXX price=10.52 vol=300
2024-06-12 14:32:07.121 WARN  buffer.overflow drop_candidate=1 queue=md_tick depth=8192
2024-06-12 14:32:07.126 INFO  recv seq=20240612-143207-000383 code=600XXX price=10.53 vol=200
2024-06-12 14:32:07.130 INFO  persist batch=88213 rows=2 gap_detected=000382

这段日志给出了三个关键证据:一是 buffer.overflow 告警出现在缺口前后,说明接收缓冲区在瞬时高水位时发生过丢弃;二是 gap_detected=000382 由落库环节主动识别,证明缺口判断逻辑生效;三是前后两条记录的价格、成交量均正常,说明链路未整体中断。由此可初步判定:丢包发生在接收缓冲到落库之间,属于本地处理环节的瞬时溢出,而非上游漏发。

进一步应核对三个位置:接收队列深度监控、落库批次号与行数、以及上游原始报文的本地镜像。若镜像中存在 000382 而落库缺失,则确认是本地丢弃;若镜像中也没有,则需转向上游链路排查。本案例中镜像存在该笔,故定位结论为本地缓冲区溢出导致单笔丢失。

三、构造补录请求

补录不是“把缺的那笔随便塞进去”,而是要保证补录记录与原序列在序列号、时间戳、业务字段上完全对齐,且不破坏下游的幂等与去重逻辑。构造请求前先明确补录来源:优先使用上游原始报文镜像,其次使用交易所历史查询接口回补,最后才考虑人工构造。本案例使用镜像报文,字段如下表所示。

字段示例值说明
seq20240612-143207-000382缺失序列号,必须与前后连续
code600XXX证券代码
trade_time14:32:07.120成交时间,落在前后两笔之间
price10.52成交价
volume100成交量(股)
sideB主动方向
sourcemirror_replay补录来源标识,便于审计

补录请求应携带幂等键,通常由 seq + code + trade_time 组合生成,避免重复补录造成双写。示例请求体如下:

{
  "idempotent_key": "20240612-143207-000382|600XXX|143207120",
  "seq": "20240612-143207-000382",
  "code": "600XXX",
  "trade_time": "2024-06-12 14:32:07.120",
  "price": 10.52,
  "volume": 100,
  "side": "B",
  "source": "mirror_replay",
  "operator": "ops_zhang",
  "reason": "buffer_overflow_single_drop"
}

构造时有两个易错点:一是时间戳精度必须与前后记录一致,若原链路为毫秒而补录写成秒,会导致下游排序错乱;二是 source 与 reason 必须如实填写,它们是后续归档与复盘的关键依据,不能省略或伪造。

四、验证补录结果

补录提交后,验证要覆盖“单笔正确、区间连续、下游一致”三个层次。单笔正确指字段值与镜像报文逐项比对一致;区间连续指补录后该秒内序列号无空洞、无重复;下游一致指策略服务收到的成交笔数与交易所汇总对齐。示例验证查询如下:

-- 检查该秒序列号连续性
SELECT seq, code, trade_time, price, volume
FROM tick_trade
WHERE code='600XXX'
  AND trade_time BETWEEN '2024-06-12 14:32:07.000' AND '2024-06-12 14:32:07.999'
ORDER BY seq;

-- 检查是否存在重复幂等键
SELECT idempotent_key, COUNT(*) AS cnt
FROM tick_trade
WHERE idempotent_key='20240612-143207-000382|600XXX|143207120'
GROUP BY idempotent_key;

预期结果是:区间内序列号从 ...000381 到 ...000383 连续,补录记录仅出现 1 次,且价格、成交量与镜像一致。若出现重复,说明幂等键未生效,需回滚后重新提交;若序列号仍不连续,说明补录写入的排序字段有误,应检查时间戳精度与排序规则。

验证时建议同时观察下游策略服务的成交笔数指标,而不是只看数据库。数据库正确但下游未刷新,往往是因为缓存或增量订阅未触发,需要额外推送一次变更通知。

五、归档与留痕

补录完成后必须归档,否则下一次对账仍会被判为异常,且无法追溯责任。归档内容至少包括:缺失序列号、定位证据(日志行号或截图索引)、补录请求原文、验证查询结果、操作人与时间、以及根因归类。本案例的归档记录可整理为一条工单摘要:

  • 缺口:20240612-143207-000382,单笔成交,价格 10.52,量 100 股;
  • 根因:接收缓冲区瞬时溢出,md_tick 队列深度达 8192 触发丢弃;
  • 补录来源:上游原始报文镜像回放,幂等键已校验唯一;
  • 验证结论:区间序列号连续、无重复、下游笔数对齐;
  • 后续动作:将该节点缓冲区上限从 8192 调整为 16384,并增加溢出即告警。

归档不是终点,而是把“这一次怎么修的”转化为“下一次怎么防”。本案例中,缓冲区扩容与溢出告警属于根因治理,应与补录工单关联,避免同类单笔丢包反复出现。

单笔丢包补录:定位 → 构造 → 验证 → 归档 上游行情源 seq 000381 / 000383 接收缓冲区 溢出丢弃 000382 落库与缺口检测 gap_detected=000382 原始报文镜像 含 000382 完整字段 定位结论:本地缓冲溢出,非上游漏发 证据:overflow 告警 + 镜像存在 + 前后记录正常 构造补录请求(镜像回放) seq=000382 · price=10.52 · vol=100 · side=B 幂等键 = seq|code|trade_time 验证:单笔正确 · 区间连续 · 下游一致 无重复幂等键 · 序列号 381→382→383 归档留痕 工单 + 根因 + 治理动作 根因治理 缓冲 8192→16384 溢出即告警

把这条链路走完,单笔丢包补录才算真正闭环:日志给出缺口与环节,镜像提供可信数据,幂等键保证不重写,验证确认区间与下游一致,归档把个案沉淀为治理动作。下一次再遇到序列号单点空洞,按同样的证据顺序推进即可,不必从重连开始试错。