過去を再実行する — バックフィルが数字を二重に数えるとき
一言でいうと
バックフィルは「過去の区間をもう一度回すこと」ではなく、定期実行と同じコードで、同じパーティションを2回触らずに、すでに出た数字は上書きせずに、もう一度回すことです。
なぜ必要なのか
バグがありました。過去2週間分の集計が間違っていました。直したコードで、その区間をもう一度回します。ここまでは、誰でもやります。
問題は、その次に来ます。バックフィルが回っている間も、定期実行は回り続けます。2つが同じ日付に同時に書き込むと、どちらが勝ったのか誰にもわかりません。累計の集計は、バックフィルが足した分だけ膨らみます。そして、そのうちの3日分は、すでに顧客にレポートとして出ています。
この3つは、別々の問題であり、別々の仕組みで防ぎます。このコースの前のラボで扱った行単位の冪等は、ここでは必要条件にすぎず、十分条件ではありません。同じ行を2回入れても結果が同じになるようにしてあっても、区間単位の調整は、別に必要です。
どう動くのか
第1の原則は、バックフィルが定期実行と同じコードであることです。バックフィル専用のスクリプトを別に置くと、2つのコードが徐々に分かれていき、ある日、バックフィルの結果と定期の結果が違うことを、誰も説明できなくなります。そのため、実行ツールは、「区間を受け取って、その区間のパーティションを計算し直す」ことだけができればよいです。定期実行は、その区間が昨日1日分の場合にすぎません。Airflowのバックフィルも、同じDAGを過去の区間に対して実行するもので、別のDAGを呼ぶものではありません。
第2は、パーティション単位で丸ごと置換することです。1つのパーティションの結果を消して入れ直せば、丸ごと回しても、1日ずつ分けて回しても、同じ答えが出ます。この性質がなければ、バックフィルを途中で止めて、もう一度始めることができません。そして、パーティションを分けて回すほうが、たいてい優れています。失敗したときに、どこまでできたかがパーティションの境界で明らかになるからです。
第3は、区間を先に予約することです。バックフィルがどの日付を使うかを状態ストアに書いておき、定期実行は、他者が押さえている日付には触れずにスキップして、その事実を報告します。黙って待ったり、黙って上書きしたりするより、スキップして伝えるほうがよいです。ロックを使うとき、SQLiteのBEGIN IMMEDIATEのように、書き込みの意図を最初から明らかにする方式のほうが、遅く失敗するよりよいというのも、同じ話です。
第4は、累計集計にバックフィルを混ぜないことです。ここが最もよく間違えられます。
누적을 '더하는' 방식
1) 2월 1일부터 14일까지 정기 실행 누적 += 5,182만원 → 5,182만원
2) 2월 9일부터 11일까지 백필 daily 는 제자리에 치환됨
3) 백필 구간을 누적에 더함 누적 += 842만원 → 6,024만원 ← 842만원이 두 번
누적을 '다시 세는' 방식
daily 표 전체를 합쳐 넣는다 누적 = 5,182만원 ← 몇 번 돌려도 같다
パーティションの表が冪等でも、累計の表が冪等でなければ意味がありません。累計は足さずに導出します。元のパーティションから数え直せば、バックフィルを何回回しても、値が揺れません。
第5は、戻ってはいけない場所があることです。すでに送ったレポート、すでに出たアラート、すでに精算された金額は、データではなく出来事です。その区間は封印し、新しい値が出たら、上書きする代わりに訂正として別に記録します。そうすれば、「そのとき私たちが何を送ったか」と「今何が正しいか」を、両方言えます。
現場での姿
1つ目は、バックフィルのスクリプトを別に作ることです。急いでいるときに、一度使うために作ったスクリプトが残って、半年後も回っています。そのスクリプトだけが知っている例外処理ができ、2つの経路の答えが分かれます。
2つ目は、区間を丸ごと1つのトランザクションに入れることです。14日分を一度にコミットすると、13日目に失敗したときに、最初からやり直す必要があり、その間、状態ストアは何も知りません。パーティションごとに終えれば、再開地点がただで手に入ります。
3つ目は、予約なしに「今は定期実行が回らない時間だ」と信じることです。リトライ・手動実行・時間のずれのために、その信頼は崩れます。予約は、コードが守る約束で、時間帯は、人が守る約束です。
4つ目は、バックフィルの範囲をログにだけ残すことです。何をいつ誰がもう一度回したかが、状態ストアに残っていなければ、数字がおかしいときに、たどり直す根拠がありません。そして、実行時間でバックフィルかどうかを推定しないでください。同じマシンでも、実行時間は2倍まで揺れます。
実務で本当に大切なこと
- バックフィルと定期実行は、同じ関数です。違うのは、区間の引数だけである必要があります。
- パーティション単位で置換し、パーティションごとにコミットします。再開地点ができます。
- 区間を予約し、他者の区間はスキップして報告します。待たず、上書きもしません。
- 累計は足さずに導出します。足す累計は、バックフィルに出会うと必ず間違えます。
- すでに出たものは封印し、訂正として残します。上書きすると、そのときの事実が消えます。
次のラボですること
14日分の注文パーティションを作ったあと、実行ツールrunner.pyを1ステップずつ育てます。区間を受け取って、パーティション単位で置換するようにし、丸ごと回した答えと、1日ずつ回した答えが同じかを確認し、足す累計と導出する累計が、バックフィルのあとにどれだけ開くかを、数字で書きます。そのあと、区間の予約を付けて、定期実行が他者の区間をスキップして報告するようにし、最後に、すでに出力した区間を封印して、遅れて届いた伝票を上書きする代わりに、訂正として残します。採点ツールは、毎回異なる日付と金額で自分のパーティションを用意して、自分で作った実行ツールを実際に動かし、状態ストアを直接読んで突き合わせます。