k6を使って簡単に負荷試験

GMOメイクショップ コアグループ エンジニアの森です。
現在のチームではずっと次世代ECのフロントエンドをメインに開発してきましたが、最近バックエンド開発も触るようになってきました。

そんな中、直近の業務で作成したAPIに対する負荷試験を担当しました。私にとっては初めての負荷試験だったのですがk6というツールを使うと簡単に実施できたので、その手順を書いていきます。
私と同じような初心者の助けになれば嬉しいです。

負荷試験とは

負荷試験は、実際に発生し得るシステムへのアクセス等を想定した負荷がかけ、それに耐えられるか等を確認する試験です。大量のアクセスがかかった場合や、長時間連続稼働し続けた場合にシステムに異常が発生しないか等を検証します。

k6とは

k6オープンソース負荷試験ツールです。JavaScriptを使ってテストシナリオを作成し、簡単に負荷試験を実施することができます。
有料のクラウド版もありますが、今回はローカルマシンで実施します。

k6.io

試験実施

1.シナリオ作成

今回は具体例として、私が実施した負荷試験のシナリオから
[ショップ情報取得] → [会員ログイン]
の処理部分を抜粋してシナリオテストの例を見ていきましょう。

まずはショップ情報取得する処理です。
このAPIは情報を取得するために複数のエンドポイントを叩く必要があるので、配列にしてループでhttp.get()APIを実行しています。
APIレスポンスからcheck() 今回はAPIレスポンスを確認してステータスが200なら成功、それ以外が返ってきたら失敗と判定するよう定義します。
httpcheck共に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秒以内で正常に処理できていること

www.ipa.go.jp

詳しくはドキュメント参照

最終的なシナリオテストのコードは以下のようになりました。

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を使うと簡単にシナリオテストを作成し、負荷試験を実施することが可能です。
今回は簡単な例のみ紹介しましたが、オプションの同時接続数や実施時間を変更することで限界性能や連続稼働の検証も可能です。

今回は負荷試験の結果を出すまでの流れを紹介しましたが、実際の負荷試験ではシナリオテストの結果に対して不合格だった場合やエラー等発生していた場合には、問題の特定・分析やパフォーマンスチューニング等が必要になります。 こちらに関しても今後記事にしていければと思います。
最後まで見ていただき、ありがとうございました。