JavaScriptで性能テストやってみた〜K6編〜

k6でシナリオを書き、性能テストする方法について紹介します。
2022.04.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちはMAD事業部のホンギです。

負荷テストや性能テストという言葉を聞いたことありますか。
プロダクトの性能をテストするためや大量のアクセスを受けた時どれだけのアクセスまで耐えられるのかなど
プロダクトが持っている性能の限界を測るために行うテストです。

今日は数多い性能テストツールの中でJSで書けるk6というツールをご紹介します。
この記事を読み終わってからはk6でテストを実施できるようになるのが今回の目標です。

k6

k6の実行イメージ(k6 GitHub 抜粋)

k6

k6を一言で言うとJSで書けるツールでユニットテストみたいに性能テストすることを目指しいるOSSです。

なおk6コミュニティーではシナリオ作成を柔軟にするためPostmanSwagger,OpenAPIなどで定義されているAPIも変換対応しています。

API実行でよく使われてるPostmanの場合postman-to-k6などのツールですぐスクリプトを作成することも可能です。

test-api-postman-collection

Postmanで利用していたAPIをExportし、変換すると下記のようになります。
詳細はこちらをご参照ください。

export default function () {
  group('Public APIs', function () {
    postman[Request]({
      name: 'List all public crocodiles',
      id: '3ddd46c4-1618-4883-82ff-1b1e3a5f1091',
      method: 'GET',
      address: '{{BASE_URL}}/public/crocodiles/',
    });

    postman[Request]({
      name: 'Get a single public crocodile',
      id: '9625f17a-b739-4f91-af99-fba1d898953b',
      method: 'GET',
      address: '{{BASE_URL}}/public/crocodiles/1/',
    });
  });
}

Lifecycle

k6のLifecycleは大きく四つあります。

// 1. 初期化
// 2. API実行前の処理
export function setup() {
  // ログイン、トークンの取得などAPI実行に必要な処理を実装する箇所
}
// 3. API実行
export default function (data) {
  // APIを実行するシナリオを実装する箇所
}
// 4. API実行後の処理
export function teardown(data) {}

やってみる

インストール

$ brew install k6

今回はログインが必要な簡単なAPIを実行するスクリプトを作成し、解説しながら理解を深めたいと思います。

スクリプト作成

script.js

import http from "k6/http";
import { sleep, check } from "k6";
import { Trend } from "k6/metrics";

const trends = {
  scenario1: new Trend("scenario1) Response time", true),
  scenario2: new Trend("scenario2) Response time", true),
};

const VUS = 1;
const DURATION = "10s";

export const options = {
  scenarios: {
    scenario1: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "1",
      },
    },
    scenario2: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "2",
      },
    },
  },
};

export function setup() {
  const url = ""; // tokenの取得先
  const params = {
    headers: {
      Authorization: "Bearer XXX",
    },
  };
  const res = http.get(url, params);
  const token = JSON.parse(res.body).account.token;

  return token;
}

export function scenarioFunc(token) {
  const scenarioUrl = ""; // 実行するAPI先
  const scenario = http.get(scenarioUrl, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
  __ENV.SCENARIO_ID === "1"
    ? trends.scenario1.add(scenario.timings.duration)
    : trends.scenario2.add(scenario.timings.duration);

  check(scenario, {
    "scenario status is 200": (res) => res.status === 200,
  });

  sleep(1);
}

解説

上記のスクリプトの解説です。

まずoptionsの設定についてです。

$ k6 run --vus 10 --duration 30s script.js

k6はCLIでの実行できるツールで実行する際パラメターを渡す方法でも実行できますが、 スクリプトの中でその設定を入れて実行させることも可能です。

export const options = {
  scenarios: {
    scenario1: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "1",
      },
    },
  },
};

executor: k6 の実行エンジンを表しています。 ここで VU(Virtual user)やスクリプト実行パタンーを指定できます。 詳細はk6のexecutorsをご参照ください。

exec: 実行したいシナリオ(関数名)を指定します。

vus: Virtual Users を示しています。要すると並列実行数をここに設定します。

duration: VUS が繰り返しシナリオを実行する時間を設定します。

env: 共通で使われる変数を設定します。

const trends = {
  scenario1: new Trend("scenario1) Response time", true),
  scenario2: new Trend("scenario2) Response time", true),
};

Trendは実行結果に含める User 指定メトリックです。
今回はシナリオ別のレスポンスタイムを図り、実行結果に含めるために利用しました。
Trend を追加すると下記のように実行結果から見れます。

scenario1) Response time.......: avg=1.29s    min=1.26s    med=1.29s max=1.35s    p(90)=1.34s    p(95)=1.34s
scenario2) Response time.......: avg=1.29s    min=1.25s    med=1.29s max=1.36s    p(90)=1.32s    p(95)=1.34s

次はsetup関数についてです。

export function setup() {
  const url = ""; // tokenの取得先
  const params = {
    headers: {
      Authorization: "Bearer XXX",
    },
  };
  const res = http.get(url, params);
  const token = JSON.parse(res.body).account.token;

  return token;
}

ここではトークン取得など、シナリオを実行する前の処理を書くところになります。
ログインが必要なサービスを想定し、トークンを取得しました。

次は実行したいシナリオを実行する関数です。

export function scenarioFunc(token) {
  const scenarioUrl = ""; // 実行するAPI先
  const scenario = http.get(scenarioUrl, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
  __ENV.SCENARIO_ID === "1"
    ? trends.scenario1.add(scenario.timings.duration)
    : trends.scenario2.add(scenario.timings.duration);

  check(scenario, {
    "scenario status is 200": (res) => res.status === 200,
  });

  sleep(1);
}

ここで実行していきたいシナリオを書きます。
今回は同じAPIを叩くシナリオを 2 個準備したのでそれに合わせて Trend を実行シナリオのIDで分離しました。

checkメソッドは値について true/false を返却します。
ここではステータスコードが意図されているのかを確認し、実行結果に記録されます。
失敗しても途中止まることなく実行していきますので柔軟に対応できます。

実行方法

スクリプトの作成が終わったら下記のコマンドで実行してみましょう。

$ k6 run script.js

実行結果

上記のスクリプトを実行した結果です。

image

実行結果の解説

checks

  • 結果
    • 100.00% ✓ 16 ✗ 0
  • 解説
    • リクエストが成功した割合(%)

data_received

  • 結果
    • 176 kB 15 kB/s
  • 解説
    • レスポンスデータ量(Total, /s)

data_sent

  • 結果
    • 3.4 kB 282 B/s
  • 解説
    • リクエエストデータ量(Total, /s)

http_req_blocked

  • 結果
    • avg=40.62ms min=0s med=1µs max=430.85ms p(90)=129.65ms p(95)=191.16ms
  • 解説
    • TCP 接続の順番待ちをした時間(avg, min, med, max, p(90), p(95)

http_req_connecting

  • 結果
    • avg=7.32ms min=0s med=0s max=42.24ms p(90)=41.03ms p(95)=41.69ms
  • 解説
    • TCP 接続にかかった時間(avg, min, med, max, p(90), p(95)

http_req_duration

  • 結果
    • avg=1.27s min=986.44ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 解説
    • http_req_sending + http_req_waiting + http_req_receiveing(avg, min, med, max, p(90), p(95)

{ expected_response:true }

  • 結果
    • avg=1.27s min=986.44ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 解説
    • 正常応答のみの http_req_duration(avg, min, med, max, p(90), p(95) 正常な応答がない場合、この項目は表示されない

http_req_failed

  • 結果
    • 0.00% ✓ 0 ✗ 17
  • 解説
    • リクエストが失敗した割合(%)

http_req_receiving

  • 結果
    • avg=194.76µs min=78µs med=95µs max=994µs p(90)=420.4µs p(95)=761.19µs
  • 解説
    • レスポンスの 1 バイト目が到達してから最後のバイトを受信するまでの時間(avg, min, med, max, p(90), p(95)

http_req_sending

  • 結果
    • avg=139.76µs min=38µs med=48µs max=1.21ms p(90)=166.4µs p(95)=401.99µs
  • 解説
    • リクエストを送信するのにかかった時間(avg, min, med, max, p(90), p(95)

http_req_tls_handshaking

  • 結果
    • avg=29.7ms min=0s med=0s max=328.35ms p(90)=88.19ms p(95)=136.75ms
  • 解説
    • TLS セッションのハンドシェイクにかかった時間(avg, min, med, max, p(90), p(95)  http では 0

http_req_waiting

  • 結果
    • avg=1.27s min=985.15ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 解説
    • リクエストが送信完了してから、レスポンスが開始されるまでの時間(avg, min, med, max, p(90), p(95)  TTFB(Time To First Byte)

http_reqs

  • 結果
    • 17 1.423196/s
  • 解説
    • リクエスト総数 (Total, /s)

iteration_duration

  • 結果
    • avg=1.31s min=1.25s med=1.29s max=1.49s p(90)=1.44s p(95)=1.48s
  • 解説
    • シナリオ 1 ループにかかった時間(avg, min, med, max, p(90), p(95)

iterations

  • 結果
    • 16 1.339479/s
  • 解説
    • シナリオを繰り返した回数(Total, /s)

vus

  • 結果
    • 2 min=0 max=2
  • 解説
    • Virtual UserS、最後のシナリオのときの並列数

vus_max

  • 結果
    • 2 min=0 max=2
  • 解説
    • 最大 Virtual UserS、テスト中の最大並列数

参照

k6 docs
postman-to-k6
Load Testing Your API with Postman