フューチャー技術ブログ

k6の使い方 シンプル&軽快な負荷試験ツールを試す

TIGの伊藤真彦です。
業務で行っている開発がいよいよリリースを視野に入れたフェーズに入り、E2Eテストや各種性能試験を行いました。

負荷試験とは

リリース前に行いたい試験の1つに負荷試験があります。

なるべく実際の運用に近い環境、データベースの状態を整え、実際に利用されるであろうユーザー数、もしくはそれ以上の同時接続にシステムが耐えられるかを試験します。特にソーシャルゲームの世界では想定を超えたアクセスによりリリース直後にサーバーがダウンしてしまう悲劇が発生しがちです。IT業界の外にいる人々にとっても覚えのある経験かもしれません。

そんな負荷試験ですが、実際に数百人、数万人規模の同時アクセスを人力で再現するには膨大な予算が必要です。そのため、一台のコンピュータで複数人からの同時アクセスを再現する負荷試験ツールが数多く存在します。

負荷試験ツールの定番と言えばApache Jmeterがあります。改めて確認してみたところ2021年をもって生誕20周年を記念する老舗プロダクトでした、おめでとうございます。

Jmeterの使用は勿論検討したのですが、モダンな技術が大好きな性分であり、我らが渋川さんもおススメしているためk6を試してみました。

k6とは

2016年頃から開発がスタートした負荷試験ツールです。
ワニのキャラクターが印象的です、かわいいですね。

OSSとして公開されている事が特徴です、リポジトリはこちらです。

k6はGoで開発されています、これだけでも今時な印象を受けますね。

公式サイトの説明にある通り。
DevOpsの概念の成熟、現在のあり方に対応し、継続的な性能試験を行えるものである事を意識して設計されています。

In tandem with the growth of DevOps, we started to notice gaps emerging in the market that we knew we could address. Users were no longer testing things one-off. APIs were becoming more prevalent in usage and required testing. Users wanted to test continuously. For many companies, testing was moving into the responsibility of developers.

k6のインストール

k6のインストールはどのプラットフォームであっても簡単です。
installationに記載があります、書いてあるコマンドをコピー&ペーストするだけで問題なく導入できました。

Windows向けにはインストーラが用意されています。

brew, apt-get, yum, dokcerでの配布がサポートされています、バイナリ形式でダウンロードすることも可能です、インストールに苦戦することはほぼ無いと言っても良いと思います。

docker形式の配布形態を利用し、CIで性能試験を行い、デフォルトの性能を下回ったら警告するような使い方もできるかもしれません。

k6の使い方

基本的な利用方法

k6は基本的に負荷試験のシナリオをJavaScriptで記載し、CLIで実行する利用形態で負荷試験を行います。

k6_test.js
import http from 'k6/http';
export default function () {
http.get('http://localhost:8080/');
}

各種ユーティリティをimportし、負荷試験のシナリオを記載します。
特定のエンドポイント1か所にGETでアクセスするだけなら、上記のような4行程度の記載でテストを行うことが可能です。

今回は下記のシンプルなWebサーバーをローカル環境に立てて上記k6_test.jsの動作を検証します。

main.go
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello k6")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}

k6 run ファイル名 で実行することが可能です。
アスキーアートと試験結果が出力されます。

k6 run .\k6_test.js

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

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

data_received..............: 124 B 7.8 kB/s
data_sent..................: 80 B 5.0 kB/s
http_req_blocked...........: avg=403.9µs min=403.9µs med=403.9µs max=403.9µs p(90)=403.9µs p(95)=403.9µs
http_req_connecting........: avg=403.9µs min=403.9µs med=403.9µs max=403.9µs p(90)=403.9µs p(95)=403.9µs
http_req_duration..........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_receiving.........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_sending...........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_reqs..................: 1 63.019517/s
iteration_duration.........: avg=1.9ms min=1.9ms med=1.9ms max=1.9ms p(90)=1.9ms p(95)=1.9ms
iterations.................: 1 63.019517/s

負荷を変更する

--vusオプション--duration等のオプションで負荷の重さを変えることが可能です。
詳しくは公式ドキュメントを参照してください。

主に使うものは、下記のオプションです。

--vus: 同時接続数
--duration: 試験の実行時間
--iteration: シナリオを繰り返す回数

--vusオプションだけではエラーが発生します、試験時間、もしくはシナリオの実行回数を指定する必要があります。

 k6 run .\k6_test.js --vus 10

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

time="2021-03-07T16:33:00+09:00" level=warning msg="the `vus=10` option will be ignored, it only works in conjunction with `iterations`, `duration`, or `stages`"
execution: local
script: .\k6_test.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)

同時接続数10人で10秒間負荷をかけてみます。
デフォルトの時間を超えるまでシナリオを繰り返します、レポートのiterationsを見ると999039回繰り返されたことがわかります。

 k6 run .\k6_test.js --vus 10 --duration 10s

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

execution: local
script: .\k6_test.js
output: -

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


running (10.0s), 00/10 VUs, 999039 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs 10s0

data_received..............: 124 MB 12 MB/s
data_sent..................: 80 MB 8.0 MB/s
http_req_blocked...........: avg=1.19µs min=0s med=0s max=1.49ms p(90)=0s p(95)=0s
http_req_connecting........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_duration..........: avg=67.92µs min=0s med=0s max=5.53ms p(90)=0s p(95)=513.9µs
http_req_receiving.........: avg=14.58µs min=0s med=0s max=1.99ms p(90)=0s p(95)=0s
http_req_sending...........: avg=6.07µs min=0s med=0s max=1.49ms p(90)=0s p(95)=0s
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...........: avg=47.26µs min=0s med=0s max=5.49ms p(90)=0s p(95)=510.2µs
http_reqs..................: 999039 99705.733857/s
iteration_duration.........: avg=96.83µs min=0s med=0s max=17.46ms p(90)=511.3µs p(95)=996.7µs
iterations.................: 999039 99705.733857/s
vus........................: 10 min=10 max=10
vus_max....................: 10 min=10 max=10

同時接続数100人で100回シナリオを繰り返す例です。

k6 run .\k6_test.js --vus 100 --iterations 100

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

execution: local
script: .\k6_test.js
output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 10m30s max duration (incl. graceful stop):
* default: 100 iterations shared among 100 VUs (maxDuration: 10m0s, gracefulStop: 30s)


running (00m00.0s), 000/100 VUs, 100 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs 00m00.0s/10m0s 100/100 shared iters

data_received..............: 12 kB 623 kB/s
data_sent..................: 8.0 kB 402 kB/s
http_req_blocked...........: avg=1ms min=0s med=775.9µs max=2.35ms p(90)=2.35ms p(95)=2.35ms
http_req_connecting........: avg=525.37µs min=0s med=579.2µs max=1.35ms p(90)=775.9µs p(95)=775.9µs
http_req_duration..........: avg=2.52ms min=1.09ms med=2.54ms max=3.57ms p(90)=3.56ms p(95)=3.57ms
http_req_receiving.........: avg=5.23µs min=0s med=0s max=505.49µs p(90)=0s p(95)=0s
http_req_sending...........: avg=103.27µs min=0s med=0s max=775.9µs p(90)=775.9µs p(95)=775.9µs
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...........: avg=2.41ms min=579.2µs med=2.54ms max=3.57ms p(90)=3.17ms p(95)=3.57ms
http_reqs..................: 100 5020.634809/s
iteration_duration.........: avg=4.07ms min=1.35ms med=4.14ms max=5.92ms p(90)=5.92ms p(95)=5.92ms
iterations.................: 100 5020.634809/s

これらの負荷設定はJavaScriptに記載することも可能です。

k6_test.js
import http from 'k6/http';

export let options = {
vus: 10,
iterations: 100
};

export default function () {
http.get('http://localhost:8080/');
}

stagesという書き方で段階的に負荷を変更することも可能です。
この場合vusではなくtargetが同時接続数です。
下記の書き方の場合、同時接続数10人で1秒負荷をかけた後に20人、30人と接続数を増やしていく試験になります。
--stagesのようにCLIオプションとして渡すことはできません。

k6_test.js
import http from 'k6/http';

export let options = {
stages: [
{ target: 10, duration: '1s' },
{ target: 20, duration: '1s' },
{ target: 30, duration: '1s' }
]
};

export default function () {
http.get('http://localhost:8080/');
}

GETリクエスト以外を検証する。

動作確認ではGETリクエストの確認を行いましたが、k6のhttpライブラリはGETリクエスト以外も検証可能です、GET, POSTなどは勿論OPTIONSなどのリクエストも対応しています。
詳しくは公式ドキュメントを確認してください。

よく使うであろうPOST, PUT, DELETEについて簡単なスニペットを用意しました。

k6_post.js
import http from 'k6/http';

export default function () {
import http from 'k6/http';

export default function () {
const body = { key: 'value' };
const headers = { 'Content-Type': 'application/json' };
http.post('http://localhost:8080/', JSON.stringify(body), headers);
}
k6_put.js
import http from 'k6/http';

export default function () {
const body = { key: 'value' };
const headers = { 'Content-Type': 'application/json' };
http.put('http://localhost:8080/', JSON.stringify(body), headers);
}
k6_delete.js
import http from 'k6/http';

export default function () {
const body = { key: 'value' };
const params = { headers: { 'X-MyHeader': 'k6test' } };
http.del('http://localhost:8080/', JSON.stringify(body), params);
}

DELETEリクエストの場合、リクエストボディにパラメータを含めることはあまり一般的ではありませんが、技術的には可能であったりといった側面に対応できるようになっています。
(k6公式ドキュメントにもDELETEリクエストのリクエストボディはセマンティクスが定まっていないため非推奨と記載があります)

リクエストの応答を検証する

サーバーが負荷に耐えきれず正常な応答を返せない割合をcheckライブラリで検証することが可能です。

k6_test.js
import http from 'k6/http';
import { check } from 'k6';

export default function () {
const res = http.get('http://localhost:8080/');
check(res, {
'response code was 200': (res) => res.status == 200,
'body size was 8 bytes': (res) => res.body.length == 8,
});
}

上記コードで同時接続2万人分のリクエストの1割を捌くことに失敗することをレポートできました。

k6 run k6_test.js --vus 20000 --iterations 20000


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

// サーバーエラー発生時にログが出力される為中略

running (01m31.8s), 00000/20000 VUs, 20000 complete and 0 interrupted iterations
default ✓ [======================================] 20000 VUs 01m31.7s/10m0s 20000/20000 shared iters

✗ response code was 200
↳ 90% — ✓ 18054 / ✗ 1946
✗ body size was 8 bytes
↳ 90% — ✓ 18054 / ✗ 1946

checks.....................: 90.27% ✓ 36108 ✗ 3892
data_received..............: 2.2 MB 24 kB/s
data_sent..................: 1.4 MB 16 kB/s
http_req_blocked...........: avg=11.47s min=0s med=16.54s max=19.94s p(90)=19.46s p(95)=19.78s
http_req_connecting........: avg=10.94s min=0s med=15.84s max=19.94s p(90)=19.45s p(95)=19.78s
http_req_duration..........: avg=3.55s min=0s med=944.07ms max=19.34s p(90)=11.12s p(95)=13.1s
http_req_receiving.........: avg=66.54ms min=0s med=0s max=11.01s p(90)=0s p(95)=997.5µs
http_req_sending...........: avg=976.15ms min=0s med=27.92ms max=16.22s p(90)=3.12s p(95)=5.93s
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...........: avg=2.5s min=0s med=602.44ms max=18.21s p(90)=8.91s p(95)=10.96s
http_reqs..................: 20000 217.879289/s
iteration_duration.........: avg=18.18s min=0s med=19.68s max=1m5s p(90)=20s p(95)=25.89s
iterations.................: 20000 217.879289/s
vus........................: 38 min=38 max=16944
vus_max....................: 20000 min=20000 max=20000

ちなみに26000人~29000人分のアクセスを再現するあたりで64GBのメモリを使い切り動かなくなりました。
要求スペック的には1000人オーダーであれば8~16GBのメモリのマシンであっても耐えられそうです。
メモリに余裕のない環境ではマシンスペックの限界を超えたときにGoのランタイムエラーが出力され、Go製ツールであることを感じることができます。

check関数はそのまま真偽値として扱う事が可能です。
試験の失敗をログ出力するfailライブラリと組み合わせることが可能です。

k6_fail.js
import http from 'k6/http';
import { check, fail } from 'k6';

export default function () {
const res = http.get('http://localhost:8080/');
if (
!check(res, {
'status code MUST be 200': (res) => res.status == 200,
})
) {
fail('status code was *not* 200');
}
}

その他テストシナリオの品質を高めるためのユーティリティ等が沢山用意されています、詳しくは公式ドキュメントのjavascript-apiの章を確認してください。
JavaScriptで再利用性の高い高品質なテストシナリオをどのように書いていくかが、腕の見せ所ですね。

負荷試験のアウトプットについて

試験レポートは標準出力で確認できるものを手作業で纏めるだけでも充分ではありますが、--outオプションで出力先、形式を変更することが可能です。

k6 run .\k6_test.js --out json=out.json

公式ドキュメントでは下記のような形式が想定されています。

[object Object] undefined

公式のダッシュボードであるk6 Cloudも提供されています。
試験結果の可視化、前回との差分の比較を様々な手段で行う事や、CloudWatchのメトリクスとして定期的なパフォーマンスチェックを行う監視システムの構築等が想像できます。
これらの機能の存在からも現在のDevOpsの成長に追従した継続的な性能試験という設計思想を感じることができます。

まとめ

  • k6はGoで開発されたモダンな負荷試験ツールです。
  • 軽快なフットワークでインストールが可能です。
  • JavaScriptで書いたシナリオを実行する形で利用します。
  • 試験結果は様々な外部システムと組み合わせることが可能です。

負荷試験を検討の際はぜひ選択肢の1つとして検討してみてください。