k6でシナリオベースの負荷テストを試してみる

6 min読了の目安(約5500字TECH技術記事

k6 はGolang製のOSS負荷テストツールとGUIを備えるクラウドサービスでもあります。今回この負荷テストツールを試しました。k6の特筆すべきはテストシナリオをJavaScriptで書けることです。

JMeter, Gatlingを以前使ったことがありましたが、クライアント・サーバー両サイドともTypeScriptで近年開発することが多く、親和性の高そうな使いやすいツールを探していたところk6を見つけました。最近はAWS CDK等XMLやYAMLベースで定義を書くよりも、実行前にチェックが効きやすいコードで定義するタイプのツールが増えてきたように思います。

シナリオの定義

シナリオ

  1. ユーザー名とパスワードを取得
  2. ログインAPIを呼ぶ
  3. ユーザー操作を想定した待ち
  4. ブックマークを追加APIを呼ぶ

コード

scenario.js

これがメインスクリプトになります。default でエクスポートした関数がシナリオになります。options は設定を定義する予約された名前です。あくまでもJavaScriptで定義するというだけで、非同期に実行されるわけではありません。そのためPromiseやasync/awaitは出てきません。

使用している機能

  • check - 評価ポイント関数です。あくまでもチェックであり、false があっても離脱しません。
  • sleep - その名のとおりスリープ関数です。
  • http - HTTP リクエストモジュールです。
  • Rate - メトリクスクラスの1つ。インスタンスにブール値をaddしていくと集計されます。
  • http.Response - headersでレスポンスヘッダが取得できます。bodyで生のテキストが取れます。jsonでGJSONによるパスベースで値を取得することもできます。
  • options.thresholds - 決められたHTTPリクエストの各種集計値やmetrics系クラスのコンストラクタで指定した名前をキーとして、それらにクリアすべき閾値ルールを設定できます。
import { check, sleep } from 'k6';
import http from 'k6/http';
import { Rate } from 'k6/metrics';

import { getUser } from './users.js';

const failRate = new Rate('failed requests');
export const options = {
  thresholds: {
    'failed requests': ['rate<0.05'], // リクエストのエラー率が5%未満
    'http_req_duration': ['p(95)<500'] // 95%のリクエストの応答時間が500msec未満
  },
};

const ENDPOINT = `http://localhost:8080`;

export default function () {
  // 1. ユーザー名とパスワードを取得
  const { username, password } = getUser();

  // ログインAPIを呼ぶ
  const loginRes = http.post(`${ENDPOINT}/login`,
    JSON.stringify({
      username, password
    }), {
    headers: {
      'Content-Type': 'application/json',
    },
  });
  const loginResult = check(loginRes, { 'login success': r => r.status === 200 });
  if (!loginResult) {
    failRate.add(true);
    return;
  }
  const reqId = loginRes.headers['X-Request-Id']; // ヘッダ名は大文字小文字を正確に
  const accessToken = loginRes.json('accessToken'); // GJSONでパス指定

  // 3. ユーザー操作を想定した待ち
  sleep(2);

  // 4. ブックマークを追加APIを呼ぶ
  const bookmarkRes = http.post(`${ENDPOINT}/bookmarks`, JSON.stringify({
    url: 'https://www.example.com/',
    title: 'サイト名',
  }), {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      'X-Request-Id': reqId,
    },
  });
  const bookmarkResult = check(bookmarkRes, { 'adding bookmark success': r => r.status === 200 });
  failRate.add(!bookmarkResult);
}

users.js

scenario.js から参照される並列処理するワーカーの各仮想ユーザーのログイン情報を提供します。
当初ジェネレーターで書いてみたものの ReferenceError: regeneratorRuntime is not defined というエラーが出て使えないので止めました。

const users = [
  { username: 'user1', password: 'pass' },
  { username: 'user2', password: 'pass' },
  { username: 'user3', password: 'pass' },
  { username: 'user4', password: 'pass' },
];

let userIndex = 0;

export function getUser() {
  const user = users[userIndex];
  if (++userIndex == users.length) {
    userIndex = 0;
  }
  return user;
}

実行

k6 コマンドは単一のバイナリをダウンロードして、ここでは試しています。Docker もありますが、今回の例のようにモジュールを使う場合はローカルで実行する方が諸々嵌らなくて済みそうです。

使用した引数

  • vus - k6 は仮想ユーザー(Virtula UserS) をワーカーとして処理します。要するに並列実行数です。
  • duration - 指定時間の間VUSが繰り返しシナリオを実行します。

実行結果

4並列で5秒間叩き続けてみます。他にも負荷のかけ方は細かく指定できます。

$ ./k6 run --vus 4 --duration 5s ./scenario.js

          /\      |‾‾| /‾‾/   /‾‾/   
     /\  /  \     |  |/  /   /  /    
    /  \/    \    |     (   /   ‾‾\  
   /          \   |  |\  \ |  (‾)  | 
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: ./scenario.js
     output: -

  scenarios: (100.00%) 1 scenario, 4 max VUs, 35s max duration (incl. graceful stop):
           * default: 4 looping VUs for 5s (gracefulStop: 30s)


running (06.0s), 0/4 VUs, 12 complete and 0 interrupted iterations
default ✓ [======================================] 4 VUs  5s

    ✓ adding bookmark success
    ✓ login success

    checks.....................: 100.00% ✓ 24  ✗ 0  
    data_received..............: 9.8 kB  1.6 kB/s
    data_sent..................: 5.2 kB  861 B/s
  ✓ failed requests............: 0.00%   ✓ 0   ✗ 12 
    http_req_blocked...........: avg=33.77µs min=1.3µs   med=3.15µs  max=252.69µs p(90)=164.07µs p(95)=179.13µs
    http_req_connecting........: avg=23.96µs min=0s      med=0s      max=192.1µs  p(90)=125µs    p(95)=138.44µs
  ✓ http_req_duration..........: avg=1.45ms  min=450.9µs med=978.9µs max=4.34ms   p(90)=2.48ms   p(95)=2.57ms  
    http_req_receiving.........: avg=60.42µs min=33.8µs  med=55.15µs max=119.4µs  p(90)=90.88µs  p(95)=101.2µs 
    http_req_sending...........: avg=36.68µs min=7.7µs   med=23.64µs max=141.9µs  p(90)=74.9µs   p(95)=130.63µs
    http_req_tls_handshaking...: avg=0s      min=0s      med=0s      max=0s       p(90)=0s       p(95)=0s      
    http_req_waiting...........: avg=1.35ms  min=407.8µs med=918.7µs max=4.23ms   p(90)=2.43ms   p(95)=2.47ms  
    http_reqs..................: 24      3.979788/s
    iteration_duration.........: avg=2s      min=2s      med=2s      max=2s       p(90)=2s       p(95)=2s      
    iterations.................: 12      1.989894/s
    vus........................: 4       min=4 max=4
    vus_max....................: 4       min=4 max=4
  • gracefulStop は5秒経過して仮想ユーザーが完全に終了しきるまでの時間です。
  • ✓マークがついている項目が閾値ルールを設けたり、チェックポイントの集計になります。
  • ✓分子✗分母 です。
  • iterations がシナリオが実行された総回数です。

結び

JMeterでのシナリオ定義を作るまでの試行錯誤の辛さ、Gatlingでの(Scalaベースで書けるものの)高CPUスペック要求が悩みの種でしたが、k6はセットアップから定義して実行まで迅速にさくさくと行うことができました。また実行結果をCSV,JSONに限らず各種メトリクスサービスにインポート可能形式で出力できるため、結果の視覚化も様々な方法でできるようです。