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

データパイプライン

途中で必ず落ちる — 実行台帳と原子的な差し替え

TT Labで続きを見る

一言でいうと

パイプラインは必ず途中で落ちます。そのときに残るのが、途中まで書かれた出力物なのか、どこまでやったかが書かれた記録なのかが、翌朝を分けます。

なぜ必要なのか

明け方の3時に、精算の作業が落ちました。朝に出勤してみると、結果ファイルはあります。サイズももっともらしいです。ところが、数字がおかしいです。ファイルを開くと、最後の行が途中で切れています。書いている途中で、プロセスが消えたのです。

ここで2つ目の事故が起きます。「もう一度回せばいいだろう」と再実行します。そして、その日の売上が2倍に集計されます。前の実行がどこまで行ったのか、誰にもわからず、再び回った実行は、最初から最後まですべてやるからです。

3つ目の事故は、もっと静かです。落ちた実行が残した一時ファイルが、次のステップの一覧に一緒に拾われ、同じシャードが2回足されます。誰もエラーを見ません。

この3つは、コードが間違っているから起きるのではありません。成功した場合だけを考えて書いたコードから生じます。

どう動くのか

防ぐ仕組みは、3つです。

1つ目、アトミックな置換。宛先ファイルに直接書きません。同じディレクトリに一時的な名前で書き終えたあと、名前を付け替えて宛先にします。Pythonでは、os.replaceです。内部で呼ばれるのはrename(2)で、そのドキュメントは、新しい名前がすでにある場合、アトミックに置き換えられ、「他のプロセスがその名前を探したときに、存在しない瞬間がない」と明記しています。そのため、読む側は、いつ見ても、古いファイル全体か、新しいファイル全体を見ます。途中まで書かれた状態は、見ることができません。

ただし、条件があります。一時ファイルと宛先が、同じファイルシステムの中にある必要があります。同じドキュメントは、2つのパスが別々のマウントにあると失敗する(EXDEV)と書いています。/tmpに書いて、結果のディレクトリに移すコードが、開発機ではうまく動き、本番で落ちる理由が、これです。そのため、一時ファイルは宛先のすぐ隣に作ります。

一時的な名前にも、ルールがあります。読む側がpart-*.jsonで一覧を取るなら、一時的な名前がそこに引っかかってはいけません。ドットで始めるか、別の接尾辞を付けます。

2つ目、実行元帳。実行1回を1行で書きます。いつ始まり、どのシャードまで終わり、成功か失敗か、失敗ならどこで落ちたのか。追記だけして、直しません。そうして初めて、「昨日のあの実行」を、あとから追えます。SQLiteのアトミックコミットのドキュメントが説明していることも、結局、同じ構造です。書いている途中の状態を別に置き、すべてできたときに、一度に印を変えます。

3つ目、シャード単位の目印。もう一度回すときに、最初からやらないためには、どこまでやったかが必要です。別の目印ファイルを置いてもよく、シャードごとの出力物そのものを目印にしてもかまいません。後者のほうが安全です。出力物がアトミックに置換されるため、出力物があるということは、そのシャードが確実に終わったという意味だからです。目印と出力物が別々に動くと、目印だけが残って、出力物は途中まで書かれた状態ができます。

나쁜 순서                         좋은 순서
  결과 파일을 열고 쓴다            임시 파일에 다 쓴다
  ... 여기서 죽음                  ... 여기서 죽어도 목적지는 멀쩡
  닫는다                           os.replace 로 바꿔 단다
  => 반쯤 쓰인 파일이 남는다        => 옛 파일 아니면 새 파일

現場での姿

1つ目は、再開と再実行は別の言葉だということです。再実行は最初からもう一度回ることで、再開は終わったシャードをスキップすることです。再開をサポートするには、「このシャードは終わった」を読めなければなりません。ログに書いておくだけでは不十分です。ログは人が読むもので、再開はプログラムが判断するものです。

2つ目は、総計を集め直すことです。再開した実行が、今回処理したものだけを足して総計を出すと、スキップしたシャードが抜けます。総計は、常にシャードの出力物全体を読み直して出します。そうして初めて、再開した実行と、一度で終えた実行の答えが、同じになります。

3つ目、2回足される場所は、たいてい最後です。シャードの処理は上書きなので何回やっても同じですが、1日分の帳簿に1行付け足すことは、追記なので、呼ぶたびに増えます。そのため、付け足す前に、この実行をすでに付けたかを見ます。判断の基準は、時刻ではなく、実行の名前です。時刻で判断すると、同じ日に2回回った実行を区別できません。

4つ目、落ちた場所がわからなければ、何もできません。元帳に、失敗したシャードの名前がなければ、次の人にできることは、最初からもう一度回すことだけです。6時間かかる作業では、その差が大きいです。

実務で本当に大切なこと

次のラボですること

夜間精算の入力をシャードにして、実行ツールrunner.pyを1ステップずつ育てます。シャードの出力物を、一時的な名前で書いてから付け替えるようにし、わざと書いている途中で落として、宛先が無事かを確認します。実行元帳を付け、途中のシャードで落として、元帳に失敗したシャードが残るかを見て、その場所から再開します。最後に、同じ実行を2回コミットしても、1日分の帳簿が増えないようにします。採点ツールは、毎回異なるシャード数と金額で、自分の入力を用意して、自分で作った実行ツールを実際に動かし、落としたあとで何が残ったかまで確認します。