share facebook facebook2 twitter menu hatena pocket slack

2016.06.13 MON

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

川原 洋平

WRITTEN BY川原 洋平

ども、かっぱです。

tl;dr

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

flood-io/ruby-jmeter
ruby-jmeter - A Ruby based DSL for building JMeter test plans

github.com

参考

有難うございますmm

ruby-jmeter とは

Ruby DSL で

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

flood.io

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

Cloud Load Testing for Everyone with JMeter or Gatling | Flood IO
Flood IO lets you run distributed load tests across the globe with JMeter or Gatling. Use our On Demand infrastructure or Host Your Own cloud.

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 に読み込む

  • ユーザー定義変数

20160512075800

  • スレッドグループ

20160512075908

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

原因は…こちらの

  
    false
    -1
  

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

  • スループットコントローラ

20160512075917

  • HTTP リクエスト

20160512081925

  • CSV Data Set Config

20160512091142

  • 定数タイマ

20160512075933

シナリオ実行

事前に動作確認用アプリケーションを起動しておいて、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

20160512082507

  • コマンドラインでもう一度

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 で可視化

20160512090133

次は 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"]

元記事はこちら

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