EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

大規模調査を支えるアンケートシステムの負荷試験

はじめに

こんにちは、バックエンドエンジニアのおおたわらです。

これまで2つの記事にわたり、弊社のアンケートシステムについて紹介させていただきました。

今回は、このシステムに対し大量アクセスを想定した負荷試験をどのように行っているかについて紹介します。

負荷試験の目標

システムの性能を評価するためには、定量的な目標を立てる必要があります。

大規模調査を支えるアンケートシステムのアーキテクチャ で紹介した非機能要件を再掲すると、以下の通りです。

  • 大規模調査における大量のリクエスト(具体的には秒間 1000〜回答を想定)を処理できるスケーラビリティ
  • 回答サーバーは可用性が高いこと
    • 停止すると、顧客の声を集める機会が失われてしまうだけでなく、アンケートを実施する顧客企業のブランドイメージ毀損に繋がる恐れがあるため
  • 回答者のユーザー体験を損ねないよう、アンケート設定内容取得・回答の送信は低レイテンシであることが望ましい
  • 回答データの損失は許容されない
  • 受け付けた回答は分析機能のユーザーがほぼリアルタイムで確認できるのが望ましい

これをもう少し具体的な数値にして、負荷試験の目標を以下のように設定しています。

  • 大規模調査を想定した負荷(1000 RPS 以上)を回答サーバーにかけた際、
    • エラーレスポンスが0件であること
    • レイテンシが99パーセンタイルで数百ミリ秒程度であること
  • 受け付けた回答がすべて保存されること

負荷試験ツール k6

以前このブログで紹介(モノリスからマイクロサービスへ k6 導入準備中Newrelic, k6, Cloud Buildでらくらく負荷試験)したことがある k6負荷試験ツールとして使用しています

アンケートシステムの負荷試験においては、以下の点をメリットに感じています。

  • テストスクリプトの自由度の高さ(動的なテストデータ作成が可能、ユーザー操作を再現したシナリオが書ける)
  • 負荷のかけ方を細かく設定できる

テストスクリプト

k6 では JavaScript または TypeScript で柔軟にテスト用スクリプトを書くことができます。

例えば、試験実施前にテストデータを作成する、サーバーから取得したデータを元にしてテストデータを作る、試験実施ごとにランダム ID を払い出す、といったことが可能です。

実際に使っているテストスクリプト(TypeScript)を雰囲気が伝わる程度に抜粋すると、以下の通りです。

import * as http from 'k6/http';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';

export const options = {
  scenarios: {
    open_model: {
      executor: 'constant-arrival-rate',
      rate: __ENV.RPS ? parseInt(__ENV.RPS) : 5,
      timeUnit: '1s',
      duration: __ENV.DURATION ? __ENV.DURATION : '30s',
      preAllocatedVUs: __ENV.RPS ? parseInt(__ENV.RPS) : 5,
      maxVUs: __ENV.RPS ? parseInt(__ENV.RPS) * 4 : 15,
    },
  },
};

// セットアップ処理
export function setup(): SetupResult {
  const cookie = login(
    email,
    password,
    ...
  );

  const get_public_survey_response = get_public_survey(
    cookie,
    surveyId
  );

  const directory = get_public_survey_response.json(
    'surveyType.directory'
  ) as string;

  const answers = JSON.parse(answerFileData);

  return {
    surveyId,
    directory,
    answers,
    ...
  };
}

// テストの実処理を記述するシナリオ関数。各VUが繰り返し実行する。
export default function (data: SetupResult) {
  ...

  // 回答画面の表示時に呼び出されるアンケート情報取得API
  http.post(
    `${get_survey_url}`,
    getSurveyRequestBody,
    {
      headers,
    }
  );

  // 回答送信API
  http.post(
    `${post_answer_url}`,
    sendAnswerRequestBody,
    {
      headers,
    }
  );
}

export function handleSummary(data) {
  return {
    'summary.txt': textSummary(data, { enableColors: false }),
    stdout: textSummary(data),
  };
}

ポイントは以下の通りです。

  • options ではシナリオの設定を行っている(次のセクションで詳しく紹介)
  • セットアップ処理では、アンケート運用担当者向け API をログインして呼び出して、必要なアンケート設定を取得して、それに応じたテスト回答データを作成している
  • シナリオ関数では実際の回答者の操作(回答画面を開いて回答を送る)をシミュレートして2つの API を呼び出している
  • handleSummary でテスト結果をファイルに出力している
    • Custom summary という機能を使っている
    • これを後述の GitHub Actions での実行時に活用しています

シナリオ

詳細は k6 公式ドキュメントに記載の通りですが、負荷やトラフィックパターンをきめ細かく設定可能です。

システムの特性を踏まえ以下のように設定しています。

  • 同時アクセスユーザーではなくリクエスト数で負荷の設定を行うため、open_model (参考:Open and closed models | Grafana k6 documentation)を指定
    • 負荷を設定する上で、旧プロダクト(先日プレスリリースを出させていただいた「EmotionTech CX/EXの新環境・GA版」に対して、従来から存在していたプロダクトのこと)の大規模調査時のリクエスト数データを参考にしたため
  • Executor:アンケート配信直後などに一斉にアクセスが来ることを想定し、厳しめに Constant arrival rate を指定しています。これによりサーバーにかかる負荷は一定となります。
    • ここはもう少し商用でのデータが溜まってきたら、それを参考に負荷が徐々に増す場合を想定し Ramping arrival rate(可変の負荷がかけられる Executor)などを使ってみても良いと考えています
  • VU (Virtual User) の設定:preAllocatedVUs は RPS の値と同じにしています。maxVUs は決めで設定していますが今のところ足りています。システムの変化とともにチューニングすることがあるかもしれません。

負荷試験サーバー

高負荷をかけるのに十分なコンピューティングリソースを確保するため、専用の試験サーバーを立てています。具体的には、Google Compute Engine のコンピューティング最適化マシンファミリーの C2 マシンシリーズを使用しています。

更にこれを GitHub の Self-hosted Runner として登録し、GitHub Actions から簡単に k6 を実行できるようにしています(対応自体は SRE チームが行ってくれました!)。

負荷試験の結果は GitHub Actions の Job Summaries (参考:Supercharging GitHub Actions with Job Summaries - The GitHub Blog)に出力しています。例えば以下の画像のような感じです。

Job Summaries への負荷試験結果出力例

GitHub Actions による実行には以下のようなメリットを感じています。

  • 負荷試験サーバーへの SSH でのログイン・コマンド実行が不要であり実行が簡単
  • 今後 QA エンジニアに試験実施の依頼も考えているが、性能試験サーバーへのアクセス権をまるごと付与するのは過剰である。GitHub Actions 経由でできることを限定することでこれを行わなくて済み、適切な権限管理ができる。
  • 試験のパラメーター設定(RPS、実行時間など)とその結果がセットで記録が残る
  • Job Summaries に k6 実行結果を出力するため、試験結果を保存できる。また、ターミナルを間違えて閉じて k6 の実行結果が消えてしまった…といった事故が起きない。

レポート作成

機能に関係するインフラのメトリクスを一覧できるダッシュボードを作り、目標を達成できなかった場合のボトルネック特定に活用しています。一部抜粋したのが以下の画像です。

インフラメトリクスダッシュボード

k6 の実行結果とダッシュボードの結果をまとめ、社内 Wiki負荷試験レポートを残していっています。

今後の改善

現在は性能に影響がありそうな機能追加があった際に適宜実施していますが、ライブラリのアップデートやアプリケーションの機能追加により意図せず性能が悪化する可能性もあります。今後は定期的に実施するため、より実施しやすい仕組みや QA エンジニアとの協働も検討しています。

他には、試験レポート作成に課題を感じています。GitHub Actions のジョブ結果から結果をコピペし、Google Cloud Monitoring ダッシュボードの期間を試験が実行された期間に絞り込んで、スクリーンショットGoogle Cloud Monitoring のデフォルトの保存期間が短いのでやむなく…)をぽちぽち取り…と言った感じで手作業が多い状態です。これはもう少し負荷の低い方法を考えていきたいです。

おわりに

負荷試験の実行環境はこれまでに様々な改善を繰り返しながら作り上げてきて、既に商用での大規模調査(例えば最も大規模なものだと、700万人にアンケートを配信する案件)を何件も成功させることができました。

更に試験方法にブラッシュアップを重ね、よりよいユーザー体験の提供や更なる超・大規模調査にも対応できるシステムを作っていきたいと考えています。