当てずっぽうをやめる:仮説を立てて消していく障害調査【第3回】
この記事を読み終えると、原因が分からない不具合に対して再現の作り方を設計でき、二分探索で候補を絞る手順を持て、「直った理由を説明できる」状態まで調査を終わらせられるようになります。ジュニアエンジニア向け 成果の出し方シリーズ(全12回)の第3回です。
🎯 テーマの主役:「仮説駆動の調査」
今回の主役は仮説駆動の調査です。一言で言えば、調査とは「候補を立てて、消していく作業」です。犯人を探す作業ではありません。候補を消す作業です。
日常の例えで言うなら、医者の診察です。患者が「お腹が痛い」と言ったとき、医師はいきなり開腹手術をしません。まず問診(いつから、どこが、どんな痛みか)、次に触診、そして疑う病気を絞り、必要なら検査をします。検査は候補を消すために行います。血液検査の結果が正常なら、その候補は消えます。消えた数だけ、診断は近づきます。そして最後に、「この病気である」と説明できる状態になって初めて治療方針が決まります。
エンジニアの調査も同じです。「なんか動かない」という症状から、いきなりコードを読み始めても、候補は絞られません。読んだ範囲が広がるだけで、確信は深まりません。そうではなく、症状から候補を列挙し、安い検査で消していく。これが調査です。
この記事の仮説はこうです。調査の速度は「読む速さ」ではなく「候補を消す順番の設計」で決まる。したがって調査能力とは、推理力ではなく、切り分け設計の能力である。もしこれが正しければ、あなたが身につけるべきはコードを読む力より、実験を設計する力になります。
なお第2回で扱った止血が終わっていることを前提にします。止血がまだなら、まず第2回の手順に戻ってください。止血前の原因調査は、被害を拡大する行為です。また観測の3本柱(メトリクス・ログ・トレース)の立て方は第8回(開発フロー入門シリーズ)で扱いました。この記事は、集めた観測データからどう推論するかに焦点を当てます。
動機:「ログを3時間眺めたが、何も分からなかった」
調査について、ジュニアエンジニアが最初に経験する挫折はこれです。不具合を調査して、ログを3時間眺める。関係しそうなコードを読み、思いついた修正を試す。しかし何も分からない。そして上長に「どうなってる?」と聞かれ、「まだ分かりません」と答える。
この3時間は、無駄ではありません。しかし設計されていません。設計されていない調査は、やった量に比例して結果が出ません。1時間で10個の候補を消せる調査と、3時間で1個も消せない調査があります。差は能力ではなく段取りです。
この記事の出発点は、次の1文に集約されます。「分からない」を「分かる」に変えるには、まず「何を消せば分かるようになるか」を決める。これが調査の技術です。
🔍 検証①:再現できれば、調査の8割は終わっている
調査の最初にやるべきは、再現条件の特定です。理由は単純で、再現できるということは、変数を1つずつ動かして観測できるということだからです。再現できれば、あとは原因を1つに絞る作業に変わります。再現できなければ、観測すらできません。
再現の作り方は3段階に分かれます。第一に、症状を正確に記述すること。「なんか遅い」ではなく「特定の検索条件で、1万件以上を表示したときだけ、3秒以上かかる」。第二に、同じ条件を手元で再現すること。第三に、条件を1つずつ削ること。第2段階で再現できた瞬間、あなたは大量の候補を一網打尽に消したことになります。
| 再現の状態 | できること | 次の一手 |
|---|---|---|
| 再現できる | 変数を1つずつ動かして観測できる | 二分探索で原因を絞る(検証④) |
| たまに再現する | 条件の候補を大量に試せる | 発生頻度を上げる条件を探す |
| 本番でのみ起きる | 環境差の比較ができる | 環境・データ・負荷の差分を洗う(検証②) |
| 一度だけ起きた | 記録を残して観測を増やすしかない | 証拠を保全し、観測点を追加する |
「たまに再現する」は厄介ですが、逆に最大の手がかりです。たまにしか起きないということは、何かの条件が揃ったときだけ起きるということだからです。頻度が上がる条件を見つけられれば、原因に近づきます。たとえば時間帯・データ量・同時実行数・特定の入力を変えて、頻度がどう変わるかを記録します。頻度の変化は、条件と原因の距離を教えてくれます。
一方で「再現しない」障害も存在します。ここで知っておいてほしいのは、ハードウェア起因で、同じ入力でも結果が変わる種類の障害があることです。宇宙線などの放射線がメモリのビットを反転させる現象は実際に観測され、報告されています(シングルイベントアップセット)。こうした障害は再現できません。だからこそ、発生時に証拠を残す(ログ・ダンプ・時刻・ハードウェア情報)ことが唯一の対策になります。再現できない障害は、記録で戦うのです。
🔍 検証②:まず「何が変わったか」を見る
次に、候補を一気に絞る最も効率的な方法です。それは「変化したものを探す」ことです。昨日まで動いていたものが今日動かないなら、変わったのは何か。ここを先に見ます。
見るべき変化は5種類あります。コード(デプロイ・マージ)、設定(環境変数・フラグ・権限)、依存(ライブラリ・外部API・証明書)、データ(量・質・分布)、環境(負荷・ノード・ネットワーク)。この5つを順に見るだけで、多くの障害は原因に到達します。
④データが最も見落とされます。コードも設定も依存も変わっていないのに壊れる。それはデータが境界を越えたからです。件数が10万件を超えた、特定の文字が入力された、日付が閏年に入った、タイムゾーンが切り替わった。「変わっていないはず」の顔をして変わるのがデータです。
🔍 検証③:仮説は「消せる形」で立てる
次に、仮説の立て方です。ここで最も重要な原則を先に書きます。仮説は「反証できる形」で立てる。
悪い仮説の例:「メモリリークが原因だろう」。これは反証できません。なぜなら、リークがあっても症状が出ないことがあり、「リークが原因だ」という主張を否定する観測が作れないからです。良い仮説の例:「大量データを扱う処理で、一時配列が解放されず、10回目のリクエストでメモリ使用量が単調増加する」。これは反証できます。メモリ使用量を10回観測して、増加していなければこの仮説は消えます。
仮説が消せる形になっていると、調査が作業になります。「次はこれを試して、消す」という手順に変わるからです。逆に消せない形の仮説は、いつまでも心の中に残り続けます。これが「3時間ログを眺めても分からない」の正体です。
候補を絞るための切り分け軸も持っておきます。時間(いつからか)、空間(どのサーバーか)、入力(どのデータか)、操作(どの手順か)、利用者(誰か)。この5軸のどれかで半分に分けられないかを考えます。分けられれば、次は二分探索です。
🔍 検証④:二分探索で候補を半分にしていく
次に、最も強力な絞り込み手法です。二分探索です。第5回(CS入門)で扱ったとおり、これは候補が半分ずつ減る手法で、1,000個の候補も10回で1個に絞れます。
調査における二分探索は、2つの方向で使えます。時間方向と空間方向です。
時間方向の二分探索は、「いつから壊れたか」を二分で特定します。1週間前は正常、今日は異常。では3日前は? その間は? と区間を半分にしていきます。これは git bisect というコマンドとして標準的に用意されています。壊れていないコミットと壊れているコミットを指定すると、間のコミットを自動で二分して調べてくれます。100個のコミットでも7回の確認で原因のコミットに到達します。
空間方向の二分探索は、「どこで壊れているか」を二分で特定します。リクエストが通る経路の途中で止めて観測します。入口では正しいデータ、出口では壊れている。では中間は? と観測点を足して半分に絞ります。これは第1回(CS入門)で扱った階層モデルと直結します。どの階で壊れているかが分かれば、見るべき層が1つに決まります。
| 方向 | 分け方 | 使う道具 | 候補1,000個の絞り込み回数 |
|---|---|---|---|
| 時間 | いつから壊れたか | デプロイ履歴・git bisect | 約10回 |
| 空間 | どの層・どのモジュールか | ログの追加・トレース | 約10回 |
| 入力 | どのデータで壊れるか | データの分割投入 | 約10回 |
| 利用者 | 誰が使うと壊れるか | 属性別の発生率比較 | 約10回 |
入力方向の二分探索は特に強力です。1万行のデータで壊れるなら、前半5,000行と後半5,000行に分けて投入します。壊れるほうだけを残し、また半分に。これはデルタデバッギングと呼ばれる手法で、入力を自動的に最小化する研究として体系化されています。この手法の価値は、原因の入力が1行に絞られることです。1行に絞られれば、原因の特定はほぼ終わっています。
🔍 検証⑤:支持する証拠より、否定する証拠を探す
次に、調査を誤らせる最大の罠です。確証バイアスです。「こうではないか」と思った瞬間から、人はその説を支持する証拠ばかり集めます。これは心理学の古典的な実験で示されている現象です(仮説検証課題)。
調査でこれを避ける方法は1つです。「この仮説が間違っているとしたら、何が観測されるはずか」を先に書く。そしてそれを観測します。
たとえば「キャッシュが原因だ」と思ったら、「キャッシュを無効化したら症状が消えるはず」と書き、実際に無効化します。症状が消えなければ、その仮説は消えます。これを反証と呼びます。支持する証拠を10個集めるより、反証を1つ見つけるほうが、確実に前進します。
そして最も危険な状態はこれです。修正したら直った。しかし理由を説明できない。これは直っていません。原因が消えていない可能性があり、別のタイミングで再発します。だから調査の完了条件は「直った」ではなく、「この条件でこれが起きる、と説明できる」です。
🔍 検証⑥:原因は1つではない——3層で捉える
最後に、原因の捉え方です。調査の終盤で、ジュニアがよく詰まる質問があります。「では、根本的な原因は何ですか」。
実はこの問いには単一の答えがありません。原因は層をなしています。少なくとも3層に分けて考えると、議論が進みます。引き金(直接のきっかけ)、根本原因(その引き金が被害につながる構造)、寄与要因(被害を大きくした条件)です。
たとえば「設定を1つ間違えて本番が止まった」という障害なら、引き金は設定ミス、根本原因は「設定ミスが即座に本番全体へ反映される仕組み」、寄与要因は「レビューが省略される手順だった」「切り戻しに時間がかかる構成だった」。3層に分けると、対策が3つ出ます。そして重要なのは、対策の効果と実現性が層によって違うことです。「気をつける」は引き金への対策で、最も効果が低く、最も持続しません。
| 層 | 問い | 対策の例 | 持続性 |
|---|---|---|---|
| 引き金 | 何が直接のきっかけか | 気をつける・確認する・注意喚起 | 低い(人の注意力に依存) |
| 根本原因 | なぜそれが被害につながったか | 反映前に検証する・段階的に適用する | 高い(仕組みに組み込める) |
| 寄与要因 | 何が被害を大きくしたか | 切り戻しを速くする・影響範囲を狭める | 高い(影響を小さくできる) |
「なぜ」を繰り返す分析法が広く知られていますが、この手法には批判も存在します。批判の要点は、「なぜ」を1本の鎖として遡ると、最後は必ず「注意不足」に行き着くというものです(John Allspaw「The Infinite Hows」)。現実の障害は複数の要因が重なって起きます。だから「なぜ」を1本の鎖ではなく、複数の枝として書くほうが実態に合います。原因を1つに決めようとすると、対策も1つになり、次の障害を防げません。
結果:調査の手順表
ここまでの内容を、実行できる手順にします。
【前提】止血が終わっていること(第2回)
STEP 1|症状を1文で書く
「〈条件〉のとき、〈何が〉〈どうなる〉」
例:検索条件に「-」を含む語を入れると、結果が0件になる
STEP 2|再現を試みる
再現できた → STEP 4 へ(大幅に近道)
できない → STEP 3 へ
STEP 3|変わったものを5領域で確認する
コード → 設定 → 依存 → データ → 環境
該当があれば、その差分を起点に STEP 4 へ
STEP 4|仮説を3〜5個、反証可能な形で書く
悪い例:「メモリリークだろう」
良い例:「10回目のリクエストで使用量が単調増加する」
STEP 5|安い検査から順に1つずつ消す
二分探索(時間・空間・入力・利用者)
1回の検査で「消えたか残ったか」を必ず記録する
STEP 6|残った仮説を深く読む
ここで初めてコードを読み込む(候補が1つに絞られている)
STEP 7|原因を3層で書く
引き金/根本原因/寄与要因
STEP 8|再現テストを書く
先に「失敗するテスト」を書き、修正後に通す
STEP 9|説明できる形で記録する
「この条件でこれが起きる」と言えるまで終わらせない
この手順で最も効くのはSTEP 4とSTEP 5です。仮説を消せる形にして、安い検査から消す。この2つを守るだけで、調査時間は体感で半分以下になります。逆に、STEP 5を飛ばしてSTEP 6に直行するのが、最も時間を溶かすパターンです。
考察:調査の速さは「観測点の多さ」で決まる
ここまでを踏まえて、1つ踏み込んだ話をします。調査の速度は推理力ではなく、観測点の数に強く依存します。どれだけ賢くても、観測できない場所は推論できません。だから調査能力を上げる最も確実な方法は、賢くなることではなく、観測点を増やすことです。
そして観測点を増やす作業は、障害が起きていないときにしかできません。障害の最中にログを追加して再現を待つのは、火事の最中に煙感知器を付けるようなものです。つまり、調査能力とは、平時にどれだけ観測点を仕込んだかで決まります。第2回で「障害対応の速さは事前の準備で決まる」と書きましたが、調査も同じです。
もう1つの考察は、「分かった」の質についてです。原因を言い当てることと、原因を説明できることは違います。説明できるとは、「この条件を変えると症状が変わる」と言えて、実際にそうなることです。自分が原因を操作できる状態が、説明できている状態です。この基準を持つと、「たぶんこれだろう」で終わる事故がなくなります。
📌 注目ポイント
核心は4点です。第一に、再現の特定が調査の最大の近道であること。第二に、まず「変わったもの」を5領域(コード・設定・依存・データ・環境)で探すこと。第三に、仮説は反証可能な形で立て、二分探索で消すこと。第四に、原因は3層(引き金・根本原因・寄与要因)で捉え、対策を分けること。
特に第三点が実務の分かれ目です。「〜だろう」という仮説は、消せないので永遠に残ります。消せない仮説を抱えた調査は、いつまでも終わりません。「これを観測して、こうでなければこの仮説は消える」と言えるかどうかを、常に自問してください。
💡 活用事例:1行に絞るまで入力を削る——デルタデバッギング
原因の特定を劇的に速くする手法として、デルタデバッギングがあります。これは「失敗を引き起こす入力を、自動的に最小化する」手法として研究され、体系化されたものです。
考え方は単純です。大きな入力(設定ファイル、データ、テストケース)で失敗するなら、(1) 入力を半分に分割し、(2) それぞれ単独で失敗するか試し、(3) 失敗するほうだけを残す。これを繰り返すと、失敗を引き起こす最小の入力が得られます。これは二分探索の入力版であり、1万行の設定からたった1行の原因設定を、数十回の試行で抽出できます。
| 手法 | 何を二分するか | 得られるもの | 得意なケース |
|---|---|---|---|
| git bisect | 時間(コミット) | 原因となった変更 | 以前は動いていた不具合 |
| デルタデバッギング | 入力(設定・データ) | 原因となる最小の入力 | 巨大な設定・データで再現する不具合 |
| 観測点の追加 | 空間(処理の途中) | 壊れている層・モジュール | どの層で壊れるか分からない不具合 |
| 頻度の比較 | 条件(発生率) | 原因に近い条件 | たまにしか再現しない不具合 |
この4つの手法に共通するのは、「賢く推理する」のではなく「機械的に絞る」という姿勢です。推理は外れますが、絞り込みは外れません。そして、絞り込みの結果として原因が分かったとき、あなたはそれを説明できます。第1回で扱った「成果」の観点でも、「たぶんこれだろう」ではなく「この条件でこれが起きると特定した」と言えるほうが、はるかに価値があります。説明できる原因は、対策を生みます。
✅ 要点まとめ
- 調査は犯人探しではなく候補消し。消せる形の仮説を立てる
- 再現条件の特定が最大の近道。再現できたら8割終わっている
- まず変わったものを5領域(コード・設定・依存・データ・環境)で探す
- データの変化は緩やかで、境界を越えた日に壊れるため最も見落とされる
- 二分探索は時間・空間・入力・利用者の4方向で使える
- 支持する証拠より、否定する証拠を探す(確証バイアスを避ける)
- 原因は引き金・根本原因・寄与要因の3層。対策も3つに分かれる
- 調査の完了条件は「直った」ではなく「この条件でこれが起きる」と説明できること
🚀 取り込み方:明日から使う3段階
今日(5分でできること):直近で「分からなかった」不具合を1つ思い出し、症状を1文で書いてください。「〈条件〉のとき、〈何が〉〈どうなる〉」。1文にできないなら、症状がまだ整理されていないことが、当時分からなかった理由です。
今週(小さく試す):不具合調査をするとき、仮説を3つ書いてから手を動かしてください。そして「この検査で何が消えるか」を書いてから検査します。検査の結果は消えたか消えなかったかだけを記録します。1週間続けると、自分がどれだけ「消えない検査」に時間を使っていたかが見えます。
今月(業務に組み込む):チームに「調査メモの型」を提案します。症状・再現条件・試した検査・消えた仮説・残った仮説の5項目です。これを書く習慣があると、引き継ぎが可能になります。あなたが休んだ日に、他の人が続きから調査できます。調査は属人的になりやすい領域なので、属人化を解くだけで大きな成果になります。第1回の言葉で言えば、これは「知られていない」を解消する仕事です。
🔥 ハマりポイント:調査が長引く3つの型
その1:「コードを読めば分かるはず」と思いがちだが、実は読むほど分からなくなる
症状は、関係しそうなファイルを10個開いて、どれも「関係なさそう」と感じる。原因は候補が絞られていないのに読み始めていることです。読む行為は候補を消しません。対処は、読む前に観測することです。「どの層で壊れているか」をログかデバッガで確定させてから、その層だけを読みます。読む範囲は、絞り込みの結果として決まるべきものです。
その2:「たまたまだから」と思いがちだが、実は偶然は条件のヒント
症状は、再現しない不具合を「環境のせい」で片付ける。原因は頻度を測っていないことです。対処は、発生率を記録すること。「100回中1回」なのか「1000回中1回」なのかで、原因の候補が変わります。そして条件を変えて頻度を測る(時間帯・データ量・同時実行数)。頻度が変われば、条件と原因が近い。偶然に見えるものほど、測ると規則性が出ます。
その3:「直ったから完了」と思いがちだが、実は説明できない修正は再発する
症状は、修正したら症状が出なくなったので完了にする。数週間後に別の形で再発する。原因は原因が特定されていないことです。対処は、「なぜ直ったのか」を1文で書くことです。書けないなら、その修正は偶然効いただけです。そして偶然は再現します。「この条件でこれが起きる。だからこの修正で消える」——この2文が書けて初めて、調査は完了します。これは第6回(開発フロー入門のテスト回)で扱った「先に失敗するテストを書く」と直結する考え方です。
🔄 代替アプローチとの比較:調査の進め方の型
調査には複数の型があります。状況によって使い分けるものです。
| 型 | 手順 | 向いているケース | 弱い点 |
|---|---|---|---|
| 差分駆動 | 変わったものを探す | 以前は動いていた不具合 | 最初から壊れている場合に無力 |
| 二分探索型 | 候補を半分に絞る | 候補が多い・範囲が広い | 観測点がないと使えない |
| 仮説演繹型 | 仮説を立てて検査で消す | 原因の見当がつく場合 | 仮説が外れると遠回り |
| 観測強化型 | まず観測点を増やして待つ | 再現しない・原因不明 | 時間がかかる |
| 最小再現型 | 入力を削って最小化する | 巨大な入力で再現する | 入力が小さいと効果が薄い |
差分駆動が使えないケースも正直に書きます。新規開発中で「以前は動いていた」状態がない場合は、変わったものを探せません。この場合は仮説演繹型か最小再現型を使います。また再現しない障害では、観測強化型しか選択肢がありません。このときの正解は「今すぐ原因を特定する」ではなく、「観測点を増やして、次に起きたときに捕まえる」です。捕まえられない障害を追いかけるより、罠を仕掛けるほうが成果になります。
📅 今後の展望:調査は「自動で絞る」方向へ進んでいる
調査の技術は、この20年で人力の推理から機械的な絞り込みへ移ってきました。git bisect のように二分探索を自動化する道具が標準になり、失敗を引き起こす入力を自動で最小化する研究が実用化されています。観測の側でも、分散トレーシング(1つのリクエストが通った経路を丸ごと記録する技術)が一般化し、どの層で時間がかかり、どこで失敗したかを自動で可視化できるようになりました。
この流れの延長に、異常の候補を機械が提示する方向があります。ログやトレースから「この時間帯にこの変更があった」といった関連を自動で並べる仕組みは、すでに商用・OSSの両方で提供されています。ただし、機械が提示するのは候補までです。その候補を消す検査を設計するのは、依然として人間の仕事です。むしろ候補が増えるほど、「消し方の設計」の価値は上がります。
もう1つの流れは、調査の記録が組織の資産になる方向です。同じ症状の調査を繰り返さないために、調査メモを検索可能な形で残す文化が広がっています。ここであなたが書いた調査メモは、半年後の誰かを助けます。第1回で扱った「成果」の定義に照らせば、調査メモは典型的な成果です。作業としては地味で、効果は長期にわたって残ります。
まとめ
この記事では、原因調査の技術を扱いました。調査は犯人探しではなく候補消しです。再現条件の特定が最大の近道で、変わったものを5領域で探し、反証可能な仮説を二分探索で消し、原因を3層で捉えます。そして調査の完了条件は「直った」ではなく、「この条件でこれが起きると説明できる」ことです。
これを読んだあなたは、手を動かす前に仮説を3つ書き、消える検査から順に実行できるようになりました。次の第4回からは、テーマが変更の影響分析に移ります。1行の変更がどこまで届くか——調査で身につけた「切り分け」の技術は、変更前に「どこが壊れうるか」を予測する技術として、そのまま使えます。
参考文献
- Andreas Zeller『Why Programs Fail: A Guide to Systematic Debugging』(2009) — 体系的なデバッグ手法の教科書。 https://www.whyprogramsfail.com/
- Andreas Zeller & Ralf Hildebrandt「Simplifying and Isolating Failure-Inducing Input」(IEEE TSE, 2002) — デルタデバッギングの原論文。
- git 公式ドキュメント「git-bisect」 — 二分探索による原因コミット特定の手順。 https://git-scm.com/docs/git-bisect
- John Allspaw「The Infinite Hows (or, the Dangers of the Five Whys)」(2014) — 「なぜ」を1本の鎖で遡る手法への批判。
- Google『Site Reliability Engineering』 — デバッグと観測に関する章。 https://sre.google/
- Brendan Gregg『Systems Performance』 — USE法など、システム側から原因を絞る手法。
- Karl Popper『Conjectures and Refutations』(1963) — 反証可能性という考え方の原典。
- Peter C. Wason(1960)「On the failure to eliminate hypotheses in a conceptual task」 — 確証バイアスを示した古典的実験。
- Amos Tversky & Daniel Kahneman(1974)「Judgment under Uncertainty」 — 利用可能性ヒューリスティックとアンカリング。
- Intel Pentium FDIV 不具合に関する公開資料(1994) — 再現条件の特定が難しい不具合の代表的事例。 https://www.intel.com/
- NASA/JPL ほか宇宙機関のシングルイベントアップセット(SEU)関連資料 — 放射線によるビット反転と、再現しない障害への備え。 https://www.nasa.gov/
- Brian W. Kernighan & Rob Pike『The Practice of Programming』(1999) — デバッグの章における実践的指針。
- Kent Beck『Test-Driven Development: By Example』(2002) — 失敗するテストを先に書くという考え方。
- Julia Evans『Debugging: The 9 Indispensable Rules』(zine) — 実務で使える調査の原則集。 https://jvns.ca/
- Erik Hollnagel, David D. Woods, Nancy Leveson『Resilience Engineering』 — 複雑系における事故の捉え方。
- 日本情報処理安全確保支援士会/IPA『脆弱性対策情報データベース(JVN iPedia)』 — 既知の不具合パターンの参照先。 https://jvndb.jvn.jp/
Rui Software