「成功」が嘘をつくとき — PaaS 運用で繰り返し踏んだサイレント障害

エラーを出さないバグが、いちばん危ない

前回、さくらのクラウドで PaaS を作るときの認証情報の使い分けを書いた。その記事で一度触れた話がある。「認証が通った」は「意図した身分で認証が通った」を意味しない、というものだ。認証情報を取り違えても、各値が整合していればそのまま通ってしまい、エラーでは気づけない。処理が成功を返したことと、処理が意図どおりに効いたことは、別のことである。

EastCloud ではさくらのクラウド上でアプリケーション実行基盤(PaaS)を開発している。その運用を続けるうちに分かってきたことがある。いちばん危険なバグは、エラーを吐くバグではない。成功を返しながら、実際には何も起きていないバグである。

エラーは、少なくとも気づける。ログに赤い行が出て、アラートが鳴り、誰かが調べ始める。だが「成功を返しながら動いていない」バグは、失敗を見張る監視には引っかからない。すべてのレコードが succeeded だからだ。これを本稿では サイレント障害(静かに壊れていて、しかし失敗の signal を一切出さない状態)と呼ぶ。

処理が成功を返し記録され監視も静かだが、本当に効いたかは検証されていない。試みの成功と効果の成功の隙間にサイレント障害が棲む
図1:成功という signal は「試みた」ことの報告であって、「効いた」ことの報告ではない

以下は、運用の中で繰り返し踏んだサイレント障害を、構造ごとに 4 つに分けたものである。個別のシステム名や数字は本質ではないので省く。大事なのは、どれも同じ一点から来ているという事実のほうだ。


パターン 1:成功を返し続ける復旧ループ

障害から自動で立ち直る仕組みを入れたとする。あるコンポーネントが落ちたら、作り直す。作り直しが済んだら「成功」を記録する。素直な設計に思える。

ところが、作り直しの処理は成功を返すのに、作り直したはずの状態が実際には回復しない、という状況が起こり得る。たとえば作り直しの API 呼び出し自体は 200 を返すが、対象が別の理由で起動しきらない。すると仕組みはこう振る舞う。落ちていると判断し、作り直し、成功を記録し、しかし状態は回復していないので、次の巡回でまた落ちていると判断し、また作り直し、また成功を記録する。

この無限ループの一周ごとに「成功」のレコードが積み上がる。 気づいたときには、成功を表す記録が大量に溜まっていた。失敗は一件も無い。だから、どの監視にも引っかからなかった。

真因はこうだ。仕組みが見ていたのは「作り直しの処理が成功したか」であって、「直したかった状態が回復したか」ではなかった。成功の件数は、健全性の signal ではない。 むしろ、成功の件数が異常に増え続けていること自体が、何かが回復していないことの signal だった。

教訓

「試みが成功したか」ではなく「直したかった状態そのもの」を測る。 復旧ループを組むなら、ループが解消しようとしている条件を、ループの成否とは独立に観測する。同じ復旧が短時間に何度も走っていたら、それは成功ではなく、収束していないという警報である。


パターン 2:止まっても「成功」に見える送り出し

バックグラウンドで何かを送り続ける処理がある。ログの転送、バックアップの取得、メトリクスの送出。こうした「継続的に動き続けるもの」には、別の形のサイレント障害が潜む。

それは、止まり方によっては、エラーを出さない。ただ静かになるだけである。

送り出しが止まったとき、エラーのログは出ない。最後の送信が成功したあと、次が来ないだけだ。監視が「失敗したら鳴る」ようにできていると、この沈黙は検知できない。失敗の不在と、正常稼働は、見た目が同じになる。 ログやメトリクスが届かなくなっても、画面上は何事もなく「最後の成功」が表示され続ける。そして、それが必要になったとき——障害調査でログを遡ろうとしたとき——初めて、ずっと前から止まっていたことに気づきかねない。

教訓

継続的な処理は、失敗ではなく「陳腐化」で監視する。 「最後に成功した時刻」を持ち、それが一定時間を超えて更新されなければ鳴らす。監視すべきは「失敗が起きたか」ではなく「成功が途切れていないか」である。この 2 つは似ているようで正反対で、前者だけを監視していると、静かに止まったものを永久に見逃す。

ひとつ注意がある。その「最後に成功した時刻」を、止まった送り出しと同じ経路で送っていると、経路ごと止まったときに更新も届かず、監視側からは単なる「データなし」に見える。データが来ないこと自体を警報にしておかないと、鮮度監視もまた同じ沈黙に陥る(いわゆる dead man’s switch の考え方だ)。


パターン 3:台帳は「消した」と言うが、実体は残っている

クラウド上にリソースを作ったり消したりする基盤を運用していると、2 つの「状態」を持つことになる。ひとつは、自分たちの台帳——「何があるべきか」を記録したデータベース。もうひとつは、クラウド事業者の側にある実体——「実際に何があるか」。

この 2 つは、ずれる。

環境を撤収する処理が途中まで成功し、途中で失敗したとする。台帳には「削除済み」と書き込まれたが、クラウド側の実体は消えずに残った。台帳は自分に「消した」と記録しているので、もう誰もそのリソースを見に行かない。だが実体は動き続け、課金も発生し続ける。 失敗のログは残っていないか、残っていても撤収の完了レコードに上書きされて見えない。

これは前回の記事で書いた話と同じ構造だ。クラウドの一覧 API は、こちらが聞きたい質問(アカウントに何があるか)とは違う質問に答えている。台帳もまた、こちらが知りたいこと(実体はもう無いか)ではなく、こちらが記録したこと(消したと書いた)を答えているにすぎない。

教訓

「あるべき状態」の台帳と、「実際の状態」の実体がある場合、台帳の自己申告を証拠にしない。 定期的に実体の側を読み、台帳と突き合わせる照合(reconciliation)を回す。台帳が「消した」と言っていても、API で引いて本当に 404 が返るまでは、消えたことにしない。ただし——これは第 1 弾の壁 4 と地続きだが——別のプロジェクトの鍵で引けば、実体が残っていても 404 は返る。照合では「どの鍵で 404 を見たか」まで揃える必要がある。自分の記録は、現実の観測の代わりにはならない。


パターン 4:宣言した側と、読む側の契約がずれる

複数のコンポーネントが、構成を受け渡して動くシステムでは、もうひとつの形がある。片方が宣言した項目を、もう片方が読んでいない。 しかも、そのずれは何のエラーも出さない。

たとえば、構成を宣言する側が、いくつかの設定を有効にすると書く。それを実行する側が、実はそのどれも読んでいない。宣言ファイルは文法的に正しいので、検証は通る。実行する側もエラーなく動く。だが、宣言したはずの設定は一つも効いていない。双方が独立に正しく動いているので、接続部にだけ空いた穴は、どちらのテストにも映らない。

別の場面でも、まったく同じ形のバグを踏んだことがある。構成ファイルはある値を A というフィールドに書き、それを読む実装は B という別のフィールドを見ていた。両者とも単独では正しく、エラーも出ず、しかし渡したつもりの値は一度も渡っていなかった。これも、宣言と読み取りの契約がずれていた例である。

教訓

2 つのコンポーネントが構成を受け渡すなら、「読む側が、書く側の宣言を本当に全部消費しているか」をテストで固定する。 各コンポーネントが単独でコンパイルを通ることは、接続部が正しいことを意味しない。宣言された項目の一つひとつが、実際に読まれ、実際に効いていることを、端から端まで通して確かめる仕組みが要る。もうひとつ安いのは、読む側が知らないフィールドを受け取ったらエラーにすること(未知フィールドの拒否)だ。書く側の宣言を読む側が知らなければ、その時点で弾ける。


共通する形と、共通する防ぎ方

4 つのパターンは、すべて同じ一点から来ている。

処理が返す「成功」は、「やってみた」ことを報告しているだけで、「効いた」ことを報告していない。 復旧の試みは成功するが、復旧は効いていない。送信の仕組みは正常だが、送信は途絶えている。削除は記録されたが、実体は消えていない。宣言は検証を通るが、読まれていない。どれも、試みの成功を、結果の成功と取り違えている。

だとすれば、防ぎ方も共通する。signal そのものを信じず、signal とは独立した ground truth(現実の、動かぬ事実)で効果を確かめる。

  • 復旧ループは、成功の件数ではなく、直したかった状態が回復したかを見る
  • 継続的な処理は、失敗ではなく、最後の成功からの経過時間を見る
  • 台帳は、自分の記録ではなく、事業者の実体を読んで突き合わせる
  • 宣言は、各側が通ることではなく、読む側が全項目を消費しているかを通しで確かめる

いずれも「調べたい対象そのもの」を観測しにいっている。調べたい対象が返してくる「成功」という自己申告を、証拠として採用しない。

4つのサイレント障害パターンと、各々で信じてはいけない成功・確かめるべきground truth・検知の仕方の対応表
図2:4 つのパターンは、すべて「signal ではなく ground truth を見る」に収束する

まとめ

前回の記事の「認証が通った、は、意図した身分で認証が通った、を意味しない」も、本稿の 4 つのパターンも、一文に畳める。

成功という signal は、試みたことの報告であって、効いたことの報告ではない。

エラーを出すバグは、少なくとも気づく手がかりを残す。だが成功を返すバグは、その手がかりを自分から消してしまう。だから、継続的に動くもの・自動で直るもの・外部の実体を持つもの・コンポーネント間で何かを受け渡すものを作るときは、最初から「これはどうやって静かに壊れ得るか」「静かに壊れたとき、何を見れば気づけるか」を設計に織り込んでおく必要がある。

成功を信じない。効果を、動かぬ事実で確かめる。地味だが、サイレント障害に対してはこれが基本の構えである。

続編として、公式ドキュメントや仕様が実機とずれる場面——仕様を読んだだけでは辿り着けない落とし穴——を別稿で扱った。


参考リンク

コメント

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です