負荷テストツール「k6」入門
こんにちは。
PharmaX でエンジニアをしている諸岡(@hakoten)です。
この記事の概要
APIの負荷テストツールにGrafana Labs社が開発している「k6」というツールがあります。
k6はオープンソースのCLIツールですが、 「Grafana Cloud k6」というクラウドベースSaaSツールも提供されている便利なツールです。
ローカルのk6は、負荷テストの時に使ったことはあったのですが、真面目に負荷テストの設計をするにあたり、ちゃんと理解したかったため、改めて基本から調べてみました。k6の入門記事としてお役に立てれば嬉しいです。
インストール
Macでは、k6を「Homebrew」でインストールすることができます。
brew install k6
その他の環境については、以下の公式ページを参照ください。
テスト実行の基本
k6を使ったテスト実行について、基本的な書き方について紹介します。
テストファイルを作成する
k6 new
k6 new
コマンドでテストファイルのテンプレートを作成することができます。
k6 new <ファイル名>
と指定することで、ファイル名を指定してテンプレートを作成することもできます。
k6 new hoge.js
出力されるテンプレート
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
// A number specifying the number of VUs to run concurrently.
vus: 10,
// A string specifying the total duration of the test run.
duration: '30s',
// The following section contains configuration options for execution of this
// test script in Grafana Cloud.
//
// See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/
// to learn about authoring and running k6 test scripts in Grafana k6 Cloud.
//
// ext: {
// loadimpact: {
// // The ID of the project to which the test is assigned in the k6 Cloud UI.
// // By default tests are executed in default project.
// projectID: "",
// // The name of the test in the k6 Cloud UI.
// // Test runs with the same name will be grouped.
// name: "hoge.js"
// }
// },
// Uncomment this section to enable the use of Browser API in your tests.
//
// See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more
// about using Browser API in your test scripts.
//
// scenarios: {
// // The scenario name appears in the result summary, tags, and so on.
// // You can give the scenario any name, as long as each name in the script is unique.
// ui: {
// // Executor is a mandatory parameter for browser-based tests.
// // Shared iterations in this case tells k6 to reuse VUs to execute iterations.
// //
// // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.
// executor: 'shared-iterations',
// options: {
// browser: {
// // This is a mandatory parameter that instructs k6 to launch and
// // connect to a chromium-based browser, and use it to run UI-based
// // tests.
// type: 'chromium',
// },
// },
// },
// }
};
// The function that defines VU logic.
//
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
// about authoring k6 scripts.
//
export default function() {
http.get('https://test.k6.io');
sleep(1);
}
テスト実行
テストの実行は、k6 run <ファイル名>
で行うことができます。
k6 run script.js
テスト実行のサンプル
k6 run script.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
data_received..................: 2.9 MB 94 kB/s
data_sent......................: 28 kB 895 B/s
http_req_blocked...............: avg=18.94ms min=2µs med=10µs max=509.55ms p(90)=17.1µs p(95)=55.74µs
http_req_connecting............: avg=7.97ms min=0s med=0s max=217.76ms p(90)=0s p(95)=0s
http_req_duration..............: avg=207.13ms min=172.76ms med=201.73ms max=420.72ms p(90)=219.72ms p(95)=251.84ms
{ expected_response:true }...: avg=207.13ms min=172.76ms med=201.73ms max=420.72ms p(90)=219.72ms p(95)=251.84ms
http_req_failed................: 0.00% ✓ 0 ✗ 250
http_req_receiving.............: avg=8.81ms min=33µs med=160.5µs max=210.73ms p(90)=266.6µs p(95)=897.19µs
http_req_sending...............: avg=46.04µs min=7µs med=43µs max=403µs p(90)=60.2µs p(95)=80µs
http_req_tls_handshaking.......: avg=8.07ms min=0s med=0s max=219.54ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=198.27ms min=172.62ms med=200.7ms max=277.44ms p(90)=216.02ms p(95)=219.71ms
http_reqs......................: 250 8.010172/s
iteration_duration.............: avg=1.22s min=1.17s med=1.2s max=1.72s p(90)=1.22s p(95)=1.41s
iterations.....................: 250 8.010172/s
vus............................: 2 min=2 max=10
vus_max........................: 10 min=10 max=10
running (0m31.2s), 00/10 VUs, 250 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 30s
実行オプション
k6では様々なテストケースに対応するために実行オプションが数多く用意されています。
オプションの指定方法には次のようなものがあります。
- スクリプトファイルの
option オブジェクト
に書く - CLIのオプションとして渡す
他にも環境変数で指定することもできます。
スクリプトファイルで指定する場合は、以下のoptionオブジェクトをテストファイルのJavascriptで定義してください。
export const options = {
vus: 10,
duration: '30s',
};
CLIのオプションで渡す場合は、次のようにコマンドのパラメタとして渡します。
k6 run --vus 10 --duration 30s script.js
主要なオプションと使い方
ここではいくつかの主要なoptionについて、紹介します。
VUs
vus
オプションは、同時に実行される仮想ユーザー(VUs)の数を指定します。このオプションは、テストの負荷レベルを設定する際に使用されます。
import http from 'k6/http';
export const options = {
vus: 10, // 10のVUsを使用
duration: '30s',
};
export default function () {
http.get('<http://test.k6.io>');
}
この例では、10のVUsを30秒間実行するテストを定義しています。
Duration
duration
オプションは、テストの総実行時間を指定します。このオプションは、テストがどのくらいの時間実行されるべきかを定義する際に使用されます。
import http from 'k6/http';
export const options = {
vus: 10,
duration: '1m', // 1分間実行
};
export default function () {
http.get('<http://test.k6.io>');
}
この例では、10のVUsを1分間実行するテストを定義しています。
Stages・Target
stages
オプションは、テストの実行中にVUsの数を段階的に増減させるために使用されます。これにより、テストの負荷を時間の経過とともに徐々に増加させたり、ピークに達した後に徐々に減少させたりすることができます。target
はそのステージでの VUsを指します。
import http from 'k6/http';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // 最初の30秒でVUsを0から20まで増加
{ duration: '1m', target: 10 }, // 次の1分間でVUsを20から10まで減少
{ duration: '30s', target: 0 }, // 最後の30秒でVUsを10から0まで減少
],
};
export default function () {
http.get('<http://test.k6.io>');
}
この例では、テストの負荷を段階的に増減させるシナリオを定義しています。
Thresholds
thresholds
オプションは、テストの成功条件を定義するために使用されます。しきい値は、特定のメトリックが満たすべき条件を指定します。
import http from 'k6/http';
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'], // 95%のリクエストが500ms未満で完了すること
},
};
export default function () {
http.get('<http://test.k6.io>');
}
この例では、95%のリクエストが500ミリ秒未満で完了することをテストの成功条件として定義しています。
他のオプションについては公式リファレンスを参照ください。
メトリクスの見方
k6をの実行結果として表示される主要メトリクスについて説明します。
テスト結果のメトリクスサンプル
run script.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
data_received..................: 2.9 MB 94 kB/s
data_sent......................: 28 kB 899 B/s
http_req_blocked...............: avg=16.83ms min=2µs med=10µs max=436.21ms p(90)=15µs p(95)=31.54µs
http_req_connecting............: avg=7.88ms min=0s med=0s max=211.37ms p(90)=0s p(95)=0s
http_req_duration..............: avg=208.75ms min=178.49ms med=193.19ms max=432.13ms p(90)=221.67ms p(95)=363.42ms
{ expected_response:true }...: avg=208.75ms min=178.49ms med=193.19ms max=432.13ms p(90)=221.67ms p(95)=363.42ms
http_req_failed................: 0.00% ✓ 0 ✗ 250
http_req_receiving.............: avg=13.57ms min=35µs med=165µs max=202.37ms p(90)=361µs p(95)=179.68ms
http_req_sending...............: avg=43.51µs min=10µs med=39.5µs max=927µs p(90)=57µs p(95)=63.55µs
http_req_tls_handshaking.......: avg=8.29ms min=0s med=0s max=212.13ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=195.13ms min=178.27ms med=191.69ms max=246.19ms p(90)=209.63ms p(95)=220.17ms
http_reqs......................: 250 8.042534/s
iteration_duration.............: avg=1.22s min=1.17s med=1.19s max=1.64s p(90)=1.35s p(95)=1.4s
iterations.....................: 250 8.042534/s
vus............................: 1 min=1 max=10
vus_max........................: 10 min=10 max=10
主なメトリクスとしては次のようなものがあります。
指標名 | 意味 |
---|---|
checks | チェック関数の成功割合 |
http_req_duration | HTTPリクエストの完了にかかった合計時間 |
http_req_failed | 失敗したHTTPリクエストの割合 |
iteration_duration | 1回のイテレーションを完了するのにかかった時間 |
vus | アクティブな仮想ユーザー(VU)の数 |
vus_max | 最大VU数 |
http_reqs | テスト中に生成されたHTTPリクエストの総数 |
http_req_blocked | リクエスト開始前にブロックされている時間 |
http_req_connecting | TCP接続の確立にかかった時間 |
他にも、様々なメトリクスを計測することができます。詳しくは公式リファレンスをご参照ください。
Check
k6のテストでは実行結果の値に対して、チェック処理を書くことができます。
このチェック処理は、ソフトウェアテストのアサートと似ていますが、条件が満たせなくてもエラーにはなりません。テスト結果のメトリクスにチェックが通っていないという出力が表示されます。
次の例では、HTTPレスポンスコードが200かどうかのチェックをしています。
import { check } from 'k6';
import http from 'k6/http';
export default function () {
const res = http.get('http://test.k6.io/');
check(res, {
'is status 200': (r) => r.status === 200,
});
}
全てのチェックが通った場合
チェックが通っていないリクエストがある場合
しきい値(Thresholds)
optionsにthresholds
を定義することでテストの実行結果(メトリクス)にしきい値を設定することができます。
例えば、リクエストの成功率, レスポンス時間などのしきい値を指定し、しきい値を満たせない場合はエラーを表示することができます。
thresholdsの指定方法
export const options = {
thresholds: {
<ここにしきい値を設定する>
},
};
設定例: リクエスト時間
例えばリクエストの合計時間が200msを下回るように設定するには、次のように書きます。
import http from 'k6/http';
export const options = {
thresholds: {
http_req_duration: ['p(95)<200'], // 95%のリクエストが200msを下回る
},
};
export default function () {
http.get('https://test-api.k6.io/public/crocodiles/1/');
}
200ms以上かかるとエラーが表示される
テストライフサイクル
ここでは、k6のテストライフサイクルについて説明します。
k6には、次の4つのライフサイクルステージが存在します。
ステージ | 目的 |
---|---|
init |
テストの開始前に一度だけ実行されるステージです。グローバル変数の宣言やテスト全体で使用する設定の初期化などを行います。 |
setup |
テストのdefault関数(VU code)が実行される前に一度だけ実行されるステージです。テストデータの準備や外部からのデータの取り込みなど、テスト実行に必要な前処理を行います。 |
VU Code |
各VU(仮想ユーザー)によって実行されるメインのテストスクリプトです。HTTPリクエストの送信やカスタムメトリクスの計測など、テストの核となるアクションが含まれます。 |
teardown |
テストのメイン部分の実行が終了した後に一度だけ実行されるステージです。テストで使用したリソースのクリーンアップや結果の後処理などを行います。 |
ライフサイクルは次のように関数で定義します。
// 1. init: 関数外のスコープは全てinitになる
export function setup() {
// 2. setup: テスト実行(`k6 run`)1回につき1度テスト実行前に呼び出される
}
export default function() {
// 3. VU code: 実際のテストシナリオ
}
export function teardown() {
// 4. teardown code: テスト実行(`k6 run`)1回につき1度テスト実行後に呼び出される
}
initは、関数に定義しないスコープを指しています。
initはMaxVUs + 3回呼ばれる
initのライフサイクルは少し変わった挙動をしているようでしたので備忘録として残しておきます。
ライフサイクルの実際の挙動を確認するために次のコードを実行してみました。
import http from 'k6/http';
console.log('Lifecycle: init');
export const options = {
vus: 1,
duration: '1s',
};
export function setup() {
console.log('Lifecycle: setup');
}
export default function () {
console.log('Lifecycle: VU code');
http.get('http://test.k6.io');
}
export function teardown() {
console.log('Lifecycle: teardown');
}
結果は次のようになります。
実行結果
k6 run lifecycle.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
INFO[0000] Lifecycle: init source=console
execution: local
script: lifecycle.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 31s max duration (incl. graceful stop):
* default: 1 looping VUs for 1s (gracefulStop: 30s)
INFO[0000] Lifecycle: init source=console
INFO[0000] Lifecycle: init source=console
INFO[0000] Lifecycle: setup source=console
INFO[0000] Lifecycle: VU code source=console
INFO[0001] Lifecycle: init source=console
INFO[0001] Lifecycle: teardown source=console
INFO[0001] Lifecycle: init source=console
█ setup
█ teardown
data_received..................: 17 kB 16 kB/s
data_sent......................: 546 B 493 B/s
http_req_blocked...............: avg=336.09ms min=225.37ms med=336.09ms max=446.81ms p(90)=424.66ms p(95)=435.73ms
http_req_connecting............: avg=216.68ms min=212.57ms med=216.68ms max=220.79ms p(90)=219.97ms p(95)=220.38ms
http_req_duration..............: avg=217.27ms min=213.16ms med=217.27ms max=221.38ms p(90)=220.56ms p(95)=220.97ms
{ expected_response:true }...: avg=217.27ms min=213.16ms med=217.27ms max=221.38ms p(90)=220.56ms p(95)=220.97ms
http_req_failed................: 0.00% ✓ 0 ✗ 2
http_req_receiving.............: avg=210µs min=80µs med=210µs max=340µs p(90)=314µs p(95)=327µs
http_req_sending...............: avg=110.5µs min=29µs med=110.5µs max=192µs p(90)=175.7µs p(95)=183.85µs
http_req_tls_handshaking.......: avg=112.94ms min=0s med=112.94ms max=225.88ms p(90)=203.29ms p(95)=214.59ms
http_req_waiting...............: avg=216.95ms min=212.8ms med=216.95ms max=221.11ms p(90)=220.28ms p(95)=220.69ms
http_reqs......................: 2 1.804891/s
iteration_duration.............: avg=369.07ms min=8.87µs med=34.25µs max=1.1s p(90)=885.74ms p(95)=996.45ms
iterations.....................: 1 0.902446/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
running (01.1s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 1s
この結果から、「initのログが4回記録されている」ことがわかります。「initは1VUごとに1回だけ実行される」と理解していたため、疑問に思い調べてみました。
このQAによれば、initは「最初の1回目でoptionsを取得するため」「setupとteardownのそれぞれで1回ずつ」「各VUコードが実行されるたびに1回」の計3つのタイミングで呼ばれるようです。つまり、「MaxVUs + 3回(最初の1回、setup、teardown)」が呼び出される回数となるようです。
setup関数の戻り値はVU codeとterdownから参照可能
setup関数では、戻り値を返すことができます。この値はVU codeのdefaul関数とterdown関数に引数として渡されるため参照することができます。
import http from 'k6/http';
export const options = {
vus: 1,
duration: '1s',
};
export function setup() {
return { variable: 'setup data' };
}
export default function (data) {
http.get('http://test.k6.io');
// setupで返したデータを参照できる
console.log('VU code:', data);
}
export function teardown(data) {
console.log('Lifecycle: teardown');
// setupで返したデータを参照できる
console.log('teardown:', data);
}
実行結果
k6 run lifecycle.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: lifecycle.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 31s max duration (incl. graceful stop):
* default: 1 looping VUs for 1s (gracefulStop: 30s)
INFO[0001] VU code: {"variable":"setup"} source=console
INFO[0001] Lifecycle: teardown source=console
INFO[0001] teardown: {"variable":"setup"} source=console
█ setup
█ teardown
data_received..................: 17 kB 17 kB/s
data_sent......................: 546 B 540 B/s
http_req_blocked...............: avg=314.89ms min=237.46ms med=314.89ms max=392.32ms p(90)=376.83ms p(95)=384.58ms
http_req_connecting............: avg=208.18ms min=191ms med=208.18ms max=225.37ms p(90)=221.93ms p(95)=223.65ms
http_req_duration..............: avg=190.19ms min=186.83ms med=190.19ms max=193.54ms p(90)=192.87ms p(95)=193.2ms
{ expected_response:true }...: avg=190.19ms min=186.83ms med=190.19ms max=193.54ms p(90)=192.87ms p(95)=193.2ms
http_req_failed................: 0.00% ✓ 0 ✗ 2
http_req_receiving.............: avg=183µs min=170µs med=183µs max=196µs p(90)=193.4µs p(95)=194.7µs
http_req_sending...............: avg=142µs min=62µs med=142µs max=222µs p(90)=206µs p(95)=214µs
http_req_tls_handshaking.......: avg=100.58ms min=0s med=100.58ms max=201.16ms p(90)=181.04ms p(95)=191.1ms
http_req_waiting...............: avg=189.86ms min=186.6ms med=189.86ms max=193.12ms p(90)=192.47ms p(95)=192.79ms
http_reqs......................: 2 1.977066/s
iteration_duration.............: avg=336.93ms min=1.45µs med=63.66µs max=1.01s p(90)=808.6ms p(95)=909.66ms
iterations.....................: 1 0.988533/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
running (01.0s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 1s
ただし、次のような注意点があるようです。
- 渡せるデータは、JSONのみ
- データを途中で加工することはできない
シナリオを使ったテスト
scenarios
オプションを使うと、複数のk6のシナリオを統合した負荷テストを実行できます。
シナリオを使うことで、いくつもの負荷テストシナリオを結合した、統合的な負荷テストを書くことも可能です。
(シナリオテストのサンプル)
import step1 from './step1.js';
import step2 from './step2.js';
import step3 from './step3.js';
// NOTE: step1, step2, step3 を export する必要がある
export { step1, step2, step3 };
export const options = {
scenarios: {
step1_scenario: {
executor: 'per-vu-iterations',
// NOTE: シナリオを実行する関数名を指定する
exec: 'step1',
vus: 1,
iterations: 1,
startTime: '0s', // テスト開始時にすぐに実行
},
step2_scenario: {
executor: 'per-vu-iterations',
exec: 'step2',
vus: 1,
iterations: 1,
startTime: '1m', // step1の後、1分後に実行
},
step3_scenario: {
executor: 'per-vu-iterations',
exec: 'step3',
vus: 1,
iterations: 1,
startTime: '2m', // step2の後、さらに1分後に実行
},
},
};
// メインの実行関数は空でOK
export default function () {}
ポイントは、importした関数名を統合シナリオを書いたファイルでexportする必要がある ところです。exportしないと、k6に認識されず、エラーになってしまいます。
Grafana k6 Browser Recorderを使ってテストシナリオを作成する
「Grafana k6 Browser Recorder」というツールを使うと、ブラウザの操作をそのままk6のシナリオに変換することができます。
このツールをつかうためには、Grafana Cloudのアカウントが必要ですが、テストのスクリプトを作成するだけであれば無料で行えます。
このツールを使うことで、シナリオ作成が画面操作から行うことがでるため、アクションが多いテストではテスト作成工数を削減できます。
chrome拡張機能からレコードボタンを押下
chrome拡張機能からStopボタンを押下
Grafana Cloud上で Script editorを選択してSave
※ Test builderを選択するとGUIでのビルダー形式に変換されます。
詳細はこちらも参照ください。
har-to-k6を使って、HARファイルをk6のシナリオに変換する
HAR(HTTP Archive File)ファイルとは、ブラウザの通信を記録したファイルで、chrome developer toolなどから作成することができます。
Developer toolからHARをダウンロードできる
前述した「Grafana k6 Browser Recorder」を使わなくとも、このHARからk6のテストファイルへ変換するツールをGrafana Labs社が提供してくれています。
このツールを使うと、Grafana Cloudに登録しなくても、ブラウザの通信記録からK6ファイルが作成でき、汎用性が高い便利なツールです。
(インストール)
npm install -g har-to-k6
(HARからk6テストの作成)
har-to-k6 myfile.har -o loadtest.js
詳細はこちらを参照ください。
終わりに
以上、k6の基本的な使い方について入門記事を書きました。実際の運用事例なども別の機会させていただきます。
個人的には「Grafana k6 Browser Recorder」や「har-to-k6」などのサブツールはあまり活用できていなかったので、改めてk6の便利さをしれて勉強になりました。今後はツールをつかった運用ノウハウなども公開していきたいです。
PharmaX では、様々なバックグラウンドを持つエンジニアの採用をお待ちしております。
もし、興味をお持ちの場合は、私の X アカウント(@hakoten)や記事のコメントにお気軽にメッセージいただけますと幸いです。まずはカジュアルにお話できれば嬉しいです!
PharmaXエンジニアチームのテックブログです。エンジニアメンバーが、PharmaXの事業を通じて得た技術的な知見や、チームマネジメントについての知見を共有します。 PharmaXエンジニアチームやメンバーの雰囲気が分かるような記事は、note(note.com/pharmax)もご覧ください。
Discussion