2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカル環境を使わずに k6 で負荷試験を書いてみた

Posted at

はじめに

CI/CD で負荷試験を実行するために Grafana k6 を使ってみたので導入部分を記事にしてみました

Grafana k6 とは

オープンソースで開発されている負荷試験用ツール
並列実行数や実行時間を指定して API を呼び出し、閾値を設けて試験の合否を判定できます
2025年5月6日にバージョン 1.0.0 が公開されました1

k6 を使ってよかった点

使っていて感じたメリットを書きました

  • シングルバイナリ
    バイナリファイルをダウンロードして配置するのみで実行できるため、リモート環境でも手軽に試せます
  • TypeScript でテストコードを作成可能
    npm でtypes/k6が提供されているため、専用コマンドにも TypeScript の補完が使えます
    また、引数に直接.ts拡張子のファイルを指定して実行できます
  • バイナリが重すぎない
    zip 化して Lambda Layer として登録できます

負荷試験を Lambda で実行する

手軽にテストするため Lambda で k6 を呼び出す関数を実装しました

  • リモート環境で編集しながら実行できます
  • API やパイプラインに組み込みやすいです
  • Step Functions を使って並列実行やリトライ処理を簡単に設定できます

テストする API

REST API と GraphQL API を検証するために API Gateway と AppSync を使って API を作成します
今回はテスト用なので API Gateway は mock で作成して AppSync もデータソースのない JavaScript リゾルバで実装しました
他のサービスに接続せず、単純な文字列のみを返す API にしています
また、条件を揃えるためにどちらの API も API Key を設定しました

k6 バイナリを Lambda Layer に登録

  1. Git Hubからlinux-arm64.tar.gzをダウンロードして解凍します
    linux-amd64.tar.gzでも大丈夫です、その場合は以降で指定するアーキテクチャーをarm64からx86_64に読み替えてください
  2. binという名前のディレクトリを作成して、解凍したk6のバイナリファイルを入れて zip 化します
    .
    └── bin/
        └── k6                    ← k6 バイナリ
    
  3. AWS コンソールで Lambda Layer を作成します
    zip ファイルは 30 MB より少し大きいくらいのサイズなので、50 MB 以内に収まります
  4. アーキテクチャーにarm64を指定して、ランタイムにNode.jsを指定して作成します
    image.png

K6 を実行する Lambda

REST API 用の Lambda と GraphQL API 用の Lambda を別々に作成します

  • ランタイム
    Node.js(今回はNode.js 22.xを指定)
  • アーキテクチャ
    arm64(Lambda Layer とそろえます)
  • タイムアウト
    デフォルトが 3 秒なのでタイムアウトしないように 30 秒に変更します
  • メモリ
    テストで使用されるメモリがデフォルトの 128MB を超えるので 1024MB 以上に設定します2
  • 環境変数
    API_KEYを定義します

  • ディレクトリ構成

    .
    ├── handler.mjs               ← Lambda ハンドラー
    ├── scripts/
    │   ├── k6.ts                 ← k6 テストスクリプト(TypeScript)
    └── layers/
        └── k6-layer.zip          ← /bin/k6 を含んだ layer(Lambda では /opt/bin/k6 に展開)
    
  • hander.mjs

    import { spawnSync } from "child_process";
    import { fileURLToPath } from "url";
    import { dirname, join } from "path";
    
    export async function handler() {
      const scriptPath = join(
        dirname(fileURLToPath(import.meta.url)),
        "scripts",
        "k6.ts"
      );
      const options = [
        "run",
        "--env",
        `API_KEY=${process.env.API_KEY}`,
        scriptPath,
      ];
      const result = spawnSync("/opt/bin/k6", options, { encoding: "utf-8" });
      console.info("k6 stdout:", result.stdout);
      if (result.error || result.status !== 0) {
        throw new Error();
      }
    }
    

    Lambda Layer で登録された k6 をコマンドにして、引数にk6.tsファイルを指定して実行させます
    エラーを受け取った場合はエラー終了にして、閾値を下回った場合も Lambda を異常終了させます
    API Key は環境変数で指定いしてprocess.env.API_KEYをコマンドのオプションとして渡しています

  • k6.ts
    下記は GraphQL API 用の実装例です

    import http from "k6/http";
    import { check, sleep } from "k6";
    
    export const options = {
      vus: 1000,
      duration: "10s",
      thresholds: {
        http_req_failed: ["rate<0.01"],
        http_req_duration: ["p(95)<1000"],
        checks: ["rate>0.9"],
      },
    };
    
    export default function () {
      const params = {
        headers: {
          "Content-Type": "application/json",
          "x-api-key": __ENV.API_KEY,
        },
      };
      const query = JSON.stringify({ query: "query MyQuery { test }" });
      const res = http.post(
        "https://<エンドポイント>.appsync-api.<リージョン>.amazonaws.com/graphql",
        query,
        params
      );
      const body = JSON.parse(res.body);
      check(body, {
        "body check": (b) => b.data.test.includes("Hello from AppSync!"),
      });
      sleep(1);
    }
    

    パラメータを以下のように設定します

    • vus: 並列実行数(今回は 1000 件)
    • duration: 試験実行時間(今回は 10 秒)
    • thresholds
      • http_req_failed: リクエスト失敗率(上記では 1% 未満)
      • http_req_duration: レスポンス応答時間(上記では 95% が 1 秒未満)
      • checks: 関数の中で設定した check の成功割合(上記では 90% より多い)

    コマンドの引数で渡した API Key は__ENVで取得して設定します
    また、API 実行ごとに 1 秒間の待機時間を入れています

Step Function で並列実行

並列にテストを走らせる Step Functions を作成します
今回は、REST API と GraphQL API それぞれを試験する Lambda を並列で実行する処理を記載しました

{
  "StartAt": "Parallel",
  "States": {
    "Parallel": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "API Gateway Load Test",
          "States": {
            "API Gateway Load Test": {
              "Type": "Task",
              "Resource": "arn:aws:states:::lambda:invoke",
              "Arguments": {
                "FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:<関数名>:$LATEST"
              },
              "End": true
            }
          }
        },
        {
          "StartAt": "AppSync Load Test",
          "States": {
            "AppSync Load Test": {
              "Type": "Task",
              "Resource": "arn:aws:states:::lambda:invoke",
              "Arguments": {
                "FunctionName": "arn:aws:lambda:<リージョン>:<アカウントID>:function:<関数名>:$LATEST"
              },
              "End": true
            }
          }
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Comment": "error",
          "Next": "Fail"
        }
      ],
      "Next": "Succeed"
    },
    "Succeed": {
      "Type": "Succeed"
    },
    "Fail": {
      "Type": "Fail"
    }
  },
  "QueryLanguage": "JSONata"
}

実行すると以下のような画面が表示されます
image.png
Lambda の実行結果は CloudWatch で確認できます

実行結果

REST API と GraphQL API それぞれで以下のような実行結果が得られます

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  ()  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: /var/task/scripts/k6.ts
        output: -

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


running (00.9s), 1000/1000 VUs, 0 complete and 0 interrupted iterations
default   [   9% ] 1000 VUs  00.9s/10s

running (01.9s), 1000/1000 VUs, 984 complete and 0 interrupted iterations
default   [  19% ] 1000 VUs  01.9s/10s

running (02.9s), 1000/1000 VUs, 1977 complete and 0 interrupted iterations
default   [  29% ] 1000 VUs  02.9s/10s

running (03.9s), 1000/1000 VUs, 2971 complete and 0 interrupted iterations
default   [  39% ] 1000 VUs  03.9s/10s

running (04.9s), 1000/1000 VUs, 3963 complete and 0 interrupted iterations
default   [  49% ] 1000 VUs  04.9s/10s

running (05.9s), 1000/1000 VUs, 4960 complete and 0 interrupted iterations
default   [  59% ] 1000 VUs  05.9s/10s

running (06.9s), 1000/1000 VUs, 5958 complete and 0 interrupted iterations
default   [  69% ] 1000 VUs  06.9s/10s

running (07.9s), 1000/1000 VUs, 6958 complete and 0 interrupted iterations
default   [  79% ] 1000 VUs  07.9s/10s

running (08.9s), 1000/1000 VUs, 7958 complete and 0 interrupted iterations
default   [  89% ] 1000 VUs  08.9s/10s

running (09.9s), 1000/1000 VUs, 8958 complete and 0 interrupted iterations
default   [  99% ] 1000 VUs  09.9s/10s

running (10.9s), 0032/1000 VUs, 9954 complete and 0 interrupted iterations
default ↓ [ 100% ] 1000 VUs  10s


  █ THRESHOLDS 

    checks
    ✓ 'rate>0.9' rate=100.00%

    http_req_duration
    ✓ 'p(95)<1000' p(95)=33.92ms

    http_req_failed
    ✓ 'rate<0.01' rate=0.00%


  █ TOTAL RESULTS 

    checks_total.......................: 9986    906.876199/s
    checks_succeeded...................: 100.00% 9986 out of 9986
    checks_failed......................: 0.00%   0 out of 9986

    ✓ body check

    HTTP
    http_req_duration.......................................................: avg=16.58ms min=8.6ms med=12.97ms max=302.7ms p(90)=18.85ms p(95)=33.92ms
      { expected_response:true }............................................: avg=16.58ms min=8.6ms med=12.97ms max=302.7ms p(90)=18.85ms p(95)=33.92ms
    http_req_failed.........................................................: 0.00%  0 out of 9986
    http_reqs...............................................................: 9986   906.876199/s

    EXECUTION
    iteration_duration......................................................: avg=1.06s   min=1s    med=1.01s   max=2.28s   p(90)=1.22s   p(95)=1.52s  
    iterations..............................................................: 9986   906.876199/s
    vus.....................................................................: 32     min=32        max=1000
    vus_max.................................................................: 1000   min=1000      max=1000

    NETWORK
    data_received...........................................................: 9.0 MB 815 kB/s
    data_sent...............................................................: 1.7 MB 151 kB/s




running (11.0s), 0000/1000 VUs, 9986 complete and 0 interrupted iterations
default ✓ [ 100% ] 1000 VUs  10s

結果

今回のような簡単な API であれば 1000 並列くらいであれば安定していました
応答速度は実行ごとにばらつきがありましたが平均的には REST API と GraphQL API であまり差はなかったです
また、どちらも API でも安定して 95% 以上の応答が 100ms 以内になっていました

終わりに

開発環境等を用意せずに AWS マネジメントコンソールのみで実装できました
サーバーレス構成で負荷試験が実行できるので、コストを抑えて試しやすいと感じました

参考

k6
サーバーレスで負荷試験!Step Functions + Lambdaを使ったk6の分散実行

  1. 0 系の開発は以前から行われています

  2. 今回は vus の値を 1000 にしたので、最大使用メモリの値は 400~600MB 程度でした

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?