負荷ツールとしてポピュラーなツールの1つがk6です。
Poc程度の開発であれば、後先のことはそこまで考慮せずに、
動作確認のためのコーディングを行うでも問題無いと思います。
しかしながら、k6で負荷ツールを開発し、長期的に運用することを考えた場合、
リポジトリ上の拡張性や保守性を確保しつつ、開発を進めたいものです。
そこで、この記事では筆者が考えるk6実行時のオプション値の渡し方のプラクティスを紹介します。
先に結論から
実現したいことをさっと要約すると、以下の通りです。
- シナリオの設定値や、シナリオ毎のオプションはjsonで管理する
- 負荷ツール共通で設定したいようなデフォルト値、プログラム上で管理する
- k6にオプションを渡す際には、上記の2つの設定をマージした上で値を渡す
- なお、優先度は デフォルト値 < jsonの設定値
以下のPRを見れば、やりたいことは何となく想像がつくかもしれません。
PR: https://github.com/gonkunkun/k6-template/pull/1/files
オプションの設定方法
直接optionsへ設定値を渡す
k6の公式ページにあるような例が、ポピュラーな設定方法だと思います。
import http from 'k6/http';
export const options = {
hosts: { 'test.k6.io': '1.2.3.4' },
stages: [
{ duration: '1m', target: 10 },
{ duration: '1m', target: 20 },
{ duration: '1m', target: 0 },
],
thresholds: { http_req_duration: ['avg<100', 'p(95)<200'] },
noConnectionReuse: true,
userAgent: 'MyK6UserAgentString/1.0',
};
export default function () {
http.get('http://test.k6.io/');
}
https://k6.io/docs/using-k6/k6-options/how-to/#set-options-in-the-script
不便な点は以下の通りです
- 1つのシナリオセットのみしか管理出来ない
- 実際に負荷試験を進めていくと、ロードテスト、スパイクテスト、etc… のような複数のシナリオセットを準備することが想定されます
- 上記の例の場合、異なるシナリオセットを実行する場合には、わざわざコードを修正する必要があります
そこで、一部の定義はどこか別の場所へ切り出して管理したくなります。
オプションをjsonファイルで管理する
これも、k6の公式ページに例があります。
https://k6.io/docs/using-k6/k6-options/how-to/#set-options-with-the-config-flag
以下のように、k6実行時の引数としてjsonファイルを渡すと良いわけです。
k6 run --config options.json script.js
{
"hosts": {
"test.k6.io": "1.2.3.4"
},
"stages": [
{
"duration": "1m",
"target": 10
},
{
"duration": "1m",
"target": 30
},
{
"duration": "1m",
"target": 0
}
],
"thresholds": {
"http_req_duration": ["avg<100", "p(95)<200"]
},
"noConnectionReuse": true,
"userAgent": "MyK6UserAgentString/1.0"
}
このようにすることで、loadTest.json, spikeTest.jsonという風に、シナリオセット毎に定義しつつ、k6実行時に自由にパラメータを渡すことが出来ます。
この時点での不便な点は以下の通りです。
- シナリオセット同士で共通の設定値を持つことが難しい(出来なくはない)
- 例えば、k6には特定のドメインにはリクエストを飛ばせないようにするオプションがあります(https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/#blacklist-ip)
- 間違ってproduction環境には負荷をかけないように、シナリオセット共通でこの設定をしておきたくなるわけです
- 各jsonファイルにこの設定をしておくのも無駄です
共通のデフォルト値を設定出来るようにする
シナリオセット毎の共通のデフォルト値は以下のように設定することが出来ます。
import exec from 'k6/execution';
export const options = {
stages: [
{ duration: '5s', target: 100 },
{ duration: '5s', target: 50 },
],
};
export default function () {
console.log(exec.test.options.scenarios.default.stages[0].target); // 100
}
個別の設定は–configオプションを経由して、jsonファイルから読み込ませます。
k6 run --config options.json script.js
{
"hosts": {
"test.k6.io": "1.2.3.4"
},
"stages": [
{
"duration": "1m",
"target": 10
},
{
"duration": "1m",
"target": 30
},
{
"duration": "1m",
"target": 0
}
],
"thresholds": {
"http_req_duration": ["avg<100", "p(95)<200"]
},
"noConnectionReuse": true,
"userAgent": "MyK6UserAgentString/1.0"
}
これは良さそうです。
ただし、この方法にも1つ問題があります。
それは、デフォルト値を上書き変更出来ないことです。
このままだと、JSONファイル側の設定が優先されません。
以下がオプションが適用される優先順位です。
https://k6.io/docs/using-k6/k6-options/how-to/#order-of-precedence
デフォルト値は”Script options”、JSONファイルは”–config”にそれぞれ該当します。
※ 紛らわしいですが、”Defaults”はk6のオプションが指定されていない場合のデフォルト値を指しています。本記事で設定したいデフォルト値とは別です。
“Script options”の方が優先順位が高いのです。
JSONファイルの設定値を使って、”Script options”で設定したデフォルト値を上書きして欲しいですよね。
JSONファイルのオプションを優先させる
ようやくこの記事で伝えたかった本題に戻ってきました。
改めて、やりたいことは以下の通りです。
- シナリオの設定値や、シナリオ毎のオプションはjsonで管理する
- 負荷ツール共通で設定したいようなデフォルト値、プログラム上で管理する
- k6にオプションを渡す際には、上記の2つの設定をマージした上で値を渡す
- なお、優先度は デフォルト値 < jsonの設定値
“–config”オプションを利用して、JSONファイルを渡す場合、上記の実現は難しいです。
ちなみに、”–config”オプションの扱いや立ち位置については、以下のコミュニティでも言及されています。
https://github.com/grafana/k6-docs/issues/688
“–config”を使うよりも、JSONファイルを読み込んで、optionsに渡す方がいいよ。という話がなされています。
In 99%, we should recommend modularizing functionality/options using ES5 modules like the k6-purina example. Not using the
--config
option.
// load test config, used to populate exported options object:
const testConfig = JSON.parse(open('./config/test.json'));
// combine the above with options set directly:
export const options = testConfig;
上記の例に手を加えて、optionsにはデフォルト値とJSONファイルから読み込んだ値の2つをマージした値を渡すようにしてあげましょう。
実際のサンプルコードはこちらにあります
https://github.com/gonkunkun/k6-template/pull/1/files
k6を実行するときのコマンドイメージは以下の通り。
smoke:sample-product": "./k6 run ./dist/loadTest.js -e CONFIG_PATH=./src/sample-product/configs/smoke.json
“CONFIG_PATH”で渡されたpathに従って、オプションを読み込みます。
env.tsを間にかませます。
// @ts-ignore
import { parse } from "k6/x/dotenv"
// eslint-disable-next-line @typescript-eslint/naming-convention
const _ENV = parse(open("../.env"))
export const ENV = _ENV.ENV || 'local'
export const CONFIG_PATH = _ENV.CONFIG_PATH || ENV.CONFIG_PATH || '../src/sample-product/configs/smoke.json'
config.tsを準備します。
import * as env from '../common/env'
export const OPTIONS_CONFIG = (): Record<string, unknown> => {
const defaultConfigs = {
blockHostnames: ['*.hogehoge.com'],
insecureSkipTLSVerify: true,
}
const configs = JSON.parse(open(env.CONFIG_PATH))
return Object.assign({}, defaultConfigs, configs)
}
env.tsから渡されてきた”CONFIG_PATH”を受け取り、JSONファイルを読み込みます。
その上で、defaultConfigsとconfigsをマージして、その値を返却します。
あとはシナリオ側から、上記の関数を呼び出してあげましょう。
import { OPTIONS_CONFIG } from './configs/config'
export const options = OPTIONS_CONFIG()
export function setup(): void {
略
これにより、例えば、blockHostnamesの設定を一時的に変更したい場合には、
JSONファイル側でオプションを追加すれば上書き出来るようになります。
ロジックとオプションを良い感じに分離出来るようになりました。
おわりに
今回はk6へのオプション値の良さげな渡し方について考えてみました。
他にも良い例があれば知りたいです。
ご意見があればどんどん教えて下さい。