【負荷試験】ブラウザ上で JMeter を操作できるツールを作ってみた話【Flanker】

こんにちは。
突然ですが、皆様は負荷試験を実施したことはございますでしょうか?
私は仕事柄、時折、負荷試験案件に携わることがございまして。
あくまで私のケースですが、おおよそ下記のようなフェーズを辿ることが多いように感じています。

1. 試験設計 ( 方針設計 / 目標決め / シナリオ作成 etc )
2. JMeter 環境構築
3. 負荷試験環境調整 ( 申請 / モニタリング環境 etc )
4. JMeter 実行 ( シナリオ調整 / JMeter 調整 含む )
5. 調査 / 解析
6. 「4」と「5」のエンドレス…

「1」に関してはお客様と詳細なやり取りを行い、いわば要件定義に近い折衝を行う必要がございまして、シナリオ作成に関しては API 仕様書を元に、JMeter から発行するリクエスト内容を定義していかなくてはなりません。

「2」や「3」に関しては、 Ansible 等のツールで省力化に取り組んでいるのですが、その他のフェーズでは依然として雑多なタスクが重なり、大変ストレスがマッハでございました。

経緯

さて、つきましては、雑多なタスク減を目的に、この度「4」の JMeter 操作周りを WEB ベースで完結できるツールを作ってみようと思った次第でございます。
始めに必要な機能を洗い出し、実装する機能を下記 5 ポイントに絞りました。

・シナリオ ( .jmx ) のアップロード
・シナリオ内 Thread group コンポーネントの調整 ( 負荷強度の調整等 etc )
・クラスタリングした JMeter に紐づく Worker ノードの操作
・JMeter の実行、および非同期での進捗確認
・実行結果生成

前提として JMeter を扱うのですから、 最終的には JMeter の WEB クライアントを作る形となります。

既に JMeter 環境のデプロイ用途で Ansible を使っているのもあり、ツールの実行環境含めて Playbook に内包しました。

できたもの

・Flanker

https://github.com/snkk1210/flanker

ツール、および JMeter 環境のデプロイは、下記 Ansible の Playbook を流していただければ完了いたします。

※ 詳細は README を参照ください。

https://github.com/snkk1210/jmeter-MS

実際にツールを使って案件を進めてみたのですが、かなり仕事が楽になったように思います。
※ 作った人間ばいあす

これからも、これ系のツールを作って業務の効率化を進めていきたいと思います。
わぁい。

【JMeter】BeanShell Pre/PostProcessor での JSON 操作例【シナリオ作成】

こんにちは。
負荷試験や性能テストでよく使われる JMeter ですが、コンポーネントの 1つに、BeanShell PreProcessor / PostProcessor という機能が存在します。
こちらは JMeter からのリクエストのレスポンス内容をパースして値を取得したり、発行するリクエストのパラメータを動的に変化させたりと、割となんでもできてしまう便利なコンポーネントになっています。
※ 処理自体は Java でスクリプトを記述していく必要があり、少し癖があるかもですが、それほど難易度が高いわけでもないと思います。

今回は実際にリクエストを投げる API を用意して、シナリオを作成しつつ良く使うパターンを備忘録として残しておこうと思います。
用意する API は Zabbix 辺りが都合よさそうだったので、こちらに対しての負荷試験シナリオ、という前提で作成していきます。
※ Zabbix API の仕様に関しては こちら を参照ください。

JMeter (始めに)

使用する JMeter は現時点で最新版である JMeter 5.5 を使用します。
また、JSON の操作が必要になってくるため、JSONObject のライブラリファイルを JMeter の lib/ ディレクトリ配下に配置しておきます。
※ ファイルは こちら から ZIP 形式でダウンロードできます。

また、シナリオ作成前に JMeter の下記辺りのコンポーネントを用意しておくのが良いかもしれないです。

・View Results Tree : シナリオデバッグ用途
・HTTP Request Defaults : リクエスト共通の設定を定義
※ 全ての Zabbix API は /zabbix/api_jsonrpc.php に対して POST メソッドを用いて API を叩きます。
・HTTP Header Manager : リクエスト共通のリクエストヘッダを定義
※ 全ての Zabbix API へのリクエストには下記のリクエストヘッダが必要です。

Content-Type: application/json-rpc

Zabbix API へは JSON 形式で POST リクエストを発行するため、作成する HTTP Request コンポーネントの Body Data には ${request} という変数を設定しておきます。
※ PreProcessor でリクエストの JSON データを生成します。

作成するシナリオは

user.login → hostgroup.get → host.create → host.get → host.delete → user.logout

の順に偏移する想定です。

シナリオ

シナリオ全体で扱う値用に PreProcessor を用意しておきます。
※ Zabbix ログインユーザとログ出力モード切替の定義用です。
今回はデフォルトの Zabbix 特権ユーザを使用します。

/** 
 * 各変数の初期化
 * 定数値等を設定
 * 
 * initialize Script
 * Bean Shell PreProcessor
 */
// Execute mode
 vars.put("MODE", "debug");
//vars.put("MODE", "release");

/** 
 * ログイン ユーザ
 */
// login User
vars.put("username", "Admin");

// Login Password
vars.put("password", "zabbix");

1. user.login

始めに Zabbix の認証キーを取得します。

・PreProcessor
Zabbix API の仕様に沿った通りに JSON のリクエストを発行します。
ユーザ情報は先の PreProcessor にて設定した値を呼び出しています。

import org.json.JSONObject;
import org.json.JSONArray;

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("user", vars.get("username"));
paramObject.put("password", vars.get("password"));

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成  
JSONObject objRequest = new JSONObject();

objRequest.put("method", "user.login");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データをセット
vars.put("request", objRequest.toString());

・PostProcessor
レスポンスから認証キーを取得します。
※ 認証キーは auth_key に格納しておき、以降のリクエストで利用します。

import org.json.JSONObject;

if (vars.get("MODE") == "debug") { log.info("-- Requested Sample(Pre): " + prev.getSampleLabel()); }

// レスポンスを取得
JSONObject json = new JSONObject(prev.getResponseDataAsString());

// レスポンスから 認証キー を取得
var result = json.getString("result");

if (vars.get("MODE") == "debug") { 
	log.info(result.toString());
}

// 変数にセット
vars.put("auth_key", result.toString());

2. hostgroup.get

ホストグループ Discovered hosts の詳細情報をリクエストします。

・PreProcessor
深い入れ子の JSON を生成する必要があるため、少しだけ工夫します。
また、先のリクエストにて取得した auth_key を呼び出して使用します。

import org.json.JSONObject;
import org.json.JSONArray;

// filter JSONObject 生成
JSONObject filterObject = new JSONObject();

filterObject.put("name", "Discovered hosts");

JSONArray filters = new JSONArray();
filters.put(filterObject);

JSONObject filter = filters.getJSONObject(0);

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("filter", filter);

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "hostgroup.get");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

3. host.create

Zabbix のホストを作成します。

・PreProcessor
作成するホストの名称は複数スレッドでリクエストを流した際の重複を避けるため、Random クラスの nextInt() を用いて、ランダムな値を付与します。
IP アドレスとポートは重複しても問題ないため、固定値に設定しています。

import org.json.JSONObject;
import org.json.JSONArray;
import java.util.Random;

// 0 ~ 999999999 の数値を取得
Random rand = new Random();
long user_id = rand.nextInt(999999999);

// groups JSONObject 生成
JSONObject groupsObject = new JSONObject();

groupsObject.put("groupid", vars.get("groupid"));

JSONArray groups = new JSONArray();
groups.put(groupsObject);

JSONObject group = groups.getJSONObject(0);

// interfaces JSONObject 生成
JSONObject interfacesObject = new JSONObject();

interfacesObject.put("type", 1);
interfacesObject.put("main", 1);
interfacesObject.put("useip", 1);
interfacesObject.put("ip", "192.168.10.100");
interfacesObject.put("dns", "");
interfacesObject.put("port", "10050");

JSONArray interfaces = new JSONArray();
interfaces.put(interfacesObject);

JSONObject interfac = interfaces.getJSONObject(0);


// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("host", "Test Server" + user_id.toString());
paramObject.put("groups", group);
paramObject.put("interfaces", interfac);

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.create");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

4. host.get

Zabbix のホスト一覧を取得し、ランダムにホスト ID を取得します。

・PreProcessor

import org.json.JSONObject;
import org.json.JSONArray;

// param JSONObject 生成
JSONObject paramObject = new JSONObject();

paramObject.put("output", "extend");

JSONArray params = new JSONArray();
params.put(paramObject);

JSONObject param = params.getJSONObject(0);

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.get");
objRequest.put("id", 1);
objRequest.put("params", param);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

・PostProcessor
こちらも Random クラスのメソッドを用いて、レスポンスからランダムな hostid を取得し変数に格納しておきます。
※ 後述のホストの削除にて利用します。

import org.json.JSONObject;
import java.util.Random;

if (vars.get("MODE") == "debug") { log.info("-- Requested Sample(Pre): " + prev.getSampleLabel()); }

// レスポンス取得
JSONObject json = new JSONObject(prev.getResponseDataAsString());

var result = json.getJSONArray("result");

// JsonObject の要素数を数える
int length = result.length();

// ランダムな要素数を選定
Random rand = new Random();
int num = rand.nextInt(length);

// ランダムな要素を取得 
JSONObject jObj1 = result.getJSONObject(num);

var hostid = jObj1.getString("hostid");

if (vars.get("MODE") == "debug") { 
	log.info(hostid.toString());
}

// 変数セット
vars.put("hostid", hostid.toString());

5. host.delete

Zabbix のホストを 1つ削除します。

・PreProcessor
先ほど取得した hostid を利用してリクエストを発行します。

import org.json.JSONObject;
import org.json.JSONArray;

String[] hostidArr;
hostidArr = new String[1];
hostidArr[0] = vars.get("hostid");

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "host.delete");
objRequest.put("id", 1);
objRequest.put("params", hostidArr);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

6. user.logout

最後にログアウトして認証キーを無効化しておきます。

・PreProcessor

import org.json.JSONObject;
import org.json.JSONArray;

// POST Body (JSON) 生成 
JSONObject objRequest = new JSONObject();

objRequest.put("auth", vars.get("auth_key"));
objRequest.put("method", "user.logout");
objRequest.put("id", 4);
objRequest.put("params", null);
objRequest.put("jsonrpc", "2.0");

if (vars.get("MODE") == "debug") {
    log.info(objRequest.toString());
}

// データのセット
vars.put("request", objRequest.toString());

終わりに

恐らく BeanShell PreProcessor / PostProcessor を使えば、書き方次第で、おおよその要件は叶えられるんじゃないでしょうか?
※ 今回のシナリオファイルのサンプルは こちら に配置しております。

話は変わりますが、こちら に JMeter クラスタ環境デプロイ用の Ansible Playbook を配置しておりますのでご利用頂ければ嬉しいです。

awk で特定の行の値を取得して計算してみる(Linux のモニタリングツールを作る)

Linux のモニタリングツールといえば、dstat、sarなんかが有名だと思いますが、環境によってはツールが入っていないこともしばしば。
というわけで、linux にプリインストールされているコマンドで必要最低限のモニタリングツールを用意してみます。

ログ調査とかテキストの整形とかでお世話になる awk ですが、実は計算が出来るんですよね。
例えば、 cat /proc/meminfo の出力結果から実メモリの使用率を計算してみようと思います。

/proc/meminfo

ちなみに meminfo の内容はこちら。
MemTotal、MemFree、MemAvailable の値を取得できれば、メモリ使用率を求めることができそうですね。
※ 今回はawkの挙動を確認したいので、あえて MemTotal と MemAvailable を取得してみます。

[root@verienv01 ~]# cat /proc/meminfo | head
MemTotal:         498672 kB
MemFree:           77104 kB
MemAvailable:     363416 kB
Buffers:               0 kB
Cached:           261200 kB
SwapCached:            4 kB
Active:           147176 kB
Inactive:         150668 kB
Active(anon):        492 kB
Inactive(anon):    43400 kB

awkでメモリ使用率を求める

awk で行を指定したい場合は「NR」を使えばよくて、1行目と3行目で且つ、スペースで区切った左から2列目の値を取得したいのでこうですね。

[root@verienv01 ~]# cat /proc/meminfo | awk 'NR==3 {print $2} NR==1 {print $2}'
498672
363332

取得した値をワンライナーで変数ava(MemAvailable)と変数total(MemTotal)に入れて、計算まで行う場合はこうです!
[root@verienv01 ~]# cat /proc/meminfo | awk 'NR==3 {ava=$2} NR==1 {total=$2} {per = ava / total}END{print 100 - per * 100}'
27.1136

小数点以下は切り捨てます。
これでバッファキャッシュも考慮したメモリ使用率を出力できましたね。
[root@verienv01 ~]# cat /proc/meminfo | awk 'NR==3 {ava=$2} NR==1 {total=$2} {per = ava / total}END{print 100 - per * 100}' | sed s/\.[0-9,]*$//g
27

モニタリングツール

というわけで、CPU使用率、メモリ使用率、ロードアベレージ、タイムスタンプを出力するシェルスクリプトを用意します。

#!/bin/bash

MEMORY=$(cat /proc/meminfo | awk 'NR==3 {ava=$2} NR==1 {total=$2} {per = ava / total}END{print 100 - per * 100}' | sed s/\.[0-9,]*$//g)
CPU=$(vmstat | awk 'NR==3 {print $13,$14}')
LA=$(uptime | awk '{print $8,$9,$10}')
TIME=$(vmstat -t | awk 'NR==3 {print $18,$19}')

echo "--cpu-- --mem--  --------la--------  --------time--------"
echo " " $CPU "    " $MEMORY "   " $LA " " $TIME

いろいろとツッコミ処があると思いますが、、まぁいいでしょう。
[root@verienv01 ~]# ./monitor.sh
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-05 23:56:43

whileでループすれば、モニタリングツールの出来上がり(小並感)
[root@verienv01 ~]# while true; do ./monitor.sh ; sleep 1; done
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:42
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:43
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:44
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:45
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:46
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:48
--cpu-- --mem--  --------la--------  --------time--------
  0 0      27     0.00, 0.01, 0.01   2020-12-06 00:02:49

終わりに

正直 dstat と sar と vmstat でいいですね。

リストファイルを分割して複数のサーバに配布するスクリプト

JMeter での負荷試験時に slave の個数分だけ CSVファイルを分割して、それぞれの slave サーバに scp で転送したかったのです。
そんな背景で取り急ぎ作成したよくわからない何かです。

スクリプトみたいな何か

#!/bin/bash

############################################
# 第一引数に配布先マシンのIPアドレス(9つまで)
# 第二引数に分割して配布するリストファイル
############################################

# ファイルの転送先ディレクトリ
DIR=/var/tmp/dlist.txt
# 接続ユーザ
USER=vagrant
# 接続に使用する秘密鍵
SECRET_KEY=/home/vagrant/.ssh/id_rsa

# 引数の数をチェック
if [ $# != 2 ]; then
    echo "引数の数が不正です"
    exit 1
fi

# 配布先マシンの数量を取得
TARGET_COLUMN=`cat $1 | wc -l`
# リストファイルの行数を取得
LIST_COLUMN=`cat $2 | wc -l`
# 1ターゲット辺りのリスト数を取得
ONE_TARGET_NUM=$[LIST_COLUMN / TARGET_COLUMN]

# 配布先マシンのIPアドレスが9つ以上あるかチェック
if [ $TARGET_COLUMN -gt 9 ]; then
    echo "配布先IPアドレスは9つまでしか設定できません"
    exit 1
fi

# 配布先マシンの数量分だけファイルを分割する
split -l $ONE_TARGET_NUM -a 1 --numeric-suffixes=1 $2 list.

# 分割したファイルを転送する
while read VAL;
do
  NUM=$((NUM+1))
  scp -i $SECRET_KEY ./list.$NUM $USER@$VAL:$DIR

  if [ $? -ne 0 ]; then
    echo "scp コマンドが正常に終了しませんでした"
    exit 1
  fi

  # 分割したファイルを削除
  rm -f ./list.$NUM
done < $1

スクリプトの第一引数に下記のような形で転送先サーバの IP アドレスを記載したファイルを渡せば OK です。
※ 第二引数に分割したいリストファイルを指定してください。
192.168.33.11
192.168.33.12
192.168.33.13
192.168.33.14
192.168.33.15
192.168.33.16

JMeter5.3 で The JVM should have exited but did not のエラーがでた話

JMeter5.3 のクラスタ環境を作って、CLI から JMeter を実行すると表題のエラーが出たんですね。

... end of run
The JVM should have exited but did not.
The following non-daemon threads are still running (DestroyJavaVM is OK):
Thread[DestroyJavaVM,5,main], stackTrace:
Thread[AWT-EventQueue-0,6,main], stackTrace:sun.misc.Unsafe#park
java.util.concurrent.locks.LockSupport#park at line:175
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject#await at line:2039
java.awt.EventQueue#getNextEvent at line:554
java.awt.EventDispatchThread#pumpOneEventForFilters at line:187
java.awt.EventDispatchThread#pumpEventsForFilter at line:116
java.awt.EventDispatchThread#pumpEventsForHierarchy at line:105
java.awt.EventDispatchThread#pumpEvents at line:101
java.awt.EventDispatchThread#pumpEvents at line:93
java.awt.EventDispatchThread#run at line:82

Thread[AWT-Shutdown,5,system], stackTrace:java.lang.Object#wait
sun.awt.AWTAutoShutdown#run at line:314
java.lang.Thread#run at line:748

どうも、シナリオ内にスクリプトレコーダを入れていると、CLI モードで実行した際に処理が正常に終了しなくなる不具合があるとのことです。
https://bz.apache.org/bugzilla/show_bug.cgi?id=64479

対応としては、シナリオ中のスクリプトレコーダを削除すればよいようです。

【Python】JMeter の実行結果をスプレッドシートに出力するスクリプトを書いてみた【gspread】

こんにちは。
JMeter が出力する結果を Google のスプレッドシートに出力してみようと考えまして、調べてみると Python に便利なライブラリがあったんで作ってみました。
※ 下記の JMeter デプロイ用 Playbook にも入れているのでよければご利用くださいませ。
https://github.com/keisukesanuki/jmeter-MS

Python スクリプト

#!/usr/bin/python3

import gspread
import json
import csv
import sys
import itertools

# シークレットキーを絶対パスで指定
SECRETJSON = "/usr/local/jmeter/bin/sacred-drive.json"
# スプレッドシートキーを定義
SPREADSHEET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

############################################################################
## 関数

# 数字と文字コンバーター
def num2alpha(num):
    if num<=26:
        return chr(64+num)
    elif num%26==0:
        return num2alpha(num//26-1)+chr(90)
    else:
        return num2alpha(num//26)+chr(64+num%26)

#############################################################################
## 認証

# お約束
from oauth2client.service_account import ServiceAccountCredentials
scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']

# ダウンロードした json ファイルを定義
credentials = ServiceAccountCredentials.from_json_keyfile_name(SECRETJSON, scope)

# Google API にログイン
gc = gspread.authorize(credentials)

# スプレッドシートのシート1を開く
worksheet = gc.open_by_key(SPREADSHEET_KEY).sheet1


##############################################################################
## 処理

# コマンドライン引数を取得
args = sys.argv
csvfile = args[1]

# CSVファイルの内容を配列に代入
with open(csvfile) as fp:
    results_list_ex = list(csv.reader(fp))

# 2次元配列を1次元配列に変換
results_list = list(itertools.chain.from_iterable(results_list_ex))

# カウント変数初期化
COUNT_NUM = 1
# 空白行探索
while str(len(worksheet.cell(COUNT_NUM, 1).value)) != "0":
        COUNT_NUM += 1

# 編集する範囲を指定
cell_list = worksheet.range('A'+str(COUNT_NUM)+':'+num2alpha(len(results_list))+str(COUNT_NUM))

# cell_listにresults_listの配列を代入
for i,cell in enumerate(cell_list):
    cell.value = results_list[i]

# 結果の保存
worksheet.update_cells(cell_list)

第一引数に csv ファイルを指定することで、結果をスプレッドシートに出力するスクリプトです。
このスクリプトを後述の JMeter 起動用スクリプトで利用します。

シェルスクリプト

#!/bin/sh

DATE=$(date +"%Y%m%d")
OPTIME=$(date +"%Y%m%d-%H%M%S")
# 結果の出力先ディレクトリを指定
LOGDIR=/var/www/html/${DATE}
# JMXファイルを指定
FILE_JMX=/usr/local/jmeter/bin/templates/build-web-test-plan.jmx

# 日付ディレクトリの作成
mkdir -p ${LOGDIR}

# JMeter 起動
/usr/local/jmeter/bin/jmeter -Dsun.net.inetaddr.ttl=0 -n -t ${FILE_JMX} -j ${LOGDIR}/${OPTIME}.log -l ${LOGDIR}/${OPTIME}.jtl -e -o ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/ -r

# CSV ファイルの作成
cat ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/statistics.json | jq  -r ". [] | [.transaction,.sampleCount,.errorCount,.errorPct,.meanResTime,.minResTime,.maxResTime,.pct1ResTime,.pct2ResTime,.pct3ResTime,.throughput,.receivedKBytesPerSec,.sentKBytesPerSec] | @csv" | grep "Total" > ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/statistics.csv

# スプレッドシートに結果を出力
/usr/local/bin/main.py ${LOGDIR}/${OPTIME}_th/statistics.csv

JMeter は json で結果を出力するので jq で無理やり csv に変換してます。

Python だと簡単に実装できて楽ですね。

補足

スクリプトの実行に下記のパッケージ導入が必要です。

yum install python3 python-devel jq
pip3 install gspread
pip3 install oauth2client

【Ansible】Master/Slave構成の JMeter をデプロイする【IaC】

Ansible で Master/Slave 構成の JMeter をデプロイする Playbook を作ってみました。
https://github.com/keisukesanuki/jmeter-MS.git
※ 詳細は GitHub の README を参照くださいませ。

ディレクトリ構成

.
├── README.md
├── ansible.cfg
├── group_vars
│   ├── all.yml
│   └── all.yml.example
├── hosts.example
├── roles
│   ├── common
│   │   ├── README.md
│   │   └── tasks
│   │       ├── etckeeper_commit.yml
│   │       ├── host_change.yml
│   │       └── main.yml
│   ├── dummy
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── dummy.txt
│   ├── jmeter51
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter53
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter54
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── jmeter55
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter.properties
│   │   │   └── start-controller_cui.sh
│   │   └── tasks
│   │       └── main.yml
│   ├── minimum
│   │   └── httpd
│   │       ├── README.md
│   │       ├── handlers
│   │       │   └── main.yml
│   │       ├── tasks
│   │       │   ├── main.yml
│   │       │   └── security.yml
│   │       └── templates
│   │           ├── mpm.conf
│   │           └── security.conf
│   ├── python-scripts
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       ├── csv2gspread.py
│   │       ├── sacred-drive.json
│   │       └── start-controller_cui.sh
│   ├── reboot
│   │   └── tasks
│   │       └── main.yml
│   ├── slave-jmeter
│   │   ├── README.md
│   │   ├── files
│   │   │   ├── jmeter-node.service
│   │   │   └── jmeter-server.sh
│   │   └── tasks
│   │       └── main.yml
│   └── tigervnc
│       ├── README.md
│       ├── files
│       │   ├── vncpasswd.sh
│       │   └── vncserver@.service_root
│       └── tasks
│           └── main.yml
├── scenario
│   └── example
│       └── zabbix
│           └── zabbix_load_scenario.jmx
├── target.yml
└── target.yml.example

38 directories, 51 files