ゼロから始める負荷試験環境構築 — Grafana Cloud k6とDatadogで本番トラフィックを再現する

こんにちは。 CTO室/Platform開発チームでSREを担当している富田(@Cooking_ENG)です。

ファインディの「Platform開発チーム」は全社横断のSREの役割を担っています。当社のサービスがどれほどの負荷に耐えられるかを把握し、性能の問題を表面化・改善することで、ユーザーに安定したサービスを提供できる状態を目指しています。

そこで、Grafana Labsが提供するGrafana Cloud k6を採用し、負荷試験環境をゼロから構築しました。今回は、Findy Conferenceを対象に負荷試験を実施しています。

Findy Conferenceは、テックカンファレンスに特化したプラットフォームサービスです。

conference.findy-code.io

この記事では、負荷試験ツールの選定から、本番トラフィックを再現するシナリオの作成、Grafana Cloud k6とDatadogを組み合わせた負荷試験の実施までの取り組みを、初めて負荷試験を担当するエンジニアの方々や、これから導入を考えているチームに向けて紹介します。

負荷試験環境を作ることになった背景

Findy Conferenceでは、セッションの開始や終了のタイミングで、参加者の方々が一斉にアクセスする瞬間的なスパイクが発生する特徴があります。

過去のBlogにまとめているので、気になる方はぜひこちらの記事もご覧ください。

tech.findy.co.jp

こうしたスパイクに備えるため、カンファレンス開催前にコンテナ数を増やす運用をしてきました。コンテナ数を決める際の判断はチームの経験値に基づいていたため、本当にその数で十分なのか、過剰になっていないかを根拠を持って判断できない状態でした。

そこで、想定するスパイクに対して実際の計測値に基づいた最適なコンテナ数を設置できる状態を目指し、負荷試験環境の整備に取り組むことにしました。

負荷試験ツールの選定

ファインディで重視した判断軸

ツール選定にあたり、ファインディの状況に合わせて次の判断軸を置きました。

  • インフラ管理が不要であること(環境の構築・運用コストを抑えたい)
  • シナリオ作成の自由度(本番に近いユーザー導線を再現できる)
  • サービスの継続性(SaaSとして長く使えるか)
  • コスト(従量課金で、使わないときに費用が膨らまないこと)

候補ツールの比較

候補として、次のツールを比較しました。

サービス シナリオ記述 提供形態 運営元 特徴・得意領域
Grafana Cloud k6 JavaScript SaaS(OSS版あり) Grafana Labs モダンな開発体験、Grafana統合、専用ダッシュボードが標準。OSS版でローカル実行も可能
BlazeMeter JMeter/GUI SaaS Perforce JMeter資産を活かしやすい。詳細なレポートとGUI操作に強み
Gatling Enterprise Scala/Java SaaS Gatling Corp 大規模負荷試験に強み。Javaエコシステムとの親和性が高い
Locust Python OSS Locustコミュニティ Pythonでシナリオを柔軟に記述できる。軽量で分散実行にも対応
JMeter GUI/XML OSS Apache Software Foundation 長年の実績と豊富なプラグイン。対応プロトコルが幅広い

それぞれのツールには得意な用途があります。今回はファインディの判断軸を基準に評価しました。

評価の結果、Grafana Cloud k6を採用しました。

先に挙げた4つの判断軸それぞれに対する評価は次のとおりです。

判断軸 Grafana Cloud k6での評価
インフラ管理 マネージドサービスのため、負荷試験用の環境を自前で構築・運用する必要がない
シナリオ作成の自由度 JavaScriptベースでシナリオを記述するため、フロントエンド/バックエンド双方のエンジニアが読み書きしやすい
サービスの継続性 運営元のGrafana Labsが監視・可視化領域のサービスを長く提供している実績があり、長期利用の見通しを立てやすい
コスト Proプランの月額が$19、500 VUh/月までは無料枠、超過分も$0.15/VUhの従量課金で、使わないときに費用が膨らまない(2026年6月時点。最新は公式料金ページを参照)

負荷試験のシステム構成

Grafana Cloud k6とDatadogの役割分担

負荷シナリオの作成

ここからは、実際に負荷シナリオをどう作っていったかを紹介します。

har-to-k6を使ったスクリプトの作成

本番に近いユーザー導線を再現するには、ユーザーがどのエンドポイントにどの順でアクセスするかをシナリオとして書き起こす必要があります。当初は手作業でシナリオを書こうとしましたが、ステージング環境ではCognitoに加えてファインディ独自の認証基盤「Findy ID」など複数の認証を突破する必要があり、認証の流れをスクリプトで再現するのは簡単ではありませんでした。

そこで、har-to-k6を使い、ブラウザの操作ログからk6のシナリオを生成する方法に切り替えました。har-to-k6は、ブラウザの開発者ツールで書き出せるHAR(HTTP Archive)ファイルを入力として、k6用のJavaScriptシナリオを生成してくれる公式ツールです。ブラウザで一度認証を通したあとの操作をHARとして記録すれば、認証後のリクエストをそのまま再現できるため、手作業で書くよりも本番に近いシナリオを効率よく用意できました。

おおまかな流れは次のとおりです。

  1. ブラウザの開発者ツールで、再現したい操作を実行し、HARファイルを書き出す
  2. har-to-k6 input.har -o script.jsでk6用のJavaScriptに変換する
  3. 生成されたスクリプトを、シナリオに不要なリクエストの除去や認証情報の環境変数化などに合わせて整える

HARから変換した直後のスクリプトには、画像・CSS・JavaScriptファイルなどの静的アセット取得のリクエストや、HARに含まれていた認証情報(パスワードなど)がそのまま残っています。これらの整理にはClaude CodeなどのAIコーディング支援ツールが便利でした。

ただし、平文のパスワードをそのままAIに渡すのはセキュリティ上避けたいので、先に認証情報を__ENV.TEST_PASSWORDのような形で環境変数化し、それから不要なリクエストの除去をAIに任せました。AIを活用することで、手作業で1つずつ削るよりも短い時間で本番に近いシナリオを用意できました。

負荷のかけ方を調整するOptionsブロックの話

k6のシナリオでは、optionsというブロックで負荷のかけ方を細かく制御できます。今回のシナリオでは、ramping-arrival-rateというexecutorを使って、スパイクの立ち上がり・ピーク・収束を1本のシナリオで再現する構造を採用しました。

なお、k6では負荷シナリオの関数が1回実行されることを「イテレーション」と呼びます。今回のシナリオは1イテレーションのなかで複数のHTTPリクエストを送る構成です。

実際のOptionsブロックは次のような形です(数値は記事用のダミー値であり、対象サービスや想定するスパイク規模に応じて調整します)。

export const options = {
  scenarios: {
    spike: {
      executor: 'ramping-arrival-rate',
      startRate: 0,
      timeUnit: '1m',
      preAllocatedVUs: 50,
      stages: [
        { duration: '10s', target: 100 },
        { duration: '10s', target: 200 },
        { duration: '50s', target: 200 },
        { duration: '10s', target: 0 },
      ],
    },
  },
  cloud: {
    distribution: {
      tokyo: { loadZone: 'amazon:jp:tokyo', percent: 100 },
    },
  },
};

主な設定項目は次のとおりです。

設定項目 説明
executor 負荷のかけ方のパターン。ramping-arrival-rateは、各ステージのtargetに向けてイテレーション数をなめらかに増減させ、山型の負荷曲線を表現できる
startRate 開始時点のレート。今回は0から立ち上げる
timeUnit targetの単位時間。'1m'にすると目標値を「1分あたりのイテレーション数」で指定できる
preAllocatedVUs 目標レートを捌くために事前に確保しておく仮想ユーザー(VU)数
target 各ステージで到達させたい目標イテレーション数/timeUnittimeUnit: '1m'ならtarget: 100は「1分あたり100イテレーション」を意味し、durationをかけてその値までなめらかに増減する

timeUnit'1m'にしているのは、過去のカンファレンスの実測値を「1分あたりのリクエスト数」で把握していたためです。この実測リクエスト数を1イテレーションあたりのリクエスト本数で割ることで、必要なイテレーション数(target)を算出し、シナリオの単位もこれに揃えました。

stagesを4段に分けているのは、本番のスパイクを次のような流れで再現するためです。

  • 1段目(10秒):本番想定の半分まで急速に立ち上げ、アクセス急増の前兆を再現
  • 2段目(10秒):本番想定まで一気に到達させる
  • 3段目(50秒):本番想定の負荷を維持して、システムが安定して捌けるかを観察する
  • 4段目(10秒):目標値を0に戻し、収束させる

cloud.distributionでは、Grafana Cloud k6上で負荷を発生させるAWSのリージョンを指定しています。今回は実際のユーザーアクセスに近づけるため、東京リージョン(amazon:jp:tokyo)を100%としました。

想定する負荷がかけられているかをDatadogを使って確認する方法

ここがいちばん試行錯誤したところでした。

k6側のレポートで「目標としていたイテレーション数を発行した」と表示されていても、それがアプリケーションのコンテナに実際に届いたリクエスト数と直接一致するわけではありません。今回のシナリオは1イテレーションで複数のリクエストを送るためです。

そこで、まず「イテレーション数 × 1イテレーションあたりのリクエスト本数」で、アプリケーションに届くはずのリクエスト数(期待リクエスト数)を見積もりました。そのうえで、k6側のtargetを微調整しながら、期待リクエスト数どおりの負荷がかかるように調整していきます。

調整したリクエストが実際に届いているかは、Datadogで確認します。ファインディではもともとDatadogでオブザーバビリティに取り組んでおり、各サービスのダッシュボードを運用しています。負荷試験のメトリクスも普段の監視と同じダッシュボードで確認できるため、状態を把握しやすい環境がすでに整っていました。

ただし、どのメトリクスを見るかで数字の意味が変わります。観測候補をいくつか比較した結果、ALBのレスポンス数を見ることにしました。今回はレスポンス数を、アプリケーションが実際に処理した負荷の指標として扱っています。

観測対象 負荷確認の用途として 理由
ALBのレスポンス数 適している アプリケーションが実際に返したレスポンス数。見積もった期待リクエスト数と突き合わせやすい
CloudFrontのリクエスト数 向かない CDNキャッシュや静的アセット分が水増しされ、k6の発行数とずれる
APMのリクエスト数 やや向かない サンプリングで一部を間引いて記録するため、実際より低めに見えることがある

ALBのレスポンス数の推移を見て、見積もった期待リクエスト数どおりの負荷が処理されているかを判断します。値が想定より少ない場合は、CloudFront側で弾かれているリクエストがあるか、シナリオ側で意図しないエンドポイントを叩いていないかを見直すサインになります。

次の図は、ある負荷試験でのALBのレスポンス数の推移です。シナリオで設計したとおり、短時間で立ち上がってピークに達し、収束していく山型になっていることが確認できます。

負荷試験中のALBのレスポンス数の推移

作成でつまずいたポイント

シナリオを書く過程でつまずきがあったので、これから取り組む方向けに共有します。

Options内の値はリテラル必須

Grafana Cloud k6のプレビュー画面では、Optionsブロックの値を静的解析してVUh(仮想ユーザー時間)を試算します。このとき、Math.round()のような関数呼び出しや、外部の定数を参照する書き方は評価できず、VUhが正しく計算されません

// NG(VUh が計算されない)
const MaxTarget = 200;
export const options = {
  scenarios: {
    spike: {
      stages: [
        { duration: '50s', target: MaxTarget }, // 変数参照は評価されない
      ],
    },
  },
};

// OK(リテラル数値で書く)
export const options = {
  scenarios: {
    spike: {
      stages: [
        { duration: '50s', target: 200 },
      ],
    },
  },
};

DRYに書きたくなる気持ちはありますが、Optionsブロックに限ってはリテラルで書くことを徹底すると安全です。

認証付きエンドポイントの扱い

ステージング環境を本番相当にスケールアップして試験する場合、認証が挟まる導線ではCloudFront → Cognito → アプリケーションのようにリダイレクトが連鎖します。最初から本番想定のシナリオを流すと、認証で弾かれてうまく負荷がかからないことがあります。

そこで、まずは/healthのような疎通確認用エンドポイントにredirects: 0を付けてリクエストを送り、想定どおりに200や302が返ってくるかを確認してからシナリオを組み立てるのがおすすめです。

負荷試験の実施と結果

ここまでで作ったシナリオを使い、ステージング環境を本番相当にスケールアップしたうえで負荷試験を実施しました。ここでは、ファインディがどのように「想定スパイクごとに必要なコンテナ数」を割り出したのかを紹介します。

次の図は、Grafana Cloud k6で負荷試験を実行したときの結果サマリです。設計したとおりに負荷がかかってピークに達し、その間エラーを出さずに処理できていることが確認できます。

Grafana Cloud k6の負荷試験結果サマリ

※画像はイメージです。

負荷規模と必要コンテナ数

過去のカンファレンスでの参加者数と実測リクエスト数から、想定する負荷規模(1分あたりのリクエスト数)を複数段階用意し、負荷規模ごとに試験しました。

コンテナ数を判定する指標は、次のように定めました。

項目 内容
判定指標 ecs.fargate.cpu.percent(Datadog)
観測対象 Findy Conferenceのなかで最もリクエストが集中するバックエンドサービス
閾値 50%

ボトルネックになりやすいバックエンドサービスのCPU使用率を直接観測することで、コンテナ数の過不足を判断します。

閾値を50%にした背景

オートスケールはより低いCPU使用率で発動するように運用していますが、スパイク時は追いつく前にアクセスが集中してしまいます。

そのため、発動を待つよりも、CPU使用率が50%を超えた時点でコンテナを増やす判断をしたほうが、カンファレンス運営では安全だと考えました。

試験は次のサイクルで進めました。

  1. 想定する負荷規模のシナリオをGrafana Cloud k6から実行する
  2. DatadogでバックエンドサービスのCPU使用率とSLOに関わるメトリクスを観測する
  3. CPU使用率が閾値(50%)を超えた場合はコンテナ数を増やし、同じシナリオを再実行する
  4. SLOの範囲内に収まる最小のコンテナ数を、その負荷規模での必要数として記録する

この流れを負荷規模ごとに繰り返した結果、想定するカンファレンスの規模ごとに必要なコンテナ数を割り出すことができました。これにより、開催前のコンテナ調整を経験頼みではなく計測値に基づいて行えるようになっています。

Datadogで取ったメトリクス

試験中はCPU使用率のほかにも、次のメトリクスをDatadogで観測しました。

メトリクス 観測する目的
ECSのCPU・メモリ使用率 コンテナのリソースに余裕があるか(コンテナ数判定の主指標)
APMのレイテンシ(p95) 遅いリクエストがSLOの範囲に収まっているか
DBのコネクション数 データベース側がボトルネックになっていないか
エラー発生率 負荷によってエラーが増えていないか

また、試験結果の分析にはDatadogのBitsAIも活用しました。試験中のメトリクスからサマリを生成してNotionに集約し、開発チームへの共有や改善提案につなげています。

まとめ

今回は、Grafana Cloud k6とDatadogを組み合わせて、Findy Conferenceの負荷試験環境をゼロから構築した取り組みを紹介しました。コンテナ数の判断を、チームの経験値から計測値に基づくものへ変えられたことが大きな成果です。

ここで整理した手順は他のサービスにも応用できると考えており、今後はファインディで提供している各サービスでも横断的に負荷試験を実施できる体制へ広げていきたいです。

この記事が、これから負荷試験に取り組むエンジニアの方々やチームの参考になれば嬉しいです。最後までお読みいただきありがとうございました!

ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers