インフラ

再起動ストームを引き起こさずにハングしたワーカーを自動再起動する

当社のモニターは、ハングしたGPUワーカーを2分で検出し、その後6時間にわたって応答がない状態を観測しました。検出と修復の間のギャップを、安全に埋める。

この記事は英語の原文をAIモデルが翻訳したものです。表現が原文と異なる場合があります。 英語の原文を読む

GPUフリートの2台のワーカーが、ある晩、6分差でハングしました。監視スタックは2分以内にそれを検知し、サーバー名、判定結果、それを停止させたジョブといった詳細なアラートを投稿しました。その後、人間が起きて docker restart と入力するまで、6時間何も起こりませんでした。

アラートは完璧でした。それでも停止時間は長くなりました。この投稿は、これら2つの事実の間のギャップ、すなわち、なぜ検知システムはしばしば目があっても手がないのか、そして健全なマシンを停止させてしまう再起動の嵐という、より悪い問題を引き起こすことなく検知パスに再起動を組み込むには何が必要か、ということについてです。

検出と修復は個別に発展する

誰もこのギャップを意図的に設計するわけではありません。それは積み重なっていくのです。

私たちのシステムには、異なるインシデントのために数ヶ月の間隔を空けて構築された、2つの独立した安全機構がありました。1つ目はハング検出器です。ワーカーが processing を報告しているにもかかわらず、そのハートビートが120秒以上古くなった場合、応答なしと判断してチャンネルに通知します。2つ目は自動再起動機能です。ワーカーが明示的な error 状態に30分間留まった場合、そのログを収集・投稿し、SSH経由でコンテナを再起動します。ただし、1サーバーあたり1日3回までというサーキットブレーカーによる再起動回数の上限があります。

それぞれは設計どおりに動作しました。罠はその結合部分にありました。再起動機能は、検出器とは異なる述語に紐付けられていたのです。再起動には state == "error" が必要でした。私たちが実際に遭遇したハングは、ジョブの途中でプロセスをフリーズさせるヒープ破損でした。そのため、ハートビートが途絶えている間も、状態フィールドは永遠に processing のままでした。検出器は作動しました。しかし、再起動機能の条件が真になることはありませんでした。さらに悪いことに、再起動機能には丁寧なルールがありました。ハング検出器がハングを検知したことを確認した場合、身を引いてそれに処理を任せるというものです。それが処理を委ねたコンポーネントには、実行する手段がなかったのです。

修復が検出とは異なる述語に紐付けられている場合、その片方しか認識しないすべての障害モードは、フォローアップのないアラートになってしまいます。このペアを1つのユニットとして監査してください。検出器が生成しうる各判定について、それに基づいて動作するコンポーネントを特定するのです。その答えが「最終的には人」となる判定はすべて、あなたが選択したギャップであり、少なくとも文書化された選択であるべきです。

デフォルトでは、再起動は安全なアクションではない

修正は些細なことに聞こえます。ハングパスから再起動関数を呼び出すだけです。1行のパッチではなく設計レビューが必要だった理由は、自動再起動が装填済みの銃だからです。不適切な再起動ポリシーの障害モードは、1台のワーカーが停止することではなく、フリート全体にわたる嵐であり、自動化がマシンを起動するよりも速く強制終了させてしまうことです。

これをリリース可能にした安全策を、実行される順に示します。

アクション前の永続性。 確認されたハングは、再起動が実行される前に、一定の遅延時間(私たちは3分を選択しました)の間持続する必要があります。検出にはすでに2回連続の異常な観測が必要でした。この遅延により、GCの一時停止、一時的な負荷の急増、監視の異常に対する許容性が追加されます。タイマーは、後述する理由により、プロセスメモリ内ではなく、SETNXを介してRedis内に存在します。

サーバーごとの実行ロック。 2つのバックエンドインスタンスが同じ監視ループを実行します。両方とも、ほぼ同時に同じ結論に達します。短いTTLを持つSETNXロックにより、そのうちの1つだけが再起動を実行することが保証されます。もう一方は単にスキップします。アラートの重複排除とアクションのシリアル化は異なる懸念事項であるため、別々にキーを設定します。重複排除キーはインシデントごとの重複通知を抑制し、ロックはサーバーごとの重複コマンドを抑制します。

最後の瞬間の再検証。 決定とSSHコマンドの間には、ログ収集とアラート投稿があり、最大1分のレイテンシーが発生します。その間にワーカーが自己回復している可能性があります。実行直前にステータスを再読み込みし、ハートビートが新しければ、キャンセルしてクリーンアップします。1分前に異常だったという理由で正常なマシンを再起動することは、まさに自動化がしてはならない種類の害です。

共有サーキットブレーカー。 すべての再起動パスは、サーバーごと、1日ごとに1つのカウンターをインクリメントし、3回を超えると自動化は停止し、アラートのみを送信します。重要なのは、カウンターはパイプラインが開始したときではなく、コマンドが実際に実行される直前にインクリメントされることです。初期のバージョンではすべての試行をカウントしていたため、ログ収集で失敗した実行でも予算を消費し、ノイズの多い検出器が一度も再起動を発生させることなく許容回数を使い果たす可能性がありました。

対策が失敗した場合のエスカレーション。 ハングを解消しない再起動で話が終わってはいけません。再起動時刻を記録します。10分後に同じサーバーがまだハングしている場合、メンション付きで一度限りの「再起動で回復しませんでした」というページが送信されます。これがないと、システムは「再起動がトリガーされました」と送信して沈黙し、読者は問題が処理されたと想定します。そのアクション後の沈黙状態こそが、元の6時間もの障害が発生した原因であり、自動化はそれを完全に再現できます。

セーフガード付きでパイプラインを再起動 ハング 確認済み 3分の遅延 + ロック 再検証 + ブレーカー restart 未回復: 人を呼び出す // 各ゲートでキャンセル可能。マシンに触れるのは最後のステップのみ

実行したばかりの再起動がハングに見える

レビューで見つかった最も巧妙な障害は、およそコイン投げの確率で発生する自己誘発的なものでした。それは二重再起動です。

docker restartの後、コンテナは1分以上かけて起動し、モデルを読み込みます。その間、古いステータスレコードは、終了中のプロセスが更新できなかったため、古いハートビートと共にprocessingと表示されたままRedisに残っています。リカバリーループにとって、これは新たなハングと区別がつきません。リカバリーループはタイマーを再設定し、遅延時間が経過するのを待って、起動途中のマシンを再び再起動します。このラウンドごとにサーキットブレーカーのバジェットが消費されるため、1回の本当のハングが、起動中のコンテナを繰り返し強制終了させることで1日の許容量を使い果たし、その結果、まさに自動化が必要なときに諦めてしまうことになります。

解決策はクールダウンキーです。再起動後、一定期間(私たちは10分を使用)タイマーの再設定を抑制し、クールダウンが存在する間は永続化タイマーをクリアします。一般的なルールは次の通りです。一時的にシステムが壊れているように見せる自動化されたアクションは、検出器に「これは私であり、新しいインシデントではない」と伝えるマーカーを残さなければなりません。

同じレビューから見つかった、隣接する2つの罠についても言及する価値があります。

インシデントの期間は、プロセスメモリではなく共有ストレージで測定する。 私たちの最初のドラフトでは、ハングの開始時刻をメモリ内の構造体から読み込んでいました。そのフィールドは再確認サイクルごとに更新されたため、測定された期間は永遠にゼロに近い値を推移し、再起動のしきい値に達することはありませんでした。動作する設計では、SETNXを使用して開始時刻をRedisに固定します。これにより、最初のオブザーバーが優先され、すべてのインスタンスが同じクロックを参照し、マルチインスタンスのデプロイメントでお互いのタイマーをリセットできなくなります。

トリガー条件は、ローカルの結論からではなく、データから導出する。 2つのモニターインスタンスがある場合、それぞれが「確認済み」について独自のビューを持ちます。インスタンスAが共有タイマーを設定し、観測が1つ遅れているインスタンスBがサーバーをまだ正常だと見なしている場合、BはAのタイマーをクリアしてしまい、2つは際限なく競合します。共有ステータスレコードから直接述語を導出することで、すべてのインスタンスが同じ答えを計算するようになります。

いくつかの障害は再起動をトリガーすべきではない

1つの判定を修正に結びつけることは、それらすべてを結びつけることを意味するものではありません。私たちは意図的に2つをそのままにしました。

ステータスレコードの欠落は、エージェントが到達不能であることを意味します。サーバーが再起動中、ネットワークが分断されている、あるいは意図的に電源がオフにされている可能性があり、また、私たちのマシンの1つは現在、コスト上の理由でオフになっています。電源の状態が不明なマシンをSSHで再起動することは、無用であるか、あるいは有害ですらあるため、その判定はアラートのみに留まります。そして、ハートビートは正常なままで進捗マーカーが停滞しているジョブも、やはり再起動されません。なぜなら、長時間実行されるジョブは正当にそのように見えることがあり、遅いタスクを理由に稼働中のワーカーを強制終了することは、誤検知と引き換えに実際の損害をもたらすからです。

これから導き出された境界ルールは次のとおりです。アクションがほぼべき等であり、影響範囲がすでに停止した1つのワーカーであり、そして誤った推測の代償が小さい障害モードに対してのみ、修正を自動化します。それ以外のすべては、迅速かつ明確に人にエスカレーションされます。

よくある質問

コンテナのヘルスチェックを使い、オーケストレーターに再起動させるだけではだめなのですか? 適切な liveness probe を設定した Kubernetes を使用している場合は、まずそれを実行してください。このパターンは、「ハング」がアプリケーションレベルでのみ(Redis のハートビート、ジョブの進捗など)可視である場合に適用されます。その一方で、プロセスはまだ TCP 接続を受け付けており、これはほとんどのポートレベルのヘルスチェックをパスします。私たちのゾンビは6時間もの間、接続に応答し続けました。

3分間の遅延は遅すぎませんか? 実際のタイムラインを合計してみてください。stale のしきい値、2回の確認サイクル、遅延、そして再起動とモデルの読み込みです。私たちの場合、障害発生から復旧まで約10分で完了しますが、人間が介在した場合は6時間かかりました。サービスの起動時間よりも短い遅延に設定してもほとんどメリットはなく、誤検知のコストが上がります。

なぜ再起動パスごとに1つずつではなく、複数のパスで1つのサーキットブレーカーを共有するのですか? ブレーカーは物理マシンを保護するものであり、マシンはどのコードパスが自分を再起動したかを気にしません。予算を分けると、最悪のケースが倍増します。もし分割する場合は、合計に上限を設けてください。

再起動コマンド自体が失敗した場合はどうなりますか? それを単なるログの1行としてではなく、第一級の結果として扱ってください。次のサイクルで再試行できるように、重複排除キーを解放し、そして直ちに人間に通知してください。なぜなら、リモートで再起動すらできないマシンは、自動化が判断すべき範囲を超えているからです。