再起動したプロセスが同じ注文をもう一度出した
目標
死んで蘇った送信プロセスのチェックポイントと送信ログから再処理区間を探し、取引所の受付確認と突き合わせて、実際に2回受け付けられた注文だけを選び出したうえで、決定的な注文IDと重複除外ウィンドウの長さをデータから求めて、復旧手順書を残します。
なぜ重要なのか
再処理はなくせません。チェックポイントを送ったあとで保存すると、死んだときにその区間が再び出ていき、送る前に保存すると、その区間が抜けます。2つの間に安全な地点がないので、送信経路は、たいてい再び送るほうを選び、重複は受け取る側で除外します。除外するには、同じシグナルが何回読み直されても同じ注文IDを出す必要がありますが、実行ごとに新しく振る連番や、時刻を混ぜたIDは、その条件を壊します。そして、訂正と取消は前のIDを指すので、ID規則が揺らぐとチェーンまで切れて、取り消そうとした注文が残ります。
ステップ
python3で/root/cap/replay/dataに6つのデータファイルを作成してください。生成スクリプトをそのまま使います。- チェックポイントと送信ログを比べて、再処理区間を
/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に書いてください。 - 受付確認と突き合わせて、2回受け入れられた注文を
/root/cap/replay/dup.csvと/root/cap/replay/dup.txtに書いてください。 - チェックポイントの保存時点の2通りをそれぞれ計算して、
/root/cap/replay/modes.csvに1行ずつ書いてください。 - 元の注文を指せない取消と訂正を、
/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で、バッチの境界を忘れてしまいます。チェックポイントは、1件ごとではなく、
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の6つを作成してください。乱数を使わない生成スクリプトを、そのまま使ってください。
エアギャップ環境には、ダウンロードできるサンプルがないので、データから自分で作ります。乱数を使わなければ、誰が何回回しても同じデータが出て、お互いの判定を突き合わせられます。採点ツールは、ファイルの内容を標準形に変換してフィンガープリントを突き合わせるので、データを手で直すと、あとのステップがすべて行き詰まります。
どこから読み直したかを探す
/root/cap/replay/crash.txtに、crashed_run・resumed_run・committed_offset・last_sent_offset・replay_from・replay_toの6行を、key=valueの形で書いてください。
チェックポイントにはどこまで処理したかが書かれており、送信ログには、死んだ実行が実際にどこまで送ったかが残っています。2つの値が違えば、その間が再び読まれる区間です。再起動は、コミットした次のオフセットから読みます。
そのまま再起動すると何が再び出ていくかを数える
/root/cap/replay/replay.csvに、1行目にoffset,intent,symbol,side,qty,limit_pxを置いて、再処理区間で注文が出ていくシグナルを1行ずつ書き、/root/cap/replay/replay.txtにwindow_signals・window_orders・window_notional_krwの3行を書いてください。
区間は、前のステップで求めたreplay_fromからreplay_toまでです。その中のシグナルをすべて数えることと、注文が出ていくシグナルだけを数えることは、違います。金額は、注文数量に指値を掛けたものの合計で、エクスポージャーを作る新規と訂正だけを足します。
読み直しても同じ注文IDを作る
/root/cap/replay/clordid.csvに、1行目にoffset,intent,clordid,orig_clordidを置いて、注文が出ていくシグナルごとに1行ずつ書き、/root/cap/replay/idcheck.txtにorder_signals・distinct_deterministic_ids・offsets_sent_twice・offsets_with_two_sent_ids・chain_resolvableの5行を書いてください。
規則は、policy.jsonのclordidにあります。fieldsをその順にsepでつなげてsha256で要約し、先頭hex_len文字にprefixを付けます。新規はorig_clordidを空欄にし、取消と訂正は、ref_offsetが指すシグナルに同じ規則を適用した値を書きます。chain_resolvableは、そうして作ったorigがこの表の中に実在する取消と訂正の数です。
本当に2回受け入れられた注文だけを残す
/root/cap/replay/dup.csvに、1行目に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の5行を書いてください。
送信ログに2回出たオフセットが、そのまま二重受付ではありません。acks.jsonlで、2つのメッセージがどちらもacceptedのものだけを残してください。clordid_aは実行名の昇順で前のほう、clordid_bは後ろのほうです。金額は数量に価格を掛けたもので、dup_notional_krwは、新規注文の行だけを足します。
チェックポイントをいつ保存するかで分かれるもの
/root/cap/replay/modes.csvに、1行目にmode,committed_offset,resume_offset,duplicate_orders,missing_ordersを置いて、commit_afterとcommit_beforeの2行を書いてください。
チェックポイントは、commit_every件ごとに保存されます。送ったあとで保存する方式なら、死ぬ直前に終わった最後のバッチまでしかコミットされておらず、送る前に保存する方式なら、今処理中だったバッチの終わりのオフセットが、すでにコミットされています。再起動は、コミットした次のオフセットから読みます。前者は重複を、後者は漏れを生みます。
取消が指していた注文が消えた
/root/cap/replay/broken.csvに、1行目にrun_id,clordid,offset,msg_type,orig_clordid,reasonを置いて、元の注文を指せない取消と訂正を書き、/root/cap/replay/broken.txtにchain_messages・broken_total・unknown_orig・orig_rejectedの4行を書いてください。reasonはunknown_origまたはorig_rejectedです。
チェーンが切れる道は2つです。指す注文IDが送信ログのどこにもなければunknown_orig、あるにはあるものの、取引所がその元の注文を拒否していたらorig_rejectedです。どちらも結果は同じです。取り消そうとした注文が残ります。chain_messagesは、送信ログの取消と訂正のすべてです。
重複除外ウィンドウをデータから求めて手順書を書く
/root/cap/replay/window.txtにoffsets_sent_twice・max_replay_delay_sec・outage_sec・recommended_window_secの4行を書き、/root/cap/replay/runbook.mdに## 무슨 일이 있었나、## 왜 두 번 나갔나、## 돈으로 얼마인가、## 복구 절차、## 무엇을 고쳐야 하나の5つの節を、その順に、それぞれ60文字以上で書いてください(見出しは韓国語で、順に「何があったか」「なぜ2回出たか」「金額でいくらか」「復旧手順」「何を直すべきか」という意味です)。手順書の本文には、ステップ5のdup_notional_krwの値と、このステップのrecommended_window_secの値を、数字で書いてください。
再処理の遅延は、同じオフセットが最初に出た時刻と、再び出た時刻の差です。2回出たオフセットのすべてで測り、最大の値を使います。推奨ウィンドウは、その値にpolicy.jsonのdedup_safety_multipleを掛けて、dedup_round_secの倍数に切り上げた値です。outage_secは、incident.jsonの2つの時刻の差です。