CalendarJavaJenkinsこんにちは、インフラストラクチャ本部の@nagaseyasuhitoです。昨日はイケメンmoritaさんによる男性エンジニアリングマネージャが長期育休を取った話でした。エンジニアブログのアカウントは2年くらい前からあるのですが、これが初エントリになります。グリーでは比較的珍しいJavaEEを始めとしたサーバーサイドJavaアプリケーションの開発、SolrやHadoopといったミドルウェアの周辺機能開発や運用などを行っています。最近はPvE/PvP/GvGなどユーザー同士がリアルタイムに協調プレイする際、クライアント-サーバー間を常時接続通信で行うゲームが増加しています。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の付加されたメソッドでクライアントから送信された文字列をログに出力した後、クライアントに送り返しています。message;}}クライアント実装クライアントもサーバーと同じように記述できます。@OnOpenはサーバーと接続が確立した際に呼ばれるメソッドに付加するアノテーションです。NUMBER_OF_LOOPSで定義された回数だけメッセージを受信したら接続を閉じます。CountDownLatchはマルチスレッド環境下で安全にスレッド終了を待機させるための同期化支援機能です。CountDownLatch.countDown()メソッドを呼び出し、別スレッドに接続が閉じたことを伝えます。{session.getBasicRemote().sendText(UUID.randomUUID().toString());}}}結合テストそれではこのサーバーとクライアントの結合テストを書いてみましょう。アプリケーションサーバーの結合テストはArquillianを使います。ArquillianはJUnitで書いたアプリケーションのテストをTomcatやJettyからGlassFish、Wildflyまで様々なアプリケーションサーバーを起動し、その上でテストを実行してくれるスグレモノです。Arquillianはテストの際、アプリケーションをデプロイするコンテキストパスにUUIDを使うため実行する度にURLが異なってしまいます。そこで@RunAsClientアノテーションを使うと@ArquillianResourceアノテーションが付与されたjava.net.URL型のフィールドにデプロイ先のURLがインジェクトされます。結合テストではこのURLからWebSocketで接続するURIを作成します。先ほどEchoClientに定義したCountDownLatchのawait()メソッドを呼ぶことで、内部的に保持しているカウンタが0になるまで処理をブロックします。is(EchoClient.NUMBER_OF_LOOPS));}}このテストを実行するにはpom.xmlにmaven-failsafe-pluginを追加します。</plugin>123456789101112<plugin><artifactId>maven-failsafe-plugin</artifactId><version>2.18</version><executions><execution><goals><goal>integration-test</goal><goal>verify</goal></goals></execution></executions></plugin>そして以下のコマンドのようにverifyフェーズまで実行すると結合テスト(デフォルトではクラス名の末尾がITのもの)が実行されます。verify実行すると以下のようなログが出力されるはずです。f7c0f8d3-b99d-4c17-9cf8-749c3245dd46デプロイデプロイの方法はアプリケーションサーバーごとに様々ですが、Mavenからアプリケーションサーバーにデプロイするcargo-maven2-pluginを使ってみましょう。今回は例としてTomcat8にデプロイしてみます。最近のServletコンテナであればほとんどがJSR356に対応しているので他のアプリケーションサーバーで試して性能比較するのも面白いかもしれません。先にTomcat8にcargo-maven2-pluginでデプロイするためのユーザーを作っておきましょう。$CATALINA_HOME/conf/tomcat-users.xmlに以下の記述を追加します。/></tomcat-users>プラグインの定義はこのようにデプロイするコンテナのIDやデプロイ先のホストの情報を記述します。</plugin>12345678910111213141516171819<plugin><groupId>org.codehaus.cargo</groupId><artifactId>cargo-maven2-plugin</artifactId><version>1.4.10</version><configuration><container><containerId>tomcat8x</containerId><type>remote</type></container><configuration><type>runtime</type><properties><cargo.hostname>${cargo.hostname}</cargo.hostname><cargo.remote.username>${cargo.remote.username}</cargo.remote.username><cargo.remote.password>${cargo.remote.password}</cargo.remote.password></properties></configuration></configuration></plugin>デプロイ先の情報は環境によって異なることが多いので、プロファイルに分割しておきます。</profile>12345678<profile><id>development-environment</id><properties><cargo.hostname>localhost</cargo.hostname><cargo.remote.username>deployer</cargo.remote.username><cargo.remote.password>passwd</cargo.remote.password></properties></profile>そして以下のコマンドのように-Pオプションでプロファイルを指定してcargo:deployゴールを実行するとアプリケーションサーバーにデプロイされます。-Pdevelopment-environmentちなみにアンデプロイはcargo:undeploy、再デプロイはcargo:redeployを指定してください。JUnitをJMeterで実行するJUnitで記述された結合テストを負荷テストシナリオとしてJMeter上で実行してみましょう。テスト内容は結合テストと同じものを実行するので継承してしまいます。結合テストではテストランナーにArquillianを使っていましたが負荷テストではデフォルトのJUnit4に戻します。@Beforeで接続先のURLをプロパティファイルから読み込むように変更しました。URL(ResourceBundle.getBundle("stress-test").getString("url"));}}stress-test.propertiesは以下のようにしてMavenのプロパティ置換を利用します。stress-test.propertiesurl=http://${cargo.hostname}:8080/${project.artifactId}1url=http://${cargo.hostname}:8080/${project.artifactId}GUIで実行JUnitをJMeterで実行するには事前に関連するjarファイルを$JMETER_HOME/lib/junitにコピーする必要があります。dependency:copy-dependenciesでtargetディレクトリに生成されるのでそれをコピーしてください。コピーができたらJMeterを起動して負荷テストシナリオを作成しましょう。Requestをぶら下げるシンプルな負荷テストシナリオです。必要があれば確認用のリスナーも作成しておきましょう。Methodに先ほど記述したテストクラスが表示されているはずです。この状態でテストを実行すると、このように実行結果がリスナーに表示されていれば成功です。アプリケーションサーバーにもログが出力されているので確認しておきましょう。複数台のサーバーを使ってJMeterの負荷テストシナリオを分散実行するには、それぞれのサーバーでjmeter-serverを起動しておく必要があります。確認だけなのでローカルホストでjmeter-serverを起動して試してみましょう。jmeterコマンドに-nオプションをつけるとCUIで起動します。-tオプションは実行する負荷テストシナリオ、-Rオプションは分散実行するサーバーのホスト名です。複数台で実行する場合はカンマ区切りで指定します。run各サーバー台数で負荷テストシナリオが実行されるので注意しましょう。10スレッド10ループする負荷テストシナリオを10台に分散して実行すると合計10000回実行されます。MavenからJMeterを実行できるようにするユニットテストや結合テスト、デプロイなどをMavenで自動化するなら負荷テストも自動化してしまいましょう。MavenにはJMeterを実行するjmeter-maven-pluginというこれまた便利なプラグインがあります。</plugin>12345678910111213141516171819202122232425<plugin><groupId>com.lazerycode.jmeter</groupId><artifactId>jmeter-maven-plugin</artifactId><version>1.10.0</version><executions><execution><id>jmeter-tests</id><phase>integration-test</phase><goals><goal>jmeter</goal></goals></execution></executions><configuration><testFilesDirectory>${project.build.testOutputDirectory}</testFilesDirectory><testResultsTimestamp>false</testResultsTimestamp><ignoreResultFailures>true</ignoreResultFailures><suppressJMeterOutput>false</suppressJMeterOutput><remoteConfig><startServersBeforeTests>true</startServersBeforeTests><serverList>${jmeter.servers}</serverList><stopServersAfterTests>true</stopServersAfterTests></remoteConfig></configuration></plugin>上記の設定でintegration-testフェーズでJMeterを実行します。分散実行後jmeter-serverを終了するかこれだけで設定終わり!になればとても楽なのですが、先述した通りjarファイルを分散実行するサーバーにコピーしてjmeter-serverを起動しておく必要があります。そんなプラグインは無いのでmaven-exec-pluginでシェルスクリプトを実行します。&"doneこれもMavenのプロパティ置換でscp先などを指定できるようにします。Groupの設定もMaven実行時に変更できるようにプロパティにしてしまいましょう。-Djmeter.rampUpPeriod=60のように負荷を調整しながら実行できます。5台のサーバーをjmeter-server用に準備して上記のコマンドを実行てみました。7000スレッド×5台を60秒で起動するシナリオです。0このようにサマリーが表示されます。JMeterの実行結果はファイルにも保存されているため、JMeterで開いてグラフ化する事もできます。PluginをインストールするとJMeterの実行結果をJenkins上でグラフ化できます。reportという項目が追加されます。filesに**/*.jtlを設定しておけば、Jenkinsから負荷テストを実行し、結果を閲覧するところまで1-clickで行えるようになるので負荷テストを継続的に行うことができます。Labelという機能を使います。Labelフィールドにその値が渡されます。JMeterのプロパティ置換も行われるのでループの回数やスレッドのIDなどもJUnit側で扱うことができます。負荷テストの際どこがボトルネックになっているか調査するためにアプリケーションのプロファイリングは欠かせません。Controlで開いてボトルネックの調査をするといいでしょう。まとめいかがだったでしょうか。本番環境に近い十分な負荷がかけられるようになって初めて負荷テストのスタートラインに立ったようなものです。アプリケーションだけではなく、カーネルやネットワークを含めたチューニングこそエンジニアの腕の見せどころですね。十分な負荷テストをせず本番運用が始まってから過負荷によるサービス停止といった不幸な事件を起こさないため、このエントリをきっかけに本番環境に近い負荷をかけたテストがより身近なものになり、皆様のサービス品質の向上につながれば幸いです。