GMOメイクショップ コアグループ エンジニアの森です。
現在のチームではずっと次世代ECのフロントエンドをメインに開発してきましたが、最近バックエンド開発も触るようになってきました。
そんな中、直近の業務で作成したAPIに対する負荷試験を担当しました。私にとっては初めての負荷試験だったのですがk6というツールを使うと簡単に実施できたので、その手順を書いていきます。
私と同じような初心者の助けになれば嬉しいです。
負荷試験とは
負荷試験は、実際に発生し得るシステムへのアクセス等を想定した負荷がかけ、それに耐えられるか等を確認する試験です。大量のアクセスがかかった場合や、長時間連続稼働し続けた場合にシステムに異常が発生しないか等を検証します。
k6とは
k6はオープンソースの負荷試験ツールです。JavaScriptを使ってテストシナリオを作成し、簡単に負荷試験を実施することができます。
有料のクラウド版もありますが、今回はローカルマシンで実施します。
試験実施
1.シナリオ作成
今回は具体例として、私が実施した負荷試験のシナリオから
[ショップ情報取得] → [会員ログイン]
の処理部分を抜粋してシナリオテストの例を見ていきましょう。
まずはショップ情報取得する処理です。
このAPIは情報を取得するために複数のエンドポイントを叩く必要があるので、配列にしてループでhttp.get()
でAPIを実行しています。
APIレスポンスからcheck()
今回はAPIレスポンスを確認してステータスが200なら成功、それ以外が返ってきたら失敗と判定するよう定義します。
http
、check
共にk6に含まれるモジュールですのでimportする必要があります。
import { check } from "k6"; import http from "k6/http"; /**リクエストヘッダ */ const headers = { "Content-Type": "application/json", Authorization: "Beraer ******", }; function getShopInfo() { const requests = [ { endpoint: "エンドポイント1" }, { endpoint: "エンドポイント2" }, ︙ { endpoint: "エンドポイントx" }, ]; requests.forEach((req) => { const res = http.get(req.endpoint, { headers: headers }); check(res, { "[getSiteInfo] status is 200": (r) => r.status == 200, }); }); }
続いてログイン処理ですが、先程と同じようにエンドポイントを叩きcheck()
でAPIの成否を確認します。
ただしログインにはID/PWの確認が必要ですのでhttp.post()
を使用してリクエストします。
const LOGIN_ID = *** const PASSWORD = *** function login() { const res = http.post( "ログインAPIのエンドポイント", JSON.stringify({ loginId: LOGIN_ID, password: PASSWORD, }), { headers: headers } ); check(res, { "[login] status is 200": (r) => r.status == 200, }); }
最後にこれらを順番に実行するメソッドを作成すればテストシナリオの完成です。
export default function () { getShopInfo() login() }
2.オプション
k6 では実行時間などをオプションとして設定できます。実行コマンドに書き込んで指定することができますが、ソースに書き込むことも可能です。ソースに書き込んでおけば、毎回コマンド実行時に書く必要もなく、また記録として残しておくこともできるので便利です。
今回は以下のように設定しました。
export const options = { scenarios: { default: { executor: "constant-vus", exec: "default", vus: 1, duration: "10m", }, }, thresholds: { http_req_failed: ["rate<0.05"], http_req_duration: ["p(95)<3000"], }, };
上から順に見ていきます scenariosの負荷試験の実行オプションを設定。
executor: "constant-vus"
:リクエストするペース、今回は一定の負荷をかけ続けるよう設定exec: "default"
:実行するメソッド名、defaultの場合はなくても問題ありません。- execの名前が間違っているとエラーが返ってきます。
ERRO[0000] There were problems with the specified script configuration:
vus: 1
:同時接続数、今回は1に設定duration: "10m"
:実行時間、今回は10分に設定
詳しくはドキュメント参照
thresholdsには負荷試験の合格条件を記述します。プロジェクトに合わせて適切に設定してください。
http_req_failed: ["rate<0.05"]
:リクエストのエラー率が5%未満
(IPAの非機能要求グレードの性能目標値のオンラインレスポンス:レベル4に設定)http_req_duration: ["p(95)<3000"]
: // 95%のレスポンスが3秒以内で正常に処理できていること
詳しくはドキュメント参照
最終的なシナリオテストのコードは以下のようになりました。
import { check } from "k6"; import http from "k6/http"; export const options = { scenarios: { default: { executor: "constant-vus", vus: 1, duration: "10m", }, }, thresholds: { http_req_failed: ["rate<0.05"], http_req_duration: ["p(95)<3000"], }, }; const LOGIN_ID = *** const PASSWORD = *** const headers = { "Content-Type": "application/json", Authorization: "Beraer ******", }; /**ショップ情報取得 */ function getShopInfo() { const requests = [ { endpoint: "エンドポイント1" }, { endpoint: "エンドポイント2" }, ︙ { endpoint: "エンドポイントx" }, ]; requests.forEach((req) => { const res = http.get(req.endpoint, { headers: headers }); check(res, { "[getSiteInfo] status is 200": (r) => r.status == 200, }); }); } /**ログイン処理 */ function login() { const res = http.post( "ログインAPIのエンドポイント", JSON.stringify({ loginId: LOGIN_ID, password: PASSWORD, }), { headers: headers } ); check(res, { "[login] status is 200": (r) => r.status == 200, }); } export default function () { getShopInfo() login() }
3.実行コマンド
先程作ったシナリオテストは、以下コマンドで実行できます。
k6 run test.js
実行コマンドに追加できるオプションがいくつかありますが、今回使って便利だったものを紹介します。
-v
:デバッグモードで実行、console.log()等が出力される- エラーが発生した際、console.log()でレスポンスの中身を確認する際に役立ちました。
--out
:実行結果をファイル出力する- データとして残せますし、エラー出ていた時間帯のログを確認する等役立ちました。
以下、output.jsonにconsole.log()の出力等を含めて出力するコマンドです。
`k6 run -v test.js --out json=output.json`
4.実行結果
以下が作成したシナリオテストの実行結果です。
$ k6 run test.js /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script:test.js output: - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): * default: 1 looping VUs for 10m0s (gracefulStop: 30s) ✓ [getSiteInfo] status is 200 ✓ [login] status is 200 checks.........................: 100.00% ✓ 5484 ✗ 0 data_received..................: 186 MB 310 kB/s data_sent......................: 2.3 MB 3.8 kB/s http_req_blocked...............: avg=45.69µs min=0s med=2µs max=236.79ms p(90)=3µs p(95)=4µs http_req_connecting............: avg=2.24µs min=0s med=0s max=12.29ms p(90)=0s p(95)=0s ✓ http_req_duration..............: avg=108.73ms min=31.83ms med=87.16ms max=697.71ms p(90)=173.62ms p(95)=196.54ms { expected_response:true }...: avg=108.73ms min=31.83ms med=87.16ms max=697.71ms p(90)=173.62ms p(95)=196.54ms ✓ http_req_failed................: 0.00% ✓ 0 ✗ 5484 http_req_receiving.............: avg=10.59ms min=48µs med=2.44ms max=589.45ms p(90)=23.33ms p(95)=39.04ms http_req_sending...............: avg=349.25µs min=29µs med=136µs max=20.92ms p(90)=684.79µs p(95)=1.31ms http_req_tls_handshaking.......: avg=15.01µs min=0s med=0s max=82.32ms p(90)=0s p(95)=0s http_req_waiting...............: avg=97.79ms min=30.28ms med=71.47ms max=613.11ms p(90)=168.85ms p(95)=180.72ms http_reqs......................: 5484 9.134889/s iteration_duration.............: avg=437.8ms min=273.34ms med=389.2ms max=1.42s p(90)=614.51ms p(95)=881.36ms iterations.....................: 1371 2.283722/s vus............................: 1 min=1 max=1 vus_max........................: 1 min=1 max=1 running (10m00.3s), 0/1 VUs, 1371 complete and 0 interrupted iterations default ✓ [======================================] 1 VUs 10m0s
以下は各check()
の結果を示しています。今回はすべてのAPIが成功しているので、すべてにチェックをついています。
✓ [getSiteInfo] status is 200 ✓ [login] status is 200
その下に項目.......結果
の形式で試験結果が表示されます。一部抜粋です。
http_req_duration
はリクエストにかかった時間とthreshold
の定義に対する合否を表示しています。
今回の95%が3秒以内で定義したので p(95)の部分を見てみると、約0.2秒以内にリクエストが完了しており合格判定なので✓がついています。
✓ http_req_duration..............: avg=108.73ms min=31.83ms med=87.16ms max=697.71ms p(90)=173.62ms p(95)=196.54ms
http_req_failed
はリクエストの失敗率とthreshold
の定義に対する合否を表示しています。
今回は失敗率0.00%(0件失敗、5484件成功)、5%未満で合格なので✓がついています。
✓ http_req_failed................: 0.00% ✓ 0 ✗ 5484
今回は各APIにエラーが見られず、試験の合格条件を満たしているのでこの負荷試験では問題なしと言えそうです。
5.実行結果(エラー例)
先ほどはリクエストが全て上手くいったパターンの結果でしたが、リクエストにエラーが発生してしまった場合の結果も見てみまししょう。
以下は先程のシナリオテストのうち、ログイン処理の何割か失敗させた場合の結果です。
$ k6 run test.js /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: test.js output: - scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): * default: 1 looping VUs for 10m0s (gracefulStop: 30s) ✓ [getSiteInfo] status is 200 ✗ [login] status is 200 ↳ 20% — ✓ 692 / ✗ 2683 checks.........................: 73.50% ✓ 7442 ✗ 2683 data_received..................: 454 MB 756 kB/s data_sent......................: 1.8 MB 3.0 kB/s http_req_blocked...............: avg=27.69µs min=0s med=2µs max=190.03ms p(90)=3µs p(95)=3µs http_req_connecting............: avg=1.16µs min=0s med=0s max=5.99ms p(90)=0s p(95)=0s ✓ http_req_duration..............: avg=58.55ms min=4.53ms med=52.61ms max=896.58ms p(90)=127.33ms p(95)=137.55ms { expected_response:true }...: avg=76.35ms min=22.13ms med=59.9ms max=694.8ms p(90)=131.67ms p(95)=142.39ms ✗ http_req_failed................: 26.49% ✓ 2683 ✗ 7442 http_req_receiving.............: avg=1.89ms min=22µs med=1.24ms max=162.86ms p(90)=4.04ms p(95)=5.78ms http_req_sending...............: avg=282.92µs min=29µs med=187µs max=28.9ms p(90)=444µs p(95)=632.79µs http_req_tls_handshaking.......: avg=9.33µs min=0s med=0s max=59.66ms p(90)=0s p(95)=0s http_req_waiting...............: avg=56.37ms min=264µs med=49.97ms max=896.08ms p(90)=124.52ms p(95)=134.53ms http_reqs......................: 10125 16.869534/s iteration_duration.............: avg=177.76ms min=101.68ms med=164.09ms max=1.07s p(90)=249.54ms p(95)=276.41ms iterations.....................: 3375 5.623178/s vus............................: 1 min=1 max=1 vus_max........................: 1 min=1 max=1 running (10m00.2s), 0/1 VUs, 3375 complete and 0 interrupted iterations default ✓ [======================================] 1 VUs 10m0s ERRO[0602] thresholds on metrics 'http_req_failed' have been crossed
失敗した場合は以下のように各check()
毎に成功率 — ✓ 成功数 / ✗ 失敗数
が表示されます。
✓ [getSiteInfo] status is 200 ✗ [login] status is 200 ↳ 20% — ✓ 692 / ✗ 2683
リクエストの失敗率が26.49%、thresholdsで定義したエラー率5%を上回ったためhttp_req_failed
に✗がつき、最後にエラーメッセージが表示されています。
checks.........................: 73.50% ✓ 7442 ✗ 2683 ✗ http_req_failed................: 26.49% ✓ 2683 ✗ 7442 ERRO[0602] thresholds on metrics 'http_req_failed' have been crossed
http_req_duration
はかかった時間だけ見ているのでエラー率とは関係なく合格になっています。
タイムアウトエラー等、時間がかかる類のエラーだった場合はここにも✗がつきそうです。
まとめ
k6を使うと簡単にシナリオテストを作成し、負荷試験を実施することが可能です。
今回は簡単な例のみ紹介しましたが、オプションの同時接続数や実施時間を変更することで限界性能や連続稼働の検証も可能です。
今回は負荷試験の結果を出すまでの流れを紹介しましたが、実際の負荷試験ではシナリオテストの結果に対して不合格だった場合やエラー等発生していた場合には、問題の特定・分析やパフォーマンスチューニング等が必要になります。 こちらに関しても今後記事にしていければと思います。
最後まで見ていただき、ありがとうございました。