重启后的进程把同一批订单又发了一遍
目标
从崩溃后又恢复的发送进程的检查点和发送日志中,找出重处理区间,与交易所受理确认对照,只挑出真正被受理了两次的订单,再从数据中求出确定性订单 ID 和去重窗口的长度,留下恢复手册。
为什么重要
重处理是无法消除的。如果在发送之后保存检查点,崩溃时这一段就会被重新发出,如果在发送之前保存,这一段就会漏掉。两者之间没有安全的点,所以发送路径通常选择重新发送的一边,并由接收方过滤重复。要想过滤,同一个信号无论被重新读取多少次,都必须得出同样的订单 ID,而每次运行重新编的序列号,或者混入时间的 ID,会破坏这个条件。而且,更正和撤销指向之前的 ID,所以如果 ID 规则不稳定,连这条链也会断开,想撤销的订单就会留下来。
步骤
- 用
python3在/root/cap/replay/data中生成六个数据文件。请原样使用生成脚本。 - 比较检查点与发送日志,把重处理区间写入
/root/cap/replay/crash.txt。 - 把该区间内将被重新发出的订单,写入
/root/cap/replay/replay.csv和/root/cap/replay/replay.txt。 - 只用信号中稳定的字段生成订单 ID,写入
/root/cap/replay/clordid.csv和/root/cap/replay/idcheck.txt。 - 与受理确认对照,把被接受了两次的订单写入
/root/cap/replay/dup.csv和/root/cap/replay/dup.txt。 - 分别计算保存检查点时机的两种方式,各写一行到
/root/cap/replay/modes.csv。 - 把无法指向原订单的撤销和更正,写入
/root/cap/replay/broken.csv和/root/cap/replay/broken.txt。 - 把去重窗口的长度留在
/root/cap/replay/window.txt中,把恢复手册留在/root/cap/replay/runbook.md中。
参考
- 数据在
/root/cap/replay/data中。signals.jsonl是带偏移量的输入信号,sent.csv是实际发出的订单消息,acks.jsonl是交易所受理确认,checkpoint.json是提交到了哪里,incident.json是崩溃的时间和恢复的时间,policy.json是订单 ID 规则和提交周期。 - 时间全部钉在数据里。如果用
date取今天,同样的数据每天都会得出不同的答案。 - 订单 ID 规则在
policy.json的clordid里。把fields按其顺序用sep连起来,用hash做摘要,只截取前hex_len个字符,再加上prefix。值全部转换成字符串再连接。 intent为hold的信号没有越过门槛,没有发出订单。信号数与订单数是不同的。- 常见错误 1:只看发送日志来数重复。在受理确认中被拒绝的笔数,没有留在交易所,所以不是重复受理。
- 常见错误 2:在第 6 步忘了批次边界。检查点不是每一条保存一次,而是每
commit_every条保存一次。 - 规范:在 FIX Trading Community 标准、FIXimate 中,可以确认
ClOrdID和OrigClOrdID的含义。本实验的标的代码和运行名称全都是合成的。
生成故障留下的数据
用 python3 在 /root/cap/replay/data 中生成 signals.jsonl、sent.csv、acks.jsonl、checkpoint.json、incident.json 和 policy.json 六个文件。请原样使用不使用随机数的生成脚本。
隔离网络里没有可以下载的样本,所以要先自己做出数据。不使用随机数,是为了无论谁运行多少次都得到同样的数据,才能互相对照判定。评分器会把文件内容转换成标准形式来核对指纹,所以如果手工修改数据,后面的步骤就会全部被堵住。
找出从哪里开始重新读取的
在 /root/cap/replay/crash.txt 中,用 key=value 的形式写下 crashed_run、resumed_run、committed_offset、last_sent_offset、replay_from 和 replay_to 六行。
检查点里写着处理到了哪里,发送日志里则留着崩溃的那次运行实际发送到了哪里。两个值不同的话,它们之间就是会被重新读取的区间。重启从已提交的下一个偏移量开始读取。
数出原样重启会有什么被重新发出
在 /root/cap/replay/replay.csv 中,首行写 offset,intent,symbol,side,qty,limit_px,并把重处理区间内会发出订单的信号逐行写出,并在 /root/cap/replay/replay.txt 中写入 window_signals、window_orders 和 window_notional_krw 三行。
区间是从上一步求出的 replay_from 到 replay_to。把其中的信号全部数一遍,与只数会发出订单的信号,是不同的。金额是订单数量乘以限价后相加,只加会产生敞口的新建和更正。
让重新读取也得到同样的订单 ID
在 /root/cap/replay/clordid.csv 中,首行写 offset,intent,clordid,orig_clordid,为每个会发出订单的信号各写一行,并在 /root/cap/replay/idcheck.txt 中写入 order_signals、distinct_deterministic_ids、offsets_sent_twice、offsets_with_two_sent_ids 和 chain_resolvable 五行。
规则在 policy.json 的 clordid 里。把 fields 按其顺序用 sep 连起来,用 sha256 做摘要,再给前 hex_len 个字符加上 prefix。新建的 orig_clordid 留空,撤销和更正,则写下对 ref_offset 所指向的信号应用同样规则得到的值。chain_resolvable 是这样生成的 orig 在这张表里实际存在的撤销和更正的个数。
只留下真正被接受了两次的订单
在 /root/cap/replay/dup.csv 中,首行写 offset,msg_type,clordid_a,clordid_b,symbol,side,qty,notional_krw,写入两次运行的消息都被受理的偏移量,并在 /root/cap/replay/dup.txt 中写入 dup_accepted、dup_new、dup_cancel、dup_replace 和 dup_notional_krw 五行。
在发送日志里出现两次的偏移量,并不等于重复受理。只留下 acks.jsonl 里两条消息都是 accepted 的。clordid_a 是按运行名称升序靠前的一方,clordid_b 是靠后的一方。金额是数量乘以价格,dup_notional_krw 只累加新建订单的行。
由检查点何时保存所分出的结果
在 /root/cap/replay/modes.csv 中,首行写 mode,committed_offset,resume_offset,duplicate_orders,missing_orders,并写入 commit_after 和 commit_before 两行。
检查点是每 commit_every 条保存一次。如果是发送之后保存的方式,就只提交到刚好在崩溃之前结束的最后一个批次,如果是发送之前保存的方式,则正在处理的那个批次的结束偏移量已经被提交。重启从已提交的下一个偏移量开始读取。前者产生重复,后者产生遗漏。
撤销所指向的订单不见了
在 /root/cap/replay/broken.csv 中,首行写 run_id,clordid,offset,msg_type,orig_clordid,reason,写入无法指向原订单的撤销和更正,并在 /root/cap/replay/broken.txt 中写入 chain_messages、broken_total、unknown_orig 和 orig_rejected 四行。reason 是 unknown_orig 或 orig_rejected。
链断开的途径有两种。所指向的订单 ID 在发送日志的任何地方都没有,就是 unknown_orig,有,但交易所拒绝了那个原订单,就是 orig_rejected。两者的结果相同——想撤销的订单留了下来。chain_messages 是发送日志中全部的撤销和更正。
从数据中求出去重窗口并写恢复手册
在 /root/cap/replay/window.txt 中写入 offsets_sent_twice、max_replay_delay_sec、outage_sec 和 recommended_window_sec 四行,并在 /root/cap/replay/runbook.md 中,按顺序写下 ## 무슨 일이 있었나、## 왜 두 번 나갔나、## 돈으로 얼마인가、## 복구 절차、## 무엇을 고쳐야 하나 五个小节,每节各不少于 60 个字符。手册正文中,要用数字写出第 5 步的 dup_notional_krw 值和这一步的 recommended_window_sec 值。
重处理延迟是同一个偏移量最初发出的时间与再次发出的时间之差。要在发送了两次的所有偏移量上测量,取最大的值。推荐窗口是把这个值乘以 policy.json 的 dedup_safety_multiple,再向上取整到 dedup_round_sec 的倍数。outage_sec 是 incident.json 中两个时间之差。