Java 8 に辿り着くまでに起きた 不思議なこと

Indeed では Java 7 から Java 8 へ移行しています。 (現段階では、移行は完了しております。)段階にわけて移行しており、まず Java 7でコンパイルされたバイナリを JRE 8 (Java 8 の実行環境)で実行するところから始めました。一度に全てを切り替えてしまうのではなく、いくつかのカナリアテストアプリを使用して、どんな問題が見つかりそうか様子を見ました。


私たちは、求人検索のウェブアプリを一つ目のカナリアテストとして選びました。昨年の 5 月に、私たちは本番環境にある 1 件の求人検索サーバーを JRE 8 に切り替えました。何も問題なく、 QA 環境で JRE 8 を数週間使用していましたが、本番環境をJRE 8 に切り替えたところ、システム負荷は 5 (通常のレベル) から 20 (異常なレベル)まであがりました。進める前に、まず修正が必要なくらいはっきりと増えていたのです。この問題の解決は、変わった方法を必要としました。

Datadog 内の本番環境の指標

まず、 Datadog を確認するところから始めました。 Datadog は、様々な指標やイベント・コリレーション(event correlation)をリアルタイムで監視するために、私たちが使用しているクラウドベースの外部サービスです。以下の Datadog のグラフは、 Indeed のあるデータセンターにおける、求人検索サーバーのホストごとにかかるシステム負荷を示しています。外れ値(青い線)は JRE8 を実行しているサーバーです。

figure-1-system-load-with-64m-codecache

図1 あるデータセンター内における求人検索サーバーのシステム負荷

こちらは、ホストごとにかかる応答時間の中央値を示しています。これも、 JRE8 を実行するサーバーが外れ値になっています。

figure-2-median-response-time-64m-codecache

図2 ホストごとにかかる応答時間の中央値

まだ JRE7 を使用している求人検索サーバーは、通常のシステム負荷と、通常の応答時間の中央値を示したので、問題は、 JRE8 のサーバー特有のものでした。

Java の処理が、ガベージコレクション(GC)のせいで、遅くなるというのは、珍しいことではありません。 そこでGC の指標を Datadog に送ってみましたが、 JRE8 のインスタンスのための GC のグラフは普通に見えました。

本番環境のログファイル

次に、 Web アプリケーションのログファイルを確認しました。 JRE 8 を使用した 4 時間に発生した全ログメッセージと、その 24 時間前から発生したログメッセージを取得しました。手作業で読むには量が多すぎたので、代わりにエラーの分布の仕方の違いに集中しました。各エラーに関連する Java クラスの名前を抽出し、異なる時点でのヒストグラムを生成しました。

ヒストグラムは、 JRE 8 を使用していた間に memcached のクライアント側のタイムアウトがわずかな増加を見せた事や、もうメンテナンスされていないオープンソースの memcached のクライアントライブラリが自社のアプリケーションで使用されている事を示しました。しかし、Google 検索では、そのライブラリに関して、 JRE 8 特有の問題があるとは出てきませんでした。ソースコードも見てみましたが、問題はありませんでした。タイムアウトの増加は、どうやら単にシステム負荷の増えたことの副作用らしい、と判断しました。

QA 環境で問題を再現

本番環境のエビデンスが、問題をはっきりと示してくれないので、次のステップはQA 環境でそれを再現してみせることでした。先ほど触れたように、 Web アプリは QA 環境の JRE 8 では、正常に実行されていたからです。パフォーマンスの問題は、システムに充分な負荷が与えられるまで姿を現さないかも知れないですし、 QA のサーバーも本番環境のサーバーほど、トラフィック量は多くないので、これは別に驚くようなことではありませんでした。通常の QA レベル以上にトラフィックを増やしたら、同じような症状が見つかるのでは、と期待しました。

Imhotep という Indeed 独自のシステムにクエリをかけて、最も典型的な受信リクエストの比率を見つけました。そして、 JMeter を使用してこういったトラフィックの比率をシミュレーションし、システム負荷を監視しながら JRE 7 と JRE 8 のインスタンスの各自で、二回実行しました。両方のケースにおいて、 Web アプリは、毎秒 50 件程度のリクエストを処理していました。システム負荷も、どちらも大体同じくらいでした。

毎秒 50 件のリクエストというトラフィックの負荷は、 QA 環境で通常シミュレーションするよりも高いですが、本番で受け取る量に比べるとまだまだです。この問題に取り組んでいた頃、本番環境の求人検索のインスタンスは各自、毎秒およそ 350 件のリクエストを処理していました。 QA で問題を再現するためには、このトラフィックの量にもっと近づける必要がありました。

ボトルネックとしての JMeter の除外

何故、アプリケーションの処理は毎秒50件のリクエストに抑えられていたのでしょう? QA 環境のせいにする前に、ボトルネックとしての JMeter を取り除く必要がありました。2番目のサーバーに JMeter をインストールして、 1 番目の JMeter インスタンスと同じように設定し、それから両方を同時に実行しました。もしこの 2 つのインスタンスが毎秒に 50 件以上処理できるようなら、 JMeter がボトルネックになっているのが分かります。その場合には、次のステップとして、 Web アプリが飽和するか、システム負荷が 20 に上がるまで、もっと JMeter のインスタンスを追加するはずでした。

ところが、 JMeter はボトルネックではありませんでした。両方のインスタンスを同時に実行してみたところ、まとめたスループットは、毎秒 50 件のリクエストのままでした。

遅いサービスの削除

求人検索のアプリは 22 個のサービスに依存しています。それらの一部が遅くなると、 Web アプリも遅くなってしまいます。 QA 環境内のとあるサービスは、本番環境内にある対応したサービスより遅いかも知れません。パフォーマンスを分析するために依存したアプリを隔離する際、私たちは、時として遅いサービスをダミーの速いバージョンに置き換えます。こうしたことから、私たちは Indeed のアプリケーションが最もよく使う 5 件のサービスのダミーバージョンを作ることに決めたのです。

新しいダミーのサービスを使用して、 JMeter は Web アプリを、毎秒あたり 100 件のリクエストのペースで動かしました。 JRE 8 は JRE 7 に比べて 15% 程度遅くなりましたが、システム負荷はそこまで高くありませんでしたし、 JRE 7 と JRE 8 の差はそれぞれの実行で異なりました。しかし、数字がまだ不揃いだったので、両方の設定のパフォーマンスを、 YourKit を使用して分析しました。

YourKit

YourKit  は、 Java プロセスにアタッチして、実行時の統計を収集し分析する Java のプロファイラです。アタッチしたらすぐに、パフォーマンスのスナップショットを作成することが出来ます。これは、複数の時点での Java の各スレッドにおけるスタックトレースをまとめたものです。 YourKit は、パフォーマンス・スナップショットを複数の方法で可視化してくれますが、これには、どのメソッドが最も時間を費やしたか示すホットスポット・ビューも含まれています。 JRE 8 上の Web アプリは JRE 7 に比べて非常に沢山の CPU を使用していたので、これらに関連するホットスポット・ビューも異なっているはず、と私たちは予想しました。

しかし、両方のビューは、だいたい同じでした。おそらく 、QA 環境でシステムにかけた負荷が、まだ足りなかったのが原因でした。

パフォーマンス・プロファイルを本番環境に

ここで、 QA 環境内で問題を再現する試みにもっと時間を割いても良かったのですが、代わりに、本番環境で YourKit を使用することに決めました。JRE 7 と JRE 8 両方の、本番環境のパフォーマンス・スナップショットを取得しました。この二つの、唯一のはっきりとした違いは JRE 8 の設定は、より多くの CPU を使用しているように見えた点でした。事実、パフォーマンス・スナップショットの結果だけでは説明できないくらいに CPU を使用していました。それは、まるでアプリケーションが YourKit に計測できない何かをバックグラウンドで実行しており、ビジー状態になっているようでした。なので、私たちは、 perf コマンドを使用して、分析を試みることにしました。

perf コマンドの使用

Java のプロセスは、単なる Java コードの集まりというだけではなく、マルチスレッドの C++ からなる JVM でもあります。 Java のプロファイラは、 C++ のコードが何を行っているか測定できません。そのような測定を行うためには、 Linux perf コマンド のような異なる種類のプロファイラが必要になります。

perf コマンドは Linux のプロセスからコールスタックを記録することが出来ます。これは、プロセスのデバッグ・シンボルテーブルを使用して、呼び出しスタックのアドレスを仮想アドレスから関数やメソッドのシンボルの名前へマッピングしてくれます。JVM のバイナリは Java コードのデバッグ・シンボルテーブルを含んでいませんが、 Java のデバッグ・シンボルテーブル用に Java のプロセスをクエリにかける方法があります。

本番環境では、 30 秒間 10 ms ごとに全ての Web アプリのスレッドの呼び出しスタックをキャプチャするように perf を設定しました。これは、およそ 11,000 件のコールスタックを記録しました。そのデータを集約し、 Flame Graph(フレーム・グラフ)にしました (図3)。

Flame Graph では y 軸がスタックの深さを表します。一番上にあるボックスはいずれも、サンプルを取ったとき CPU 上にあったコードを表しています。その下のボックスは呼び出し元、というように続いていきます。図 3 は非常に高く伸びているのは、コールスタックが深いからです。 x 軸は、ただのアルファベット順です。ボックスの幅が広ければ広いほど、より多くのコールスタックが現れたようです。緑のボックスは Java コード、黄色は JVM 内の C++ コード、赤は JVM 内の C コードです。明度や彩度は、対比しやすいように選んでおり、特に意味はありません

figure-3-first-flame-graph

図 3 求人検索のコールスタックを表示する Flame Graph

図 4 は Flame Graph の最下部を示しています。

figure-4-zoomed-first-flame-graph

図 4 拡大した Flame Graph

CompileBroker::compiler_thread_loop (左側の黄色いエリアにあります)はコールスタックの 39% に出現します。これはバイトコードのコンパイラで、頻繁に使われるバイトコードを機械語命令に変換を行う JVM の一部です。 39% という値は多そうでしたし、 YourKit では測定できないバックグラウンドの何かを表していました。

なぜそんなに時間がかかっていたのでしょうか。バイトコードのコンパイラは、設定可能なサイズを持つ codecache とよばれるメモリの特定の領域に対し、機械語を書き込みます。 Codecache がいっぱいになった際何が起きるか設定できる JVM の設定がありますが、デフォルトでは JVM は古いエントリをフラッシュして空き容量を増やすように設定されています。 YourKit が示すには、私たちの codecache の空き容量がほぼ無くなっていたので、最大サイズを増やしてみる価値はありそうでした。

Codecache を倍の容量へ

私たちは、本番環境にある codecache のサイズを、 64MB から 128MB と二倍に増やし、 Webアプリを再起動し、週末のあいだ実行したままにしました。アプリケーションは、正常に動作し、システムの負荷も通常レベルに維持されていました。perf コマンドを実行し、結果を Flame Graph 化しました。( 図5 )

figure-5-zoomed-second-flame-graph-128m-codecache

図 5 codecache のサイズを二倍に増やした後の Flame Graph の拡大図

CompileBroker::compiler_thread_loop  のボックス(右)の幅は、ホットスポットのコンパイラが以前ほどのビジー状態になかったことを表しています。以前 40% だったコールスタックは 20% になりました。

Codecache の変動

128MB の codecache があっても、私たちはじきに問題にぶつかりました。
図 6 は何が起きたのかを表しています。平坦な青い線が codecache の最大サイズで、その下の紫の曲線は使用した codecache の量を示しています。午前 8 時に、容量の曲線は変動し始め、翌朝 3 時まで安定しませんでした。

figure-6-datadog-codecache-fluctuation

図6 変動を示す Datadog からの Codecache グラフ

コードキャッシュの揺れ動きは、応答時間を遅くさせました。図 7 は、このデータセンター内の全ての求人検索サーバーの応答時間の中央値を示しています。オレンジ色の外れ値が JRE 8 です。その他のものに比べて 50% 程度遅くなっていました。

figure-7-median-response-times-128m-codecache

図 7 求人検索の Webアプリの応答時間の中央値

Indeed Web アプリにとって適正なサイズ

新しい codecache の容量である 128MB は、それでもまだ Java 8 のデフォルト値の半分でしかありませんでした。パフォーマンスの問題を解決するために、私たちは再び容量を 2 倍の 256MB にしました。 Codecache の使用量がこの容量の下で安定してくれることを願いました。もし 256MB でも足りないようなら、もう一度二倍にすることも出来ました。それでもうまく行かない場合には、 codecache の使用量を減らすために JVM の最適化の設定で実験しようと考えました。

ここで、私たちに有利に働いた点が一つありました。それは Indeed の開発サイクルでした。通常では、最低週に一度もしくは週に複数回、私たちはこの Web アプリを再度デプロイするのです。クリスマス休暇を除き、再デプロイせずに2週間あけるということは絶対ありません。合計 8 件の 256 MB の codecache をもつインスタンスを設定し、少なくとも一週間は実行することに決めました。問題は何も検知されず、これをもって成功としました。

何故 Codecache の容量を増やすのか?

JRE 8 向けのデフォルトの codecache の容量はおよそ 250 MB です。これは48MB がデフォルトの JRE 7 に比べると 5 倍ほど大きい値です。私たちが経験で学んだことは、 JRE 8 がこの増えた分の codecache を必要とする、と言うことです。今のところ 10 個のサービスを JRE 8 に切り替えましたが、それら全てが、以前に比べると約 4 倍の codecache を使用しています。

この 4 倍になった原因は、階層型コンパイルでした。JVM には 2 種類のバイトコード・コンパイラがあります。その一つは C1 と呼ばれ、素早い起動のために最適化してくれます。もう一つは、 C2 と呼ばれ、長期で実行するプロセスのスループットを最適化してくれます。初期の Java バージョンでは、 C1 と C2 は相互に排他的なものでしたので、 C1 を -client スイッチで選択するか、 C2 を -server スイッチで選択するか、のどちらかでした。そして、 Java 7 から階層型コンパイルが導入されました。これは C1 を起動時に実行し、そして、残りの処理を C2 に切り替えてくれます。階層型コンパイルは、長期で実行するサーバー処理のために起動時間をブーストする目的で作られ、 Oracle 社によると、より良い長期間のスループットを生み出せるとのことです。これは、 Java 7 では明示して有効化する必要がありますが、 Java 8 ではデフォルトで有効化されています。QA 環境で JRE 8 上で階層型コンパイルを無効にした際には、 codecache の量は 50% も減少しました。

また、 Java 8 がより多くの codecache を使うのは、 Java 7 よりもアグレッシブに最適化を行うからです。例えば、 InlineSmallCode オプションは、インライン化するには、一つ前のコンパイル・メソッドがどれくらい短くなければいけないかを管理しますが、 Java 8 では、このデフォルトを 1000 から 2000 に引き上げています。つまり、 Java 8 のオプティマイザーは、より長いメソッドのインライン化を行えるのです。より多くのメソッドをインライン化し、そしてそのインライン化のメソッドもさらに大きくなるようでしたら、より多くの codecache が必要となります。

Java 8 に移行する開発者は、心配すべき?

もし、デフォルトの codecache の容量をオーバーライドしてしまうけれど、メモリに空きがある場合、おそらくオーバーライドを削除するので事足りるでしょう。

万一のために、 codecache の容量を監視することを推奨します。こちらの Github プロジェクトは Datadog に JMX codecache の統計をアップロードする方法の例を載せています。

もしメモリに既に空きがあまりない場合、階層型コンパイルを無効にして、起動時間、長時間のパフォーマンス、そしてメモリ使用量にどう影響があるか測定して見るのが良いかもしれません。


謝辞

このプロジェクトを支援してくれた Indeed の Ops チームの Connor Kelly とMike Salsone は沢山の変わったリクエストにも忍耐強く対応してくれました。

Indeed のソフトウェアエンジニアである David Wahler と Chen Yang も YourKit プロファイルを解析するのを助けてくれました。 David はまた、 perf コマンドを使用することを薦めてくれ、 Flame Graph を読み解くのを助けてくれました。

参考文献

Brendan Gregg は Netflix のエンジニアで、パフォーマンス測定のスペシャリストです。彼のウェブサイトで、沢山のパフォーマンス関連の記事を読むことができます。また、 YouTube 上にある彼のテックトークは視聴する価値があります。Brendanはパフォーマンス・データを視覚化する際の Flame Graphの使用を普及させました。また、彼はperf データ内でJavaのシンボルを閲覧可能にした JVM の変更に貢献しています。

最初に codecache について調べた際には、概念の資料と関連する JVM の設定に関して、 Oracle 社のドキュメンテーションを確認しました。

セキュリティには報奨金

「求職者にとっての最善を果たそう」

これは、Indeed 設立当初からの基本理念です。求職者の情報を安全かつ確実に保つのも、求職者を第一に考える取り組みの一つです。毎日何百万人のユーザーが使用するサービスを開発するにあたり、私たちは常にシステムのセキュリティについて考えています。それでも、誰かが私たちのシステムの裏をかくこともあるでしょう。ハッカーは、いつでもセキュリティをくぐり抜け、システムや情報への不正アクセスを行うために新たな方法を試みているからです。私たちの取り組みは、言わばセキュリティに関してはプロであるハッカーに逆に協力してもらい、彼らの発見に助けてもらおうというものでした。

画像: stockarch – stockarch.com ( ライセンス: Creative Commons

この挑戦に対し、私たちの出した答えは…… お金でした。実際には、名声です。

Indeedはセキュリティのチェックを行う有志の協力者に、彼らがまっとうな方法でそれを報告できるような手段を提供し、貢献してくれた時間を称えて、彼らに現金と表彰を報奨として与えています。我々のバグ・バウンティ・プログラムを通じて過去一年半の間で、これまで300件ほどの通知を表彰してきました。一番深刻なバグには最も高い賞金である$5000を贈りました。一番成功した参加者(あなたのことですよ、Angrylogic さんAvlidienbrunn さん、それからMongo さん)はIndeedで最も評価されているテスターとしての評判を手にしました。

過去 18 か月間における各バグ報告に対する報奨金の金額
重要度 報奨金額 関連性のある報告数 (割合)
重要 最大 $5000 0.7%
最大 $1800 4%
最大 $600 31%
最大 $100 64%

このプログラムを始めた理由

バグ・バウンティ・プログラムを始める以前には、時折、脅迫めいたメッセージを受け取ることがありました。匿名のユーザーが連絡してきては、「支払をしろ、さもなくば、まだ言えないけど非常に重要なセキュリティバグをばらす」というのです。こういう方々は、前払いを求めるのですが、そもそもそんな公開するようなバグが、本当に存在するかも確かめようがありません。Indeed のサービスを改善・向上するために、バグ・リサーチの協力者の皆さんには喜んでお礼をお支払したいのですが、こういった威圧的な行動の後押しはしたくありませんでした。何かが間違っている気がしたのです。

こうした相互間の不信感を取り除くために、わたしたちは  Bugcrowd.com(バグ・クラウド)に公正なアービターとして機能してもらうべく採用しました。 バグ・クラウドを使用することで、セキュリティのリサーチに協力してくださる皆さんは、エビデンスを前もって提供することに積極的になってくれましたし、私たち自身にもバグの深刻度を公平に評価する機会が与えられました。 Indeed はこれで弊害などなく賞金を提供できるようになり、皆末永く幸せにくらしましたとさ….

理論 vs 実践

「末永く幸せに……」というのは、実践するにはもっと難しいものです。プログラムが開始してから、およそ2500件ほどのバグ報告を受けました。もしかすると、各問題を確認するのに、それぞれ長時間かかるかもしれません。バウンティ・プログラムのことを宣伝したり、支払額をアップする度にバグ報告の数が跳ねあがるのですが、外部の方から見たら、私たちはのんびりやっているように見えるかもしれません。しかし、実際には総動員でこれらの報告に返信しています。このブログの記事だけでも、プログラムの認知度があがるおかげで、あらたに数時間分のバグ検証を生み出すはずです。

当初は、検証者の報告に素早く返答するのに苦労し、処理待ちのものも発生していました。処理しきれないくらいの数の報告をもらい、このバックログは膨らんでいきました。倍以上の時間を投資した、しんどい数週間を経て、私たちは応答時間の新しいスタンダードとなるものを実装しました。それ以来、応答時間はきちんと制御下にあります。

時間の経過に伴う管理中チケットの合計数

: チケット日の値は、ある特定の日にオープンになった全てのチケットの日数の合計です。たとえば、ある日に、一つのチケットが三日間オープンになっていて、もう一つのチケットが二日間オープンだったのならば、合計5チケット日と換算します

バグ・リサーチ協力者の皆さんが、自分が Indeed にうまく利用されていると誤解しないように彼らと明快なコミュニケーションをとることも重要でした。私たちに比べて、彼らにはプロセスがあまりよく見えないという事も念頭においています。共通した問題としては重複した事象の取り扱いです。初めて報告を受けた問題に対してお金を払うのは、納得がいきますが、その重複した報告を別のリサーチ協力者から受けた場合どう対応すべきなのでしょうか?二件目の報告は、何か付加価値をもたらすものではありませんが、報告した方にしてみれば、彼らはきちんと本物のバグを見つけているのです。チケットに、重複と記載する理由を明白に書き、特定された問題を素早く修正することで、この懸念点を最小限にとどめています。ある場合においては、もし素晴らしい再現工程や、概念証明を示している重複のケースにも報奨金をお支払いしています。

さらには、わたしたちは新規バグ発見と、既知のバグ修正の時間のバランスを取るように心がけています。人気のあるバウンティ・プログラムを組み立て管理することは、多くの良質な報告につながっていますが、もしバグ修正に時間をかけなければ、意味がなくなってしまいます。Indeed では、バグ・バウンティ・プログラムを改善するために投資する時間は大げさでなく、非常に大事なものです。

ここまでの成功

どうやらいい方向に向かっているようです。Bugcrowd は最近バグ・リサーチ協力者の皆さんに、どの企業のプログラムが一番好きか質問をしていたのですが、さて一番はいったいどの企業だったでしょうか!?

正解は…… Tesla です。(素敵な Tesla のせいなら仕方ないですね)しかし、Indeed は全投票の 8% を獲得し、競合社他 35 社のなかで二位になりました。私たちのプログラムに対しての具体的なコメントとしては、わたしたちが公平に支払いをすること、きちんとしたコミュニケーションをとること、そして伝播性のないスコープ、などが挙げられていました。まだまだ、向上の余地があると思いますが、私たちがちゃんと正しい道を進んでいるという「検証」がもらえて嬉しいです。

形にとらわれず、大事なことを見つめる

Indeed では非常に大規模で、興味深く、取り組み甲斐のある問題に取り組んでいます。できるだけ早くアイデアを実装の形まで持っていき、逐次増えていく変更や、コードを頻繁に本番環境に送ります。こうして小さなリリースを行うことでリスクを減らし、品質を向上しています。

しかし、どんなソリューションに取り組む前にも、私たちは自問します。「どうやって、成功したかどうかを計測するのか?」と。この問いが、私たちのソリューションにとって重要な「計測できる結果」になるように、焦点を絞ってくれます。

私たちのソフトウェア開発へのアプローチは、「計測-学び-進化」と呼べるかもしれません。私たちは、さまざまなソフトウェア開発の手法からテクニックを取り入れますが、何か特定の手法にこだわりません。協力し合い、実装を繰り返し、計測するのです。時には成功もしますし、また失敗もします。しかし、常に何かを学んでいます。

MeasureLearnEvolve_blog

私たちは、どう実装や改善するかというプロセス自体を、成功とは見ていません。プロセスというのは、目的を達成する為の手段の一つでしかありません。終了に対する一つの方法です。プロセスは、成功するプロダクトを生み出すわけではありません。(人間が生み出すのです。)プロセスは、才能や情熱を与えてくれるわけでもありません。(人間が作るのです。)けれど、正しいプロセスやツールは、上記のような作業を助け、以下の状況において、予測できる仕組を提供してくれます。

  • やるべきことを計画し、それに関連した事柄の優先順位を設定
  • 実際に実行する事柄と、実行するかもしれない事柄の情報共有
  • 達成した事柄の記録
  • リスクの管理

私たちは、 Atlassian 社の JIRA を使って、上記のような仕組を実装しています。アイデアを提案し、要件を定義し、プロジェクトを企画するのです。依存性を記録し、工程管理をし、リリースを管理します。テストを説明し、結果を記録します。 JIRA を必要に応じてカスタマイズすることで、成功の指標との協力をとエンジニアリング・ベロシティ(開発の速度)を保つことができるのです。

昔からこういうやり方をしていたわけではありません。初めはシンプルでした。スタートアップ企業だったので、素早く完成させる、という点に注力しました。

会社として成長しても、こうした素早い開発と高い品質とを手放してしまうような事をしたくありませんでした。しかし、当時の私たちのその場その場のやり方では、同じ事を繰り返したり、結果を予想したりすることは不可能でした。一貫性のなさが多く見受けられ、将来のための記録なども残していませんでした。そうして、私たちは開発プロセスの形作りを JIRA を通じて始めたのです。

JIRA のカスタマイズ

Indeed では、オリジナルの JIRA のプロジェクトの種類、ワークフロー、フィールド、ロールなどを使用しています。このようにカスタマイズすることで、計画、情報共有、そしてソフトウェア開発を、望むやり方で行うことが出来ます。

カスタムしたプロジェクトの種類の紐付け

プロダクト開発において、 2 つの種類の JIRA プロジェクトを使用します。一つは「計画プロジェクト」と言う、プロダクト自体に対応するものです。もう一つは「エンジンリアリング・プロジェクト」という、デプロイ可能なアプリケーションやサービスなどに対応するものです。

計画プロジェクトには、イニシアティブ(構想)と実験と言う種類があります。イニシアティブという課題の種類を使用して、ゴール、計画、そしてプロダクト変更における成功指標を設定します。プロダクトのイニシアティブを四半期ごとに計画し、各期の間、その実装を繰り返すのです。その反復の中で、実験という種類の課題を使用して、プロダクトを最適化するために、テストしたい具体的なアイデアを説明します。

エンジニアリング・プロジェクトは、イニシアティブと実験に必要な実装を詳細に練り上げる課題(issue)を含みます。それぞれのデプロイ可能なアプリケーションやサービスに対応するエンジニアリング・プロジェクトが存在します。課題のリンクは、関連する課題同士を結んでいます。JIRA は複数のタイプの双方向のリンクを提供します。上に述べたこれらの使用例を、以下の表にまとめました。

組み入れ/組み入れ先 プロダクトのイニシアティブが、エンジニアリング・プロジェクトの課題(issue)を組み入れている。
依存/依存元 課題が、その他の課題に依存している。これは、機能の開発の依存性を形作るほか、例として順序の依存性などをデプロイすることが可能。
参照/参照元 機能の回帰のための課題が、バグを発生させた課題を参照する。

課題の種類とワークフロー

Indeed では JIRA の標準的な課題の種類(バグ、改善、新機能)を使用しています。これらの標準的な課題の種類のワークフローは、典型的な JIRA ワークフローをほんの少し改変したものです。

  1. 課題を作成し、プロジェクトのリードに割り振ります。課題は、「トリアージ待ち」というステータスに移行します。
  2. 短期間でのリリースに作業のターゲットを絞り、課題の優先順位を選定します。そしてバージョンを設定し、開発者を割り振るのです。そのうえで、「受諾待ち」というステータスに課題を移行します。他の課題をバックログに移動します。
  3. 開発者は、課題を承認し、作業開始する計画を立てたら 「受諾」 に移動します。
  4. コードが完了したら、開発者は課題を解決し、 「レビュー待ち」 に移動します。
  5. コードのレビュー後、課題を 「マージ待ち」 に移動します。
  6. リリース候補の作成の準備ができたら、変更をリリース・ブランチの中にマージし、 QA 環境にデプロイします。この時、課題を 「検証待ち」 に移動します。
  7. QA アナリストはこの成果物を検証し、課題を差し戻すか、検証を完了させ、 「終了の確認待ち」に移動させます。
  8. ターゲットとなったリリースの中で、全ての課題を実証した後、本番環境に開発物をリリースし、全課題を 「終了」 にします。

また、私たちは自社のプロセスをモデル化するために、カスタマイズした課題の種類を使用しています。前回の投稿では、 ProTest という課題のタイプ (略称。正式にはProctor Test。)について説明しましたが、このカスタマイズされた課題の種類を使用することで、新しい Proctor の A/B テストをリクエストしたり、テストの割り当てを変更することができます。

Indeed には、もう一つカスタマイズされた課題タイプと、ローカライゼーション(ローカリゼーション)に関連したワークフローが存在します。グローバルに成長を続けるにあたり、私たちは、成長スピードを妨げることのないローカライゼーションを必要としています。多くの翻訳者との連携は、難しいこともあるので、翻訳プロセスを JIRA 内でモデル化しました。Indeed の Explosion (爆発) の課題タイプでは、各翻訳対象言語のための課題を取り入れます。以下がそのワークフローになります。

  1. 翻訳を必要とする英語のストリングに課題を作成する。
  2. 課題のトリアージを行い、レビューに回す。
  3. ストリングが翻訳できる状態になったら、自動化されたステップが、一件の翻訳の課題を翻訳対象言語ごとに作成し、「爆発」の課題に全てをリンクさせる。(補足:爆発という呼び名は 1つの課題から 28 言語分の課題を一気に自動生成することから、シャレでそうネーミングされています。)
  4. それぞれの「爆発済み」の課題は、それぞれのワークフロー(1.受諾、2.解決、3.検証、4.終了)に沿って進む。
  5. 全ての翻訳の課題が閉じたところで、実証し、 Explosion の課題を閉じる。

爆発と翻訳のカスタマイズされた課題タイプやワークフローは、かかわる人数が多いプロセスの効率化を助けてくれます。言語と機能をトリアージするので、翻訳の課題が全機能のリリースを妨害することはありません。また、JIRA を使用することで、機械翻訳と外部の翻訳サービスの統合が可能になります。

チームのトリアージ

開発チームの多くは、 JIRA 内のダッシュボードとアジャイル・ボードを使用して、プロダクトに関連した課題に簡単にアクセスできます。定期的なトリアージ・ミーティングでは、プロダクト開発チームはこれらのツールを使用して、開発作業に優先順位をつけ、課題を分配しています。

メモリーのループを無くしていくこと

Git の中の各コードのコミットは、 JIRA 内で対応した課題として追跡可能です。さらに、参照した JIRA が、イニシアチブ(構想)に紐づく場合、その道すじは、イニシアチブにたどり着くのです。これは、エンジニアがいかなるコードのコミットもレビュー可能で、 JIRA 内の道筋を追うことで、全ての関係した実装の詳細、要件定義、そしてビジネスとしての同期を理解することが可能である、ということを意味しています。

本番のデプロイ

コードを本番環境にデプロイするのは、明確なコミュニケーションと連携を要しますが、Indeed のデプロイ用の課題は、このプロセスを追跡しやすくしました。デプロイを追跡するために JIRA を使用することは、結果として、スムーズな切り替えと透明性を全関係者にもたらしたのです。

デプロイのチケットは、各バージョンに関連付けられており、ビルドからリリースのプロセスまで成果物を移動するのに、連絡をしやすくさせる、ユニークなワークフローをもっています。課題のリンクも使用することで、成功したデプロイに必要な全てのシスアドのタスクも文書化することができます。デプロイチケットはリリースの中の、同じ修正バージョンもあります。

ほとんどのチームは週ごとに作業の計画をしますが、作業が完了次第、本番環境に移します。
よくあるサイクルとしては、週2回、毎日、あるいはそれ以上、などがありますが、リリースマネージャーはオープン中のマージリクエストからリリースの候補を作成します。Git (ブランチ管理)、JIRA (コード変更とデプロイ)、Crucible (コード・レビュー)、そして Jenkins (ビルド)、これらのツール間で連携する社内用のウェブアプリを開発しました。デプロイチケットにステータス変更をすることは、課題の割り振りを発生させ、スムーズな引き継ぎを促します。

このアプローチは、チームに精査すべき情報を提供し、本番環境へのリリースのリスク管理を可能にします。QA アナリストは、変更が引き起こしかねない潜在的な不具合について、さらに理解を深めることができます。リリースマネジャーは、何が変更されるかという全体論的な視点を持ち、問題が起きても素早く対処できるようになります。小規模なリリースはバグの調査をもっと単純にしてくれます。

開かれた環境で働くこと

Indeed において JIRA は、ソフトウェア開発とデプロイのプロセスを、効果的で、効率の良い連携を可能にしました。要件定義を明らかし、実装の選択肢を話し合い、変更を実証し、本番にデプロイするために、これを使用しています。

チーム間、そして組織の上下をこえて、Indeed の JIRA の使い方は、作業を終えるために、透明性をもたらしてくれるのです。開かれた環境で働くことで、計画に対し共有された理解を持ち、前進し、何百という進行中のプロジェクトやイニシアチブに取り組むことができるのです。

自分にとって腑に落ちることをしよう

方法論やプロセスは、計画や、コミュニケーション、デリバリーのために反復と予測を可能にする仕組みを提供できる場合のみ、役立つものです。JIRA はこれらの仕組みを形作るのを助けてくれました。

「既製品」の中から、方法論を組み立て、実装しないようにしてみましょう。そして、問題解決にあたり、ツールに依存するのはやめましょう。その代わりに、どうやって自分のチームが計画し、コミュニケーションを図り、デリバリーをしなければならないかを考えましょう。そのうえで、自分のニーズを満たす最善のプロセスとツールを定義し、必要なだけ、プロセスを反復してみてください。そうして、本当に一番大事なこと―成果を出すことに集中するのです。


原案: 弊社エンジニア Jack Humphrey によるプレゼンテーションより翻案
発表場所:Keep Austin Agile 2014にて発表。