耐障害性テストとフォールトインジェクション
翻訳についての注記: 本ドキュメントは英語原文
11-observability/09-resilience-testing-and-fault-injection.mdを日本語に翻訳したものです。コードブロックおよびMermaidダイアグラムは原文のまま維持しています。
TL;DR
障害時の挙動は、テストするか発見するかのどちらかでしか手に入らない仕様である。この本のすべてのシステムは、作者が意図しなかった仕方で失敗する。そして5つの9の制御平面を出すチームと、フェイルオーバーのバグを顧客チケットで知るチームの違いは、ほぼ完全に「誰かが顧客より先にその障害を実行したか」だけだ。耐障害性テストが生む主張は狭く、そして検証可能である: 障害Fを期間D・範囲Sで加えたとき、エラーバジェットの燃焼率Bを超えず、復旧は人間の操作なしに、または氏名の決まったオンコールエンジニアが過去に1度実行した手順書で完了する。
忠実度の階層は3つあり、互いに置き換えはきかない。文書化済み事実: 設計層での決定論的シミュレーションは、ステージングのトラフィックではいくら流しても再現できないインターリーブ を見つける。FoundationDBはデプロイ前にCPU年単位で測られる正確性バグをシミュレーションで検出したと報告し、MACEのフォールトインジェクション研究は、テスト対象より小さいハーネスでKafka・Cassandra・ZooKeeperの実バグを数十件見つけた。モデル検査はプロトコルレベルの「不可能」を見つける。本番のフォールトインジェクションは本番にしか存在しない障害を見つける — 誤設定のDNSリゾルバ、誰も更新しなかった証明書、隣接チームと共有しているクォータ。3段目だけをやるチームは1・2段目に永遠に驚かれ、1段目だけのチームは現実に驚かれる。
「わざと壊す」とこれを分けるのは2つの規律だ。すべての実験は、走る前に合格条件と中断条件を備えた仮説を持つ。定常状態は「ダッシュボードが緑」ではなくエラーバジェットの燃焼率で定義する。そしてすべての発見は恒久的なリグレッションになる: シミュレータの自動化テストか、訓練スクリプトか、その障害クラスを消すアーキテクチャ変更のいずれかだ。推論: 多くのチームで最初の四半期の最高収益は、風変わりなカオス道具ではなく、リストア訓練・リーダ殺害の自動化・時計とディスクの障害シミュレーション・そして深刻パス1本あたりの誠実なゲームデー1回である。
耐障害性テストが証明すべきもの
道具を選ぶ前に主張を書く。階層ごとに証明できる範囲が違うからだ。
| 主張 | 形 | 証明できる階層 |
|---|---|---|
| インターリーブ 下での安全性 | 書き込み喪失なし、二重コミットなし、リーダ2台なし、ack成功後の古い読み取りなし | 決定論的シミュレーション、モデル検査 |
| 障害下でのプログレス | リクエストはいずれ完了するか、締切X以内に明示的に失敗する | タイムアウト付きシミュレーション、本番注入 |
| 発明なしの復旧 | 文書化された手順が、現リリースで、このリージョンで機能する | オンコールが実行した訓練 |
| 爆発半径の上限 | 障害はSに留まり、隣接は合意済みの幅で劣化する | 対照群を置いた本番注入 |
| 実トラフィック構成下の容量 | レイテンシ目標と余裕Hを保ったまま目標負荷を持続 | 本番形状への負荷とソーク |
語彙を正直に保つ3つの区別がある。
- 障害(fault)・失敗(failure)・エラー(error): faultは注入される(遅延、殺されたプロセス、満杯のディスク)。failureはサービス提供が止まった状態。errorは観測可能な逸脱だ。テストはfaultを注入してfailureを引き起こし、errorが見え、かつ隔離できる場所で表面することを確認する
- 耐障害性テストは負荷テストではない。負荷テストは「どれだけ耐えるか」を問う。耐障害性テストはその一部が嘘をつき、停滞し、消えたときに何が起きるかを問う。両方のテストにパスしていても、フェイルオーバーは誤っている可能性がある
- カオスエンジニアリングがこの分野の全部ではない。本番実験は最も忠実で最も高い階層だ。この分野で最も安いバグ修正は、コードレビュー前に済ませたシミュレータと仕様の仕事から出る。この本の障害クラスの大半はまさにそこで捕まる
範囲の所有権はこの本の他のすべてと同じ規則: 障害モードを所有するチームがそのテストも所有する。プラットフォームチームはノード喪失とAZ喪失を証明し、アプリケーションチームは「依存先が30秒タイムアウトで500を返す」「キューが2回再配送する」を証明する。横断的な主張は合同訓練以外で誰も証明しない — 訓練が儀礼ではなく構造を支える理由がここにある。
忠実度の3階層
階層は「バグ1件あたりのコスト」「本番との忠実度」「そもそも表現できること」が違う。
| 階層 | 1回あたりのコスト | 忠実度 | 表現できる | 表現できない |
|---|---|---|---|---|
| モデル検査(TLA+、TLC) | プロパティ単位で数秒〜数時間 | モデルのみ | プロトコルロジック、不変条件、公平性 | コード、I/O、実時間 |
| 決定論的シミュレーション | CPU数分〜数日 | ロジックには高い。ハードウェアとOSにはゼロ | インターリーブ 、タイムアウト、パーティション、アップグレード | カーネル挙動、NICの癖、隣接干渉 |
| ステージングとカナリ注入 | 実験あたり数分 | 中。トラフィックも規模も違う | デプロイ機構、リソース制限、設定ドリフト | まれなインターリーブ 、実際の悪意ある負荷 |
| 本番注入 | 高(信頼とガードレールが必要) | 完全 | 組織の失敗を含むすべて | 爆発半径が予算を超えるバグ |
各行の最終列は「なぜ全階層が必要か」の理由として読むこと。シミュレータは、クラウドのプロバイダmetadataエンドポイントが資格情報の並列リフレッシュ時に429を返すことを見つけられない。本番は、プロトコル出荷前にシミュレータが見つける1,000万分の1のメッセージ順序バグを経済的に再現できない。
変更コストの順に並べる: 設計時に仕様、CIで連続的にシミュレーション、リリースごとにステージング注入、四半期ごとに本番実験(最も深刻なパスと、訓練でついさっき失敗したパスに対して)。
設計層での決定論的シミュレーション
決定論的シミュレーションの核心は、非決定性の供給元をすべてシード付きに変えることだ: メッセージ順序、ランダム選択、時計、ネットワークスケジューリング、ディスク挙動、プロセス寿命。そのループで本物のロジックを走らせる。すると実行全体がシードの純関数になり、あらゆる失敗が再生可能・縮小可能・数千コアで並列化可能になる。
これを成立させる機構:
- 仮想時間を持つ単一スレッドイベントループ。アクタかコルーチンがメモリ上のスケジューラを介してメッセージを交換する。時計はスケジューラが決めたときだけ進むので、 仮想時間の1週間のハートビートは何ミリかで済み、タイムアウトは「来ない」のではなく望んだときに発火する
- 第一級の乱択としての障害注入。各スケジューリング点で、ハーネスはメッセージの並べ替え・破棄・タイムアウト超えの遅延・複製、プロセスのクラッシュと時計オフセット違いでの再起動、ディスク書き込みの破損、1時間の停止を選べる。障害スケジュール自体がシードの一部である
- 正確性のオラクル。常時検査する不変条件(プライマリは高々1つ、コミット済みレコードを失わない、バージョン単調性)か、単純な参照モデルとのエンドツーエンド比較。古典的なトリックは、全トランザクションを素朴なメモリ内データベースにも実行し、観測結果が同一であることをアサートすること
- 縮小。シードが失敗したら、自動リデューサが最小シーケンスが見つかるまでイベントを削除する。4行の再現は修正できる。4万イベントのトレースは1週の読解だ
- カバレッジ誘導。乱択シードは長い尾の組合せを見つけ、どの分岐と障害クラスを実行したか記録する計装が「分布が狭すぎる」と知らせる。深い状態空間での純粋なランダム探索は浅い部分ばかり探すでるため両方が必要だ
文書化済み事実: FoundationDBチームは正確性シミュレーションを連続実行し、シミュレーションで見つかったバグを障害ではなく日常業務として扱うと説明してきた。Appleの"Deterministic Simulation 2.0"は、より高いレベルでシミュレートし過去の実行を再利用することで1テストあたりのコストを桁で下げた。実務的な教訓は、手法ではなく1回あたりのコストが、チームが本当に回すかを決めるということだ。
シミュレーションにできないこと: OSのTCPバッファ挙動、trimでEIOを返す特定のディスクファームウェア、2つのサービスが同一の接続追跡テーブルを共有している事実。そして間違った対象をモデル化することから救ってくれない — 仕様が「リトライは冪等」と書き本番でそうでなければ、シミュレータは欠陥のない空想に同意する。この隙間を埋めるために本番階層が存在する。
モデル検査とプロトコルの不変条件
モデル検査は別の質問に答える: 「この実装がランダムな悪日を多数生き延えるか」ではなく「この設計は、そもそもそのプロパティを侵害しうるか」。形式的な状態機械と必ず成立すべきプロパティを書くと、網羅的または範囲付き探索が、あるスコープ内での証明か反例パスのどちらかを返す。
この本の主題で元を取る場所:
- コンセンサスとレプリケーション: 単一書込手、コミット済みログエントリの喪失なし、単調読み取り、リーダ排他。Raft系プロトコルのTLA+風仕様は、テストが長年取り逃した微妙なprogressと構成変更の欠陥を表面化させてきた
- 効果のコミットと saga ロジック: exactly-onceの主張、補償の順序、「シーケンスの任意の地点でクラッシュしても孤立した予約が残らない」。クラッシュ点を網羅するのはチェッカが得意で、結合テストが最も苦手なことだ
- キャッシュ無効化とバージョン管理: より新しい値を読んだ後に古い値を読むインターリーブ や、書込手のdeleteが同時書き込みを失うインターリーブ
採用チームからの実務ガイダンス: 仕様は実装前に書く。価値は書いている間の議論にある。プロパティに関係ないものを全部抽象化してモデルを小さく保つ(変数5個なら数秒、50個なら永遠に終わらない)。対称性削減と少数ノードで状態空間に上限を与える。そしてすべての反例を、テスト失敗ではなく設計の会話として扱う。
道具の選択よりプロパティの選択が効く。実際の障害を捕まえるプロパティは通常、権限(誰が書き込めるか)、永続化の境界(何をackして安全か)、時間(リース保持者が停止している間に期限が切れたら何が起きるか)に関する。Jepsen風のテストはモデル検査と本番注入の生産的な中間にいる: 実デプロイへワークロードを流し、パーティションと殺害を注入し、履歴から古い読み取りや書き込み喪失といった異常を検査する。つまり実装とデプロイを同時にテストし、顧客が実際に気にする保証の言葉で報告する。
エラーバジェットに対して本番でフォールトインジェクションする
本番実験は対照実験であり、障害と分ける規律は「走る前の書き上げ」である。正典的なループは5ステップで、そのすべてでチームは静かに手を抜く。
- 定常状態を定義した仮説。定常状態は重要な指標の実測レンジ — エラーバジェットの燃焼率、ベースラインからXミリ秒以内のp99、Y未満の飽和 — であり「ダッシュボードが緑」ではない。対照群(触っていないセル・リージョン・テナント群)と比較する。よそで起きた劣化を誤った結果にしないためだ
- 爆発半径の設計。意味のある最小範囲: Pod 1つ、シェード1つ、AZ 1つ、トラフィックの1%、同意のある非重要テナント群。範囲はそのまま中断閾値でもある — 範囲外が動いたら実験はすでに失敗しており、止める
- 自動的な中断。燃焼率超過・実際のページ発報・計画時間超過で注入を戻すキルスイッチ。自動であって「オンコールが気づく」ではない。手動中断は注意が使える前提に立つが、注意こそが実験が攪乱する資源だ
- 立会人と時計を持って実行。中断できる人、入力していない人、そしていつ何を入れたかの書きられたタイムライン。変更フリーズ中・リリース中・ピークイベント中の実験は、誰も信じない結果と不要な障害を生む
- 結論を設計上の決定として記録。システムが主張どおりに動けば(その主張はテスト付きの文書化された挙動になる)、そうでなければ(発見はバグと同じキューに入る。深刻度は「実験が証明した顧客影響」から導く)
文書化済み事実: カオスエンジニアリングの原論文(Basiri et al., ICSE-SEIP 2017)はこれを正確に枠付けする — 定常状態を計測可能な挙動として表現し、現実世界のイベントを変え、本番で実行し、爆発半径を最小化するよう自動化する。そして「長期連続実験」の重要性に明示的に触れている。週次で変わるシステムに対して1回のゲームデーがほとんど何も証明しないからだ。
実験ではなくプログラムの失敗モードを2つ挙げておく。劇場としての実験は、アーキテクチャがすでに処理済みの障害を注入し(レプリカ5つのステートレスPodを1つ殺す)、成功と報告する。無価値ではない — ハーネスの検証にはなる — が、壊れうる障害へ段階を上げる計画が必要だ。上限のない実験は「安全だと分かっている」ため自動中断なしに注入し、安全議論の穴がまさに実験が突こうとした場所にあることを発見する。
注入可能な障害のカタログ
以下のカタログは「その障害が何を教えるか」で整理した。どの道具で注入するかではない。各行が高深刻パスを列にしたカバレッジ行列の候補である。
| 障害 | 注入方法 | 何をテストするか | よくある発見 |
|---|---|---|---|
| 依存先の遅延(ms〜秒) | プロキシやegress規則で遅付与 | タイムアウトの順序、サーキットブレーカ、リトライ予算 | 呼び出し元の締切より長いクライアントタイムアウト。遅いが成功した呼び出しへのリトライ |
| 依存先のエラーと5xxバースト | 障害注入サイドカーかモック | エラー分類、リトライ対fail-fast | 再試行可能と永続的エラーを同じに扱う。ユーザーは生の依存先エラーを見る |
| ネットワークパーティション(非対称) | パケットドロップ規則 | フェンシング、クォーラム動作、クライアントのトポロジ更新 | フェンシングなき脳分裂。古いリーダへ永遠にリトライするクライアント |
| クロックのジャンプとドリフト | 時計オフセットでVM停止/再開 | リース期限、有効期間の計算、順序付け | 停止中のプロセスがリースを保持したまま期限切れ。壁時計減算による負の時間 |
| 書込手のみディスク満杯 | 領域を埋めるかクォータ設定 | バックプレッシャ、WAL処理、エラー伝播 | 静かな書き込み喪失。またはディスクが満杯のまま再起動できないクラッシュループ |
| 遅いfsyncとIOスタール | cgroup IOスロットル、dm-delay | グループコミットの仮定、永続性の主張 | フェイルオーバー後にack済み書き込みが失われる。特定キュー深度でのレイテンシの崖 |
| DNS障害と古い応答 | リゾルバ遮断、SERVFAIL、長いTTL | コネクション再利用の仮定、リトライの嵐 | リゾルバ停止中は完全停止。復帰時にサンダーリングハード |
| 接続とconntrackの枯渇 | 下限を引きsocketを保持 | プールサイズ、keep-aliveの仮定、ヘルスチェック | ヘルスチェックが枯渇中のプールを共有し不健康と報告して現象を増幅する |
| リーダ/プライマリ殺害(正常とSIGKILL) | ドレインありなしのプロセス終了 | フェイルオーバー時間、フェンシング、クライアント再接続 | フェンシングなき脳分裂。古いリーダのキャッシュ済みアドレスへ永遠にリトライ |
| AZ喪失 | LBからゾーン切り離し、ゼロへスケール | レプリカの算数、AZ間のデータ局所性、クォータ | 容量予約がゾーン単位で制御平面がスケールできない |
| リージョン喪失と隣接の劣化 | 訓練でルーティング層から模擬 | フェイルオーバー手順書、DNS TTLの実態、遅延の仮定 | 手順書のステップが障害リージョンから届かないコンソールを参照している |
| 資格情報と証明書の期限切れ | 意図的に短い有効期間のフィクスチャ | ローテーション自動化、期限前のアラート | クライアントが先に検出。ローテーションに誰も覚えていない対話ログインが必要 |
| キュー内の毒メッセージ | 検証に落ちるペイロードを公開 | デッドレター処理、コンシューマ隔離 | 悪いメッセージ1つがパーティションを止め、可視性なしに永遠に再起動する |
| 重複と順序乱れ | ハーネスで再配送・並べ替え | 冪等キー、重複排除ウィンドウ | 二重課金、二重出荷、状態機械の巻き戻り |
| ロールアウト中のバージョンスキュー | 1レプリカを旧ビルドに据え置く | 線形式の互換性、マイグレーション安全、キャッシュ形式 | 新しい読取手が古い書込手のペイロードを解析できない。バージョン間でフラグ既定が違う |
| 復旧時のサンダーリングハード | 全レプリカ同時リストア・コールドキャッシュ | ジッタ付きバックオフ、ウォームアップ、負荷シェディング | 復旧が次の障害になる。第2波でしか見えないリトライ増幅 |
使い方の規則は3つ。現実で起こる境界で注入する(ネットワーク層の遅延とアプリ層のsleepは別の実験だ。片方だけがソケットとバッファを満たす)。障害クラスが個別に通ったら組合せを注入する — 結合(ディスク満杯+リーダフェイルオーバー+リトライの嵐)が実際の障害の住処だから。そして必ず復旧経路を注入すること。この本の失敗トレースの痛手の大半は、キャッシュが冷たく、再接続がスロットルされず、全クライアントが同時にリトライする帰りの途中で起こる。
ゲームデーと復旧訓練
ゲームデーは組織を対象とする耐障害性テストだ。異なる欠陥を見つける2つの形式を分けておく。
- テーブルトップ(60〜90分、キーボードに触れない): 実際の障害テンプレートからシナリオを読み上げ、実際の手順書に対して意思決定を歩いて確認する。見つかるのは役割の欠落、フェイルオーバー権限の曖昧さ、退職者を指す連絡先リスト、「バックアップから復旧」とだけ書いてどのバックアップかも所要時間も書かれていないステップだ
- ライブ訓練(計画し、ガードレール付きで実行): 実際の手順書を現在のオンコールが追う形で実際の障害を注入し、検知までの時間・緩和までの時間・緩和が文書どおりだったかを測る。見つかるのは作者のノートPCでしか動かない道具、負荷下で誰も探せないダッシュボード、「データベースチーム」のうち手順を知る人が1人だけという事実
訓練は最高深刻パスを中心に設計する。直近1年で顧客に見える影響として誰かをページしたアラートと、データ喪失がありうるパスだ。カバレッジ行列を持つ: 行はカタログと障害セマンティクスの章の障害モード、列は四半期、セルはどの階層がいつ証明したか。12行で3列が空の行列は、どんな耐障害性ロードマップ文書よりも良い計画成果物である。
リストア訓練は固有の周期に値する。失敗の仕方が他と違うからだ。要件: 本番へ書き込めない環境へリストアする。「データベースが起動した」ではなくアプリレベルの不変条件で検証する。所要時間(権限を持つ人を待った時間を含む)を正直に測る。担当を持ち回りにして知識が個人にならないようにする。一度もリストアしていないバックアップは、ファイル形式についての仮説にすぎない。
負荷・ソーク・飽和の境界
負荷テストはキャパシティプランニングに属し、ソークテストは耐障害性に属する。後者が見つかるのは時間の依存する失敗だからだ。違いは持続時間と見るものだ。
| モード | 持続 | 目的 | 短いテストで見えないもの |
|---|---|---|---|
| ベースライン負荷 | 数分 | 想定トラフィックでのレイテンシとスループット | 新しいものはなし。比較の較正 |
| 破綻までストレス | 数分〜1時間 | 天井の位置と壊れ方 | 劣化が滑らかか崖か、最初に飽和する資源 |
| スパイク | 数秒 | オートスケーラの反応時間とコールドスタートコスト | スパイクより長く生き延えるキューの積み上がり。スケールダウン振動 |
| ソーク | 12〜72時間の持続 | 定常負荷での経時挙動 | FDと接続のリーク、メモリ断片化、ログ容量枯渇、インデックス膨張、トークン境界、キャッシュ退避の嵐 |
| シャドウ再生 | 連続 | 候補に対する実トラフィック構成 | 合成負荷と実トラフィック形状のカバレッジギャップ |
結果を信じられるものにしておく手法メモ: 閉ループ生成器は並列度が固定パラメータるときだけ使い、負荷下でのレイテンシを測るときは開ループの到着パターンを寄せる(応答を待ってから次を送る生成器は待ち行列遅延を隠す。古典的な協調的省略)。思考時間かクライアント別のペースを入れ、自業自得のDoSテストにならないようにする。キャッシュとプールは意図的にウォームアップし、結果がホットかコールドかを報告する。そして飽和を障害クラスとして扱うこと — CPU100%に達する実行は干渉についての実験であり、専用ハードを仮定せず同じホストの騒がしい隣接を測る。
ソークテストは、誰かが経時の信号を見るときにだけカレンダー上の席を得る: ディスクリプタ数、プール待ち、GC頻度のドリフト、ページフォールトとスワップの傾向、書増幅、固定負荷でのキュー深度、ログ残り容量。「p99は安定だった」しか出力がないソークは、それが捕まえるべき失敗について何も証明しない。
プログラムを測る
耐障害性プログラムは活動数を数える指標で死ぬ。実験実施数、ゲームデー開催数、導入したカオス道具。こうした数字は顧客リスクが変わらないまま伸びる。ページ減少と相関する指標は以下。
- 高深刻パスのカバレッジ: 実際にページした障害(データ喪失がありうるすべてのパス)に紐づく障害モードのうち、いずれかの階層で合格テストを持つ割合。階層も記録する。誰も引き起こせない障害を扱った訓練は1ではなく0
- 実験あたりの発見数(トレンド): 直近12実験が無発見のプログラムは、例外的に成熟しているか、すでに通っている障害を注入している。どこで(シミュレータ/ステージング/本番)見つかったかを追跡し、使われていない階層を見る
- テスト由来のMTTDとMTTM: 訓練を実行し、SLOの章のエラーバジェット算数に対して測り、四半期比較する。この2つが組織の耐障害性に関する唯一の正直な要約だ
- 復旧リグレッション率: リストア訓練の失敗数と、直近30日の変更が原因の失敗数。「3月の移行以来バックアップが壊れていた」の先行指標
- 書かれたアクション対出荷されたアクション: 訓練での発見が四半期内にコードかアーキテクチャ変更になったか。担当者付きで出荷されない耐障害性バックログは、プログラムではなくキューだ
- エスケープ相関: 各障害について「この障害モードはテスト済みだったか」。Yesならテストが間違ったものをアサートしていた(オラクルを直す)。Noならカバレッジ行列が間違っていた(行を足す)。どちらも安い修正で、レビューで問い続ける以外に生まれない
セキュリティ、プライバシー、マルチリージョンの注意
フォールトインジェクションは可用性の制御であると同時にセキュリティの制御でもある。フェイルオープンの挙動は負荷下で姿を見せる: ポリシーサービスがタイムアウトしたときallowを既定にする認可チェック、設定取得に失敗したとき緩い既定値へ落ちる機能フラグ、裏側ストレージに届かないとき自らを無効化するレートリミッタ。セキュリティに関わるすべての決定経路へ依存先の障害を注入し、書き込みは進行を拒否し、読み取りは予測可能に劣化することをアサートする。脅威モデリングの記事と対にする: 制御が生きていると仮定する悪用ケースには、その制御が死んだときに何が起きるかの実験が必要だ。
プライバシーは負荷テストの忠実度を縛る。本番リクエストをコピーするシャドウトラフィックは顧客データを複製するため、フィールドレベルの最小化・保持期間・目的制限の境界を超えるかの明示的な決定が要る。データ形状仕様から作る合成生成器は、ペイロードの一部を犠牲にする代わりにこの問題を回避する。
マルチリージョンで訓練が難しくなる理由:
- DNSは文書どおりに拡散しない。リゾルバとレシーバは設定TTLよりはるかに長く応答をキャッシュするため、「DNSを切り替えた」は完了時間が既知の緩和策ではない。60秒と仮定せず訓練で実際の減衰曲線を測る
- 制御平面はリージョン単位で、復旧中のものとクォータを共有する。リージョンBでのスケールアウトが失敗するのは、インスタンスクォータ・IP範囲・資格情報発行がリージョンAスコープであることが多い。最悪の瞬間に発見される(訓練済みなら別だが)
- レプリカ遅延によりフェイルオーバーはスイッチではなくデータの決定になる。手順書は、許容するack済み書き込み喪失量、その承認者、事後の乖離解消を明記しなければならない。これはディザスタリカバリ算数であり、午前3時の電話ではなく訓練スクリプトの中にある
失敗トレース
トレース: 11分を失ったフェイルオーバー。 マネージドデータベースがプライマリ殺害後にレプリカを昇格させる。フェイルオーバーは40秒で完了したが、クライアントが旧プライマリへの書き込みをやめるまで11分かかった。接続プールが古いトポロジをキャッシュし、「not primary」をリフレッシュなしのリトライ可能として扱ったためだ。検知はSLO燃焼アラート、続いてその結果のキュー滞留でもう1本。プライマリを殺す訓練をしていれば正確にこれを測定できていた。修正(not primaryでリフレッシュ+古いトポロジの有効期間に上限)は1日仕事だ。
トレース: 成功した訓練の裏のリトライの嵐。 実験は決済依存先に200msの遅延を注入する。サーキットブレーカは設計どおり開き、エラー率は予算内、チームは成功と宣言する。注入を止めて30秒後、全クライアントがコールドな接続プールへ同時にリトライし、エラー率が4分間3倍になった。実験は失敗だけをテストし、復旧を実験なしに出荷した。ジッタ付きバックオフと半開への遷移時のウォームアップラで閉じる。
トレース: クロックジャンプと切れたリース。 VMがホスト保守中に停止し、11分進んだ時計で再開する。停止中にリースは切れ、他のワーカーが取得し、両者が同じジョブ列を処理した。下流の重複排除は二重課金を抑えたが二重出荷は抑えなかった。シミュレーションはこのクラスを安く捕まえる — 仮想時間では1時間の停止がただで済むから。本番は計画してもそうでなくてもフリート保守ウィンドウごとに1回これをやる。
トレース: リーダだけ満杯のディスク。 書込先行ログのボリュームが満杯になる。フォロワは読み取りを続け、ヘルスチェックは通り、書き込みはストレージエラーとして失敗するが第2ホップで汎用内部エラーに変換されている。ディスク指標とエラーレートが別ダッシュボードなので誰も2つを結ばない。検知に9分。修正は3つのアラートと1つのエラー分類変更で、ステージングでボリュームを埋める実験で見つけられた。
トレース: 誰も更新しなかった証明書。 内部サービス間のmTLS証明書が切れる。両サービスは正常起動した — ローテーションのサイドカーが60日間にわたり静かに失敗していたのだ。アラートは証明書の年齢ではなくプロセスの健全性に書かれていた。これを見つける訓練はカオス実験ではなくフィクスチャだ: ステージングで四半期に1度、意図的に短い資格情報を発行して、クライアントより先に誰かが気づくかを見る。
検証戦略
ここでの検証はメタ検証である — 耐障害性テストそのものが機能していることを、どう知るのか。
- アサートは顧客から見て観測できる結果に置く。内部信号に置いてはいけない。「フェイルオーバーは完了した」で通り、顧客は11分間のエラーを見ていたテストはオラクルのバグだ。オラクルバグが耐障害性スイートの支配的な失敗モードである
- 自動化的な障害テストすべてに陰性対照を要求する。無害な変種が通ること、そして意図的に入れたリグレッションをハーネスが検出することをリリースごとに最低1回確認する。失敗できない障害ハーネスは、自信を作り出すので何もないより悪い
- 障害テストは機能テストと同じCIパイプラインに置き、守っているコンポーネントの変更時に毎回実行し、失敗は不安定テストのバックログではなくリリースブロッカーとして扱う。障害テストのflakinessはほぼ常に本物の非決定性の発見であり、そのようにトリアージする
- カバレッジ行列を設計文書の隣に置き、アラートルールへ新しい高深刻パスを追加ときは対応する行の追加を必須にする。この1つのゲートが、最初の熱狂の後に行列が朽ちるのを防ぐ
- 実験の安全性を実験と同じ周期でレビューする: 中断経路のテスト、キルスイッチの資格情報の有効期限、トポロジ変更後も爆発半径の設計が正しいか。ガードレールは守っているシステムより速く朽ちる
判断の枠組み
| 状況 | どこから始めるか | その四半期の「完了」 |
|---|---|---|
| 新しいサービス、テスト履歴なし | 依存先セットに対応するカタログ行。リリースごとのステージング注入とライブ訓練1回 | すべての高深刻パスに行と担当者がある |
| 状態を持つ/コンセンサス系の部品 | まず不変条件をモデル化し、決定論的シミュレーションかJepsen風チェックをCIへ | 安全性プロパティが機械検証され、発見が出荷前に修正済み |
| 同じクラスの反復障害 | そのクラスだけを再発が止まるまで訓練。アラートを足すよりアーキテクチャを直す | 人間の介入なしで訓練が2回連続合格 |
| まだ本番注入の許可がない | シミュレータ+ステージング注入+テーブルトップ。文書化した安全ケースと検証済みの中断経路で権利を得る | 安全性の議論が承認され、最初の範囲付き本番実験が予定された |
| 成熟した基盤で低インシデント率 | 組合せの階段を上がる: 複数障害、リージョン喪失、制御平面障害、サプライチェーン | 発見が単一障害ではなく組合せから出る |
| 規制・契約上の耐障害性の主張 | 書かれたとおりの主張(RTOとRPO)を、レビュアーが再現できる証拠付きでテストする | 文書化した手順書と主張ごとの日付入り訓練記録 |
「中断条件を障害として受け入れたくない」実験を予定してはならない。内部イベントだけをアサートするテストに基づいてシステムが耐障害性を持つと宣言してはならない。そして、耐障害性の仕事が終わったという議論には、丁寧かつ永久に否と答えること — システムは週次で変わるため、昨日の証明には誰も書かなかった有効期限が必ず付いている。
主要な要点
- 障害時の挙動は仕様である。テストで実行するか、顧客に実行してもらうか。文書になった結果が出るのは前者だけだ
- 忠実度の3階層は3つの異なるバグ母集団を見つける — シミュレーションはインターリーブ を、モデル検査は不可能を、本番注入は現実を。どれかが他を代替することはない
- 決定論的シミュレーションは1週間のハートビートをミリ秒にし、すべての失敗をシードで再生可能にする。採用を決めるのは手法ではなく1回あたりのコストだ
- 障害を書く前に不変条件を書け: 単一書込手、ack済み書き込みの喪失なし、リース権限の排他。オラクルのない障害スケジュールは証拠ではなく動画を生む
- 本番実験は仮説である。実測した定常状態(エラーバジェット燃焼)、境界のある範囲、そして人間が気づく以外の自動中断を伴う
- 復旧経路も注入する。コールドキャッシュ・スロットルなしの再接続・同期したリトライが、きれいなフェイルオーバーを2番目の障害に変える
- ソークテストは誰かが経時の信号を見るときの席を得る: ディスクリプタ、プール待ち、GCドリフト、書増幅、ログ容量、トークン期限境界
- 訓練は組織をテストする。リストア訓練はとりわけそうだ。誰もリストアしたことがないバックアップはファイル形式についての仮説にすぎない
- 測るのはページした障害モードのカバレッジ・実験あたりの発見数・訓練で測った緩和までの時間。実施回数を決して測らない
- フェイルオープンするセキュリティ制御は変装した可用性テストである。認可・フラグ・レートリミッタの全経路に依存先障害を注入し、書き込みが拒否されることをアサートせよ
参考文献
- Basiri, A., Behnam, N., de Oca, S. M., et al. "Chaos Engineering." ICSE-SEIP 2017 — 定常状態の計測、現実イベントの変調、本番実行、自動化、爆発半径最小化という5原則
- Alvaro P. ら "Deterministic Simulation" 系の議論、Apple "Deterministic Simulation 2.0" — 1テストあたりのコストを桁で下げた実務
- Hunt P. ら "Jepsen" — 実デプロイへ障害を注入し履歴から異常を検査する手法
- Heland J. et al. "MACE: Model-independent ... fault injection"(OSDI 2010) — 対象より小さいハーネスで分散システムのバグを見つける
- FoundationDB の開発ログ — 正確性シミュレーションを連続実行し、そこで出たバグを日常として扱う運用