サイトアイコン cocone engineering

k6による負荷テスト入門

はじめに

皆様こんにちは!ココネでビリングシステムを担当しているサーバエンジニアのKBです。

負荷テストは、ユーザーエクスペリエンスを向上させるために欠かせない重要な工程です。今回は、Webサービスのパフォーマンステストに役立つ負荷テストツール「k6」についてご紹介します。

k6とは

k6は、開発者と運用者向けに設計されたオープンソースの負荷テストツールです。Grafana Labsによって開発されており、Grafanaのエコシステムとシームレスに統合することができます。

スクリプトはJavaScriptで書かれており、高いスケーラビリティとパフォーマンスを提供します。k6はCLIツールとして動作し、シンプルかつ直感的な操作が可能で、使いやすさと高性能が特長です。

k6の特徴

  1. 使いやすさ: JavaScriptベースのスクリプトで簡単に負荷テストを作成できます。
  2. 高性能: 高負荷環境でもスムーズに動作し、リソースの効率的な利用が可能です。
    (参考:[Comparing k6 and JMeter for load testing > Performance benefits in practice])
  3. 拡張性: プラグインや拡張機能を利用してカスタマイズ可能です。
    1. [Create custom metrics]
    2. [k6 Extensions]
  4. 統合性: CI/CDパイプラインに統合しやすく、継続的なパフォーマンステストが行えます。
  5. 詳細なレポート機能: テスト結果を詳細に分析できる豊富なレポート機能を備えています。
  6. スクリプトの再利用性: スクリプトを簡単に共有・再利用することができ、チーム全体で効率的にテストを実施できます。

Dockerでk6のセットアップ

Dockerを使用すると、k6のセットアップと実行がさらに簡単になります。以下の手順で、Docker上でk6を実行する方法を説明します。

セットアップ

まず、Dockerがインストールされていることを確認してください。その後、以下のコマンドでk6のDockerイメージを取得します。

docker pull grafana/k6

スクリプトの作成

次に、テストスクリプトを作成します。例えば、test.jsという名前のファイルを作成し、次の内容を入力します。

// test.js
import http from 'k6/http';
import { check } from 'k6';

// テストオプションを設定
export let options = {
  vus: 10, // 仮想ユーザー数
  duration: '30s', // テストの実行時間
};

// テストのメイン関数
export default function () {
  let res = http.get('https://test-api.com/');
  // レスポンスのステータスが200であることを確認
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
}
 

実行

次に、以下のコマンドを使ってDockerコンテナ上でk6を実行します。

docker run -i grafana/k6 run - <test.js

以下のスクリーンショットのような結果が表示されます。

レポート

handleSummary

handleSummary関数を使用すると、テストの終了時にカスタムレポートを生成することができます。以下にその使用例を示します。

// test.js
// 省略
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';

export let options = {
  // 省略
};

export default function () {
  // 省略
}

// handleSummary関数を定義
export function handleSummary(data) {
  return {
    'stdout': textSummary(data, { indent: ' ', enableColors: true }), // コンソール出力用のテキストサマリー
    'summary.json': JSON.stringify(data), // JSON形式のサマリーをファイルに保存
  }
}

textSummary

ちょい見やすくなります

JSON

JSON形式のサマリーを生成して保存することができます。これにより、テスト結果を後で詳細に解析したり、他のツールで利用することが容易になります。

jUnit

JUnit形式の出力も可能ですが、サマリー全体を出力するのではなく、thresholdの結果を表示する例を示します。

// test.js
// 省略
import { jUnit } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';

export let options = {
  // 省略
  thresholds: {
    'http_req_duration': ['p(95)<500'], // 95%のリクエストが500ms以下であること checks: ['rate>0.9'], // 全体のcheck成功率が90%以上であること
  },
};

export default function () {
  // 省略
}

// handleSummary関数を定義
export function handleSummary(data) {
  return {
    'summary-junit.xml': jUnit(data, {classname: 'k6-loadtest'}) // JUnit形式のサマリーを生成して保存
  }
}

以下のスクリーンショットは、XMLを解析した結果です。

JUnit形式のレポートは多くのCI/CDツールでサポートされているため、CI/CD環境での利用に非常に便利です。JenkinsやGitLab CIでは、JUnit形式のレポートを使ってテスト結果を視覚的に確認したり、ビルドプロセスに組み込んだりすることができます。

(参考:[Custom summary])

Web Dashboard

k6には、組み込み機能としてWebダッシュボードが用意されています。テストスクリプトを実行する際に環境変数`K6_WEB_DASHBOARD`を`true`に設定することで、Webダッシュボードを有効にすることができます。例えば、以下のように設定します。

docker run -e K6_WEB_DASHBOARD=true -p 5665:5665 -i grafana/k6 run - <test.js

これで、テスト実行中にリアルタイムでパフォーマンスデータをWebブラウザで確認することができます。ブラウザで http://127.0.0.1:5665 を開くと、リアルタイムでテストの進行状況を確認することができます。

 

 

Webダッシュボードを使用することで、テスト結果を視覚的に確認しやすくなり、問題の特定やパフォーマンスの分析が簡単になります。

その他のオプション

docker run \
  -e K6_WEB_DASHBOARD=true \
  -e K6_WEB_DASHBOARD_EXPORT=html-report.html \
  -e K6_WEB_DASHBOARD_PERIOD=1s \
  -p 5665:5665 \
  -i grafana/k6 run - 
  1. K6_WEB_DASHBOARD_EXPORT 環境変数を使用すると、Webダッシュボードの内容を指定したHTMLファイルにエクスポートできます。これにより、テストの結果を保存し、後で参照することができます。
  2. K6_WEB_DASHBOARD_PERIOD 環境変数は、ダッシュボードがデータを更新する間隔を指定します。例えば、1sと設定すると、ダッシュボードは1秒ごとにデータを更新します。デフォルトでは10sに設定されています。この値を設定しない場合、テストの実行時間が10秒未満であると、html-report.htmlファイルやWebダッシュボードにチャートが表示されません。リアルタイムで詳細なデータ変動を監視するためには、適切な値を設定することが重要です。
    (参考:[Web dashboard])

テストの実行 – RPS(Requests Per Second)

負荷テストを実施する際、単に仮想ユーザー(VUs)の数とテストの持続時間を設定するだけでは、ターゲットシステムにRPSを維持することが難しい場合があります。特に、ターゲットシステムがストレスを受けて応答時間が遅くなると、仮想ユーザーの反復回数が減少し、リクエストの到達率が低下してしまうためです。この問題を解決するためには、RPSを設定することが重要です。

k6では、--rpsオプションを使用することもできますが、このオプションには注意が必要です。例えば、クラウドや分散実行環境では、このオプションは各k6インスタンスに独立して影響を与えるため、仮想ユーザー(VUs)のように分割されません。そのため、一定のRPSをシミュレートするにはconstant-arrival-rateエグゼキュータを使用することを強くお勧めします。

ローカルで実行する場合、–rpsオプションを使用する方が簡単かもしれませんが、それでも定常的な毎秒リクエスト数(RPS)をシミュレートするためには、arrival-rate executors を使用することをお勧めします。この方法の方が信頼性が高く、負荷パターンをより良く制御できます。
(参考:[Options reference > RPS])

Open and Closed Models

k6には、仮想ユーザー(VUs)をスケジューリングするための異なるモデルがあります。ここでは、 Closed Models と Open Models の違いを簡単に紹介します。

Closed Models

クローズドモデルでは、各反復が完了するまで次の反復が開始されません。これにより、ターゲットシステムの応答時間が遅くなると、テストのスループットが低下する可能性があります。この問題は「coordinated omission」として知られています。

Open Models

オープンモデルでは、反復の開始が反復の完了に依存しません。これにより、ターゲットシステムの応答時間が負荷に影響を与えることなく、RPSを維持することができます。k6では、constant-arrival-rateramping-arrival-rateの2つのエグゼキュータを使用してオープンモデルを実装しています。

(参考:[Open and Closed Models])

constant-arrival-rate

constant-arrival-rateエグゼキュータを使用すると、一定のリクエストレートでテストを実行できます。以下の例では、1秒あたり50リクエストの負荷をかけるテストを設定しています。

// test.js
// 省略

export let options = {
  scenarios: {
    constant_request_rate: {
      executor: 'constant-arrival-rate',
      rate: 50, // 1秒あたり50リクエストを送信
      timeUnit: '1s', // リクエストレートの単位
      duration: '1m', // テストの実行時間
      preAllocatedVUs: 50, // 事前に割り当てる仮想ユーザー数
      maxVUs: 100, // 最大仮想ユーザー数
    },
  },
  thresholds: {
    // 省略
  },
};

export default function () {
  // 省略
}

ramping-arrival-rate

ramping-arrival-rateエグゼキュータを使用すると、リクエストレートを段階的に増加させるテストを実行できます。以下の例では、10秒ごとにリクエストレートを10rpsから100rpsまで増加させるテストを設定しています。

// test.js
// 省略

export let options = {
  scenarios: {
    ramping_request_rate: {
      executor: 'ramping-arrival-rate',
      startRate: 10, // 開始時のリクエストレート
      timeUnit: '1s', // リクエストレートの単位
      stages: [
        { target: 100, duration: '1m' }, // 1分間で100リクエスト/秒まで増加
      ],
      preAllocatedVUs: 50, // 事前に割り当てる仮想ユーザー数
      maxVUs: 100, // 最大仮想ユーザー数
    },
  },
  thresholds: {
    // 省略
  },
};

export default function () {
  // 省略
}

パラメータ化

k6を使用してテストを実行する際には、パラメータ化を使用することで、より現実的な負荷テストを実施することができます。

パラメータ化の利点

        1. 現実的なシナリオのシミュレーション: 異なるユーザーやデータセットをシミュレートすることで、実際の使用状況に近い負荷テストを実施できます。
        2. システムの堅牢性の確認: 多様なリクエストを処理するシステムの能力を評価することができます。
        3. バグの発見: 固定されたリクエストでは発見できないバグやパフォーマンスの問題を見つけることができます。

SharedArray

SharedArrayを使用すると、テスト実行中に複数の仮想ユーザーが同じデータセットを共有できます。

(参考:[Data Parameterization])

csv

data.csv

1,John Doe,john.doe@example.com
2,Jane Smith,jane.smith@example.com
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

// CSVファイルからデータを読み込む
const csvData = new SharedArray('ユーザーデータ', function() {
  return open('./data.csv').split('\n').map(line => line.split(','));
});

export let options = {
  // 省略
};

export default function () {
  // 仮想ユーザーのインデックスに基づいて行を選択
  const user = csvData[__VU % csvData.length];
  const url = 'https://test-api.com/users';
  const payload = JSON.stringify({
    userId: user[0],
    name: user[1],
    email: user[2],
  });
  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  // POSTリクエストを送信
  let res = http.post(url, payload, params);
  
  // レスポンスをチェック
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
}

json

data.json

{
  "users": [
    { "userId": 1, "name": "John Doe", "email": "john.doe@example.com" },
    { "userId": 2, "username": "Jane Smith", "password": "jane.smith@example.com" }
  ]
}
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

// JSONファイルからデータを読み込む
const jsonData = new SharedArray('ユーザーデータ', function() {
  return JSON.parse(open('./data.json')).users;
});

export let options = {
  // 省略
};

export default function () {
  // 仮想ユーザーのインデックスに基づいて行を選択
  const user = jsonData[__VU % jsonData.length];
  const url = 'https://test-api.com/users';
  const payload = JSON.stringify({
    userId: user.userId,
    name: user.name,
    email: user.email,
  });
  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  // POSTリクエストを送信
  let res = http.post(url, payload, params);
  
  // レスポンスをチェック
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
}

__VUは現在の仮想ユーザー(Virtual User)を示すインデックスです。各仮想ユーザーは、テスト実行中に一意の__VU値を持ちます。この値を使用することで、仮想ユーザーごとに異なるデータを使用することができます。

環境変数

環境変数を使用してパラメータを設定することで、テスト実行時に異なる値を使用することができます。

import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

// 環境変数からパラメータを取得
const BASE_URL = __ENV.BASE_URL || 'https://test-api.com';
const USER_ID = __ENV.USER_ID || 'defaultUser';
const USER_NAME = __ENV.USER_NAME || 'defaultName';
const USER_EMAIL = __ENV.USER_EMAIL || 'defaultEmail@example.com';

export let options = {
  // 省略
};

export default function () {
  const url = `${BASE_URL}/users`;
  const payload = JSON.stringify({
    userId: USER_ID,
    name: USER_NAME,
    email: USER_EMAIL,
  });
  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  // POSTリクエストを送信
  let res = http.post(url, payload, params);
  
  // レスポンスをチェック
  check(res, {
    'status is 200': (r) => r.status === 200,
  });
}

Github Action CI/CD 統合

k6をGitHub Actionsと統合することで、自動化されたCI/CDパイプラインに負荷テストを組み込むことができます。ここで設定方法について簡単に説明します。

(参考:[k6-action])

name: Main Workflow
on: [push]
jobs:
  build:
    name: Run k6 test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Run k6 local test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: test.js
        env:
          BASE_URL: 'https://test-alpha-api.com'
          USER_ID: 'alphaDefaultUser'
          USER_NAME: 'alphaDefaultName'
          USER_EMAIL: 'alphaDefaultEmail@example.com'

環境変数設定を通して、違う環境に対して負荷テストを実行できます。

まとめ

k6は、負荷テストをシンプルかつ効果的に実施できる強力なツールです。さらに、GitHub Actionsとの統合により、CI/CDパイプラインに組み込むことで、継続的なパフォーマンステストを自動化することができます。これにより、開発プロセスの一環としてパフォーマンステストを行うことができ、システムの信頼性とユーザーエクスペリエンスの向上に寄与します。

ぜひ、このガイドを参考にして、k6を使った負荷テストを実施してみてください。

ココネエンジニアリングでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ココネエンジニアリング株式会社エンジニアの求人一覧
モバイルバージョンを終了