k6使ったら今までで一番負荷テストが捗った


これは「[「はじめに」の Advent Calendar 2021](https://adventar.org/calendars/7037)」21日目の記事です。

## k6とは
https://k6.io/

Load ImpactというSaaSで負荷テストサービスを提供していたが、2020年に名称変更したものらしいです。

https://k6.io/blog/load-impact-rebranding-to-k6/

クラウドサービスはリモートから手軽に負荷テストを実行出来るようですが、今回ネタにするのはそのOSS版。

ちなみに、Google検索だと4番目。(k6ってなんて発音するんだろ？ケーシックスでいいのかな？)
![スクリーンショット 2021-12-21 23.18.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1546/8a4ba3af-a74d-e145-39a6-18bbf8139148.png)

## 使ってみる

インストールのページの通り。Macで動かすときは

```sh
brew install k6
```

ローカルで適当にWebサーバーを立ち上げておく
```sh
docker run -d -p 80:80 nginx
```

javascriptでテストシナリオを記述する

```sample.js
import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
  http.get('http://localhost/');
  sleep(1);
}
```

テスト実行

```
$ k6 run sample.js

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

  execution: local
     script: sample2.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.0s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m01.0s/10m0s  1/1 iters, 1 per VU

     data_received..................: 853 B 819 B/s
     data_sent......................: 75 B  72 B/s
     http_req_blocked...............: avg=7.45ms  min=7.45ms  med=7.45ms  max=7.45ms  p(90)=7.45ms  p(95)=7.45ms 
     http_req_connecting............: avg=533µs   min=533µs   med=533µs   max=533µs   p(90)=533µs   p(95)=533µs  
     http_req_duration..............: avg=26.41ms min=26.41ms med=26.41ms max=26.41ms p(90)=26.41ms p(95)=26.41ms
       { expected_response:true }...: avg=26.41ms min=26.41ms med=26.41ms max=26.41ms p(90)=26.41ms p(95)=26.41ms
     http_req_failed................: 0.00% ✓ 0        ✗ 1  
     http_req_receiving.............: avg=718µs   min=718µs   med=718µs   max=718µs   p(90)=718µs   p(95)=718µs  
     http_req_sending...............: avg=170µs   min=170µs   med=170µs   max=170µs   p(90)=170µs   p(95)=170µs  
     http_req_tls_handshaking.......: avg=0s      min=0s      med=0s      max=0s      p(90)=0s      p(95)=0s     
     http_req_waiting...............: avg=25.52ms min=25.52ms med=25.52ms max=25.52ms p(90)=25.52ms p(95)=25.52ms
     http_reqs......................: 1     0.959874/s
     iteration_duration.............: avg=1.03s   min=1.03s   med=1.03s   max=1.03s   p(90)=1.03s   p(95)=1.03s  
     iterations.....................: 1     0.959874/s
     vus............................: 1     min=1      max=1
     vus_max........................: 1     min=1      max=1
```

### 項目

大体書いているとおりだけど一応解説

|項目名|意味|
|---|---|
|data_received|レスポンスデータ量(Total, /s)|
|data_sent|リクエエストデータ量(Total, /s)|
|http_req_blocked|TCP接続の順番待ちをした時間(avg, min, med, max, p(90), p(95)|
|http_req_connecting|TCP接続にかかった時間(avg, min, med, max, p(90), p(95)|
|http_req_duration|http_req_sending + http_req_waiting + http_req_receiveing(avg, min, med, max, p(90), p(95)|
|expected_response|正常応答のみのhttp_req_duration(avg, min, med, max, p(90), p(95)。正常な応答がない場合、この項目は表示されない|
|http_req_failed|リクエストが失敗した割合(%)|
|http_req_receiving|レスポンスの1バイト目が到達してから最後のバイトを受信するまでの時間(avg, min, med, max, p(90), p(95)|
|http_req_sending|リクエストを送信するのにかかった時間(avg, min, med, max, p(90), p(95)|
|http_req_tls_handshaking|TLSセッションのハンドシェイクにかかった時間(avg, min, med, max, p(90), p(95)。httpでは0|
|http_req_waiting|リクエストが送信完了してから、レスポンスが開始されるまでの時間(avg, min, med, max, p(90), p(95)。TTFB(Time To First Byte)|
|http_reqs|リクエスト総数。(Total, /s)|
|iteration_duration|シナリオ1ループにかかった時間(avg, min, med, max, p(90), p(95)。|
|iterations|シナリオを繰り返した回数(Total, /s)|
|vus|Virtual UserS。最後のシナリオのときの並列数(だと思う)|
|vus_max|最大Virtual UserS。テスト中の最大並列数(だと思う)|

https://k6.io/docs/using-k6/metrics/

### 並列度を上げて試してみる

`--vus 10` で並列数=10、 `--duration 30s` で30秒継続

```sh
$ k6 run --vus 10 --duration 30s sample.js
```

実行中はこんな感じで進む

```

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

  execution: local
     script: sample2.js
     output: -

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


running (0m16.5s), 10/10 VUs, 160 complete and 0 interrupted iterations
default   [===================>------------------] 10 VUs  16.5s/30s
```
バーが右端まで到達したら完了。

結果

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

  execution: local
     script: sample2.js
     output: -

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


running (0m30.2s), 00/10 VUs, 300 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  30s

     data_received..............: 92 kB   3.1 kB/s
     data_sent..................: 24 kB   785 B/s
     http_req_blocked...........: avg=56.54µs min=1µs    med=4µs    max=1.74ms  p(90)=11µs   p(95)=28.05µs 
     http_req_connecting........: avg=21.89µs min=0s     med=0s     max=835µs   p(90)=0s     p(95)=0s      
     http_req_duration..........: avg=4.4ms   min=1.07ms med=3.84ms max=16.27ms p(90)=6.99ms p(95)=9.86ms  
     http_req_failed............: 100.00% ✓ 300      ✗ 0   
     http_req_receiving.........: avg=47.36µs min=12µs   med=38µs   max=265µs   p(90)=79.1µs p(95)=108.35µs
     http_req_sending...........: avg=39.9µs  min=6µs    med=14µs   max=2.2ms   p(90)=37µs   p(95)=83.75µs 
     http_req_tls_handshaking...: avg=0s      min=0s     med=0s     max=0s      p(90)=0s     p(95)=0s      
     http_req_waiting...........: avg=4.31ms  min=1.01ms med=3.79ms max=16.04ms p(90)=6.92ms p(95)=9.39ms  
     http_reqs..................: 300     9.934892/s
     iteration_duration.........: avg=1s      min=1s     med=1s     max=1.02s   p(90)=1.01s  p(95)=1.01s   
     iterations.................: 300     9.934892/s
     vus........................: 10      min=10     max=10
     vus_max....................: 10      min=10     max=10
```

予定通り、並列度 10 x 30s で300リクエスト処理されていることがわかりますね。

## ファイルアップロード

これを記事にした理由です。ファイルアップロードを含む負荷テストをしたくて探しました。Locust、Gatlingは使ったことがあるものの今はPythonもScalaも使っていないので、今後を考えると別のツールが欲しいと思いました。
トップ画像が物議を醸すvegetaも考えましたが、multipartのテストデータを用意するのが~~めんどう~~向いていなさそうなツールでした。

ファイルアップロードするときのシナリオはこう

```upload.js
import http from "k6/http";

const binFile = open('./test.bin', 'b');

export const options = {
  stages: [
    { duration: '3s', target: 10 },
    { duration: '30s', target: 10 },
  ],
};

export default function() {
    let url =  'http://localhost/upload'

    const data = {
      file: http.file(binFile, 'test.bin'),
    };

    const response = http.post(url, data);
};
```
組み込みの `open` を使いファイルを読み込み、 `http.file`の行で`multipart/form-data`のパラメータ`file` としてファイルとしてbodyデータを作っています。

また、`options` でランプアップしています。
記述している内容だと、最初の3秒で10並列まで徐々に増やし、次の30秒は10並列固定で流しています。
ドキュメントはこちら。

https://k6.io/docs/using-k6/scenarios/executors/ramping-vus

## サンプル・ドキュメントも豊富

コードを読んで雰囲気を掴み、

https://github.com/grafana/k6/tree/master/samples

ドキュメントでコードの意味を調べることで機能把握が捗りました。

https://k6.io/docs/

この記事で位置から説明できる量じゃないので、詳しくは公式サイトを見たほうが速いです。

## 気をつける点

### rps

```
--rps int                             limit requests per second
```
`rps` という便利なオプションがありますが要注意らしく、公式では到達率で制御することを推奨しています。分散して並列実行する仕組みがあるっぽい(k6 cloudでは出来る)けど、`rps` は１インスタンスあたりの制御しかできないらしいです。

https://k6.io/docs/using-k6/scenarios/arrival-rate

１インスタンスだとしても、`vus`での並列リクエストの合計が`rps`になるように制御するので、想定以上の負荷をかけたときのレポートはどういう制御がされたのか、パット見わからない状況でした。(もっと使い倒してからまた書くかもしれません)

### ファイル

シナリオ中に `const binFile = open('./test.bin', 'b');` は書けない。exportしているfunctionは毎回呼ばれるっぽいので当然といえば当然。


## まとめ

見つけてテストするまで本当に５分くらいで出来たので、嬉しくなって書きました。良いツールはどんどん紹介していかないと。