インフラ

k6を使ったAPI負荷試験入門|スクリプト構成から結果の見方まで

kim

おはようございます。DWSのkimです!

今回、プロジェクトでk6を使った負荷試験を実施しました。

「負荷試験の準備って、結構大掛かりなのでは?」と思っていましたが、実際に触ってみると「案外手軽にできるなぁ〜」と感じました。

この記事では、k6を初めて使う方向けに、k6の概要、スクリプトの構成、実行方法、結果の見方をできるだけ分かりやすく紹介します!

k6とは?

k6 は、Grafana が提供している負荷試験ツールです。
JavaScript でテストシナリオを記述でき、API や Web アプリケーションに対して負荷をかけることができます。

ざっくり言うと、以下のようなことが可能です。

  • 仮想ユーザー(VU)を立ち上げ、指定した期間、API にリクエストを送信する
  • レスポンス時間やエラー率などを自動で集計する
  • 負荷をかけた際に、アプリケーションがどの程度耐えられるかを確認する

なお、k6を実行するには、事前に実行環境へk6をインストールしておく必要があります。
環境ごとのインストール方法は、以下のk6公式ドキュメントをご確認ください。

Install k6

https://grafana.com/docs/k6/latest/set-up/install-k6/

実務でやったこと

実務では社内APIの性能試験用に、k6スクリプトを用意しました。

やりたかったことはシンプルで、「ある程度の同時アクセスが続いた場合でも、APIが性能基準を満たし、問題なくレスポンスを返せるかを確認したい」というものです。

項目内容
対象社内のREST API(複数エンドポイント)
仮想ユーザー数300〜1000 VU(パターンによる)
試験時間5分
リクエストGETとPOSTを別々の試験として実行

負荷試験を実施する際の注意

負荷試験は、対象システムや周辺サービスに影響を与える可能性があります。実施前に対象環境や負荷条件、実施時間帯、監視方法、停止基準などを関係者と確認し、必ず許可された環境で実施してください。

また、第三者が提供するサービスや本番環境に対して、許可なく高負荷のリクエストを送信しないよう注意してください。

スクリプトの構成

ディレクトリ構成

loadtest/
└── services/
    └── api/                 # 対象 API ごとの試験
        ├── smoke.js         # 対象APIを1回ずつ呼び出して疎通確認
        ├── scenario.js      # 負荷試験本体
        └── lib/
            ├── config.js    # 環境変数・試験条件
            ├── endpoints.js # 叩く API の一覧
            └── request.js   # 実際の HTTP リクエスト

各ファイルの役割

ファイル何をするか
config.jsURL・VU数・時間・METHODを環境変数から読む
endpoints.js叩くAPI一覧を配列で返す
request.jsGET/POSTを送ってレスポンスをチェックする
smoke.js対象APIを1回ずつ呼び出して疎通確認する
scenario.jsk6の設定と、VUが繰り返す処理を定義する

処理の流れ

ポイントは次の2つです。

  1. 各VUの初期化時に、buildEndpoints()を呼び出してAPI一覧を取得する
  2. 試験中は繰り返し、一覧からランダムに1件選んでリクエストし、結果を検証する

いきなり本格的な負荷試験を実行する前に、smoke.jsで各エンドポイントを1回ずつ呼び出し、疎通を確認できるようにしているのもポイントです。

スクリプトの中身を見てみる

ここからは、ポイントになる主なスクリプトを見ていきます。ここでは説明用に、一般化した短いコードで紹介します!

scenario.js — 負荷試験の本体

scenario.jsでは、どれくらいの人数で、どれくらいの時間実行し、どの条件を満たせば成功とするかを設定します。

以下にコードを記載します。

// scenario.js
import { VUS, DURATION } from './lib/config.js';
import { buildEndpoints } from './lib/endpoints.js';
import { callEndpoint } from './lib/request.js';

const endpoints = buildEndpoints(); // 各VUの初期化時にAPI一覧を取得

export const options = {
  scenarios: {
    api_load: {
      executor: 'constant-vus', // 一定数の VU を維持する
      vus: VUS,                 // 同時仮想ユーザー数
      duration: DURATION,       // 試験時間
    },
  },
  thresholds: {
    http_req_failed: ['rate==0'], // エラー率 0%
    checks: ['rate==1'],          // チェック合格率 100%
  },
};

export default function () {
  const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)];
  callEndpoint(endpoint);
}

1. options — 試験の設定

optionsでは、何人・どれくらいの時間・合格条件を決めます。

export const options = {
  scenarios: {
    api_load: {
      executor: 'constant-vus', // 一定数の VU を維持する
      vus: VUS,                 // 同時仮想ユーザー数
      duration: DURATION,       // 試験時間
    },
  },
  thresholds: {
    http_req_failed: ['rate==0'], // エラー率 0%
    checks: ['rate==1'],          // チェック合格率 100%
  },
};

VUSDURATION は、次章で説明するconfig.jsで環境変数から読み込んだ値です。スクリプトを書き換えなくても、実行時に試験条件を変えられるようにしています。

今回使った executor: 'constant-vus' は、一定数の仮想ユーザーを、指定時間ずっと維持する設定です。

他にも、例えば次のような executor がありますので、負荷試験で再現したいアクセスパターンに応じて、適切なexecutorを選択してください!

executorざっくり説明
constant-vusVU数を一定に保つ ← 今回使用したもの
ramping-vusVU数を段階的に増減させる
constant-arrival-rate1秒あたりなど、一定の頻度で処理を開始する

また、thresholds は試験の合格ラインです。
ここに書いた条件を満たさない場合、k6は試験終了時に失敗扱いになります。

ここに記載したscenariosthresholdsの詳細については、以下の公式ドキュメントをご参照ください。

Scenarios

https://grafana.com/docs/k6/latest/using-k6/scenarios/

Thresholds

https://grafana.com/docs/k6/latest/using-k6/thresholds/

2. default function — 繰り返す処理

各VU(仮想ユーザー)が繰り返し実行する処理を記載します。
今回はリクエスト先のエンドポイントをランダムに1つ選んで、呼び出す処理を記載しています。

export default function () {
  const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)];
  callEndpoint(endpoint);
}

やっていることは次のとおりです。

  1. endpoints(呼び出すAPIの一覧)から、今回呼び出すAPIを1件選ぶ
  2. 選んだ情報を callEndpoint() に渡す

今回は、複数のAPIが並行して利用される実運用を想定し、リクエストを送るたびに、呼び出すAPIを一覧からランダムに選択しています。
本記事の例では各APIを同じ確率で選択していますが、実際の利用頻度に合わせて、特定のAPIを多めに呼び出すよう変更することもできます。

HTTPの送信(GET または POST)とレスポンス検証は callEndpoint() の中で行い、詳細は後述の request.js で説明します。

config.js — 試験条件の値をまとめる

URLやVU数、試験時間など、環境ごとに変わる値は config.js にまとめています。

k6では __ENV で環境変数を読めるので、スクリプトを書き換えなくても、実行時に試験条件を変えられます。

// lib/config.js

// 対象APIのURL
export const BASE_URL = __ENV.BASE_URL || 'https://your-api.example';

// GET または POST(デフォルトは GET)
export const METHOD = (__ENV.METHOD || 'GET').toUpperCase();

// 同時ユーザー数
export const VUS = __ENV.VUS ? Number(__ENV.VUS) : 1;

// 試験時間
export const DURATION = __ENV.DURATION || '10s';

// リクエストヘッダー
export const HEADERS = {
  Accept: 'application/json',
};

VUSDURATIONのデフォルト値は、説明・手元確認用に小さくしています。本試験では、試験条件に合わせて値を変更しました。
BASE_URLのデフォルト値は例です。実行時は、負荷試験の許可を得た対象環境のURLに置き換えてください。

※認証付きAPIを対象にする場合、APIキーやBearerトークンなどの認証情報をスクリプトへ直接記載しないようにしてください。環境変数やシークレット管理の仕組みを利用し、Gitなどのリポジトリや実行ログに認証情報が残らないよう注意してください。

__ENV.METHODのように書くと環境変数を参照でき、値がなければ || の右側(デフォルト値)が使われます。

上記のように指定しておけば、次のように実行時だけ条件を変えられます。

# GET(METHODは省略可)
BASE_URL=https://your-api.example METHOD=GET VUS=600 DURATION=5m k6 run services/api/scenario.js

# POST
BASE_URL=https://your-api.example METHOD=POST VUS=600 DURATION=5m k6 run services/api/scenario.js

BASE_URLは例です。実行時は、負荷試験の許可を得た対象環境のURLに置き換えてください。

ここで定義した VUS や DURATIONMETHOD は、scenario.js の options やリクエスト処理から参照しています。実行時に環境変数を渡せば、そちらの値が使われます。

endpoints.js — 叩く API の一覧を作る

負荷試験では、1つの API だけでなく 複数エンドポイントに負荷を分散する ことが多いです。
その一覧は endpoints.js にまとめています。

// lib/endpoints.js
import { BASE_URL, METHOD } from './config.js';

function buildGetEndpoints() {
  return [
    {
      name: 'list_users',
      baseUrl: BASE_URL,
      path: '/api/users',
      query: 'page=1&limit=20',
    },
    {
      name: 'daily_report',
      baseUrl: BASE_URL,
      path: '/api/reports/daily',
      query: 'date=20250101',
    },
  ];
}

function buildPostEndpoints() {
  return [
    {
      name: 'search',
      baseUrl: BASE_URL,
      path: '/api/search',
      body: JSON.stringify({
        keyword: 'test',
        start_date: '20250101',
        end_date: '20250107',
      }),
    },
  ];
}

export function buildEndpoints() {
  return METHOD === 'POST' ? buildPostEndpoints() : buildGetEndpoints();
}

※上記コードのエンドポイントやパラメータは、記事掲載用に一般化したサンプルです。実際の業務で使用したAPIとは異なります。

ポイントは次のとおりです。

  1. エンドポイント一覧を取得する
    scenario.jsからbuildEndpoints()を呼び出し、エンドポイント一覧を取得します。各VUは、その一覧からランダムに1件を選んで呼び出します。
  2. チェック名に使用するnameを付ける
    request.jsでチェック名にnameを使用しているため、試験結果からどのAPIのチェックなのかを確認できます。
  3. GET 用は query、POST 用は body
    request.js 側で METHOD に応じて使い分けます。

エンドポイントを増やしたいときは、この配列に1件足すだけです。

request.js — HTTPリクエスト送信のロジック

scenario.js からは callEndpoint() を呼ぶだけにして、送信ロジックは request.js に閉じ込めています。

// lib/request.js
import http from 'k6/http';
import { check } from 'k6';
import { METHOD, HEADERS } from './config.js';

export function callEndpoint(endpoint) {
  const url = `${endpoint.baseUrl}${endpoint.path}`;

  const response =
    METHOD === 'POST'
      ? http.post(url, endpoint.body, {
          headers: { ...HEADERS, 'Content-Type': 'application/json' },
        })
      : http.get(`${url}?${endpoint.query || ''}`, { headers: HEADERS });

  // 2xx かどうかをチェック
  check(response, {
    [`${endpoint.name} status 2xx`]: (r) => r.status >= 200 && r.status < 300,
  });
}

ここでやっていることは、次の2つです。

  1. config.js の METHOD に応じて GET または POST を送る
  2. k6 の check でステータスが 2xx かどうかを記録する

smoke.js — 疎通確認

いきなり本試験と同じ大きな負荷をかけると、設定ミスやネットワーク不通で全部失敗しがちです。
そこで、対象のエンドポイントを1回ずつ呼び出し、正常に接続できるかを確認するスクリプト(smoke)を用意しています。

// smoke.js
import { buildEndpoints } from './lib/endpoints.js';
import { callEndpoint } from './lib/request.js';

const endpoints = buildEndpoints();

export const options = {
  vus: 1, // ユーザー1人
  iterations: 1, // 1回だけ実行
};

export default function () {
  for (const endpoint of endpoints) {
    callEndpoint(endpoint);
  }
}

※POST APIでは、繰り返し実行しても問題のないテストデータを使用します。

ポイントは次の2つです。

  1. vus: 1 / iterations: 1 … 最低限の実行回数
  2. 各エンドポイントを1回ずつ呼び出す … 本試験前に「接続できるか・2xxが返るか」を確認する

scenario.jsと同じconfig.jsendpoints.jsrequest.jsを利用するため、本試験で使用するエンドポイントとリクエスト処理を事前に確認できます。

実行の流れ(smoke → scenario)

実際の運用では、次の順で実行します。

# 1. 疎通確認(1 VU・各エンドポイントを1回ずつ)
BASE_URL=https://your-api.example METHOD=GET k6 run services/api/smoke.js

# 本試験
BASE_URL=https://your-api.example METHOD=GET VUS=600 DURATION=5m k6 run services/api/scenario.js

BASE_URLは例です。実行時は、負荷試験の許可を得た対象環境のURLに置き換えてください。

まずは、疎通をsmoke.js で確認して、本試験でscenario.jsを実行するといった流れになります。

結果

ここでは、ローカル環境で実行する場合のコマンド例を示します。

BASE_URL=http://localhost:8080 METHOD=GET k6 run services/api/scenario.js

k6では、実行後に以下のような形式で結果が表示されます。ここでは項目の見方を説明するため、簡略化した出力例を掲載します。

█ TOTAL RESULTS

    checks_total.......: 100     9.98/s
    checks_succeeded...: 100.00% 100 out of 100
    checks_failed......: 0.00%   0 out of 100

    ✓ daily_report status 2xx
    ✓ list_users status 2xx

    HTTP
    http_req_duration..............: avg=92.3ms min=41.8ms med=84.7ms max=248.6ms p(90)=132.4ms p(95)=158.7ms
      { expected_response:true }...: avg=92.3ms min=41.8ms med=84.7ms max=248.6ms p(90)=132.4ms p(95)=158.7ms
    http_req_failed................: 0.00%  0 out of 100
    http_reqs......................: 100    9.98/s

    EXECUTION
    iteration_duration.............: avg=100.1ms min=49.2ms med=92.1ms max=255.3ms p(90)=140.2ms p(95)=166.4ms
    iterations.....................: 100     9.98/s
    vus............................: 1       min=1        max=1
    vus_max........................: 1       min=1        max=1

    NETWORK
    data_received..................: 220 kB  22 kB/s
    data_sent......................: 55 kB   5.5 kB/s

※上記は結果の見方を説明するためのサンプルです。実際の性能試験で使用した環境や具体的な結果は掲載していません。

試験が終わると、k6はターミナルにサマリーを表示してくれます。
上記の出力例をもとに、よく見る項目だけ拾います。

まず見るところ

項目今回の例
checks成功 100%(100/100)
http_req_failed0%
http_req_durationmed 約85ms / p95 約159ms
http_reqs100回 / 約10 RPS
iterations100回 / 約10回/秒
vus1

daily_report / list_users のチェックも、すべて成功しています。

上記は、k6の出力項目と結果の見方を説明するためのサンプルです。実際の性能試験で使用した環境や具体的な結果は掲載していません。実際の性能試験では、想定するアクセス量や性能基準に基づいて試験条件を設定し、エラー率やレスポンス時間などを評価します。

見るポイント

  • checks … 自分で書いたチェックの成功率
  • http_req_failed … HTTPリクエストの失敗率
  • http_req_duration … レスポンス時間(med=中央値、p95=95%のリクエストがこの時間以内に完了したことを示す値)
  • http_reqs の RPS … 1秒あたりの平均リクエスト数(スループット)

本試験では、想定するアクセス量や性能基準に基づいてVU数や試験時間などの条件を設定し、エラー率やレイテンシを確認します。

まとめ

今回、k6を使って社内APIの負荷試験を実施してみました。

「負荷試験=大変そう」というイメージはありましたが、実際はJavaScriptでスクリプトを書いて、コマンド1つで実行できるので、思ったよりハードルは低かったです。

負荷試験を行う際は今回のブログを参考にしていただけますと幸いです。

AUTHOR
kim
kim
記事URLをコピーしました