ようへいの日々精進XP

よかろうもん

XML は解らなくても JMeter はキライにならないでクダサイ!(ruby-jmeter チュートリアル)

ども、かっぱです。

tl;dr

JMeter で行う負荷試験のシナリオを触る機会を得たので、以前から導入したかった ruby-jmeter を導入しようとしたメモ。

github.com


参考

有難うございますmm


ruby-jmeter とは

Ruby DSL

JMeter のシナリオファイル(XML)を書き出して下さるツール

flood.io

という負荷試験の SaaS ベンダーがメインでメンテナンスしている。

flood.io

DSL

ざっくりとソースコードを拝見したところ、各機能の XML をヒアドキュメントで読み込んでいるメソッドを読み込んで、メソッドのパラメータを埋め込んで、最終的に一つの XML を生成するような挙動に見える。実際に lib/ruby-jmeter/dsl/ 以下を見ると沢山の機能別 Ruby スクリプトが保存されている。

github.com

実際には、もっと奥深い仕組みが施されていると思われるが...。


試しにシナリオを書いてみる

ruby-jmeter のインストール

% bundle init
% vim Gemfile

以下を追記。

gem "ruby-jmeter", :git => 'https://github.com/inokappa/ruby-jmeter.git',
                   :branch => "thread_loop_string"

本家では無く、fork したものを使う理由は後述。尚、ユーザー定義変数を利用しない場合には本家を利用する。

求められるシナリオ

  • スレッド数(デフォルト:100) / Ramp-up(デフォルト:10) / ループ回数(デフォルト:5)はユーザー定義変数で試験毎に任意の値を設定するようにする
  • 定数タイマ(デフォルト:1000)を利用(値はユーザー定義変数で試験毎に任意の値を設定するようにする)
  • 各エンドポイントへのアクセス割合をスループットコントローラーを利用して定義する
  • URL のパラメータは param_id をセットする
  • パラメータに設定する値は csv ファイルより読み込む
  • 以下の 5 つの API エンドポイントにアクセスする
エンドポイント アクセス割合(パーセント換算)
${domain}/01?pramid=${param_id} 4(40%)
${domain}/02?pramid=${param_id} 2(20%)
${domain}/03?pramid=${param_id} 2(20%)
${domain}/04?pramid=${param_id} 1(10%)
${domain}/05?pramid=${param_id} 1(10%)

シナリオ

require "ruby-jmeter"

#
# テスト計画を定義する
#
test name: "my-test" do

  #
  # ユーザー定義変数は user_defined_variables で定義する(複数の場合には配列で書く)
  #
  user_defined_variables([
    { name: "host", value: "jmeter.example.com" },
    { name: "thread_count", value: 100 },
    { name: "ramp_up_second", value: 10 },
    { name: "loop_count", value: 5 },
    { name: "loop_interval_ms", value: 1000 }
  ])

  #
  # スレッドグループを定義
  # 
  threads name: "my-test", count: "${thread_count}", loops: "${loop_count}", rampup: "${ramp_up_second}" do

    #
    # アクセスの割合を  throughput_controller で定義(割合を % で定義する)
    #   - percent パラメータが定義されている場合には自動的に「パーセント実行」がセットされる
    #
    throughput_controller percent: 40 do
      #
      # csv から読み込んだ param_id を ${param_id} に定義
      #
      visit name: "01", url: "http://${host}/01?param_id=${param_id}"
    end

    throughput_controller percent: 20 do
      visit name: "02", url: "http://${host}/02?param_id=${param_id}"
    end

    throughput_controller percent: 20 do
      visit name: "03", url: "http://${host}/03?param_id=${param_id}"
    end

    throughput_controller percent: 10 do
      visit name: "04", url: "http://${host}/04?param_id=${param_id}"
    end

    throughput_controller percent: 10 do
      visit name: "05", url: "http://${host}/05?param_id=${param_id}"
    end

  end

  #
  # ローカルに保存した csv ファイルのパスと JMeter から参照する為の変数を定義
  # 
  csv_data_set_config(filename: "/path/to/dummy_data.csv", variableNames: "param_id")

  #
  # 定数タイマを定義
  #
  constant_timer(delay: "${loop_interval_ms}")

  # 結果をツリーで表示
  view_results_tree
  # サマリーレポート
  summary_report
  # 結果を表で表示
  view_results_in_table
end.jmx(file: "./jmx/sample.jmx")

XML シナリオの生成

Ruby シナリオを sample.rb というファイルで保存して、以下のように XML シナリオファイルを生成する。

% bundle exec ruby sample.rb

以下のように出力される。

I, [2016-05-12T07:50:26.584367 #34128]  INFO -- : Test plan saved to: ./jmx/sample.jmx

念の為、生成されているかを確認する。

 ls -l ./jmx/sample.jmx
-rw-r--r--  1 user group  19572 May 12 07:50 ./jmx/sample.jmx

JMeter に読み込む

  • ユーザー定義変数

f:id:inokara:20160512075800p:plain

  • スレッドグループ

f:id:inokara:20160512075908p:plain

本家の ruby-jmeter の場合「ループ回数」をユーザー定義変数に合わせて変数(${loop_count})を入れておくと JMeter で読み込んだ際にエラーとなる。

原因は...こちらの...

  <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="#{testname}" enabled="true">
    <boolProp name="LoopController.continue_forever">false</boolProp>
    <intProp name="LoopController.loops">-1</intProp>
  </elementProp>

<intProp name="LoopController.loops">-1</intProp> 数値しか許容していない部分。ということで、スレッドグループでユーザー定義変数を埋め込みたい場合には注意が必要。(直接 XML を書いたり、JMeterGUI で設定する場合には問題は起きない)

f:id:inokara:20160512075917p:plain

  • HTTP リクエスト

f:id:inokara:20160512081925p:plain

  • CSV Data Set Config

f:id:inokara:20160512091142p:plain

  • 定数タイマ

f:id:inokara:20160512075933p:plain

シナリオ実行

事前に動作確認用アプリケーションを起動しておいて、GUIJMeter でシナリオを実行すると...

  • アプリケーションのログ
(snip)

I, [2016-05-11T23:22:58.426024 #13]  INFO -- : {"endpoint":"03","param_id":"s2VxnMAkgLodCXO7"}
I, [2016-05-11T23:22:58.441140 #13]  INFO -- : {"endpoint":"01","param_id":"0WBhWXFcJBgW5txp"}
I, [2016-05-11T23:22:59.260873 #13]  INFO -- : {"endpoint":"03","param_id":"uPCmIwQYlWfjVU51"}
I, [2016-05-11T23:22:59.417142 #13]  INFO -- : {"endpoint":"02","param_id":"2gSWNFxHfxeen3AA"}
I, [2016-05-11T23:22:59.451105 #13]  INFO -- : {"endpoint":"05","param_id":"YwMAP32nd9o8Y7DE"}
I, [2016-05-11T23:22:59.492414 #13]  INFO -- : {"endpoint":"01","param_id":"4s1sf5InIZO25lW8"}
I, [2016-05-11T23:23:00.486291 #13]  INFO -- : {"endpoint":"03","param_id":"2gSWNFxHfxeen3AA"}

(snip)
  • Summary Report

f:id:inokara:20160512082507p:plain

GUI 版の JMeter では結果の CSV の出力がうまく出来なかった(エラーになる)のでコマンドラインツールでももう一回。(エラーになる原因は調査する)

% cd /path/to/bin/apache-jmeter-2.13/bin
% sh ./jmeter --nongui --testfile /path/to/jmx/sample.jmx --logfile /path/to/result/sample.csv
  • CSV を読み込んで Kibana で可視化

f:id:inokara:20160512090133p:plain

次は JMeter プラグインで用意されている可視化ツールを使ってみたい。


ということで...

以上

JMeter のシナリオを作る際に XML を直接触ったり、GUI をポチポチするのではなく、RubyDSL を使って書いてみた。XML を直接触る状況があるかどうか判らない判らないけど、GUI ポチポチよりもコードで管理出来るという点では ruby-jmeter を利用する価値はあると思う。

ということで、引続き、以下のような点について調べてみたい。

  • JMeter プラグインで提供されている結果可視化ツール
  • CSV を読み込むことが出来るのは判ったけど、ランダムではなく始めから読み込む方法 → デフォルトが頭からシーケンシャルに読み込む
  • GUICSV の出力が上手く動かなかった原因

ruby-jmeter でシナリオを書く時のコツ的なもの

こちらの記事でも言及されているが、スレッドグループやロジックコントローラ等のコンポーネントの各種定義は以下をチェックしていくと捗った。

有難うございます。


appendix

動作確認用アプリケーション

  • app.rb
require "sinatra"
require "unicorn"
require "socket"
require "logger"
require "json"

class App < Sinatra::Base

  logger = Logger.new($stdout)

  get "/hostname" do
    Socket.gethostname
  end

  get "/01" do
    data = {endpoint: "01", param_id: params["param_id"]}
    data.to_json
    logger.info data.to_json
  end

  get "/02" do
    data = {endpoint: "02", param_id: params["param_id"]}
    data.to_json
    logger.info data.to_json
  end

  get "/03" do
    data = {endpoint: "03", param_id: params["param_id"]}
    data.to_json
    logger.info data.to_json
  end

  get "/04" do
    data = {endpoint: "04", param_id: params["param_id"]}
    data.to_json
    logger.info data.to_json
  end

  get "/05" do
    data = {endpoint: "05", param_id: params["param_id"]}
    data.to_json
    logger.info data.to_json
  end

end
  • config.ru
require './app.rb'
run App
@path = "/myapp"

worker_processes 1
working_directory @path
timeout 300
listen 4567
pid "#{@path}/tmp/pids/unicorn.pid"
#stderr_path "#{@path}/log/unicorn.stderr.log"
#stdout_path "#{@path}/log/unicorn.stdout.log"
preload_app true
  • Dockerfile
FROM ruby:2.3.0
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev && \
    apt-get install -y nginx && \
    mkdir -p /myapp/tmp/pids /myapp/logs
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
ADD . /myapp
RUN chmod 755 run-app.sh && mkdir log && mkdir -p tmp/pids
#
CMD ["sh", "run-app.sh"]