この記事は 弁護士ドットコム Advent Calendar 2022 の 6 日目の記事です。
k6 とは
k6 とは Grafana Labs とそのコミュニティによって開発されている OSS で、パフォーマンステストを簡単に行うことができる負荷テストツールです。特徴としては以下のような点があります。
- OSS 版とクラウド版がある
- テストシナリオを Javascript で記述する
- HTTP / WebSockets / gRPC などのプロトコルに対応している
- 結果を Grafana はもちろん Datadog等でも可視化できる
インストール方法は ここから 確認できます。
今回やること
今回は k6 を使って golang製サーバーへ負荷テストを実施し、試験経過を Datadog で可視化するところまでやってみようと思います。
ファイルは以下のように配置しました。それでは実際に k6 を実行する前の準備から行きましょう。
.
├── .env
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── main.go # golang サーバー
└── scripts
└── performance-test.js # テストシナリオ
また、私のローカル環境は以下となります。
ProductName: macOS
ProductVersion: 12.6.1
BuildVersion: 21G217
準備 (golang サーバー)
まずは golang の何の変哲もない http サーバーを用意します。
go.mod ファイルはサンプルとして k6-performance-test
としています。
module k6-performance-test
go 1.19
サーバーには一つのエンドポイントだけを用意し、何かしらの処理の代わりとして少し wait するような形にしています。
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
をビルドしてサーバーをたち上げています。
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アカウントでログインした時のドメイン部になります)
$ 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
です。
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
そしてコンテナ起動をすれば準備完了です。
$ docker compose up -d
(k6
コンテナの entrypoint
はコメントアウトしていて、あとから docker compose run
時に同様コマンドを実行しますが、コメントアウトを外せば docker compose up
時にそのまま試験まで実施もできます。)
テストシナリオを書く
さて、本題の k6 のテストシナリオを書いていきましょう。今回は最もシンプルに、サーバーに対して GET のリクエストを送信しているケースです。
k6 では結果のチェックやレイテンシの評価といったこともできます。今回は以下をリクエストを成功とみなしています。
- レスポンスステータスコードが
200
であること - 95 パーセンタイルが 500 ミリ秒以内であること
また、下記の場合は golang サーバーに対して並列数=30 で 1分間リクエストを継続します。
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
})
}
それでは下記コマンドで試験を実施してみましょう。
$ docker compose run --rm k6 run --out statsd /scripts/performance-test.js
ポイントとしては --out statsd
を設定することで、環境変数 K6_STATSD_ADDR
に設定したアドレス先へ結果を送信しています。これによって datadog-agent が結果を datadog へ送信し、datadog画面上で結果を可視化することができます。
コンソール上では下記のような結果が返ってきます。
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .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
にアクセスするとデフォルトダッシュボードが見つかります。
ダッシュボードを見てみましょう。今回は以下の6つのグラグが表示されます。(ちょっと見栄えよくするために試験時間を15分にしています)
- Virtual users
- Request per second
- Data sent/received
- HTTP request duration
- Response timings - 95th
- Response time 95th - HeatMap
もちろん Metrics Explorer ではこれらメトリクスを読み取ることができるので(プレフィックスが k6
)、カスタマイズダッシュボードを生成することもできます。メトリクスの詳細は こちらに まとまっています。
このように k6 と Datadog の連携はとても簡単にできることがわかりました。
おまけ
今回は簡単なシナリオテストでしたが、実際の試験ではもっと複雑になってくるかと思います。私自身が k6 を使った際に便利だった tips を最後に少し紹介したいと思います。
ランダムな UUID を生成したい
k6 ではデフォルトで UUID 生成のサポートは無いのでライブラリ生成の手段を取ります。k6 では node.js のコードは動かないため browserify
を使ってコード変換します。まず、npm で uuid をインストールします。
$ npm install uuid@3.4.0
browserify
で uuid.js
を生成します。
$ browserify node_modules/uuid/index.js -s uuid > uuid.js
uuid.js
を利用して uuid を生成することができます。
// 生成したファイルをインポート
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
を読み込むことができます。
__ENV.BASE_URL
テストデータを使いたい(CSV)
単純なランダム値ではなく、実データのランダム値を用意してテストしたいといったケースで、CSVを用意してそこからランダムに取得するようなテストも可能です。
例として以下のような CSV を用意します。
"id","name","age"
1,"田中太郎",23
2,"山田花子",25
CSV を読み込んだあと、k6 のデータとして扱うには SharedArray
を利用します。この関数は一回だけ実行され、VU間で共有される仕組みとなっているようです。
const users = new SharedArray('users', function () {
return papaparse.parse(open('./test_datas/users.csv'), { header: true }).data
})
そして例えば users
からランダムの user を取得する関数を用意します。
function randomeUser () {
return users[Math.floor(Math.random() * users.length)]
}
テストでは以下のように実装します。
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 へ組み込むなど面白い内容がいくつもあるので興味がある方は是非読んでみてください。
記事のネタは常にストックしておかないと急遽書くのは大変ですね。来年は何事も余裕を持って取り組んでいきたいと思います。
明日は @S-Masakatsu さんです。お楽しみに。