ども、かっぱです。
tl;dr
JMeter で行う負荷試験のシナリオを触る機会を得たので、以前から導入したかった ruby-jmeter を導入しようとしたメモ。
参考
- https://github.com/flood-io/ruby-jmeter
- http://jmeter.apache.org/usermanual/
- http://sy5.sakura.ne.jp/jmeter/ref/debugsampler.html
- http://sy5.sakura.ne.jp/jmeter/ref/contoller/throughputcontroller.html
- http://gongo.hatenablog.com/entry/2013/10/14/215157
有難うございますmm
ruby-jmeter とは
Ruby DSL で
JMeter のシナリオファイル(XML)を書き出して下さるツール。
flood.io
という負荷試験の SaaS ベンダーがメインでメンテナンスしている。
DSL
ざっくりとソースコードを拝見したところ、各機能の XML をヒアドキュメントで読み込んでいるメソッドを読み込んで、メソッドのパラメータを埋め込んで、最終的に一つの XML を生成するような挙動に見える。実際に lib/ruby-jmeter/dsl/ 以下を見ると沢山の機能別 Ruby スクリプトが保存されている。
実際には、もっと奥深い仕組みが施されていると思われるが...。
試しにシナリオを書いてみる
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 に読み込む
- ユーザー定義変数
- スレッドグループ
本家の 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 を書いたり、JMeter の GUI で設定する場合には問題は起きない)
- スループットコントローラ
- HTTP リクエスト
- CSV Data Set Config
- 定数タイマ
シナリオ実行
事前に動作確認用アプリケーションを起動しておいて、GUI 版 JMeter でシナリオを実行すると...
- アプリケーションのログ
(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
- コマンドラインでもう一度
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 で可視化
次は JMeter プラグインで用意されている可視化ツールを使ってみたい。
ということで...
以上
JMeter のシナリオを作る際に XML を直接触ったり、GUI をポチポチするのではなく、Ruby の DSL を使って書いてみた。XML を直接触る状況があるかどうか判らない判らないけど、GUI ポチポチよりもコードで管理出来るという点では ruby-jmeter を利用する価値はあると思う。
ということで、引続き、以下のような点について調べてみたい。
- JMeter プラグインで提供されている結果可視化ツール
- CSV を読み込むことが出来るのは判ったけど、ランダムではなく始めから読み込む方法 → デフォルトが頭からシーケンシャルに読み込む
- GUI で CSV の出力が上手く動かなかった原因
ruby-jmeter でシナリオを書く時のコツ的なもの
こちらの記事でも言及されているが、スレッドグループやロジックコントローラ等のコンポーネントの各種定義は以下をチェックしていくと捗った。
- README.md を確認する
- サンプルを写経する
- lib/ruby-jmeter/DSL.md をチェックする
- lib/ruby-jmeter/dsl をチェックする
- lib/ruby-jmeter/dsl.rb をチェックする
有難うございます。
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
- unicorn.rb
@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"]