ワーカーは落ちても、同期は止まらなかった

バッチの途中でワーカーを強制終了し、1件のコンタクトを追います。どのカーソルが保存されたのか、再実行された書き込みがなぜ二重にならないのか、再起動の間は誰が同期を受け持つのか。そして、復旧は仕事の半分にすぎないと示した2つのインシデントを紹介します。

データ同期 障害 復旧

バッチの途中でワーカーが落ちたとき、実行中の同期はどうなりますか?

カーソルは問題なく終わったバッチの最後にしか進まないので、バッチの途中で強制終了されたワーカーはカーソルをそのバッチの先頭に残し、次の実行は同じページを読みます。各レコードの配信キーはページのカーソルと位置から導かれ、完了した書き込みを7日間保持する台帳が、再実行による二重書き込みを防ぎます。同期ごとに1つのリース(lease)があり、2つの実行が同時に動くことはありません。ここまでは簡単な半分です。ある同期は、何かが一時停止させるまで、実行のたびに失敗し続けました。あるワークスペースは、エラーばかりで成功が1度もないまま、何かが気づくまで沈黙していました。そこで今は、5回連続で失敗すると一時停止するブレーカーと、24時間成功がないと1時間ごとに確認して知らせるアラートがあります。

詳しく見る
このページの内容

AI Engineer が2026年10月3日に公開した講演で、Crusoe のデベロッパーアドボケイトが2台の A100 ノードで PyTorch の学習ジョブを起動しました。続いて、デモ自身の監視システムに、ある GPU が XID 79 エラーを出したと伝えました。ノードはダウンとしてマークされ、ジョブはキャンセルされました。プロセスは SIGTERM を受け取り、「その SIGTERM を処理して、チェックポイントを保存するかログを書き出すまでに最大2分」の猶予を得ます。予備のノードが入り、「slurm ジョブが再起動します。チェックポイントを読み込み、中断したところから学習を続けます」。最初から最後まで15分弱でした。誰も呼び出されませんでした。

同期は学習ジョブよりずっと小さな生き物です。それでも問いは同じです。ワーカーを落としても失うのが数分だけで、重複も生まれないようにするには、実行に何が必要でしょうか。

ここに書くのは Tajo 自身の同期ランナーについての答えで、パンフレットではなくコードから読み取ったものです。リバース ETL はこの仕事の形を説明しています。この記事は、その仕事が落ちたときに何が起きるかです。

1件のコンタクトを追う

実行は、保存されたカーソルから、ソースを1ページずつ読みます。ページのカーソルを c3、追いかけるコンタクトをそのページの212番目のレコードとしましょう。何かが書き込まれる前に、このコンタクトについて3つのことが成り立っています。

1つ目に、配信キーがあります。キーは同期の id、ソース、ページのカーソル、ページ内の位置、レコード自体のハッシュです。カーソルとレコードはハッシュに入力されるだけでキーに埋め込まれることはないので、キーにメールアドレスは含まれません。

2つ目に、書き込みは送信先に直接ではなく、台帳(ledger)を通ります。送信先を呼び出す前に、キーは保留中として確保されます。書き込みが成功すると、その行は完了済みとしてマークされ、7日間保持されます。

3つ目に、カーソルはまだ動いていません。カーソルが進むのはバッチの最後だけで、そのバッチのすべてのレコードが書き込まれた場合に限り、しかも実行がまだ同期を受け持っていると証明した後に限ります。

では、書き込みとチェックポイントの間でワーカーを落としてみます。

SIGTERM を受けると、すべてのキューワーカーは共通の15秒の期限内で処理を排出し、プロセスはまだ何が動いていても25秒後に終了します。systemd は強制終了まで45秒を与えます。この時間内に終わらなかった実行は処理中のままマークされ、次のワーカーがキューの停滞ジョブ復旧(stalled-job recovery)を通じて引き継ぎます。強制終了(hard kill)の場合、排出はまったく行われません。どちらの場合も、同じ2つの事実が残ります。送信先にはコンタクトが書き込まれています。カーソルはまだ c3 のままです。

再生

次の実行は c3 から始まり、同じページを読み、212番目のレコードに同じキーを導き、台帳に問い合わせます。台帳の答えは完了済みです。送信先は呼び出されず、保存された結果が再生され、実行は先に進みます。Brevo のインポートエンドポイントが1回の呼び出しでページ全体を受け取る一括処理の経路でも、リクエストを組み立てる前にキーごとに同じチェックが走ります。すべてのキーが完了済みのページは何も送信しません。

仕掛けはこれで全部ですが、なぜ機能するのかは述べておく価値があります。キーはバッチ開始時点のページのカーソルから導かれ、たまたま実行していた実行からは導かれません。クラッシュ、60秒後のリトライ、明日の定期実行は、同じレコードからすべて同じキーを導きます。台帳は、誰が問い合わせているかを気にしません。

次にコストです。台帳は7日後に忘れます。コード自身のコメントには、7日間は遅れたリトライと短い障害をカバーすると書かれており、主張しているのはそれだけです。それより古いカーソルからの再生は、もう一度書き込みます。また、強制的に中断された実行は待たされることがあります。落ちた実行のリース(lease)は最大15分保持され、定期同期でリースが空いている実行が回収されて再スケジュールされるのは、開始から1時間以上たってからです。Crusoe のデモは15分以内に戻ります。私たちの場合は、いつもそうとは限りません。

再起動の間、誰が同期を受け持つのか

1つの同期を受け持つのは、一度に1つの実行だけです。実行は Redis に自分のトークン付きのリースを取り、その寿命は15分で、5分ごとに更新されます。チェックポイントの前には毎回、まだそのリースを保持していることを確認します。保持していなければ停止します。その実行は、利用者が読むことになる平易な一文で記録されます。「The run lost its slot part-way through and stopped to avoid writing twice.」(実行が途中で枠を失ったため、二重書き込みを避けて停止しました)。生きている所有者と衝突した2つ目の実行はリトライされません。「Another run of this sync was already in progress」(この同期の別の実行がすでに進行中でした)で終了し、次の定期実行は通常どおり始まります。

修正を誰が受け持つかも明記されています。すべての失敗コードは、何が起きたかを説明する一文、次に何をすべきかを説明する一文、そして担当者(お客さまか Tajo か)に対応しています。マッピングが誤っているために送信先がすべてのレコードを拒否した場合は、お客さまの担当です。リースを失った場合は私たちの担当です。実行のページには次の手順が表示され、どちらが担当するかは同期の復旧ガイドに載っています。

ここまでが簡単な半分です。クラッシュのコストは1回の再生で、何も二重には書き込まれません。残りの半分は、2つのインシデントが教えてくれました。

失敗に次ぐ失敗

ある同期が何度も失敗しました。短い間隔のループではなく、スケジューラーの間隔ごとに1回ずつ、実行のたびに、毎回同じメッセージで失敗しました。カスタム id にコロンを含めることはできない、というものです。実行は送信先に一度も到達しませんでした。キューに入れられる途中で、どのランナーにも見られる前に落ちていたため、ランナーに紐づいたアラートには紐づける対象が何もありませんでした。

連続した失敗を見ているものは何もありませんでした。発見はそれに尽きます。

修正はブレーカーです。失敗が記録されるたびに、ブレーカーは同期が最後に変更されて以降に終了した直近5回の実行を読みます。5回すべてがエラーなら、同期を一時停止し、理由 circuit_breaker_tripped を付けた監査行を1行書き、重大アラートを1回発報します。連続失敗は、ワーカー内のカウンターではなく、保存された実行から導かれます。どの経路で記録された失敗も数えられ、インシデントの原因となったキュー投入時の失敗も含まれます。コードのコメントには、そのインシデントを5回目の実行で止められたはずだと書かれています。1回の成功で連続失敗はリセットされます。一時停止を解除すると、古い行で再びトリップするのではなく、ブレーカーが改めて有効になります。Shopify コネクターガイド も同じことを1行で述べています。5回連続で実行が失敗すると同期は自ら一時停止し、そのページに最後のエラーが表示されます。

ブレーカーがしないのは、何かを直すことです。Crusoe は確認なしにノードを交換してジョブを再起動します。Tajo は止まって確認します。一時停止した同期は、人が原因を修正して再び公開するまで停止したままです。この慎重なデフォルトは意図したものです。顧客レコードに書き込む同期を自動でリトライすること自体が、1つの書き込みだからです。

誰も見なかった沈黙

もっともな反論があり、それはこうです。「5回失敗したら一時停止するブレーカーがあれば、2つ目のインシデントも捕まえられたはずです。なぜ2つ目の監視役を作るのですか?」 2つ目のインシデントは失敗のようには見えなかったからです。長い間、あるワークスペースには有効な同期があり、エラー行が積み上がり続け、成功は一度もありませんでしたが、何もアラートを出しませんでした。存在したシグナルはすべて活動に紐づいていました。実行が失敗した、Webhook が届いた、予算を超えた。成功がゼロであることはイベントではありません。イベントがないということであり、ブレーカーは失敗が記録されたときにしか動きません。止まったスケジューラー、詰まったキュー、一度も起動しないワーカー。これらは何も記録せず、ブレーカーは決して目を覚ましません。

機関車のデッドマン装置は逆の仕組みで動きます。運転士は押し続けなければならず、沈黙すると列車が止まります。同期版は1時間に1回、毎時13分に動きます。有効な同期があるすべてのワークスペースについて、1つだけ問いかけます。直近24時間に成功した実行はあるか? 最も古い有効な同期が有効化されてから1日未満のワークスペースは対象外です。まだ成功するための1日を経ていないからです。それより古く、沈黙しているワークスペースには重大アラートが1回送られます。アラートは、どの沈黙なのかを伝えます。実行は記録されているが1つも成功していないのか、実行がまったく記録されていないのか。発報はインシデントごとに1回です。Redis のマーカーが最初の実行成功まで発報を閉じておき、その後クリアされるので、次のインシデントでは再びアラートが出ます。

2つの監視役は、事後に1つのコミットで追加されました。ブレーカーのコメントには、最初のインシデントを5回目の実行で止められたはずだと書かれています。デッドマンのコメントには、ずっと前に有効化されて実行がゼロの同期はアラートを出さなければならないと書かれています。どちらも必要なときには存在しませんでした。私たちは後からそれらを書き、誰も理由を忘れないよう、2つのインシデントをファイルの冒頭に記録しました。

作ったもの、作っていないもの

この記事の執筆時点で作ったもの:

  • カーソルは問題なく終わったバッチの最後ごとに保存され、クラッシュ時にも再度保存されます。
  • 各レコードの配信キーはページのカーソルと位置から導かれ、送信先を呼び出す前に毎回7日間の台帳と照合されます。
  • 同期ごとに1つのリースがあり、寿命は15分です。
  • 1回の実行につき5回まで試行し、60秒から間隔を延ばしていきます。
  • 5回連続の失敗で同期を一時停止し、その理由を記録するブレーカー。
  • 24時間成功がないことを知らせる、1時間ごとのアラート。

作っていないもので、頼る前に知っておくべきこと:

  • デッドマンが見ているのは同期ではなくワークスペースです。健全な同期が1つあると、同じワークスペースで沈黙している別の同期が隠れてしまいます。
  • そのアラートが届くのは Tajo の運用担当者です。ログ行は常に出力され、設定されていれば Sentry、Webhook、メールにも届きます。同期のページには表示されません。ブレーカーによる一時停止は表示されます。
  • 一時停止した同期を再開するものは何もありません。再開するのは人です。
  • 強制終了後の復旧は速くありません。リースは15分保持されることがあり、回収処理は1時間待ちます。
  • 2つのしきい値、5回の失敗と24時間は、デプロイ単位の設定です。同期ごとには設定できません。

ほぼ完璧なステップの長い連鎖でも失敗します。抜け出す方法は、最終状態を定義し、それを確認することです。この記事はその運用面の半分です。落ちた実行が何を残すかを定義し、沈黙が残す「何もない」を確認することです。

Crusoe の締めくくりの言葉は「障害は避けられない」でした。私たちの言葉はその一歩先にあります。クラッシュは行を残すので、簡単な半分でした。沈黙は、私たちが探しに行かなければならない半分でした。

よくある質問

バッチの途中でワーカーが落ちたとき、実行中の同期はどうなりますか?

カーソルは中断されたバッチの先頭を指したままです。Tajo はバッチが問題なく書き込まれた後にしかカーソルを進めないからです。次の実行は同じページをもう一度読みます。各レコードはページのカーソルとページ内の位置から導かれる配信キーを持ち、完了した書き込みはすべて台帳(ledger)に7日間保持されます。そのため、すでに書き込まれたレコードは送信先に二度送られず、台帳から再生されます。

再実行された同期が同じコンタクトを二度書き込まないのはなぜですか?

送信先を呼び出す前に、実行は毎回レコードの配信キーを台帳と照合します。キーは同期、ソース、ページのカーソル、ページ内の位置、レコード自体のハッシュです。そのため、クラッシュ、1分後のリトライ、翌日の定期実行のいずれでも、同じレコードからは同じキーが導かれます。7日間の範囲内で完了済みになっているキーは再送されません。

同期のデッドマンアラートとは何ですか?

失敗ではなく「何も起きていないこと」で発報するチェックです。Tajo は1時間に1回、有効な同期があるすべてのワークスペースを確認し、直近24時間に成功した実行が1つもない場合に、実行が1件も記録されていない場合も含めてアラートを出します。発報はインシデントごとに1回で、最初の実行が成功すると再び有効になります。失敗アラートは記録された失敗しか見えないため、このチェックが存在します。

先行利用を申し込む

お名前とメールアドレスを入力してください。Tajoへのアクセス申請を確認するためのリンクをメールでお送りします。

Brevoを取得

このページのMarkdown: worker-died-sync-didnt.md

Markdownを読み込み中…
Claudeで開く ChatGPTで開く