JMeterとJUnitとMavenで独自プロトコルサーバーの負荷テストを自動化するぞ

こんにちは、インフラストラクチャ本部の@nagaseyasuhitoです。このエントリは GREE Advent Calendar 2014 10日目の記事です。昨日はイケメンmoritaさんによる男性エンジニアリングマネージャが長期育休を取った話でした。

エンジニアブログのアカウントは2年くらい前からあるのですが、これが初エントリになります。グリーでは比較的珍しいJavaEEを始めとしたサーバーサイドJavaアプリケーションの開発、SolrやHadoopといったミドルウェアの周辺機能開発や運用などを行っています。どうぞよろしくお願いします。

最近はPvE/PvP/GvGなどユーザー同士がリアルタイムに協調プレイする際、クライアント-サーバー間を常時接続通信で行うゲームが増加しています。このような場合はHTTPのREST APIなど慣れ親しんだプロトコルでは要件を満たしきれないため、WebSocketやNettyをベースにThriftやProtocolBufferなどを使った独自プロトコルで通信をすることが多くなってきたのではないでしょうか。

HTTPベースのプロトコルであればWebブラウザやcurlなどのコマンドラインツールからでも動作確認できますしテストツールも充実していますが、独自プロトコルとなると一般的なツールも使えないため、どうしてもテストが手薄になりがちです。

しかも多人数で同時接続通信を行う場合、サーバー側でクライアント間の協調動作をさせる場合が多いため、マルチスレッドアクセスが原因のバグや、負荷が上がった場合にのみ現れるデータの不整合、効率の悪い同期化によって同時接続数が伸びないなどパフォーマンスの面にも注意を払わなければならないため、負荷テストは必須と言えます。

そこでこのエントリでは独自プロトコルサーバーに対する負荷テストを、JMeterとJUnitを使いMavenから複数のサーバーで分散実行できるようにしてCIに乗せる方法をご紹介します。

今回のサンプルはこちらのGitHubのリポジトリで公開しているのでぜひご自身の端末で実行してみてください。負荷テストがより身近に感じられると思います。

独自プロトコルの負荷試験

負荷テストツールと言えばJMeterやGatling、Grinderあたりが有名ですが今回はJMeterを使います。JMeterで独自プロトコルのテストと言うと「新規にプラグインを作るのかな」と思われるかもしれませんが違います。JMeterにはJUnitSamplerというその名の通りJUnitで書かれたテストを実行するサンプラーが標準で存在するのです。

つまり慣れ親しんだJUnitで結合テストを書くように負荷テストのシナリオを記述し、それをJMeterを使って実行するといういつも通りの開発に少しだけ手間をかけるだけで簡単に複数台のサーバーを使った負荷テストを実行できるのです。

Echoサーバーとクライアント

今回はWebSocketを使ったEchoサーバーを題材に取り上げます。JavaEEの話になるのでJMeterの話を早く読みたい方はこの章は飛ばしてもらって構いません。

JavaにはJSR356というWebSocketアプリケーションの仕様があるので、それに則って記述します。

サーバー実装

@ServerEndpointでエンドポイントのパスを指定し、@OnMessageの付加されたメソッドでクライアントから送信された文字列をログに出力した後、クライアントに送り返しています。

クライアント実装

クライアントもサーバーと同じように記述できます。@OnOpenはサーバーと接続が確立した際に呼ばれるメソッドに付加するアノテーションです。NUMBER_OF_LOOPSで定義された回数だけメッセージを受信したら接続を閉じます。CountDownLatchはマルチスレッド環境下で安全にスレッド終了を待機させるための同期化支援機能です。CountDownLatch.countDown()メソッドを呼び出し、別スレッドに接続が閉じたことを伝えます。

結合テスト

それではこのサーバーとクライアントの結合テストを書いてみましょう。アプリケーションサーバーの結合テストはArquillianを使います。ArquillianはJUnitで書いたアプリケーションのテストをTomcatやJettyからGlassFish、Wildflyまで様々なアプリケーションサーバーを起動し、その上でテストを実行してくれるスグレモノです。

Arquillianはテストの際、アプリケーションをデプロイするコンテキストパスにUUIDを使うため実行する度にURLが異なってしまいます。そこで@RunAsClientアノテーションを使うと@ArquillianResourceアノテーションが付与されたjava.net.URL型のフィールドにデプロイ先のURLがインジェクトされます。結合テストではこのURLからWebSocketで接続するURIを作成します。

先ほどEchoClientに定義したCountDownLatchのawait()メソッドを呼ぶことで、内部的に保持しているカウンタが0になるまで処理をブロックします。

このテストを実行するにはpom.xmlにmaven-failsafe-pluginを追加します。

そして以下のコマンドのようにverifyフェーズまで実行すると結合テスト(デフォルトではクラス名の末尾がITのもの)が実行されます。

実行すると以下のようなログが出力されるはずです。

デプロイ

デプロイの方法はアプリケーションサーバーごとに様々ですが、Mavenからアプリケーションサーバーにデプロイするcargo-maven2-pluginを使ってみましょう。今回は例としてTomcat8にデプロイしてみます。最近のServletコンテナであればほとんどがJSR356に対応しているので他のアプリケーションサーバーで試して性能比較するのも面白いかもしれません。

先にTomcat8にcargo-maven2-pluginでデプロイするためのユーザーを作っておきましょう。$CATALINA_HOME/conf/tomcat-users.xmlに以下の記述を追加します。

プラグインの定義はこのようにデプロイするコンテナのIDやデプロイ先のホストの情報を記述します。

デプロイ先の情報は環境によって異なることが多いので、プロファイルに分割しておきます。

そして以下のコマンドのように-Pオプションでプロファイルを指定してcargo:deployゴールを実行するとアプリケーションサーバーにデプロイされます。

ちなみにアンデプロイはcargo:undeploy、再デプロイはcargo:redeployを指定してください。

JUnitをJMeterで実行する

JUnitで記述された結合テストを負荷テストシナリオとしてJMeter上で実行してみましょう。テスト内容は結合テストと同じものを実行するので継承してしまいます。結合テストではテストランナーにArquillianを使っていましたが負荷テストではデフォルトのJUnit4に戻します。そして@Beforeで接続先のURLをプロパティファイルから読み込むように変更しました。

stress-test.propertiesは以下のようにしてMavenのプロパティ置換を利用します。

GUIで実行

JUnitをJMeterで実行するには事前に関連するjarファイルを$JMETER_HOME/lib/junitにコピーする必要があります。必要なjarファイルは、

でtargetディレクトリに生成されるのでそれをコピーしてください。

コピーができたらJMeterを起動して負荷テストシナリオを作成しましょう。Test Planの下にThread Groupを作成し、その下にJUnit Requestをぶら下げるシンプルな負荷テストシナリオです。必要があれば確認用のリスナーも作成しておきましょう。

Screen Shot 2014-12-06 at 11.24.19 AM

JUnit Requestサンプラーの設定でClassnameとTest Methodに先ほど記述したテストクラスが表示されているはずです。この状態でテストを実行すると、

Screen Shot 2014-12-06 at 11.24.05 AM

このように実行結果がリスナーに表示されていれば成功です。アプリケーションサーバーにもログが出力されているので確認しておきましょう。

リモートのサーバーで実行

複数台のサーバーを使ってJMeterの負荷テストシナリオを分散実行するには、それぞれのサーバーでjmeter-serverを起動しておく必要があります。確認だけなのでローカルホストでjmeter-serverを起動して試してみましょう。

jmeterコマンドに-nオプションをつけるとCUIで起動します。-tオプションは実行する負荷テストシナリオ、-Rオプションは分散実行するサーバーのホスト名です。複数台で実行する場合はカンマ区切りで指定します。

各サーバー台数で負荷テストシナリオが実行されるので注意しましょう。例えば10スレッド10ループする負荷テストシナリオを10台に分散して実行すると合計10000回実行されます。

MavenからJMeterを実行できるようにする

ユニットテストや結合テスト、デプロイなどをMavenで自動化するなら負荷テストも自動化してしまいましょう。MavenにはJMeterを実行するjmeter-maven-pluginというこれまた便利なプラグインがあります。

上記の設定でintegration-testフェーズでJMeterを実行します。設定はそれぞれ

  • testFilesDirectory ... 負荷テストシナリオがあるディレクトリ
  • testResultsTimestamp ... 結果ファイルにタイムスタンプを使うか
  • ignoreResultFailures ... 負荷テストシナリオが失敗してもMavenの処理を続けるか
  • suppressJMeterOutput ... JMeterの出力を抑制するか
  • remoteConfig/startServersBeforeTests ... ちょっとよくわからないけどtrueにしておくと分散実行される
  • remoteConfig/serverList ... 分散実行するサーバー
  • remoteConfig/stopServersAfterTests ... 分散実行後jmeter-serverを終了するか

これだけで設定終わり!になればとても楽なのですが、先述した通りjarファイルを分散実行するサーバーにコピーしてjmeter-serverを起動しておく必要があります。さすがにそんなプラグインは無いのでmaven-exec-pluginでシェルスクリプトを実行します。

これもMavenのプロパティ置換でscp先などを指定できるようにします。また負荷テストシナリオのThread Groupの設定もMaven実行時に変更できるようにプロパティにしてしまいましょう。

こうしておくとMaven実行時に

のように負荷を調整しながら実行できます。5台のサーバーをjmeter-server用に準備して上記のコマンドを実行てみました。7000スレッド×5台を60秒で起動するシナリオです。しばらくすると、

このようにサマリーが表示されます。JMeterの実行結果はファイルにも保存されているため、JMeterで開いてグラフ化する事もできます。

Jenkinsでレポート出力する

JenkinsにPerformance PluginをインストールするとJMeterの実行結果をJenkins上でグラフ化できます。

Jenkins

プラグインをインストールするとPost-build ActionにPublish Performance test result reportという項目が追加されます。Add a new reportよりJMeterを選択してReport filesに**/*.jtlを設定しておけば、Jenkinsから負荷テストを実行し、結果を閲覧するところまで1-clickで行えるようになるので負荷テストを継続的に行うことができます。

おまけ JMeterのプロパティをJUnitに渡すには

JUnitのテストにJMeterのプロパティを渡したい場合、JUnitSamplerのConstructor String Labelという機能を使います。JUnitのテストにStringの引数をとるコンストラクタを定義すると、負荷テストシナリオのConstructor String Labelフィールドにその値が渡されます。JMeterのプロパティ置換も行われるのでループの回数やスレッドのIDなどもJUnit側で扱うことができます。

おまけ FlightRecoderで調査

負荷テストの際どこがボトルネックになっているか調査するためにアプリケーションのプロファイリングは欠かせません。最近のJavaで標準装備になったFlightRecorderを使う場合、Tomcat8であれば$CATALINA_HOME/bin/setenv.shに

と記述しておくとTomcat8を終了した際にプロファイルの結果がdumponexitpathで指定したパスに保存されるので、手元のマシンのJava Mission Controlで開いてボトルネックの調査をするといいでしょう。

まとめ

いかがだったでしょうか。本番環境に近い十分な負荷がかけられるようになって初めて負荷テストのスタートラインに立ったようなものです。アプリケーションだけではなく、カーネルやネットワークを含めたチューニングこそエンジニアの腕の見せどころですね。

十分な負荷テストをせず本番運用が始まってから過負荷によるサービス停止といった不幸な事件を起こさないため、このエントリをきっかけに本番環境に近い負荷をかけたテストがより身近なものになり、皆様のサービス品質の向上につながれば幸いです。

明日はネットワークのスペシャリスト、黒河内さんのたぶんなんかネットワーク系の話です!