k6 のテスト結果を datadog で可視化してみる

この記事は [弁護士ドットコム Advent Calendar 2022](https://qiita.com/advent-calendar/2022/bengo4com) の 6 日目の記事です。

# k6 とは
k6 とは Grafana Labs とそのコミュニティによって開発されている OSS で、パフォーマンステストを簡単に行うことができる負荷テストツールです。特徴としては以下のような点があります。

* OSS 版とクラウド版がある
* テストシナリオを Javascript で記述する
* HTTP / WebSockets / gRPC などのプロトコルに対応している
* 結果を Grafana はもちろん Datadog等でも可視化できる

https://k6.io/

インストール方法は [ここから](https://k6.io/docs/get-started/installation/) 確認できます。

# 今回やること
今回は k6 を使って golang製サーバーへ負荷テストを実施し、試験経過を Datadog で可視化するところまでやってみようと思います。


ファイルは以下のように配置しました。それでは実際に k6 を実行する前の準備から行きましょう。

```sh:tree
.
├── .env
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── main.go                  # golang サーバー
└── scripts
    └── performance-test.js  # テストシナリオ
```

また、私のローカル環境は以下となります。
```sh
ProductName:	macOS
ProductVersion:	12.6.1
BuildVersion:	21G217
```

# 準備 （golang サーバー）
まずは golang の何の変哲もない http サーバーを用意します。
go.mod ファイルはサンプルとして `k6-performance-test` としています。


```go:go.mod
module k6-performance-test

go 1.19
```

サーバーには一つのエンドポイントだけを用意し、何かしらの処理の代わりとして少し wait するような形にしています。

```go:main.go
package main

import (
	"io"
	"log"
	"math/rand"
	"net/http"
	"time"
)

func main() {
	handler := func(w http.ResponseWriter, req *http.Request) {

		//  do something
		rand.Seed(time.Now().UnixNano())
		n := rand.Intn(300)
		time.Sleep(time.Duration(n) * time.Millisecond) // 0〜300 ミリ秒待つ

		w.WriteHeader(http.StatusOK)
		io.WriteString(w, "Hello, world!\n")
	}

	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

```

datadog-agent を別コンテナで用意したい関係上、 `Dockerfile` 及び `docker-compose.yml` を用意しています。
Dockerfile は最低限のことしかやっていませんが、 `mian.go` をビルドしてサーバーをたち上げています。

```dockerfile:Dockerfile
FROM golang:1.19-alpine3.16 as builder
WORKDIR /var/www
COPY . .
RUN go build -o app .

FROM alpine:3.16 as app
WORKDIR /var/www
COPY --from=builder /var/www/app /usr/local/bin/app

EXPOSE 8080
ENTRYPOINT ["app"]

```

# 準備 （datadog-agent）
datadog-agent に渡す必要がある環境変数を `.env` ファイルに用意します。
* `DD_API_KEY` は datadog のサインアップ時に入手できる値です。
* `DD_SITE` は `datadoghq.com`等の値をご自身の環境に合わせて設定します。（ご自身の Datadogアカウントでログインした時のドメイン部になります）

```sh
$ cat .env
DD_API_KEY=<DD_API_KEY>
DD_SITE=<DD_SITE>
```
他の設定は後述の `docker-compose.yml` に記載してあります。

# 準備 （docker-compose）
最後に `docker-compose.yml` を用意します。
それぞれ、負荷テストを実行する `k6` と datadog-agentが常駐している`dd-agent` 、 golang サーバーの`server` です。

```yaml:docker-compose.yml
version: '3.8'

services:
  k6:
    image: loadimpact/k6:latest
    container_name: k6
    # entrypoint: k6 run --out statsd /scripts/performance-test.js
    environment:
      - K6_STATSD_ENABLE_TAGS=true
      - K6_STATSD_ADDR=dd-agent:8125  # datadog-agent のアドレスと合わせる
    volumes:
      - ./scripts:/scripts
    depends_on:
      - dd-agent
      - server

  dd-agent:
    image: datadog/agent:latest
    container_name: dd-agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /proc/:/host/proc/:ro
      - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro
    environment:
      - DD_API_KEY=${DD_API_KEY}
      - DD_SITE=${DD_SITE}
      - DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1
    env_file:
      - .env
    ports:
      - "8125:8125/udp"

  server:
    build:
      context: .
      dockerfile: Dockerfile
      target: app
    volumes:
      - .:/var/www
```

そしてコンテナ起動をすれば準備完了です。
```shell:コンテナ起動
$ docker compose up -d
```

（`k6` コンテナの `entrypoint` はコメントアウトしていて、あとから `docker compose run` 時に同様コマンドを実行しますが、コメントアウトを外せば `docker compose up` 時にそのまま試験まで実施もできます。）

# テストシナリオを書く

さて、本題の k6 のテストシナリオを書いていきましょう。今回は最もシンプルに、サーバーに対して GET のリクエストを送信しているケースです。

k6 では結果のチェックやレイテンシの評価といったこともできます。今回は以下をリクエストを成功とみなしています。
* レスポンスステータスコードが `200` であること
* 95 パーセンタイルが 500 ミリ秒以内であること

また、下記の場合は golang サーバーに対して並列数=30 で 1分間リクエストを継続します。

```js:scripts/performance-test.js
import http from 'k6/http';
import { check } from 'k6'

export const options = {
  duration: '1m',
  vus: 30,

  thresholds: {
    http_req_duration: ['p(95)<500'],
  },
};

export default function () {
  const res = http.get('http://server:8080');

  check(res, {
    'is status 200': (r) => r.status === 200
  })
}

```

それでは下記コマンドで試験を実施してみましょう。

```shell:k6のパフォーマンステストを実行
$ docker compose run --rm k6 run --out statsd /scripts/performance-test.js
```
ポイントとしては `--out statsd` を設定することで、環境変数 `K6_STATSD_ADDR` に設定したアドレス先へ結果を送信しています。これによって datadog-agent が結果を datadog へ送信し、datadog画面上で結果を可視化することができます。

コンソール上では下記のような結果が返ってきます。

```sh: k6の結果画面

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

  execution: local
     script: /scripts/performance-test.js
     output: statsd (dd-agent:8125)

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


running (1m00.2s), 00/30 VUs, 11884 complete and 0 interrupted iterations
default ✓ [======================================] 30 VUs  1m0s

     ✓ is status 200

     checks.........................: 100.00% ✓ 11884      ✗ 0    
     data_received..................: 1.6 MB  26 kB/s
     data_sent......................: 915 kB  15 kB/s
     http_req_blocked...............: avg=12.34µs  min=886ns    med=4.11µs   max=3.17ms   p(90)=6.68µs   p(95)=8.65µs  
     http_req_connecting............: avg=974ns    min=0s       med=0s       max=1.24ms   p(90)=0s       p(95)=0s      
   ✓ http_req_duration..............: avg=151.31ms min=68.73µs  med=150.75ms max=313.02ms p(90)=270.19ms p(95)=285.91ms
       { expected_response:true }...: avg=151.31ms min=68.73µs  med=150.75ms max=313.02ms p(90)=270.19ms p(95)=285.91ms
     http_req_failed................: 0.00%   ✓ 0          ✗ 11884
     http_req_receiving.............: avg=148.51µs min=15.63µs  med=112.33µs max=8.32ms   p(90)=270.16µs p(95)=367.74µs
     http_req_sending...............: avg=28.64µs  min=4.29µs   med=17.59µs  max=2.39ms   p(90)=37.99µs  p(95)=89.46µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=151.13ms min=0s       med=150.53ms max=312.69ms p(90)=269.99ms p(95)=285.77ms
     http_reqs......................: 11884   197.400516/s
     iteration_duration.............: avg=151.67ms min=260.48µs med=151.12ms max=313.17ms p(90)=270.79ms p(95)=286.3ms 
     iterations.....................: 11884   197.400516/s
     vus............................: 30      min=30       max=30 
     vus_max........................: 30      min=30       max=30 
```

`http_req_duration` の `p(95)` の結果は `285.91ms` となっており、閾値の 500ms 内なので `✓` マークが付いているのがわかります。また、 `checks` が `100.00% ✓ 11884` となっており、全てのリクエストが正常だったこともわかります。

これだけでもリクエスト結果、平均値・中央値・外れ値等から性能を判断できるかもしれませんが、途中経過も見れると尚いいと思いませんかね？


# Datadog で可視化
先程のテストで既に Datadog にメトリクスが送信されているはずです。
ありがたいことに、Datadog では　k6 から送信されたメトリクスを元にデフォルトのダッシュボードを作成してくれます。

`/dashboard/lists?q=k6` にアクセスするとデフォルトダッシュボードが見つかります。
![スクリーンショット 2022-12-05 0.31.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122328/ae17bbcd-da86-5440-4358-a2f5ecebf009.png)

ダッシュボードを見てみましょう。今回は以下の6つのグラグが表示されます。（ちょっと見栄えよくするために試験時間を15分にしています）
* Virtual users
* Request per second
* Data sent/received
* HTTP request duration
* Response timings - 95th
* Response time 95th - HeatMap
![スクリーンショット 2022-12-05 1.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122328/08df65f3-be9e-0436-f231-47cdca00088b.png)



もちろん Metrics Explorer ではこれらメトリクスを読み取ることができるので（プレフィックスが `k6`）、カスタマイズダッシュボードを生成することもできます。メトリクスの詳細は [こちらに](https://k6.io/docs/using-k6/metrics/) まとまっています。
![スクリーンショット 2022-12-05 0.33.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122328/a41652df-7bab-245c-b951-a383fb53427f.png)

このように k6 と Datadog の連携はとても簡単にできることがわかりました。


# おまけ
今回は簡単なシナリオテストでしたが、実際の試験ではもっと複雑になってくるかと思います。私自身が k6 を使った際に便利だった tips を最後に少し紹介したいと思います。

## ランダムな UUID を生成したい
k6 ではデフォルトで UUID 生成のサポートは無いのでライブラリ生成の手段を取ります。k6 では node.js のコードは動かないため `browserify` を使ってコード変換します。まず、npm で uuid をインストールします。
```sh: uuid をインストール
$ npm install uuid@3.4.0
```

`browserify` で `uuid.js` を生成します。

```sh: uuid.js を生成
$ browserify node_modules/uuid/index.js -s uuid > uuid.js
```

`uuid.js` を利用して uuid を生成することができます。

```js:サンプル
// 生成したファイルをインポート
import uuid from './uuid.js';

export default function () {
  // v1 UUID の生成
  const uuid1 = uuid.v1();

  // v4 UUID の生成
  const uuid4 = uuid.v4();
}
```

## 環境変数を使いたい
例えば、試験対象のベースURL を環境毎で変えたい（ローカル環境で試し撃ちしたい等）場合に一々コードを直すのは面倒です。
k6 で環境変数を読み込むには `__ENV` と記述します。以下では環境変数 `BASE_URL` を読み込むことができます。

```js
__ENV.BASE_URL
```

## テストデータを使いたい（CSV）
単純なランダム値ではなく、実データのランダム値を用意してテストしたいといったケースで、CSVを用意してそこからランダムに取得するようなテストも可能です。

例として以下のような CSV を用意します。
```:test_datas/users.csv
"id","name","age"
1,"田中太郎",23
2,"山田花子",25
```
CSV を読み込んだあと、k6 のデータとして扱うには `SharedArray` を利用します。この関数は一回だけ実行され、VU間で共有される仕組みとなっているようです。

```js: CSVのテストデータを読み込む
const users = new SharedArray('users', function () {
  return papaparse.parse(open('./test_datas/users.csv'), { header: true }).data
})
```

そして例えば `users` からランダムの user を取得する関数を用意します。

```js: ランダムユーザーの取得
function randomeUser () {
  return users[Math.floor(Math.random() * users.length)]
}
```

テストでは以下のように実装します。

```js: ランダム取得サンプル
import { SharedArray } from 'k6/data'
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js'

export default function () {
  const user = randomeUser()
  console.log(user.id)
  console.log(user.name)
  console.log(user.age)
}

const users = new SharedArray('users', function () {
  return papaparse.parse(open('./test_datas/users.csv'), { header: true }).data
})

function randomeUser () {
  return users[Math.floor(Math.random() * users.length)]
}
```

# 最後に
k6 の試験結果を Datadog で可視化する方法を実践していきました。今回のような簡単な試験では可視化の恩恵はそこまで無いかもしれませんが、リクエストが詰まるような場合にどのような過程かを可視化できるのは、結果調査に大きく役立つかと思います。

また、公式ドキュメントには [CI/CD へ組み込む](https://k6.io/docs/integrations/#continuous-integration-and-continuous-delivery)など面白い内容がいくつもあるので興味がある方は是非読んでみてください。

記事のネタは常にストックしておかないと急遽書くのは大変ですね。来年は何事も余裕を持って取り組んでいきたいと思います。
明日は @S-Masakatsu さんです。お楽しみに。

# 参考
* [k6 - Result store and visualization - Datadog](https://k6.io/docs/results-output/real-time/datadog/)