TT Lab
はじめる
学ぶ 学習パス コース

データパイプライン

過去を再実行する — バックフィルが数字を二重に数えるとき

TT Labで続きを見る

一言でいうと

バックフィルは「過去の区間をもう一度回すこと」ではなく、定期実行と同じコードで、同じパーティションを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만원   ← 몇 번 돌려도 같다

2月1日から14日までの日付軸の上で、定期実行の区間と、9日から11日までのバックフィルの区間が重なる図。累計を足す方式なら、5,182万ウォンに842万ウォンをさらに足して6,024万ウォンになり、842万ウォンを2回数え、累計を数え直す方式なら、5,182万ウォンのままで、何回回しても同じです

パーティションの表が冪等でも、累計の表が冪等でなければ意味がありません。累計は足さずに導出します。元のパーティションから数え直せば、バックフィルを何回回しても、値が揺れません。

第5は、戻ってはいけない場所があることです。すでに送ったレポート、すでに出たアラート、すでに精算された金額は、データではなく出来事です。その区間は封印し、新しい値が出たら、上書きする代わりに訂正として別に記録します。そうすれば、「そのとき私たちが何を送ったか」と「今何が正しいか」を、両方言えます。

現場での姿

1つ目は、バックフィルのスクリプトを別に作ることです。急いでいるときに、一度使うために作ったスクリプトが残って、半年後も回っています。そのスクリプトだけが知っている例外処理ができ、2つの経路の答えが分かれます。

2つ目は、区間を丸ごと1つのトランザクションに入れることです。14日分を一度にコミットすると、13日目に失敗したときに、最初からやり直す必要があり、その間、状態ストアは何も知りません。パーティションごとに終えれば、再開地点がただで手に入ります。

3つ目は、予約なしに「今は定期実行が回らない時間だ」と信じることです。リトライ・手動実行・時間のずれのために、その信頼は崩れます。予約は、コードが守る約束で、時間帯は、人が守る約束です。

4つ目は、バックフィルの範囲をログにだけ残すことです。何をいつ誰がもう一度回したかが、状態ストアに残っていなければ、数字がおかしいときに、たどり直す根拠がありません。そして、実行時間でバックフィルかどうかを推定しないでください。同じマシンでも、実行時間は2倍まで揺れます。

実務で本当に大切なこと

次のラボですること

14日分の注文パーティションを作ったあと、実行ツールrunner.pyを1ステップずつ育てます。区間を受け取って、パーティション単位で置換するようにし、丸ごと回した答えと、1日ずつ回した答えが同じかを確認し、足す累計と導出する累計が、バックフィルのあとにどれだけ開くかを、数字で書きます。そのあと、区間の予約を付けて、定期実行が他者の区間をスキップして報告するようにし、最後に、すでに出力した区間を封印して、遅れて届いた伝票を上書きする代わりに、訂正として残します。採点ツールは、毎回異なる日付と金額で自分のパーティションを用意して、自分で作った実行ツールを実際に動かし、状態ストアを直接読んで突き合わせます。