こんにちは、古川です。もうそろそろ入社3年目になりそうだな~
さて、今回はjMeterで負荷テストをしたとき、やってよかったことを共有します。
ちなみに負荷テストは以前紹介した大曲と一緒にやっていました。
まずはじめにお断りですが、ここで紹介されたやり方は試行錯誤してやった内容のため、必ずしもベストではないです。
ただ、同じところで困った人に対して少しでも参考になればというつもりで共有します。
DB直接の負荷テストはJDBC RequestよりJSR223 Samplerを使う
DBに対して直接テストして、DBの性能を図るというのは負荷のかけているサービスでは良くあると思います。
テストにはお勧めできません。
理由としては、2つあげられます。
- 1つのクエリしか取り扱えない
- JDBC Requestを呼ぶたびにDBへコネクションの接続・切断をやっている
まず1つ目は、JDBC Requestは1つのクエリしかかけません。2つ以上書くとエラーでます。
複数のクエリをたたこうと思ったらJDBC Requestを複数書かなければいけません。
ただこれだけだったら問題ないですが、2つ目の理由で結構致命的になります。
というのも、通常のWebアプリの処理は
接続→一通りのクエリを実行→切断
のはずですが、JDBC Requestを複数書くと、
接続→クエリ1を実行→切断→接続→クエリ2を実行→切断
という形になってしまいます。見ての通り、余分な処理が挟まって遅くなってしまいます。
というわけで、複数のJDBC Requestをてんこ盛り書いてテスト、、、なんてしてはいけません!!!
(自分も最初はこのことを知らずやってしまい、うまく負荷がかけられませんでした。
結局どうしたかといいますと、JSR223 Samplerで直接Javaでクエリをなげるようにしました。
JSR223 Samplerを作成し、Script Languageを java にします。
スクリプトの内容はこんな感じに記述します。
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.Statement; import java.sql.ResultSet; import java.lang.Exception; import org.apache.jmeter.protocol.jdbc.config.DataSourceElement; import java.security.MessageDigest; // ここに設定する変数は「JDBC Connection Configuration」の // 「Variable Name Bound to Pool」内で設定した Variable Name を設定 Connection conn = DataSourceElement.getConnection("first"); Statement update_st = conn.createStatement(); Statement select_st = conn.createStatement(); ResultSet rs = null; try { // 検索クエリを投げ、結果をjmeter変数へ格納する例 rs = select_st.executeQuery( "SELECT id, user_id from blacklist where user_id = " + vars.get("blacklist_id") + " AND delete_flag = 0;" ); if (rs.next()) { vars.put("user_id", rs.getString(2) ); } // 検索クエリを投げている例 select_st.execute( "SELECT id, user_id from user where id = " + vars.get("user_id") + ";" ); ・・・ // 更新クエリを投げている例 update_st.executeUpdate( "UPDATE blacklist SET delete_flag = 1 WHERE user_id = " + vars.get("user_id") + ";" ); } catch (Exception e) { // 何か問題が起きたら例外エラーをログに流しておきます log.info("update achieve error: "); log.info(e.toString()); if (e.getStackTrace() != null) { for (StackTraceElement ste : e.getStackTrace()) log.warn(ste.toString()); } } finally { // 最後はコネクションを閉じておきます if (update_st != null) { update_st.close(); } if (select_st != null) { select_st.close(); } if (conn != null) { conn.close(); } }
見ての通り、接続から切断全部javaで記述しています。。正直泥臭いです!
でもここまでやらないと本格的な性能を発揮してくれなかったんです(もっときれいなやり方でできないだろうか・・・
これでDBに対して一気にクエリを投げれるようになりました。
普段Javaで仕事している人にとっては、当たり前かと思います。
しかし、初めてこのツールで負荷テストをやろうとした時良く引っかかります。
チューニングしなかった場合何が起こるのかといいますと、
大きな負荷テストをしたときに、何の前触れもなくフリーズしてテストができなくなります。
(とくに大きな負荷をかけてテストしないかぎりなかなかでないので厄介なんですが。
これはだいたいFullGCが発生しているからです。
アプリが一切動かなくなります。この現象を「stop the world」と呼ばれます。
こうならないためにも、JVMのチューニングは必須です!
つぎの内容を追加します。
rem ヒープ領域 old + new set HEAP=-Xms1024m -Xmx1024m rem ヒープ領域 new (短時間で破棄されるオブジェクトに使用) set NEW=-XX:NewSize=512m -XX:MaxNewSize=512m rem -XX:SurvivorRatio:ヒープ領域newをEden領域(オブジェクトが作成されたときに配置される場所)に割り当てる割合 rem -XX:TargetSurvivorRatio=90%:young generation が fullと考えない、survivor スペースの利用率 set SURVIVOR=-XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=90% rem New領域において、オブジェクトがマイナーGCを何回超えて生き残ると、Old領域に移動するかのしきい値の最大値(すぐにold領域に移動させない) set TENURING=-XX:MaxTenuringThreshold=32 rem コンカレントGCを使用 set JVM_ARGS=%JVM_ARGS% -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseParNewGC rem GCログを出力 set JVM_ARGS=%JVM_ARGS% -XX:+PrintGCDetails -XX:+PrintGCDateStamps
上記の設定のなかで、重要なところはつぎの個所です。
まずはこの設定でヒープ領域を広げています。(複数プロセスで動かす予定のため、ここでは少なめにしています)
負荷をかけるサーバ側のメモリに応じて設定してください。
set HEAP=-Xms1024m -Xmx1024m set NEW=-XX:NewSize=512m -XX:MaxNewSize=512m
つぎはこの設定でold領域に流れる基準を厳しくしています。
old領域がいっぱいになるとFullGCなどの重いGCが走ってしまいます。
set TENURING=-XX:MaxTenuringThreshold=32
最後は設定でコンカレントGCを使っています
一部のスレッドでGCを使うようにします。
全体のスピードは落ちますが、フリーズは防げます。
set JVM_ARGS=%JVM_ARGS% -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseParNewGC
ヒープ領域の詳細は説明すると長くなるため、割愛します。
こんな感じに設定して、jmeterの負荷テスト時の挙動がましになりました。
まともにできるようになりましたが、長くなってしまいましたので、今日はここまでにしておきます。
つぎの機会で、複数プロセスの立ち上げ方および、スループットの安定させ方について書きたいと思います。
ではまた