バグゼロを実現した話とその後の顛末

こんにちは、アプリケーション基盤チームの青木(@a_o_k_i_n_g)です。好きなメソッドは emptyIfNull です。

僕は、自社クラウドである cybozu.com のミドルウェアを開発するチームで働いています。具体的には、検索サービスやファイルサーバー、非同期処理用ワーカー、セッションマネージャーなどなどを提供しています。

僕がこのチームに来たのは数年前ですが、当時はバグの多いプロダクトでした。今はすべての既知のバグを直し、残存不具合件数が 0 件、つまりバグゼロな状態になりました。また、バグゼロを実現してから 2 年ほど経過していますが今もその品質を保っています。今回はこのバグゼロを実現した方法と、その後の顛末について記そうと思います。

以前のコード

数年前に提供されていたこのミドルウェア群は、はっきり言って、バグの塊のようなプロダクトでした。

当時のコードは保守性とは程遠いものでした。カプセル化はされておらず、if 文や for 文のネストは幾重にも重なり、サーバーの処理はサーブレット内にベタ書きされ、使われていない変数やメソッドが多数存在していました。不適切な名付け、直感に反する継承をふんだんに用いてマルチスレッドなプログラムが書かれ、デバッグには大変な時間を要しました。とあるセッションマネージャークラスはあまりに複雑化し「改行すら入れたくない」という声まで上がるほどでした。

仕様の面でも優れてない面がありました。あるサムネイル作成機能は縦横無尽に仕様が拡張され、全体像を誰も把握できなくなっていました。僕が担当になって数年後、仕様に存在しない API が発掘されたこともありました。

これらのミドルウェア群が引き起こす不具合でサービスが停止、稼働率に影響を与えることもあるという状況でした。緊急改修してリリースすることがしばしばあり、プログラマのみならず品質保証部隊や運用部隊も疲弊感が高まっていたように思います。

バグゼロに向けて

バグを減らす事は簡単なように見えて色々と障壁があります。すべての既知の不具合を直すにはそれらの壁を乗り越えなければなりません。何より一番大きい壁が工数の問題です。特に試験的な工数の壁が大きく立ちはだかり、単純な不具合の改修が後回しになるのはもはや日常茶飯事と言えるでしょう。実際、僕がバグゼロに向けて直そうとしている時には

「これはとりあえず放置しておこう」

「一旦後に回そう」

「現状はとりあえずこのままで」

という声を何度も聞きました。もちろんこれも一理あります。影響範囲が広ければその分試験もしなければならないからです。だから無闇矢鱈にバグ改修をするわけにはいきません。それでも僕は、バグ含みのコードで運用されるような状態に我慢できませんでした。

そこで僕は下記のいくつかの手段を取って、バグを減らしていきました。

  1. テストコードを書く
  2. ログを監視
  3. リリースサイクルを短くする
  4. 古いコードを破棄する
  5. バグ改修の際に周辺のコードを綺麗にする

テストコードを書く

僕がこのチームの担当になってからまずやったことは、テストコードをひたすらに書くことでした。毎日 8 時間以上、ただひたすらに、です。このテストを書き続ける作業を 2〜3 ヶ月は続けました。あくまで目安ではありますが、カバレッジは 90% 弱程度まで達しており、はた目にも「ほとんどのコードを通っている」と言えるような状況になりました。

これだけテストが揃ってくると挙動の変更に気づきやすくなります。空文字を返していたはずが null を返すようになった、というような些細な変化にも気づけるようになり、品質の向上に貢献しました。同時に「テストコードを書く」という今となっては当たり前の文化が形成されました。

ログを監視

次に僕が行ったのはログの解析です。日々膨大な量が書きだされるログはトラブルが無い限りスルーしがちですが、これを毎日監視し、未知の例外や想定していないログメッセージが出たらすぐ気づけるような仕組みを構築しました。このログ解析スクリプトは、ログを ERROR や WARN, Exception 等で grep し、既知のログを除外していくことで最後には未知のログが残るという至極単純なものです。とは言えこのログ解析スクリプトは極めて有用に働き、累計では相当数の不具合を検出できました。

ログを監視すると、どういったログが必要でどういったログが必要無いのかが見えてきます。その知見をコードに取り込み、不具合を発見しやすく解析しやすいログを吐き出すよう日々改善していきました。

リリースサイクルを短くする

それからリリースタイミングを増やしました。それまでは 1 年に 4 回程度のリリースでしたが、毎月リリースするようにし、バグを直した版のアーカイブをできるだけ早く適用出来るようにしました。これは直接的にはバグゼロには関係無いですが、バグを発見・改修・リリースのサイクルが早まったことでモチベーションが高まり、結果的にはバグゼロへの道に進めたように思います。

古いコードを破棄する

ソースコードには古い実装が何かと残りがちです。そういったゴミコードは一つ一つは些細なものかもしれませんが、だんだん蓄積してくると品質に影響が出てきます。例えば障害対応時にコードを眺めていたらそれは今は使われていないコードだった、というような事もありました。また、プログラマは「いつか使うだろう」という機能をつい入れ込みがちですが、いつか使うと思って実装したコードも結局使われなかった事も何度かありました。そういった使われていないゴミコードは早急に削除すべきでしょう。

個人的には、行数を削減したプルリクエストはもっと評価されて良いように思います。不要なコードを削除したり既存の処理をより簡潔に記述できたなら、それは今後のメンテナンス工数の削減という意味では大きな意味を持っているのでは無いでしょうか。

バグ改修の際に周辺のコードを綺麗にする

バグ改修の仕方には 2 つの方法があります。ひとつは、修正するコード量を最小にしてバグを直す方法。プルリクの diff が小さくなり、レビュー時も低コストで確認出来ます。もうひとつは、原因となった箇所を含めたある程度の範囲を修正し、バグを直すと共に保守性を向上させるような修正の仕方です。

僕の経験上、前者の方法では結局バグゼロになることは無いと判断し、できるだけ後者の方法で修正するよう心がけました。もちろん試験工数に影響の出ない範囲で、です。バグの直し方はケースバイケースではありますが、少なくとも「日々コードの品質を向上させていく」という心がけが無いとバグゼロ実現は困難でしょう。

これらを継続して続けた結果、残存不具合件数がついに 0 になりました。

バグゼロ実現後

そうして、僕のチームのミドルウェア群はバグゼロを達成しました。それからおよそ 2 年ほど経過しましたが、今でも残存不具合件数は 0 件を保っています。

バグゼロを実現して良かったことがたくさんあります。

まず第一に、当然ではありますが、ユーザーに提供するサービスの品質が向上しました。

次に、何か新規に不具合を見つけても、ほとんどの場合は現在開発中のバージョンに取り込めるようになりました。以前は工数の関係で次のバージョンで修正、リリースするのは半年先、というようなこともしばしばありましたが、そういうケースはかなり減りました。

ある程度工数に余裕が出来てきたので、機能改善タスクを当たり前のように取り込めるようになりました。以前は、不具合を直さなければならないのでその分工数が取られてしまい、一度のリリースで行える機能改善には限界がありました。

それから、これは品質保証部やプロダクトマネージャーからの意見ですが、管理コストが減って計画が立てやすくなったという意見も得られました。例えば残存不具合があると不具合再現確認をする必要があるのですが、それに割くコストをまるっと削減できます。

デメリットはちょっと思いつきません。バグゼロを実現する過程では品質保証部の負荷が高まる事もありましたが、それを乗り越えた今では特にデメリットらしいものは無いと言って良いでしょう。

まとめ

プログラマの皆様なら共感してくれるかと思いますが、コードをリファクタしたり、不要になったコードを削除して保守性を高めることは大きな価値を持っています。新機能を提供する価値にも匹敵するかも知れません。でもプログラマ以外に響くのはたいてい新機能であり、その結果、いつも保守性はないがしろにされがちです。

今回のケースでは、保守性を高めることは結果的にユーザーの幸福につながる、という点を示せたように思います。

バグゼロであることは、何より気持ちいいです。自分の担当する製品が日々安定して稼働し、不具合を見つけても後回しにならずにすぐ直せる環境にいることは、プログラマのひとつの喜びと言えるのではないでしょうか。