この記事は MIXI DEVELOPERS Advent Calendar 2022 6 日目の記事です。
負荷試験を行う機会が年に何度かあるのですが、以前まではvegetaを使っていましたがちょっと高めの負荷をかけた時の挙動がよろしくなく、k6を試してみたところ不満が無かったので最近はk6を常用しています。
そんなk6をもうちょっと使いこなすために色々とまとめてみようかと思います。
k6とは?
Grafana Labsが開発した負荷ツール。
ツール自体はGo製で、負荷シナリオをJavaScriptで書きます。
負荷シナリオはk6 Browser RecorderというChrome拡張を使えばブラウジングしているだけで作成可能で、k6 Cloudを使ったWeb上でのシナリオ作成・管理・実行が可能です。
わざわざGitHub上でシナリオを管理しなくてもいいというのは個人的に便利だなと思った点ではあります👀
(どこのリポジトリに置くの?問題とか考えなくて済む)
それ以外にもHTTP、WebSocket、gRPCなどのマルチプロトコルサポートや、カスタムビルドを使うことにより例えばIAPの突破をk6自体に組み込んだりなどの拡張性を持たせることが出来ます。(詳しくはxk6を参照)
その他、充実したドキュメントやマメに更新されているブログなどユーザーに対してのサポートも抜かりがないです。
基本的な使い方
まずはインストールから。Macの場合は brew install k6
でok、それ以外はこちらを参照。
インストールが終わりましたら、次にシナリオの作成です。ドキュメントにもある最小の形でまずは試してみましょう。
シナリオを格納するディレクトリとして k6
という名称でディレクトリを作成し、その直下に以下の内容の test.js
を作成してください。
k6側でテスト用に用意されている https://test.k6.io
に対してGetリクエストを投げ、その1秒後にテストを終了するシナリオになります。では、そのようになっているのか実行してみましょう。
$ cd k6 $ k6 run test.js /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) running (00m01.7s), 0/1 VUs, 1 complete and 0 interrupted iterations default ✓ [======================================] 1 VUs 00m01.7s/10m0s 1/1 iters, 1 per VU data_received..................: 17 kB 10 kB/s data_sent......................: 442 B 260 B/s http_req_blocked...............: avg=518.34ms min=518.34ms med=518.34ms max=518.34ms p(90)=518.34ms p(95)=518.34ms http_req_connecting............: avg=175.25ms min=175.25ms med=175.25ms max=175.25ms p(90)=175.25ms p(95)=175.25ms http_req_duration..............: avg=179.72ms min=179.72ms med=179.72ms max=179.72ms p(90)=179.72ms p(95)=179.72ms { expected_response:true }...: avg=179.72ms min=179.72ms med=179.72ms max=179.72ms p(90)=179.72ms p(95)=179.72ms http_req_failed................: 0.00% ✓ 0 ✗ 1 http_req_receiving.............: avg=465µs min=465µs med=465µs max=465µs p(90)=465µs p(95)=465µs http_req_sending...............: avg=50µs min=50µs med=50µs max=50µs p(90)=50µs p(95)=50µs http_req_tls_handshaking.......: avg=330.41ms min=330.41ms med=330.41ms max=330.41ms p(90)=330.41ms p(95)=330.41ms http_req_waiting...............: avg=179.21ms min=179.21ms med=179.21ms max=179.21ms p(90)=179.21ms p(95)=179.21ms http_reqs......................: 1 0.587392/s iteration_duration.............: avg=1.7s min=1.7s med=1.7s max=1.7s p(90)=1.7s p(95)=1.7s iterations.....................: 1 0.587392/s vus............................: 1 min=1 max=1 vus_max........................: 1 min=1 max=1
予想通りにGetリクエストを1度投げてますね!このようにCLIでスクリプトを実行すると統計データが表示されます。
では表示された項目について一つずつ見ていきましょう。
running (00m01.7s), 0/1 VUs, 1 complete and 0 interrupted iterations default ✓ [======================================] 1 VUs 00m01.7s/10m0s 1/1 iters, 1 per VU
テスト実行時のプログレスバーですが、ここにはVUS(virtual users:仮想ユーザー)の数と実行時間、イテレート数と仮想ユーザーの実行数になります。
- data_received:総データ受信数
- data_sent:総データ送信数
- http_req_blocked:TCP接続がブロッキングされた時間
- http_req_connecting:TCP接続の確立にかかった時間
- http_req_duration:リクエストを送信してレスポンスを受け取るまでの時間(http_req_sending + http_req_waiting + http_req_receiving)
- expected_response:http_req_durationの中でもHTTPステータスコードが200~399のリクエストのみの値
- http_req_failed:リクエストが失敗した数
- http_req_receiving:レスポンスを受信していた時間
- http_req_sending:リクエストを送信していた時間
- http_req_tls_handshaking:TLSハンドシェイクにかかった時間
- http_req_waiting:レスポンスを待っていた時間(TTFBのこと)
- http_reqs:送信されたリクエスト数
- iteration_duration:シナリオで設定された1イテレーションにかかった全ての時間
- iterations:1VUが実行したイテレーションの数
- vus:終了時点でのVU数
- vus_max:実行中での最大VU数
色々ありますが、個人的に見ている指標は http_req_failed
と http_req_duration
くらいですかね。勿論、他の指標も何かしらの異常な値が出ていれば見ますが、基本はこの2つを見ていればいいかなと思います。
シナリオをCLIで実行した場合はこのような出力ですが、k6 Cloudではこの指標がグラフで可視化された図を確認することができます。
※ただし、詳細に出力されるのはCLIの方なので特別なこだわりが無ければCLIで実行するのをオススメします(高負荷の場合はk6 Cloudの方がいいかも)
また、この指標は開発者がカスタムすることが出来、これは後ほど説明致します。
ここまでが最小の形でのシナリオ作成→実行までのフローになります。
k6 run
さきほど実行の際に利用した k6 run
についてもう少し詳しく見ていきます。
k6 run
はオプションでシナリオの実行の仕方を変えることができます。例えば、さきほどはシナリオが一回終われば実行を終了していましたが、「30sの間、50rpsで負荷をかけたい」などといった要望は出てくると思います。そういった時にオプションを付けてみましょう。
今回はテストのためにもう少し軽い負荷にしてみます。
$ k6 run test.js -d 5s --rps 20 /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 1 max VUs, 35s max duration (incl. graceful stop): * default: 1 looping VUs for 5s (gracefulStop: 30s) running (05.2s), 0/1 VUs, 4 complete and 0 interrupted iterations default ✓ [======================================] 1 VUs 5s data_received..................: 52 kB 9.9 kB/s data_sent......................: 736 B 141 B/s http_req_blocked...............: avg=120.06ms min=6µs med=11.5µs max=480.22ms p(90)=336.16ms p(95)=408.19ms http_req_connecting............: avg=44.02ms min=0s med=0s max=176.08ms p(90)=123.26ms p(95)=149.67ms http_req_duration..............: avg=182.34ms min=179.94ms med=182.49ms max=184.42ms p(90)=184.05ms p(95)=184.24ms { expected_response:true }...: avg=182.34ms min=179.94ms med=182.49ms max=184.42ms p(90)=184.05ms p(95)=184.24ms http_req_failed................: 0.00% ✓ 0 ✗ 4 http_req_receiving.............: avg=143.75µs min=86µs med=137.49µs max=214µs p(90)=201.7µs p(95)=207.84µs http_req_sending...............: avg=36.25µs min=26µs med=30µs max=59µs p(90)=50.6µs p(95)=54.8µs http_req_tls_handshaking.......: avg=75.71ms min=0s med=0s max=302.86ms p(90)=212ms p(95)=257.43ms http_req_waiting...............: avg=182.16ms min=179.81ms med=182.26ms max=184.31ms p(90)=183.9ms p(95)=184.11ms http_reqs......................: 4 0.766934/s iteration_duration.............: avg=1.3s min=1.18s med=1.18s max=1.66s p(90)=1.51s p(95)=1.59s iterations.....................: 4 0.766934/s vus............................: 1 min=1 max=1 vus_max........................: 1 min=1 max=1
実行してみましたが、20rpsを指定したのに0.7rpsしか出ていませんね。vusに注目していただくとvusが1しか生成されていないので、20rpsを実行するためにvusを増やしてみましょう。
$ k6 run test.js -d 5s --rps 20 -u 40 /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 60 max VUs, 35s max duration (incl. graceful stop): * default: 60 looping VUs for 5s (gracefulStop: 30s) running (08.0s), 00/60 VUs, 135 complete and 0 interrupted iterations default ✓ [======================================] 60 VUs 5s data_received..................: 1.9 MB 237 kB/s data_sent......................: 34 kB 4.3 kB/s http_req_blocked...............: avg=158.32ms min=3µs med=13µs max=465.14ms p(90)=358.31ms p(95)=364.97ms http_req_connecting............: avg=77.78ms min=0s med=0s max=183.75ms p(90)=177.89ms p(95)=180.75ms http_req_duration..............: avg=193.77ms min=171.88ms med=179.31ms max=362.84ms p(90)=188.16ms p(95)=346.3ms { expected_response:true }...: avg=193.77ms min=171.88ms med=179.31ms max=362.84ms p(90)=188.16ms p(95)=346.3ms http_req_failed................: 0.00% ✓ 0 ✗ 135 http_req_receiving.............: avg=15.43ms min=40µs med=122µs max=178.5ms p(90)=418.8µs p(95)=170.85ms http_req_sending...............: avg=47.08µs min=12µs med=44µs max=122µs p(90)=83.2µs p(95)=95.3µs http_req_tls_handshaking.......: avg=80.48ms min=0s med=0s max=280.26ms p(90)=180.7ms p(95)=184.18ms http_req_waiting...............: avg=178.29ms min=171.72ms med=178.65ms max=188.4ms p(90)=184.01ms p(95)=185.03ms http_reqs......................: 135 16.97481/s iteration_duration.............: avg=2.85s min=1.63s med=2.65s max=4.5s p(90)=3.81s p(95)=4.15s iterations.....................: 135 16.97481/s vus............................: 18 min=18 max=60 vus_max........................: 60 min=60 max=60
vusを40にしてみたところ、20rpsに近付きましたね!このように実行する環境をオプションによって調整することが出来ます。様々なオプションが用意されているので是非とも k6 run --help
でオプション一覧を眺めて頂きたいのですが、その中から使い所の多いオプションを抜粋します。
// --user-agent:ユーザーエージェントの指定 $ k6 run test.js --user-agent "test-agent" // --out:指標の詳細データを出力、リクエスト毎にHTTPステータスコードやリクエスト/レスポンスの日時などの情報が確認できる $ k6 run test.js --out json=result.json // --log-output:画面上でのログの表示設定、リクエストのタイムアウトなど頻発する出力を抑制するのに使う $ k6 --log-output none run test.js
k6 Browser Recorderを利用したシナリオの作成
k6とは?で紹介したk6のChrome拡張・をk6 Browser Recorder使ってシナリオを作成してみましょう。今回はexample.comを使ってシナリオを作成してみます。
事前作業として k6 Browser Recorderをインストールし、https://app.k6.io/account/login にアクセスし、ログインまたは新規登録を済ましておいてください。
まずは、example.comに飛びましょう。
k6 Browser Recorderを実行してみます。
Start recordingを押下します。
ツールウィンドウが読み込み状態になり、暫くすると上記の画面になりますので、そうしましたらページを一度リロードしてみてください。 その後、完全にページの読み込みが完了したらStopを押下します。
すると、新規タブが開きk6 Cloudが表示されます。
Test builderとScript editorが最初にあるのですが、個人的にはScript editorを選択するのをオススメしています。後から変えられるのでどちらでもいいのですが、開発者が扱うにはScript editorの方が馴染みあるJSの書かれたエディターが開かれるのでこちらを選ぶのが良いかと。
Generate sleepは500ms以上かかっている処理間に自動でsleepを入れてくれるのですが、自分は必要ないと感じているのでいつも選択していません。
また、今回は表示がされていませんがcssやjsなどのstatic assetsが存在する場合は、そのアクセス処理もスクリプトに含めるかどうか、外部ドメインへのリクエストも含めるかどうかも選択できます。外部ドメインへのリクエストは自分で管理しているもの以外は除くのが良さそうで、static assetsに関しては基本は含んでおいて状況によっては除いたりするのが良いかなと思います。
選択が終わりましたらSaveでScript editorに移動します。
ここからRun Testを押下することでそのまま実行できますし、一度生成されたスクリプトを手元でjsファイルにしてCLIで実行するも良し。状況によって使い分けてみてください。
シナリオの作成例
ここからはもう少しシナリオの作成を具体的にやっていきます。さきほど作成したexapmle.comへのリクエストを土台に改造していきます。
生成されたこのコードを見たときに注目すべきは export const options
です。 k6 run
のオプションを紹介しましたが、さらに細かい調整等を可能にするのがこのoptionsです。
現在は「vusが10で5分間負荷をかける」という条件でシナリオが書かれていますが、これを「40vus(必要があれば増やす)で20rpsの負荷を5秒間かける」としてみましょう。
確認のため、こちらを実行してみると
$ k6 run test.js /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 100 max VUs, 35s max duration (incl. graceful stop): * test_scenario: 20.00 iterations/s for 5s (maxVUs: 40-100, gracefulStop: 30s) running (06.1s), 000/040 VUs, 101 complete and 0 interrupted iterations test_scenario ✓ [======================================] 000/040 VUs 5s 20 iters/s █ page_1 - http://example.com/ data_received..................: 162 kB 26 kB/s data_sent......................: 11 kB 1.8 kB/s group_duration.................: avg=159.18ms min=108.62ms med=117.98ms max=247.32ms p(90)=231.3ms p(95)=238.23ms http_req_blocked...............: avg=44.95ms min=4µs med=15µs max=121.48ms p(90)=114.97ms p(95)=117.98ms http_req_connecting............: avg=44.88ms min=0s med=0s max=121.35ms p(90)=114.84ms p(95)=117.84ms http_req_duration..............: avg=113.97ms min=108.32ms med=114.34ms max=129.69ms p(90)=120.07ms p(95)=120.83ms { expected_response:true }...: avg=113.97ms min=108.32ms med=114.34ms max=129.69ms p(90)=120.07ms p(95)=120.83ms http_req_failed................: 0.00% ✓ 0 ✗ 101 http_req_receiving.............: avg=124.53µs min=44µs med=125µs max=254µs p(90)=182µs p(95)=186µs http_req_sending...............: avg=75.37µs min=24µs med=57µs max=426µs p(90)=138µs p(95)=168µs http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=113.77ms min=108.13ms med=114.13ms max=129.51ms p(90)=119.93ms p(95)=120.63ms http_reqs......................: 101 16.524309/s iteration_duration.............: avg=1.16s min=1.1s med=1.11s max=1.24s p(90)=1.23s p(95)=1.23s iterations.....................: 101 16.524309/s vus............................: 40 min=40 max=40 vus_max........................: 40 min=40 max=40
16.5rpsということで20rpsに近い値が出せていますね!それでは土台からの変更点を見てみます。
export const options = { scenarios: { test_scenario: {
さて、optionsの中で scenarios
というのが設定されています。これはVUSやイテレーションの具体的な設定を行う際に使われるもので、詳しくはこちらに書いてありますが、エグゼキューターと呼ばれる実行エンジンの指定や、実行パターンの詳細な設定でより現実的な負荷計測が可能になります。
optionsに rps
という設定項目もあるのですが、非推奨となっているため constant-arrival-rate
エグゼキューターを使った一定のリクエスト送出を行う方法を取っています。
test_scenario
は任意のキーとなっていますので、適当なシナリオに基づく名前を付けてください。
// rate, timeUnitを実行するためのExecutor executor: 'constant-arrival-rate', duration: '5s', // timeUnitで指定された時間毎に反復するテスト回数 rate: 20, // rateを反復させる時間 timeUnit: '1s', // 初期に割り当てられるVUS数 preAllocatedVUs: 40, // VUSが足りなかった場合に増える最大VUS数 maxVUs: 100,
scenarios
で設定されている内容ですが、コメントに書いてある通りの設定をしている感じです。設定値はoptionsでも設定できる値は設定でき、エグゼキューター毎に設定できるものが違ったりするので、まずはエグゼキューターの把握。それから各エグゼキューターの設定値を確認していくのがいいでしょう。
あと、私が必ず付けているオプションがありまして discardResponseBodies: true
を付けておくのをオススメします。こちらはレスポンスボディを破棄するかどうかの設定で、デフォルトでは破棄しない設定になっているのですが必要がないことがほとんどだと思いますので、テスト負荷を減らすために破棄をする設定にしておきましょう。
もう一つ、こちらはオプションではないのですが http.setResponseCallback(http.expectedStatuses({ min: 200, max: 399 }));
という「何のHTTPステータスコードを成功とみなすのか?」を設定することができます。デフォルトで200~399までは成功扱いなのですが、場合によっては特定のHTTPステータスコード以外は成功としたくないという時に使えます。
試しに201~399までは成功とした場合にどうなるかやってみましょう。
/\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 100 max VUs, 35s max duration (incl. graceful stop): * test_scenario: 20.00 iterations/s for 5s (maxVUs: 40-100, gracefulStop: 30s) running (06.1s), 000/040 VUs, 101 complete and 0 interrupted iterations test_scenario ✓ [======================================] 000/040 VUs 5s 20 iters/s █ page_1 - http://example.com/ data_received..............: 162 kB 26 kB/s data_sent..................: 11 kB 1.8 kB/s group_duration.............: avg=160.16ms min=107.11ms med=118.32ms max=253.38ms p(90)=235.91ms p(95)=239.88ms http_req_blocked...........: avg=45.4ms min=3µs med=15µs max=132.49ms p(90)=117.31ms p(95)=118.75ms http_req_connecting........: avg=45.25ms min=0s med=0s max=121.77ms p(90)=117.18ms p(95)=118.65ms http_req_duration..........: avg=114.49ms min=106.91ms med=115.03ms max=123.16ms p(90)=119.73ms p(95)=120.56ms http_req_failed............: 100.00% ✓ 101 ✗ 0 http_req_receiving.........: avg=117.94µs min=19µs med=93µs max=2.62ms p(90)=135µs p(95)=152µs http_req_sending...........: avg=73.29µs min=17µs med=56µs max=281µs p(90)=147µs p(95)=167µs http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...........: avg=114.3ms min=106.79ms med=114.85ms max=122.99ms p(90)=119.6ms p(95)=120.32ms http_reqs..................: 101 16.504622/s iteration_duration.........: avg=1.16s min=1.1s med=1.11s max=1.25s p(90)=1.23s p(95)=1.24s iterations.................: 101 16.504622/s vus........................: 40 min=40 max=40 vus_max....................: 40 min=40 max=40
想定通り、 http_req_failed
が100%で全て失敗となっていますね。
カスタムメトリクスの作成
負荷の実行後に表示される指標をカスタマイズしてみましょう。設定できるカスタムメトリクスには以下の4種類があります。
- Counter:累積値を判定する指標。例えば、HTTPステータスコード上は成功しているが、エラーコードをレスポンスで返している場合にそういったレスポンスが1件でもあれば失敗とする、みたいなことが出来る。
- Gauge:最新の値を常に判定する指標。例えば、レスポンスボディのサイズが〇〇以上だったら失敗、のようなことが出来る。
- Rate:追加した値の割合を判定する指標。例えば、エラー率が10%以上なら失敗のようなことが出来る。
- Trend:追加した値の統計(最小、最大、平均、パーセンタイル)を判定する指標。例えば、平均レスポンスタイムが〇〇ms以下なら失敗のようなことが出来る。
それぞれのカスタムメトリクスに対して、 thresholds
という条件を記述することで機能するのですが、ここで abortOnFail
を設定すれば失敗した時点で負荷計測を中断することが出来たり、 delayAbortEval
で失敗後も指定時間計測は続けるといったことも可能です。
この4つのカスタムメトリクスを加えたシナリオを書いてみましたので、参考にしてみてください。
まだまだ奥が深いk6
今回紹介したのはk6の要素のまだまだ一部です。他にも setup
teardown
や、 check
を使ったシナリオの書き方などもありますので、ドキュメントを読んで習熟してみてください!