TT Lab
开始
学习 学习路径 课程

资本市场与清结算

重启后的进程把同一批订单又发了一遍

在 TT Lab 中继续学习

目标

从崩溃后又恢复的发送进程的检查点和发送日志中,找出重处理区间,与交易所受理确认对照,只挑出真正被受理了两次的订单,再从数据中求出确定性订单 ID 和去重窗口的长度,留下恢复手册。

为什么重要

重处理是无法消除的。如果在发送之后保存检查点,崩溃时这一段就会被重新发出,如果在发送之前保存,这一段就会漏掉。两者之间没有安全的点,所以发送路径通常选择重新发送的一边,并由接收方过滤重复。要想过滤,同一个信号无论被重新读取多少次,都必须得出同样的订单 ID,而每次运行重新编的序列号,或者混入时间的 ID,会破坏这个条件。而且,更正和撤销指向之前的 ID,所以如果 ID 规则不稳定,连这条链也会断开,想撤销的订单就会留下来。

步骤

  1. 用 python3 在 /root/cap/replay/data 中生成六个数据文件。请原样使用生成脚本。
  2. 比较检查点与发送日志,把重处理区间写入 /root/cap/replay/crash.txt。
  3. 把该区间内将被重新发出的订单,写入 /root/cap/replay/replay.csv 和 /root/cap/replay/replay.txt。
  4. 只用信号中稳定的字段生成订单 ID,写入 /root/cap/replay/clordid.csv 和 /root/cap/replay/idcheck.txt。
  5. 与受理确认对照,把被接受了两次的订单写入 /root/cap/replay/dup.csv 和 /root/cap/replay/dup.txt。
  6. 分别计算保存检查点时机的两种方式,各写一行到 /root/cap/replay/modes.csv。
  7. 把无法指向原订单的撤销和更正,写入 /root/cap/replay/broken.csv 和 /root/cap/replay/broken.txt。
  8. 把去重窗口的长度留在 /root/cap/replay/window.txt 中,把恢复手册留在 /root/cap/replay/runbook.md 中。

参考

生成故障留下的数据

用 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 中两个时间之差。