デバッグって何をすればいいのか?を考える


はじめに

Wikipediaより

デバッグ(debug)とは、コンピュータプログラムや電気機器中のバグ・欠陥を発見および修正し、動作を仕様通りのものとするための作業である。

開発をする際に一番苦労することは何か?一番感じるのは、デバッグ、特にバグ調査なのかなと感じています。
結構経験の長い短い関係なく、一つのやり方しか知らずに手詰まりになる方のことをよく見聞きします。

なので、今回はデバッグ(主に調査部分)について、何をすればいいのかについて言語化していこうと思います。
どんなツールを使うか?みたいなテクニックの話よりも、どういった考え方でアプローチしていくか?といった広い観点でデバッグについて記載していますので、ご了承ください。

デバッグのその前に

なんのために調査をするのか?

デバッグをする目的は、今発生しているバグの原因を特定して修正すること。そのために自分がどの立場でデバッグしているのかが大事だと思います。
例えば、nginxをプロキシサーバーとして構築した自分のwebシステムにバグがあったとして、この原因調査でnginxがエラーログを出してることが分かったとします。この時、果たしてバグの調査は完了でしょうか?

場合によってはそうではないでしょう。このエラー原因はnginx側の設定ファイルが正しくないのかもしれない。ルーティングを組んでいるネットワークが間違っていて繋がらないのかもしれない。もしくはルーティング先のサービスからエラーが返ってきてるのかもしれない。いずれにせよもう一歩踏み込んだ原因を特定しないと、修正に結びつかないわけです。

デバッグ時のバグ調査は、バグを修正するために行っている という基本を忘れないようにする必要があります。

最初にやること:再現手順を明確に

何はともあれバグが再現する手順を確立することが必要です。これが調査のお供になります。
また、この再現手順は同時に作業完了時の確認手段にもなります。デバッグ完了後には、必ず再現手順で現象が再現しないことを確認した方がいいです。

開発者は、自分の実装部分をよく知っています。そのため、バグの再現を行う際にもロジックを意識して効率のいい再現テストコードを書き、デバッグに没頭することがあります。
このこと自体は素晴らしいのですが、実は今修正しているバグ以外にも潜在バグがあり、再現テストコードでは潜在バグの方は再現しない。ということが稀にあります。
なので、デバッグが終わった後は元々の再現手順でバグが修正出来ていることを確認する方がベターです。

デバッグ時のアプローチ

思いつくものを上げていきます。

アプローチその1. バグの発生個所からとにかく追う

ログをちゃんと出力しているシステムなら、まずはこういった着手方法になると思います。
JSならデベロッパーツールを開いてconsoleのログを、Apacheのようなサーバーならサーバーログを、とにかく問題が発生している箇所を見て、そこに関するコードを見て、問題の根本から追っていくやり方。
簡単なシステムや、中身をよく知ったシステムであったり、現象の深い部分が見えているバグであれば、このやり方だけで原因の特定が可能だと思います。
とりあえず出力されたログをそのままググるなんてこともよくやる手です。原因の元を探すやり方なので、迷子になりにくいです。

注意. ものによっては時間がかかる

これは問題の発生している箇所から上がっていくようなアプローチなので、例えば見えているのがOSSのログだけ等だった場合は、その箇所を見るのも一苦労で時間がかかります。

アプローチその2. インターフェースで切り分ける

こちらは問題が発生しているルートを切り分けるやり方になります。

少しシステムが大きくなると、複数の要素から構成されると思います。例えばフロントエンドとバックエンド、クラスによる階層分け等
そういった複数の要素には大抵インターフェースとなるものが存在します。フロントエンドとバックエンドならWeb APIのHTTP request/response、クラスならpublic methodといった形で。
こういった切り口に対してどう動作しているのかを見るのがこの方法になります。特にネットワーク越しがイメージしやすいですかね

Webサービスならhttp requestとresponseのどこで失敗してるのか。例えばhttp responseが来ないなら、サーバー側でrequestは受けてるのか。受けてるのならその中の‥
と、どこで想定外のことが起こってるのかを切り分けていきます。
これはシステムとして細分されたものだけの話ではなく、モノリシックなシステムにも当てはまります。クラスや関数レベルでシーケンスを書いて切り分けることが出来ます。

注意. 切り分けしながら掘り下げて迷子にならないように

注意する必要があるのが、切り分けの為に原因を掘り下げてる最中、今どこの何を追っているのかを見失わないようにすることです。そんなことあるわけ無いと思うかもしれませんが、システムが複雑になると迷子になることはよくあります。
なので、今調べているものがシステム全体での現在地がどこか見失わないようシーケンスがわかるものを残しておき可視化しておくことが有効です。
後目的を見失わない強い心を持つこと

アプローチその3. 正常動作しているものと比較する

こちらは近い条件で正常動作しているものとそうでないものを比較して、その差分から原因を推測していく方法です。どう比較条件を設定するかは結構経験がものをいうところがあるかもしれません。
環境・修正前後・既製品との比較等、利用できるケースが多く、特に中々再現しない問題や、中身が触れないものを見るときに効果を発揮します。

注意その1. 推測ばかり繰り返して前に進まないことのないように

推測というのは、挙げだすときりがなかったりします。このアプローチの場合、推測が当たれば問題解決ですが、当たらなければ先に進めないという状態になることがあります。

なので、推測時にコードを読んだりログを仕込んで実証したりして、推測の根拠を得ていく必要があります。

注意その2. 潜在バグを見過ごす可能性がある

この調査方法は、比較対象が正しいという前提で行うことになります。この比較対象にコーディング修正前のものを設定していたりすると、実はタイミング依存で発生しなかったバグというケースを見過ごしてしまうことがあります。
対象を正とみて推測する立場も大事ですが、それだけではなく比較対象は一旦忘れて今起きている現象に集中して調査することも大事です。

バグ調査の時に避けたいのは一つの見方しかせずに前に進めない状態になることです。
それが過去は上手くいっていた方法であったとしても、その時の成功体験にとらわれ過ぎず今どうするべきかの視点を無くさないようにすることが大事です。

アプローチその4. とにかく思いついたことを試してみる

何も手がない時の最終手段です。とにかく話を聞いて、仮説を立てて試してみる。
やるなら意思を持って作業することが大事です。

その他のアプローチ

2020/1/25追記
@aminevsky さんという方のツイートがとてもよかったので、紹介させていただきます。

ブレークポイントを設定する

切り口となる部分を決めて、デバッガ―の機能でブレークポイントを設定したり、print文を挿入したりしてどこまで行ってどうなってるかを見る方法。滅茶苦茶効果的ですよね。
バグ調査だけじゃなくて構造の理解にも使える手段で、あー、これやるとこのクラスが使われるのねーって理解が進んだり。
どこを切り口にすればいいかわからない?そんな時は思いつく限りの場所にぶち込むんだ!

一方でブレークポイントを貼りすぎたりprintをし過ぎると、その分処理タイミングがずれる点を頭の片隅に置いておくといいと思います。マルチスレッドだと現象が変わったりすることがあるので。

状況をアウトプットする

これもめっちゃ効果的ですよね。話して相談できるという以上に、アウトプットすることで思考が整理できるのが大きいですね。
ラバーダッキングってやつです。

このアプローチは、いただいたコメントの

結果として関係ないことが関係ないかどうかを調べ続けた結果迷子になるのはたまにやってしまいますね。。。

という状況の改善にも役立つと思います。

色々なアプローチを組み合わせ、バグ修正の為の原因特定に向かう

このように、デバッグのアプローチは色々な方法があると思います。ここに挙げたもの以外にもあるでしょう。
ただ、これらは「この方法が最強!他はオワコン!」といったものでは無いということだけははっきり言えます。
ひとつのアプローチに詰まったら別のアプローチを試して、また詰まったら視点を変えてと、アプローチを組み合わせて調査を前に進め、原因を突き止めるまで思考を巡らせて行動をすることで問題解決に努めましょう。

困るデバッグアプローチ

一つにこだわり、頑固に

他のやり方を知らないからなのか、そのやり方が好きなのか、頑なに同じことを繰り返す。無限ループって怖くね?
例えば、私が出会ったのは比較しかしない人。問題が起きたらログも見ずにとりあえず動いていたものと比較。なんとなく違うように見える部分をなんとなく合わせてみる。ダメならまた比較。ほんとにこれを延々と繰り返す。
比較するアプローチの場合は、中々正解にたどり着かない根が深い問題だと前進しないことがあるので、いくら経ってもバグ調査が進まないなんてことも。このタイプの方、経験年数関係なく案外います。

関係ないことまで調べ、迷子に

とにかく原因のログを追うのに熱中。今追っているバグだけじゃなく、その周囲も気になっちゃう。
気になるまではいいんですが、そのまま「え、このログが出るのもおかしいよね。これも追わないと」と調査の手を広げて、その結果本来追うべき先を見失ってしまう。たまにいます。

冷静に問題を切り分けて別バグとして扱える人は素敵なデバッガ―

目的に向かうことが大事

先程挙げた例は、どちらも本来の目的であるバグの原因調査に近づいているのかわからないことが問題。目的を見失わないように、目的に向かったアプローチをとることが重要です。
こういったことは、頭ではわかるのですがデバッグに頭を使い過ぎると見失いがちなので、「元々の問題はなんだっけ?どうしたいんだっけ?」と立ち返ることが大事です。

おまけ:デバッグのお供

ここではデバッグのお供になるものを紹介します。具体的なツールというよりももう少し一般的な紹介で、初心者向けの記述となります。

デバッガ

デバッグを行う為の支援ツールのことで、色々なデバッガツールが世の中にはあります。

HTML/CSS/JavaScriptのdeveloper tool
Linux C/C++のgdb/valgrind
WindowsのVisualStudio
AndroidのAndroid Studio

「言語 デバッガ」といったキーワードでググれば、便利なツールが出てくると思います。

切り分けのお供

wireshark

パケットキャプチャツールといって、ネットワーク通信上でやり取りされているパケットと呼ばれるデータの確認が出来ます。
その為、例えば画面からWebサーバーへのHTTP requestやサーバーからのresponseが送信されているのかといった、通信状況の確認が出来ます。

Wiresharkを使った通信監視(前編)――基本的な使い方とパケット解析の図を参照

このツールはネットワーク上のパケットを見ることが出来るので、HTTPだけではなくTCP/UDP通信を確認することが出来ます。可視化オプションもあります。
また、Linuxサーバーなら一旦tcpdumpというコマンドでパケットをキャプチャしたファイルを保存して、そのファイルをwiresharkで確認したりもします。

bash
tcpdump -s 0 -w filename

ブラウザのdeveloper tool

Networkタブを開けばHTTPレベルでのやり取りを見ることが出来ます。こちらもhttp限定ですが、通信状況を見ることが出来ます。速度も見れるので楽しいです。

ノートやホワイトボード && シーケンス

ツールじゃないけど。個人的にはとりあえずシーケンスを書いて切り分けることが多いです。ごちゃごちゃした話をまとめるときにも便利

ノート型のホワイトボードなんてのもあります。

比較のお供

Git等のversion管理ツール

手元で再現できる場合、再現するコードとしないコードを比較することはあると思います。
例えば修正を入れて動かなくなったら、Gitを使って現象再現するコードとしないコードで履歴を見たり、実際にそれぞれのブランチで動作を見たりして比較。
特にまだ慣れていない環境の場合は効果的だと思います。

Jenkins等のCIツール

継続的にテストが実施されている環境があるなら、その実行ログがそのまま比較対象となったりもします。
昨日時点のビルドではテストが通っているので、今日以降のコミットに当たりを付けて調査するといったアプローチが可能になります。

最後に

今回はデバッグについて記載しましたが、目的に向かい、一方向にとらわれずにアプローチを続けるというのはバグだけに限った話ではないです。
何が原因なんだろうと考えたり、問題を切り分けて絞っていったり、うまく行っているものと比較したりというアプローチは、チームやプロジェクトに対する問題への解決に向かう際のアプローチにもなると思います。
デバッグ力を鍛えて、問題解決能力を向上させていきましょう!

その他

図の素材: いらすとや