パフォーマンステストでk6を使った時の備忘録

## はじめに
はじめまして、ARISEに8月に入社しました森と申します。
この記事はARISE analytics Advent Calendar 2023の22日目の記事になります。

前職ではフロントエンジニアだった私ですが、ARISEに転職してからはバックエンドやインフラを触らせてもらっています。
そんな中、案件でバックエンドのパフォーマンステストを初めて担当した際に、初めて聞くk6というツールを使ったのでその備忘録としてこの記事を書きました。
## k6の使い方
[k6の公式ドキュメント](https://grafana.com/docs/k6/latest/)
### テストスクリプトの作成
k6のライフサイクルは
1. init
1. Setup
1. VUcode
1. Teardown

の4ステージで構成されています。それぞれのステージの目的と、テスト実行時に呼び出される回数は以下の通りです。（VUs：同時接続人数（Virtual Users））

| ステージ | 目的 | 呼び出し回数 |
| ---- | ---- | ---- |
| init | テストで使用するファイルの読み込みやモジュールのインポート| VUsごとに1回 |
| Setup | リクエストを投げるのに必要な情報の取得（トークンなど） | 1回 |
| VUcode | パフォーマンスを測定したいAPIにリクエストを投げる | イテレーションごとに1回 |
| Teardown| setupの実行結果やテスト終了後に実施したいアクションの設定 | 1回 |

Setupで取得した値はVUcodeの呼び出し時に引数で渡すことができます。今回はSetupで生成したダミーのトークンをVUcodeに渡し、そのトークンを使ってk6のサイトにGETリクエストを送信するテストスクリプトを作成しました。
```javascript
import http from 'k6/http';
import { check } from 'k6';

// setupでdummyTokenListを作成
export function setup() {
  const dummyTokenList = [...Array(5)].map((_, i) => {
    return `dummyToken_${i}`
  });
  return dummyTokenList;
}

// VUごとにdummyTokenListからtokenを取得してリクエストを送信
// dummyTokenListはsetupから受け取る
export default function testFunction(dummyTokenList) {
  const token = dummyTokenList[__VU - 1];
  const testEndpoint = 'https://test.k6.io/';

  // 対象のAPIに対してGETリクエストを送信
  const response = http.get(
    testEndpoint,
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );

  // レスポンスのステータスコードが200であることを確認
  check(response, {
    'Status is 200': (r) => r.status === 200,
  });
}
```
テストスクリプトについて少し解説します。
```javascript
const token = dummyTokenList[__VU - 1];
```
__VUには**1〜実行時に設定した同時接続人数**が入ります。例えばVUsを5人と設定してテストスクリプトを実行した場合、`__VU`には以下のような値が入ります。
```bash
INFO[0001] __VUに入っている値: 2                                source=console
INFO[0001] __VUに入っている値: 3                                source=console
INFO[0001] __VUに入っている値: 4                                source=console
INFO[0001] __VUに入っている値: 5                                source=console
INFO[0001] __VUに入っている値: 1                                source=console
```
これを使ってユーザーごとにトークンを発行してAPIを呼び出す、といった処理を実現することができます。
```javascript
// レスポンスのステータスコードが200であることを確認
  check(response, {
    'Status is 200': (r) => r.status === 200,
  });
```
k6の`check`関数を使うことでパフォーマンステストを実行時のレスポンスが正しいかを確認することができます。これにより、
- テストスクリプトに不正がなく、テストを正しく行えているか
- 負荷がかかることによるエラーが発生していないか

をチェックすることができます。
全リクエストが成功すると実行結果に
```bash
✓ Status is 200
```
が表示されます。一方、1つでも失敗したリクエストがあった場合は以下のように全リクエストのうちいくつ失敗したのか表示してくれます。
```bash
✗ Status is 200
      ↳  80% — ✓ 4 / ✗ 1
```
今回はステータスのみチェックしていますが、他にもbodyの中身が想定通りか確認することもできます。

### 実行
k6コマンドを使ってテストスクリプトを実行します。この時に同時接続人数やリクエストを投げる回数を指すイテレーション数、実行時間など様々なオプションを設定することができます。
オプションはk6コマンド以外にもテストスクリプト内でさらに細かく指定することもできますが、今回はコマンド実行時に指定する方法を紹介したいと思います。
（参考：[オプションの種類とその設定方法](https://grafana.com/docs/k6/latest/using-k6/k6-options/)）

例えば、5人のユーザーが同時に接続した状態で1リクエストずつ送るシナリオでパフォーマンステストを実施したい場合は以下のコマンドを実行します。
```bash
k6 run --vus 5 --iterations 5 testscript.js
```
`--vus`で同時接続数を、`--iterations`でリクエストを送る回数を指定しています。

5人のユーザーが同時に接続した状態で10分間リクエストを送るシナリオで実施したい場合は以下のコマンドを実行します。
```bash
k6 run --vus 5 --durations 10m testscript.js
```
:::note warn
`--vus`は`--iterations`か`--durations`とセットでないと適用されません。
:::
他にも様々なオプションが用意されているので、それらを組み合わせてパフォーマンステストを実施する際のRPSなどの状況を再現できるようチューニングしていきます。
### 結果の見方
k6コマンドを実行して処理が終了すると以下のような結果が出力されます。
```bash
     ✓ Status is 200

     █ setup

     checks.........................: 100.00% ✓ 5        ✗ 0
     data_received..................: 85 kB   139 kB/s
     data_sent......................: 2.4 kB  3.9 kB/s
     http_req_blocked...............: avg=419.94ms min=417.08ms med=420.29ms max=421.44ms p(90)=421.14ms p(95)=421.29ms
     http_req_connecting............: avg=184.05ms min=182.78ms med=182.96ms max=185.83ms p(90)=185.81ms p(95)=185.82ms
     http_req_duration..............: avg=188.88ms min=185.78ms med=189.65ms max=190.11ms p(90)=190.1ms  p(95)=190.1ms 
       { expected_response:true }...: avg=188.88ms min=185.78ms med=189.65ms max=190.11ms p(90)=190.1ms  p(95)=190.1ms 
     http_req_failed................: 0.00%   ✓ 0        ✗ 5
     http_req_receiving.............: avg=167.5µs  min=85.45µs  med=169.61µs max=285.95µs p(90)=240.02µs p(95)=262.99µs
     http_req_sending...............: avg=100.34µs min=18.63µs  med=76.24µs  max=231.6µs  p(90)=193.18µs p(95)=212.39µs
     http_req_tls_handshaking.......: avg=190.58ms min=188.56ms med=190.09ms max=192.6ms  p(90)=192.52ms p(95)=192.56ms
     http_req_waiting...............: avg=188.61ms min=185.36ms med=189.44ms max=189.96ms p(90)=189.85ms p(95)=189.91ms
     http_reqs......................: 5       8.169356/s
     iteration_duration.............: avg=507.96ms min=82.19µs  med=610.79ms max=611.2ms  p(90)=611.19ms p(95)=611.2ms 
     iterations.....................: 5       8.169356/s
```
このメトリクスのうち、今回は私がパフォーマンステスト実施時にテスト観点として使用した`checks`, `http_req_duration`, `http_reqs`をピックアップして解説しようと思います。
（参考：[メトリクスについて](https://k6.io/docs/using-k6/metrics/reference/)）
#### checks
check関数を使ってチェックした項目が正常にパスした数と割合を表しています。
この値が100%でない場合はパフォーマンスに問題があると考えられるため、ログなどをみながら原因を追求する必要があります。
#### http_req_duration
リクエストを送ってからレスポンスを受け取るまでの時間を表しているので、ターンアラウンドタイムを表す指標になります。そのため、`http_req_duration`の値を見ることでAPIが使用通りのパフォーマンスを発揮できているかを確認することができます。

:::note info
http_req_durationの値が指定された値以下であることをチェックするようオプションで指定することもできます。
:::
P(90), P(95)という見慣れない指標はパーセンタイルを表しており、例えば`p(90)=190.1ms`であれば全リクエストのうち90%のレスポンスタイムは190.1msより早かった、ということを表しています。
#### http_reqs
k6が送ったリクエストの総数とRPSを表しています。パフォーマンステスト実施時に**30RPS**という負荷のもとでテストを実施しする必要がある場合はこの指標をみながらオプションを使って負荷を調整していきます。
## まとめ
k6はそこまで複雑なコードを書かずにパフォマンステストを行うことができ、様々なOptionが用意されているので自身が実現したい環境を構築しやすくとても便利なツールだなと感じました。

また、[グラフで可視化することのできるライブラリとの連携](https://k6.io/docs/results-output/grafana-dashboards/)も便利そうなので別の機会にそちらも触ってみたいなと思いました。