TypeScriptで負荷シナリオテストを作りたくなった話

f:id:kaminashi-developer:20210124225852p:plain

はじめに

初めまして。株式会社カミナシPMの@gtongy1です。

負荷量の増加。どの企業にとっても悩みの種じゃないでしょうか?
そんな時にこそ、負荷シナリオテストツールを導入したい!となると、負荷シナリオテストだとLocustとかが有名所。
ただPythonを知らないメンバーに、一からチームへ布教するのってなかなか腰が重くなってしまう。
せっかくならフロント中心のメンバーでも、自分が投げるリクエストの負荷量ってどんな感じだろうとか、気になる状態にはしておきたい。
負荷問題はインフラでも、サーバーでも、フロントでもなく全体の横断的な問題です。
どこか分からないからこそ、問題を特定する人は多岐にわたるはず。
そこでフロントもサーバーも何かと馴染みの深いJS、強いてはTypeScriptで記述出来るいい感じの負荷テストツールがないかと探して導入してみました。
導入してみて、機能もリッチだしグラフの見え方も綺麗だったりとなかなかに体験よくおすすめです。

要約

負荷テストにk6, グラフの可視化にGrafanaを使うと無料でTSの負荷テストツール作れます。

github.com

スタータキットを作成したので、是非試してみてください。

k6って?

k6.io

k6 はJSで書ける負荷テストツールです。

  • CLI Likeな設計の作り込み
  • ES2015/ES6のjsに対応、ローカル/リモートのモジュールを利用可能(importも使えるよ)
  • チェックとしきい値の記述も直感的なAPI

と開発者のコーディング体験もよく、痒いところによく手が届きます。
負荷テスト, パフォーマンス監視のどちらにも利用することが出来て、耐久性テスト, 高負荷テスト, ストレステストと幅広いシナリオを実現できるのも目を見張ります。
また、k6フリーでオープンソース です。
無料でOSSなのは、まず素晴らしいですね🎉
後webpackってJSへコンパイルすれば当然ですが、TypeScriptでもかける!
では肝心のコードの書き味はどうなのでしょう?気になりますね。
ではこのk6を使って、負荷テストの入り口から一緒に入っていきましょう。

k6でのsetupを実行してみる

まず、k6のsetupを書いていきます。src/tests/soak.test.tsをご覧ください。

import { Options } from 'k6/options';
import { randomUser } from '../lib/test-data.helpers';
import * as adminActions from '../actions/roles/admin.role';
import * as ownerActions from '../actions/roles/owner.role';

// {@link https://docs.k6.io/docs/options}
export let options: Partial<Options> = {
  stages: [{ target: 5, duration: '1s' }],
  // {@link https://docs.k6.io/docs/thresholds}
  thresholds: {
    http_req_duration: ['avg<1000', 'p(95)<1500'],
  },
};

const BASE_URL = 'https://test-api.loadimpact.com';

// {@link https://k6.io/docs/using-k6/test-life-cycle#setup-and-teardown-stages}
export function setup() {
  const user = randomUser();
  adminActions.signup(user, BASE_URL);
  const authToken = ownerActions.login(user, BASE_URL);
  return authToken;
}

export default (authToken: string) => {
  // ここに実際のテストケースを追加していきます
};

そもそもsetupとはなんでしょうか?
Unit Testをよく書いているみなさんは既にお気づきかもしれませんが、k6にも実際のシナリオを実行する前処理を記述することが出来ます。
ユーザーの作成/ログインなど、実際のAPIを実行する前の処理を記述することが出来ます。
ここで作成した値を後のシナリオへ渡すことが出来るので、ここで認証時のKeyを渡すとか、実際処理を実行する前に行いたい初期シナリオを記述できます。

export defaultから記述された関数に後ほどシナリオを追加していきます。
その前に一回処理を実行してみましょう!
yarn go:k6で処理を実行します。

gyazo.com

うまく実行出来ていますね!
ここで最後に表示されているオプションに関してですが、実際に処理を実行した結果を確認出来ます。
表示されたオプションのうちで特に重要なものを説明します。

  • vus
    • 同時実行したVUの数
  • iteration_duration
    • default/main 関数の完全な反復処理を 1 回完了するのに要した時間
  • checks
    • チェックの成功率
  • http_req_duration
    • リクエストの合計時間

負荷テストという観点だとこのあたりは必ず一読します。
k6の特殊な概念としてVU(Virtual User)という概念があります。

k6.io

k6同時に複数人がアクセスすることをコード上で表現でき、この中の1人をVUとして仮に立ててテストを実行することが出来ます。
設定された人数でx秒間アクセスし続ける、のようなケースを作れます。

その他オプションに関してはこちらをご覧ください。秒数やテスト成功条件あたりは実装が捗るので一読おすすめです。

実際のシナリオの実装

では、メイン所のシナリオを記述していきましょう!
上記でも実は使っていたのですが、k6が公式で提供しているモックサーバーがあるので、それを使って今回テストを作成していきます。

import { group, check, fail } from 'k6';
import http from 'k6/http';

import { randomCrocodile } from '../lib/test-data.helpers';
import { setSleep } from '../lib/sleep.helpers';
import { Crocodile } from '../lib/types/crocodile.api';
import { Counter } from 'k6/metrics';

export function createCrocodile(createRequestConfigWithTag: any, url: string, count: Counter): string {
  // 実行時のグループの指定
  group('Create Crocodiles', () => {
    const payload: Crocodile = randomCrocodile();
    // httpリクエストの実行
    const res = http.post(url, payload as {}, createRequestConfigWithTag({ name: 'Create' }));
    const crocodile: Crocodile = JSON.parse(res.body as string);
    // アサーション
    if (check(res, { 'Crocodile created correctly': (r) => r.status === 201 })) {
      url = `${url}${crocodile.id}/`;
      // 成功件数のカウント
      count.add(1);
    } else {
      fail(`Unable to create a Crocodile ${res.status} ${res.body}`);
    }
    setSleep(0.5, 1);
  });

  return url;
}
// ...

// 作成成功数のカウント
let numberOfCrocodilesCreated = new Counter('NumberOfCrocodilesCreated');

// ...
export default (authToken: string) => {
  // 階層的にグループを作成可能
  group('Create Crocodiles', () => {
    let URL = `${BASE_URL}/my/crocodiles/`;
    ownerActions.createCrocodile(createRequestConfigWithTag(authToken), URL, numberOfCrocodilesCreated);
  });
  // {@link https://docs.k6.io/docs/sleep-t-1}
  setSleep();
};

これで実行してみます。

gyazo.com

実行成功時のこの行数をみると、実際にリクエスト成功した行数も合わせて確認することができます。
今回は15sの実行ですが、26件のリクエストが成功しているのが確認できます。
こういった形でCLI上から確認することも可能となります。非常にシンプルですね。
それでは、この結果を可視化するためにはどうしたら良いでしょうか?
そう、Grafanaの出番です。

実行した結果の可視化

ダッシュボードを起動して確認してみましょう!
スタータキットのディレクトリ以下でこのコマンドを実行してください。

$ docker network create k6-network && yarn monitors

グラフがdocker上で起動したはずです。中をみていきましょう。
http://localhost:3000/d/2jJz71_Wz/k6-dashboard こちらへアクセスしてみてください。

gyazo.com

グラフで結果を確認出来るようになりました!
このように、StepごとのCheck成功率/リクエスト作成成功数/リクエストの合計時間などを見れるようになります。
dashboards/k6-dashboard_rev1.json内に各グラフの設定が書き込まれています。
ここを変えて、実際の形にあった自分だけのグラフも作ってみてください!自分のプロダクトにグラフを当てはめた時、ワクワクしますよ。

終わりに

負荷テストツールk6 + Grafanaの紹介をしました。
ここまで実装を進めていく中で、概念の慣れに時間をかけつつも記述で難しいところなく、書き味よかったです。

データロスト = 事業的なダメージが大きい部分に対して、やり方分からず手作業で進めていくことって結構多いと思っていて。
単性能テスト(datadog syntheticsとかvegitaとか)は結構パッと出てくるけど、TypeScriptかつシナリオテストはなかなか日本の記事は見当たらなかったので記事にしました。
エラー発生確度を下げられる対応策として、負荷テストを導入出来る企業が増えることを願っています。
ユーザー増加した時のデータの安定性を話せるようになったりと、Biz側にもメリットも大きいシナリオ負荷テスト。
是非一度お試しあれ。

最後に宣伝です!

僕らは伝統産業に向けて、現場向けのちょっとニッチなB2B SaaSを提供しているスタートアップです。
エンタプライズの企業様にも導入され始め、徐々に負荷量が増えてきて課題感も出始めました。
今まさに面白い時です!課題って大きい方が楽しくないですか?

ブルーカラー面白そうとか、難しい課題好きという方は、是非!

ここまでお読みいただき、ありがとうございました。

参考文献