Grafana k6 × AWS Fargate で実現する効果的な負荷テスト

k6 は JavaScript を使用してテストシナリオを作成できるため柔軟なテスト設計が可能です。JavaScript は多くの開発者にとって馴染みのある言語であるため、学習コストを抑えつつ高い生産性を実現します。その結果、シンプルな API 負荷テストから複数エンドポイントを組み合わせた複雑なシナリオまで、幅広いテストケースを容易に記述できます。変数や関数を利用した条件分岐、ループなどを取り入れることで実運用に近いシナリオも表現しやすくなっています。
 
本記事の後半で紹介する負荷テストの実践例は、実際に Backlog の一部バックエンドの刷新プロジェクトで使用したものです。AWS Fargate と Grafana k6 を活用した具体的な使用例として、参考にしていただければ幸いです!

k6 がサポートしているテストシナリオ

k6 は次のとおり多様なテストシナリオをサポートしています(参考)。なお、本記事ではシンプルな負荷テスト用の構成を紹介します。

Load and performance testing

スパイク、ストレス、ソークテストなどの高負荷パフォーマンステストを効率的に実行するよう最適化されています。

Browser performance testing

ブラウザ API を利用して、ブラウザベースのパフォーマンステストを実施し、ブラウザ関連のパフォーマンス問題を特定できます。

Performance and synthetic monitoring

​定期的なテストをスケジュールして、運用環境のパフォーマンスと可用性を継続的に検証できます。

Automation of performance tests

CI/CD や自動化ツールとシームレスに統合でき、開発およびリリースサイクルの一環としてパフォーマンステストを自動化できます。​

Chaos and resilience testing

​カオス実験の一環としてトラフィックをシミュレートしたり、テストから直接実験をトリガーしたり、xk6-disruptor を用いて Kubernetes 内でさまざまな障害を注入できます。

Infrastructure testing

​拡張機能を利用して、新しいプロトコルのサポートを追加したり、特定のクライアントを使用してインフラ内の個々のシステムを直接テストできます。

基礎知識

この記事を読み進めるうえで、「k6 って何ができるの?」「VU って何?」といった基本的な用語や仕組みを知っておくと、内容がぐっと分かりやすくなると思います。このセクションでは、そんなk6の基礎知識をサクッと紹介します。ぜひざっと目を通してみてください。

用語集

用語 意味
モデル化 現実のユーザー行動やシステムの負荷パターンを再現するためにテストシナリオを設計・構築すること。
VU(Virtual User) テスト環境においてシステム負荷をシミュレーションするために用いられる仮想的なユーザー。
Iteration VUが実行する一連の処理の1回のサイクル。
Scenario VUとイテレーションに関する設定を行う機能。シナリオを使用することで負荷テストにおける多様なワークロードやトラフィックパターンを再現することが可能になる。
Group テストコードを論理的に集約するための機能。テストの可読性を高め、結果の分析やトラブルシューティングを容易にする。
Executor VUのワークロードをスケジューリングする機能。テストの実行時間や、トラフィックが一定か変動するかなどを設定する。

メトリクスの種類

k6 が収集するメトリクスは次の 4 つのタイプに分類されます。
 
タイプ 特徴 用途
Counters 単調増加する値 イベントの総発生回数を計測します。例えば、リクエストの総数やエラーの総数をカウントするのに適しています。
Gauges 増減する値 ある瞬間の値を測定します。例えば、現在のアクティブユーザー数やメモリ使用量など、時間とともに変化する値を追跡します。
Rates 二値結果(成功/失敗など)を持つイベントの割合 特定のイベントが全体に対してどれだけ発生したかの割合を計算します。例えば、エラーレートや成功率の測定に使用します。
Trends 数値データの統計情報(平均値、中央値、最大値など) レスポンスタイムやレイテンシなど、パフォーマンスの分布や傾向を分析するために利用します。
 
代表的なメトリクスは次に示す 3 つの built-in メトリクスです。
 
メトリクス 特徴
http_req_duration 各 HTTP リクエストの全体的な応答時間をミリ秒単位で計測するトレンドメトリクスです。リクエストの開始から終了までにかかった合計時間を示します。
http_req_failed HTTP リクエストが失敗したかどうかを示すブール値を持つレートメトリクスです。
iterations 仮想ユーザー(VU)が完了したシナリオの反復回数をカウントするカウンターメトリクスです。スクリプトの default 関数が 1 回実行されるごとに 1 増加します。
 
これらのメトリクスに着目する手法は The RED Method(Rate、Error、Durationの頭文字を取ったもの)として知られています。
 
関連資料

ライフサイクル

k6のライフサイクルは次のとおり 4 つのステージで構成されます(参考:Overview of the lifecycle stages)。
  • init
  • Setup
  • VU
  • Teardown
次の図はこれらの遷移を図示したものです。

init ステージ

ライフサイクル関数(default or scenario / setup / teardown)の外側で定義された関数はすべて init コンテキストに所属します。init ステージでは「HTTP リクエストを行えない」という制約があります(プロトコルリクエストの応答は動的で予測が困難なため)。これにより、すべての VU に渡って init ステージが再現可能であることを保証しています。

VU ステージ

VUコード(default 関数もしくは scenario 関数に含まれるもの)には次の制約があります。
  • ローカルファイルシステムからファイルを読み込めない。
  • モジュールをインポートできない。
これらは init ステージで実施する必要があります。

Setup / Teardown ステージ

  • init ステージとは異なり、すべての JavaScript API を利用できます。
  • --no-setup および --no-teardown フラグを使用して、このステージをスキップできます。
  • setup 関数から default 関数および teardown 関数にデータのコピーを渡すことができます。
    • ただし、次の事柄に留意してください。
      • 渡せるのはデータのみで関数は渡せません
      • データが大きい場合はメモリ消費量に注意してください
      • default 関数内でデータを加工して teardown 関数に渡すことはできません

シナリオ

シナリオは VUs とイテレーションに関する設定を行う機能です。シナリオを使用することで負荷テストにおいて多様なワークロードやトラフィックパターンを再現することが可能になります。シナリオの特徴は次のとおりです。
  • テストを柔軟に構成できる
    • 同じスクリプト内に複数のシナリオを宣言でき、それぞれが異なる JavaScript 関数を独立して実行します。
  • 現実的なトラフィックのシミュレーション
    • 各シナリオが特定の VUs やイテレーションスケジュールを使用でき、個別の Executor によって制御されます。
  • 並列または順次のワークロード実行
    • シナリオは互いに独立しており並行して実行されますが、startTime プロパティを慎重に設定することでシーケンシャルな実行に見せることも可能です。
  • 詳細な結果分析
    • 各シナリオに異なる環境変数やメトリックタグを設定でき、詳細な結果分析が可能です。
シナリオは次のサンプルコードのとおり options オブジェクトが内包します。
export const options = {
  scenarios: {
    example_scenario: {
      // name of the executor to use
      executor: 'shared-iterations',

      // common scenario configuration
      startTime: '10s',
      gracefulStop: '5s',
      env: { EXAMPLEVAR: 'testing' },
      tags: { example_tag: 'testing' },

      // executor-specific configuration
      vus: 10,
      iterations: 200,
      maxDuration: '10s',
    },
    another_scenario: {
      /*...*/
    },
  },
};
関連資料

Executor

仮想ユーザー(VU)のワークロードをスケジューリングする機能です。テストの実行時間や、トラフィックが一定か変動するかなどを設定します。シナリオには必ず executor プロパティを定義します。利用可能な Executor は次の 6 つです。
 
Executor モデル 特徴 用途
Shared iterations Closed イテレーションをすべての仮想ユーザーで共有します。速い VU がより多くのイテレーションを実行します。 不均一な負荷パターンをテストするシナリオに適しています。また、この Executor は時間効率が最も良いため、例えば開発ビルドサイクルにおけるパフォーマンス低下のチェックにも適しています。
Per VU iterations Closed 各 VU が既定の回数のイテレーションを実行します。総イテレーション回数は VU 数 * イテレーション数 となります。 各 VU が同じテストケースを実行するシナリオに適しています。
Constant VUs Closed 指定された期間内に一定数の VU が可能な限り多くのイテレーションを実行します。 一定の負荷が所定の時間だけ継続するシナリオに適しています。
Ramping VUs Closed 段階的に VU 数を増減させてイテレーションを実行します。 特定の期間内に負荷が増減するシナリオに適しています。
Constant arrival rate Open システムの応答に関係なく一定の到達レート(時間あたりのリクエスト数)でイテレーションを実行します。到達レートを担保するため、Executor は動的に VU 数を決定します。詳しくは Arrival-rate VU allocation を参照してください。 スループットが一定であるべきシナリオに適しています。
Ramping arrival rate Open 段階的に到達レートを増減させてイテレーションを実行します。 短期間に急激なスパイクを発生させるシナリオに適しています。
 
なお、各 Executor のページには「Observations」というセクションがあり、Executor の挙動が図示されています。

macOS への k6 の導入と初期設定

私が macOS を使っているため、これ以降は macOS を前提として操作方法を紹介します。Linux や Windows でも同様の操作が可能なので、これらについては Install k6 を参照してください 🙏

インストール手順

次のコマンドで k6 をインストールします。
brew install k6

OS のチューニング

もし k6 実行時に Too Many Open Files に遭遇した場合は Fine-tune OS を参考に OS のパラメーターを調整してみてください。

使用状況レポート

k6 はデフォルトで匿名の使用状況レポートを送信します。送信される内容は Usage collection のとおりです。なお、環境変数 K6_NO_USAGE_REPORT もしくはコマンドフラグ --no-usage-report で送信を停止できます。

基本的なテストシナリオの作成と実行

スクリプトの生成・編集

次のコマンドを実行すると script.js が生成されます(macOS 以外の OS での手順は Running k6 に記載されています)。
k6 new
スクリプトのエントリーポイントは次の default 関数です。
export default function () {
  // ...
}
また、同ファイルに含まれる options オブジェクトでテストシナリオの設定を変更できます。次のコードは VUs とテストの実行時間を指定する例です。
export const options = {
  vus: 10,
  duration: '30s',
};
これらのオプションはテスト実行時にコマンドフラグで上書き可能です。
 
関連資料

シナリオ実行と結果確認

k6 の run コマンドにスクリプトを渡してテストを実行します。
k6 run script.js
実行が完了すると要約統計(summary statistics)が標準出力に表示されます。この統計には次の情報が含まれます。
  • Built-in メトリクス、およびユーザーが定義したカスタムメトリクス
    • Built-in メトリクスには次の値が含まれます
      • Median
      • Average
      • Minimum and Maximum
      • p90, p95 and p99
  • Scenarios およびこれに所属する Groups のリスト
  • Thresholds および Checks の成否
    • Thresholds は特定のパフォーマンス基準を設定し、それを超えるかどうかを検証するための条件です。例えばレスポンスタイムやエラー率などに対して閾値を設定し、それを満たしているかどうかを判定します。
    • Checks は個別のリクエストやレスポンスに対して、特定の条件を満たしているかどうかを確認するための機能です。これは HTTP ステータスコードやレスポンスボディの内容など、細かい要素に対して検証を行いたい場合に利用されます。
要約統計のサンプルを次に示します。
Ramp_Up ✓ [======================================] 00/20 VUs  30s
     █ GET home - https://example.com/

       ✓ status equals 200

     █ Create resource - https://example.com/create

       ✗ status equals 201
        ↳  0% — ✓ 0 / ✗ 45

     checks.........................: 50.00% ✓ 45       ✗ 45
     data_received..................: 1.3 MB 31 kB/s
     data_sent......................: 81 kB  2.0 kB/s
     group_duration.................: avg=6.45s    min=4.01s    med=6.78s    max=10.15s   p(90)=9.29s    p(95)=9.32s
     http_req_blocked...............: avg=57.62ms  min=7µs      med=12.25µs  max=1.35s    p(90)=209.41ms p(95)=763.61ms
     http_req_connecting............: avg=20.51ms  min=0s       med=0s       max=1.1s     p(90)=100.76ms p(95)=173.41ms
   ✗ http_req_duration..............: avg=144.56ms min=104.11ms med=110.47ms max=1.14s    p(90)=203.54ms p(95)=215.95ms
       { expected_response:true }...: avg=144.56ms min=104.11ms med=110.47ms max=1.14s    p(90)=203.54ms p(95)=215.95ms
     http_req_failed................: 0.00%  ✓ 0        ✗ 180
     http_req_receiving.............: avg=663.96µs min=128.46µs med=759.82µs max=1.66ms   p(90)=1.3ms    p(95)=1.46ms
     http_req_sending...............: avg=88.01µs  min=43.07µs  med=78.03µs  max=318.81µs p(90)=133.15µs p(95)=158.3µs
     http_req_tls_handshaking.......: avg=29.25ms  min=0s       med=0s       max=458.71ms p(90)=108.31ms p(95)=222.46ms
     http_req_waiting...............: avg=143.8ms  min=103.5ms  med=109.5ms  max=1.14s    p(90)=203.19ms p(95)=215.56ms
     http_reqs......................: 180    4.36938/s
     iteration_duration.............: avg=12.91s   min=12.53s   med=12.77s   max=14.35s   p(90)=13.36s   p(95)=13.37s
     iterations.....................: 45     1.092345/s
     vus............................: 1      min=1      max=19
     vus_max........................: 20     min=20     max=20

ERRO[0044] some thresholds have failed
本サンプルの特徴は次のとおりです。
  • Ramp_Up シナリオが実行されている
    • これには GET home および Create resource グループが含まれている
  • 95%のリクエストが 200ms 以内に完了することを要求する Thresholds が失敗している
    • サンプルの http_req_duration に ✗ が付いており、エラーメッセージとして ERRO\[0044\] some thresholds have failed が出力されています
なお、要約統計に含まれる情報はコマンドフラグで制御可能です(参考:Summary options)。

応用的な使い方

オープンモデルとクローズドモデル

k6 の Executor は、それぞれ異なる方法で VU のスケジューリングを行います。前出のとおり到達レート Executor は オープンモデル となり、それ以外の Executor は クローズドモデル となります。クローズドモデルでは、現在のイテレーションが完了してから次のイテレーションを実行します。対してオープンモデルではイテレーションの完了に関わらず次のイテレーションが実行されます。

クローズドモデルが適さないケース

クローズドモデルではイテレーションの開始と終了が密接に関係しているため、システムの応答時間がイテレーションの到達レートに影響を与える可能性があります。この問題は「coordinated omission」として知られており、テスト結果が実際よりもよく見えてしまう現象を引き起こします。VU の到達レートやスループットをシミュレートすることが重要なシナリオにおいて、この影響は好ましくありません。
 
関連資料

グループの活用方法

グループ はテストコードを論理的に集約するための機能です。テストの可読性を高め、結果の分析やトラブルシューティングを容易にします。グループの特徴は次のとおりです。
  • グループは入れ子にできます。
  • グループ内で排出されたメトリクスにはグループ名がタグとして付与されます。
    • グループが入れ子になっている場合、すべてのグループ名が :: で結合されます。
  • k6はグループごとの duration をメトリクスとして排出します。
  • タグ付け可能なリソース(Checks / Requests / Custom Metrics)がグループ内で実行された場合、それらのメトリクスには実行されたグループの名前がタグとして付与されます。
関連資料

負荷テストの実践

ここからは負荷テストで実際に使用したコードを見ていきます。次の図は簡略化した k6 プロジェクトのディレクトリ構成です。実際のプロジェクトは HTTP メソッドごとに用意したシナリオファイルや Fargate 用の設定ファイルなどを大量に含んでいますが、今回は必要最低限のものを記載しています。
.
├── Dockerfile
├── Makefile
├── compose.yml
├── ecs
│   ├── config.json
│   └── overrides
│        └── fargate
│             └── get_peek.json
└── k6
     ├── modules
     │    ├── utils
     │    │    ├── file_picker.js
     │    │    └── math.js
     │    └── get.js
     └── scenarios
          └── get_peek
               └── main.js
ディレクトリは大きく「ecs」と「k6」に分かれています。負荷テストの実行基盤として Fargate を使用したため、前者のディレクトリには Fargate 用の設定ファイルを収めています。また、後者のディレクトリには次の 2 つの k6 用ファイルを収めています。
  • シナリオを定義するファイル
  • HTTP リクエストを生成するファイル
続いて、それぞれの内容を見ていきます。

シナリオの定義

対象ファイル:k6/scenarios/get_peek/main.js
 
このファイルではシナリオ(Executor や RPS の設定など)の定義、セットアップ、および HTTP リクエストの送信を行います。options オブジェクトにシナリオのパラメーターを設定し、setup 関数でリクエスト送信前の準備を行い、getPeek 関数で GET リクエストを送信します。
 
なお、シナリオの内容は次のとおりです。
  • Executor として constant-arrival-rate を使用する
  • 100 RPS を維持する
  • テスト期間は 5 分
  • 初期 VU として 100 を割り当てる(小さすぎると VU が枯渇するため、RPS に併せて適切な値を設定します)
  • Checks を使用してレスポンスのステータスコードが 200 であるか検証する
import { check } from 'k6';
import encoding from 'k6/encoding';

import { get } from '../../modules/get.js';
import { random } from '../../modules/utils/math.js';
import { dataFileIds } from '../../testdata/data.js';

export const options = {
  scenarios: {
    getPeek: {
      exec: 'getPeek',
      executor: 'constant-arrival-rate',
      rate: 100,
      duration: '5m',
      preAllocatedVUs: 100,
    },
  },
  summaryTimeUnit: 'ms',
  systemTags: [],
};

export function setup() {
  return {
    credentials: encoding.b64encode(`${__ENV.USERNAME}:${__ENV.PASSWORD}`),
    endpoint: __ENV.TEST_ENDPOINT,
    dataDirectoryRoot: __ENV.TEST_DATA_DIRECTORY_ROOT,
    dataSubDirectory: __ENV.TEST_DATA_SUB_DIRECTORY,
  };
}

export function getPeek(data) {
  let dir = {};
  for (let i = 0; 0 < dataFileIds.length; i++) {
    if (dataFileIds[i].name === data.dataSubDirectory) {
      dir = dataFileIds[i];
      break;
    }
  }

  const minFileId = dir.minFileId;
  const maxFileId = dir.maxFileId;

  const fileId = random(minFileId, maxFileId);
  const filePath = `${data.dataDirectoryRoot}/${data.dataSubDirectory}/file_${fileId}.dat`;

  const resp = get(data.endpoint, data.credentials, `${data.dataDirectoryRoot}/${filePath}`);

  check(resp, {
  'is status 200': (r) => r.status === 200,
  });
}

GET リクエストの生成

対象ファイル:k6/modules/get.js
 
GET リクエストを送信する処理は main.js には含めずにこちらのファイルに記述しました。このようにしておくと、複数のシナリオで GET リクエストの送信処理を使い回すことができます。
import http from 'k6/http';

export function get(endpoint, credentials, filePath) {
  const url = `${endpoint}/${filePath}`;
  return http.get(url, {
    headers: {
      'Authorization': `Basic ${credentials}`,
    },
  }, {
    tags: {
      name: 'get',
    },
  });
}

AWS Fargate で k6 を実行する

ここからは Fargate で k6 を実行する手順を見ていきます。今回は k6 を自前でビルドするカスタムイメージを使用しました。Dockerfile の内容は次のとおりです。
FROM golang:1.23.0 AS build

ARG XK6_VERSION=v0.13.3

# Build a k6 binary using Go
# https://grafana.com/docs/k6/latest/extensions/build-k6-binary-using-go/
RUN go install go.k6.io/xk6/cmd/xk6@${XK6_VERSION} && \
    xk6 build latest

FROM alpine:3.20 AS deploy

RUN apk add --no-cache aws-cli bash curl tzdata

COPY --from=build /go/k6 /usr/bin/k6

WORKDIR /usr/local/k6

COPY k6 .

CMD [ "./run.sh" ]

テストのエントリーポイント

続いて Dockerfile の CMD に指定している run.sh の内容を以下に示します。このシェルスクリプトは次のタスクを実行します。
  • k6シナリオの実行
  • k6が出力した統計情報のS3へのアップロード
#!/usr/bin/env bash
set -e

. ./.env

readonly EXEC_DATETIME="$(TZ=Asia/Tokyo date +"%Y%m%d-%H%M%S")"
readonly WORK_DIR=$(cd "$(dirname "$0")" && pwd)
readonly test_id=$(echo "${TEST_ID}" | tr "[:upper:]" "[:lower:]")

if k6 run \
  --quiet \
  --summary-export="summary.json" \
  -e USERNAME="${USERNAME}" \
  -e PASSWORD="${PASSWORD}" \
  "${WORK_DIR}/scenarios/${test_id}/main.js" 2>&1 \
| tee "console.log"; then {
  aws s3 cp summary.json "s3://backlog-k6-report/${TEST_PLATFORM}/${test_id}/${EXEC_DATETIME}/summary.json"
  aws s3 cp console.log "s3://backlog-k6-report/${TEST_PLATFORM}/${test_id}/${EXEC_DATETIME}/console.log"
}
fi

環境変数によるテストの制御

対象ファイル:ecs/overrides/fargate/efs+s3/get_peek.json
 
このファイルには Fargate 用の設定を記述します。複雑なものはなく、主な内容は環境変数です。環境変数でシナリオや HTTP リクエストを送信するエンドポイントなどを実行時に切り替えられるようにしています。
{
    "containerOverrides": [
        {
            "name": "k6",
            "environment": [
                {
                    "name": "TEST_PLATFORM",
                    "value": "fargate"
                },
                {
                    "name": "TEST_ID",
                    "value": "GET_PEEK"
                },
                {
                    "name": "TEST_ENDPOINT",
                    "value": "https://fargate-1360027822.ap-northeast-1.elb.amazonaws.com"
                }
            ]
        }
    ]
}

ここまで準備ができたら、いよいよテストの実行です。Fargate タスクの実行には次のように AWS CLI を使用します。

aws ecs run-task \
  --cli-input-json file://ecs/config.json \
  --overrides file://ecs/overrides/fargate/get_peek.json
実行が完了すると S3 バケットに統計情報が JSON 形式でアップロードされます。あとは統計情報を参照して、アプリケーションのチューニングをしたり、シナリオを更新するなどして、テストを調整していきます。

まとめ

k6 は JavaScript でテストを記述できるため、学習コストが低く、多くの人にとって扱いやすい負荷テストツールです。さらに、テスト結果をわかりやすい要約統計として表示してくれるため、パフォーマンスの分析もスムーズに行えます。シンプルな API テストから複雑なシナリオまで幅広く対応でき、Grafana と連携することで視覚的な分析も可能になります。手軽に導入でき、効率的に負荷テストを実施できることから、開発や運用の現場でも活用しやすいツールといえるでしょう。ぜひ一度、試してみてください!
開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる