はじめてのK6負荷試験ツール

## はじめに
K6という負荷試験ツールの使い方をまとめたいと思います。
今回はサーバサイドのパフォーマンスに絞ってまとめます。
ブラウザベースのパフォーマンスについては、別途まとめます。

https://grafana.com/docs/k6/next/

## インストール
https://grafana.com/docs/k6/next/set-up/install-k6/

筆者はMacなので、Brewでインストールしてます。

## テストファイル作成

~~~shell
k6 new <ファイル名>.js
~~~

ターミナルでコマンド入力してもらうと、テンプレート作成してくれます。
絶対、newコマンドでファイル作成しないといけないということではないです。
touchとかで新規ファイル作成しても、中身が動くスクリプトなら問題ないです。

## テスト実行

~~~shell
k6 run <ファイル名>.js
~~~

先ほど、作成したテンプレートを動かしてみましょう。

~~~shell
k6 run auth_senario.js

          /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: auth_senario.js
     output: -

  scenarios: (100.00%) 1 scenario, 10 max VUs, 33s max duration (incl. graceful stop):
           * default: 10 looping VUs for 3s (gracefulStop: 30s)


     data_received..................: 273 kB 89 kB/s
     data_sent......................: 5.4 kB 1.8 kB/s
     http_req_blocked...............: avg=283.78ms min=2µs      med=282.73ms max=571.57ms p(90)=569.62ms p(95)=570.6ms 
     http_req_connecting............: avg=99.43ms  min=0s       med=95.4ms   max=237.47ms p(90)=202.8ms  p(95)=206.21ms
     http_req_duration..............: avg=239.42ms min=190.62ms med=243.6ms  max=282.24ms p(90)=282.03ms p(95)=282.06ms
       { expected_response:true }...: avg=239.42ms min=190.62ms med=243.6ms  max=282.24ms p(90)=282.03ms p(95)=282.06ms
     http_req_failed................: 0.00%  ✓ 0        ✗ 20  
     http_req_receiving.............: avg=2.36ms   min=49µs     med=312µs    max=7.56ms   p(90)=6.47ms   p(95)=6.66ms  
     http_req_sending...............: avg=348.95µs min=9µs      med=24µs     max=1.73ms   p(90)=1.69ms   p(95)=1.7ms   
     http_req_tls_handshaking.......: avg=181.48ms min=0s       med=161.83ms max=373.62ms p(90)=371.68ms p(95)=372.57ms
     http_req_waiting...............: avg=236.71ms min=185.29ms med=243.37ms max=281.96ms p(90)=281.73ms p(95)=281.77ms
     http_reqs......................: 20     6.531551/s
     iteration_duration.............: avg=1.52s    min=1.28s    med=1.52s    max=1.77s    p(90)=1.77s    p(95)=1.77s   
     iterations.....................: 20     6.531551/s
     vus............................: 10     min=10     max=10
     vus_max........................: 10     min=10     max=10


running (03.1s), 00/10 VUs, 20 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  3s
~~~

上記のような結果になれば、問題なく動いてます。

### テストファイル構成

K6のテストファイルは大きく4つのブロックに分かれます。

### オプションブロック

~~~js
export const options = {
    vus: 10,  // 仮想ユーザー数
    duration: '20s',  // テストの実行時間
};
~~~

テスト実行時のオプションをファイルに記載できます。

### ファンクションブロック

~~~js
export default function() {
  http.get('https://test.k6.io');
  sleep(1);
}
~~~

実行するAPIの情報を記載します。

### セットアップブロック

~~~js
export function setup() {
  
}
~~~

処理するデータを設定し、VU間でデータを共有できます。

### tear downブロック

~~~js
export function teardown(data) {
  
}
~~~

セットアップコードの処理結果、テスト環境の停止します。


## 実行オプション

https://grafana.com/docs/k6/next/using-k6/k6-options/reference/

よく使用するオプションを紹介します。

### vus

同時に実行する VU の数を指定する数値

### duration

テスト実行の合計期間を指定する.

vusと組み合わせることで、指定期間で同時実行時のシナリオを流せる。

### stages

増加または減少する VU の目標数を指定するオブジェクトのリスト

~~~js
export const options = {

    stages: [
        {duration: '30s', target: 5},
        {duration: '30s', target: 5}
    ]
}
~~~

このように記述できる。
例だと、最初の30秒でVuを5まで増やしていき、次の30秒でVuを0に減らしていくことを表現している。

### Thresholds

テストが成功するか失敗するかの条件を設定する

## Checkメソッド

K6はレスポンスを評価することができます。
試しにレスポンスのステータスコードが200かをチェックします。

~~~js
import { check, sleep } from 'k6';


export default function() {
  const res = http.get('https://test.k6.io');
  check(res, {
    'status was 200': (r) => r.status === 200,
  });
  sleep(1);
}
~~~

作成したテンプレートに上記のcheckを追加します。

~~~shell
✓ status was 200
~~~

このようにターミナルに表示されていればOKです。

~~~shell
✗ status was 200
      ↳  0% — ✓ 0 / ✗ 28
~~~

失敗するとこのように表示されます。

## シナリオテスト

K6では、複数のシナリオを統合してテストすることができます。

~~~js
import senario1 from './sinario1.js'
import senario2 from './sinario2.js'

export {
  senario1,
  senario2
}

export const options = {
  scenarios: {
    scenario1: {
      
      executor: 'per-vu-iterations',

      exec: '<finction_name>', // デフォルトだとdefaultという名前の関数が呼ばれる
      startTime: '10s',
      gracefulStop: '5s',
      env: { EXAMPLEVAR: 'testing' },
      tags: { example_tag: 'testing' },

      // executor-specific configuration
      vus: 10,
      iterations: 200,
      maxDuration: '10s',
    },
    scenario2: {
      
      executor: 'per-vu-iterations',

      
      exec: '<finction_name>', // デフォルトだとdefaultという名前の関数が呼ばれる
      startTime: '10s',
      gracefulStop: '5s',
      env: { EXAMPLEVAR: 'testing' },
      tags: { example_tag: 'testing' },

      // executor-specific configuration
      vus: 10,
      iterations: 200,
      maxDuration: '10s',
    },
  },
};

export default function() {}

~~~

サンプルのように、それぞれの呼び出したいテストファイルをインポートした後に、export記載しないと認識されないのでご注意ください。


https://grafana.com/docs/k6/next/using-k6/scenarios/

オプションは公式を参照してください。

## 閾値

~~~js
export const options = {
    thresholds: {
        http_req_duration: ['p(95) < 500'] // 95%のリクエストが500ms未満で完了すること
    },
};
~~~

サンプルのように、簡単に閾値を設定できます。

## Jsonを使用したデータインポート

K6でもJson形式でデータをインポートしてリクエストに使用できます。

~~~json
[
    { "bookId": "20c22160-a72f-4b3b-9bf2-0d6832934e22", "userId": "usebc9e6634-89c6-4e93-b2c7-049a1e53cc71"},
    { "bookId": "20c22160-a72f-4b3b-9bf2-0d6832934e23", "userId": "usebc9e6634-89c6-4e93-b2c7-049a1e53cc72"},
    { "bookId": "20c22160-a72f-4b3b-9bf2-0d6832934e24", "userId": "usebc9e6634-89c6-4e93-b2c7-049a1e53cc73"},
    { "bookId": "20c22160-a72f-4b3b-9bf2-0d6832934e25", "userId": "usebc9e6634-89c6-4e93-b2c7-049a1e53cc74"},
    { "bookId": "20c22160-a72f-4b3b-9bf2-0d6832934e26", "userId": "usebc9e6634-89c6-4e93-b2c7-049a1e53cc75"}
]
~~~

~~~js
import http from 'k6/http';

// 外部ファイル 'loan_data.json' から配列データを読み込み
const data = new SharedArray('loan data', function () {
  return JSON.parse(open('./loan_data.json')); 
});

export default function () {

    const url = 'http://localhost:8080/graphql';  // GraphQLエンドポイント

    data.forEach(item => {
        for (let i = 0; i < 10; i++) {

            const payload = JSON.stringify({
                query: `
                    mutation {
                        loan(bookId: "${item.bookId}", userId: "${item.userId}")
                    }
                `,
            });

            // ヘッダー設定（JSON形式でリクエストを送る）
            const params = {
                headers: {
                    'Content-Type': 'application/json',
                },
            };

            // POSTリクエストでGraphQLのMutationを送信
            const res = http.post(url, payload, params);

            // レスポンスのステータスコードが200であるかをチェック
            check(res, {
                'status was 200': (r) => r.status === 200,
            });

        }
    });
}

~~~

サンプルでは、Jsonで表現したデータの配列をループで処理するようになってます。
負荷テスト以外でも機能テストでリクエストパターン試したいときなんかも便利です。

## メトリクス

確認することが多いメトリクスは以下かなと思うので、ピックアップします。

| メトリクス名 | 意味 |
| --- | --- |
| checks | チェック関数の成功割合 |
|iteration_duration | 一度のイテレーションにかかった時間|
| vus |アクティブな仮想ユーザ数 |
| vus_max | 最大仮想ユーザ数|
| http_req_connecting |TCP確立までの時間 |
| http_req_duration | レスポンスを受け取るまでの時間|
| http_reqs | HTTPリクエストの総数|

https://grafana.com/docs/k6/next/using-k6/metrics/reference/

他にもメトリクスは存在するので、公式を参照ください。

## さいごに

個人的にK6は使いやすく、シナリオを書きやすいので重宝してます。
ブラウザテストで使用するパターンもタイミングいい時に記載できればなと。