12
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

バグの原因を効率的に特定する方法

Posted at

最近バグ調査をすることが多く、手元にバグ調査をするためのメモが溜まってきたので公開します。

なお、これは「ハマった」状態になったときに使うためのメモです。サクッと調べてすぐに解決できるならそれが一番です。

ただ、ハマったときには視野狭窄がおきるので、堂々巡りしたり、1つの場所にこだわりすぎて、結局解決が長引いてしまうことがあります。また、手当たり次第に調べるのも非効率です。効率的に調べるには戦略が必要です。

このメモではその戦略を示したいともいます。

1つ1つの手順は必ずしも守る必要はなく、過去の経験からあたりが付くならいきなりそこを調べても良いです。ただ、頭の中では戦略を意識し、いつでも戻れるようにしておくと、ハマったときには強い味方になります。

バグの原因を探索問題と捉える

バグ調査を、原因をMECEに分類した探索木と捉えるところから始まります。

探索木にするためには、調査の過程をツリー状にメモを書き出していきます。こうすれば自然とMECEを意識するようになります。

最初から全部を書き出す必要はありませんが、プログラム、データ、環境、構成のように原因の大分類だけでも洗い出しておくと、思い込みの防止に繋がります。

また、探索問題なので、深さ優先なのか、幅優先なのか、ヒューリスティクスを用いるのか、トップダウンなのかボトムアップなのかなどを意識しながら進めることになります。

私はたいてい、ヒューリスティクスを用いて深さ優先で探した後、一定期間で解決しない場合は幅優先探索に切り替えるようにしています。

Notionなどのツールを使えばコードや画像も貼り付けられるし、履歴がそのまま残るので、後日別のバグを調査するときにも役立ちます。

調査の手順は、1.バグの再現⇒2.発生するコードの特定⇒3.原因調査の順に進める

バグの原因調査の出発点は、まずバグを再現することです。バグに再現性がない場合、どの条件で発生するのかわかりません。

次に、バグが発生するコードを特定します。

例えばこのような感じです。

擬似コード
var x = 0;
print(x); // 👈ここで発生していた!
var y = x + 1;

発生する場所=原因の場所とは限りません。ですが、この作業によって調査の出発点を得ることができます。

なお、サーバーの起動時に発生する場合など、必ずしもコードを特定できない場合もあると思います。その場合は「発生する瞬間」を特定するようにします。

発生場所がわかったら原因調査に入ります。

バグの発生箇所は二分探索で効率的に調べることができる

バグが発生する状況/しない状況は、二分探索を使って効率的に調べることができます。

例えば、ある関数の一部をコメントアウトしてバグが発生しなくなるかどうかを調べるとしたら、1つ1つの処理をコメントアウトするのではなく、全体を半分ずつコメントアウトしていきます。

(例)
1.関数の後半半分をコメントアウトする

擬似コード
func test() {
  処理1
  処理2
//  処理3
//  処理4
}

2.後半のうち、さらに後半だけをコメントアウトする

擬似コード
func test() {
  処理1
  処理2
  処理3
//  処理4
}

このような絞り込み方をすることで効率的に発生箇所を調べることができます。

バグが発生する状況/しない状況を用意し、その差を縮めていくように調査する

バグが発生する状況と、バグの発生しない状況を用意します。その差を縮めていくことで、最後にはバグの原因を突き止めることができます。

(例)
・ このマシンでは起きるが、このマシンでは起きない
・ このコードを実行すると起きるが、このコードをコメントアウトすると起きない
・ この画面では起きるが、この画面では起きない

用意するのが難しい場合は、ほとんど何も処理のないシンプルなプログラムを作ることもあります。

例えば、新しくプロジェクト作成して何も処理を入れていないアプリなどです。そこにバグが発生するプログラムが利用しているライブラリを加えたらどうなるかなど、少しずつバグが発生する状況に近づけていきます。

また、MECEに分類した探索木を使って二分探索で調べることもできます。

例えば、最初の図であれば次のように2つに分けます。

  1. プログラムと構成
  2. 環境とデータ

まず環境とデータを以前のバージョンに戻し、バグが再現したら今度は環境だけを戻す、といった方法で探索することができます。

この考え方を応用すれば、実験計画法なども活用できそうですが、私は試したことはありません。

それでも発生しない状況を作るのが難しい/手間がかかりすぎる場合もあると思います。その場合は頭の中で「こういう状況なら必ずバグがでない」という想定だけは持っておきます。

バグが発生する状況/しない状況が用意できたら、両者を近づけていき、差が出るポイントを見つけます。

ログや内部情報のスナップショットを取得する。取得できない場合は取得できるようにする

ログや内部情報のスナップショット(デバッグ出力など)は、バグ調査全体を通じて利用しますが、バグの発生する状況/しない状況の差を縮めていくと、どうしても差が縮まないポイントがでてきます。

例えば、マシンAとマシンBでは全く同じコード、データ、環境なのに、マシンAだけバグが発生する、といった状況です。

その時は、より詳細な情報を出力できるようにします。

例えば、発生する状況/しない状況それぞれについて

  1. 問題が発生する箇所の前後で、問題に関係する変数の値をログに残し、差を確認する
  2. 差異がない場合は、その変数の元となるAPIのレスポンス値をログに残し、差を確認する
  3. 差異がない場合は、APIのレスポンスの元になったデータベースの値をログに残し、差を確認する

(もちろんログではなくデバッガで値を確認しても良いです)

という流れで進めます。

どれだけ調べても一見差が無いように見える場合もありますが、思い込みの可能性もあるので、diffをとると良いでしょう。

それでも解決しない場合に備える

それでも解決しないことはあります。

その場合は根本的な原因調査を諦めて回避策を取る、などのプロジェクト的な判断を伴うことがあります。つまり、バグ調査というタスクの上位のタスクに戻ることになります。

バグ調査にハマってしまい、上位のタスクに戻るまでに時間がかかりすぎることも問題です。

これを避けるために、1つ1つの調査に時間的な区切りを入れます。

私はポモドーロ・テクニックを用いて、25分のターンで区切って作業をしています。

次のようなステップで進めています。

1.ターンの前にターンのゴールを明確にする(例:環境の問題か否かを見極める)
2.作業を実施する
3.ターンが終わったら、振り返り、次に何をするのかを考える

ときには、何ターンも同じ場所を調査することもありますが、ターンで区切っておくと「いくらなんでも時間がかかり過ぎだな」という反省をするきっかけを得ることができます。

12
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?