生涯未熟

生涯未熟

プログラミングをちょこちょこと。

k6を使いこなしてみよう

この記事は MIXI DEVELOPERS Advent Calendar 2022 6 日目の記事です。

負荷試験を行う機会が年に何度かあるのですが、以前まではvegetaを使っていましたがちょっと高めの負荷をかけた時の挙動がよろしくなく、k6を試してみたところ不満が無かったので最近はk6を常用しています。

そんなk6をもうちょっと使いこなすために色々とまとめてみようかと思います。

k6とは?

Grafana Labsが開発した負荷ツール。

github.com

ツール自体はGo製で、負荷シナリオをJavaScriptで書きます。
負荷シナリオはk6 Browser RecorderというChrome拡張を使えばブラウジングしているだけで作成可能で、k6 Cloudを使ったWeb上でのシナリオ作成・管理・実行が可能です。
わざわざGitHub上でシナリオを管理しなくてもいいというのは個人的に便利だなと思った点ではあります👀 (どこのリポジトリに置くの?問題とか考えなくて済む)

chrome.google.com

k6.io

それ以外にもHTTP、WebSocket、gRPCなどのマルチプロトコルサポートや、カスタムビルドを使うことにより例えばIAPの突破をk6自体に組み込んだりなどの拡張性を持たせることが出来ます。(詳しくはxk6を参照)

github.com

その他、充実したドキュメントやマメに更新されているブログなどユーザーに対してのサポートも抜かりがないです。

k6.io

基本的な使い方

まずはインストールから。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 を使ったシナリオの書き方などもありますので、ドキュメントを読んで習熟してみてください!

k6.io