ECS × k6 による数千名規模のビデオ会議負荷試験
ウェビナーツールを提供していると、「数千名規模のイベントでも安定して動作するかどうか」を試験する必要性が出てきます。
もちろんアプリケーションサーバーに対する単純な負荷試験も必要なんですが、それだけだと本当に「イベントを開催できている」とは言えません。実際の参加者と同じように、ブラウザからウェビナーに接続する動作をテストする必要があります。
でも社内で数千人を集めるなんて無理ですし、毎回そんな規模感の人を動員できるわけもありません。そこでコマンド一発でヘッドレスブラウザを立ち上げて大量参加者を入室させる仕組みを作りました。
構成
最終的な構成はこうなりました。

- Amazon ECS → 入室処理を行う単体タスクを立ち上げる
 - k6 → ECS タスク内でヘッドレスブラウザを操作してウェビナールームに入室する
 - AWS Amplify → ウェビナー視聴アプリケーションをホスティング
 
踏み台用タスクから Rails の rake タスクを叩くと、指定した人数に応じて ECS タスクが起動し、それぞれのコンテナがブラウザを立ち上げてウェビナー視聴画面に入室していきます。
各タスクは 最大5人分のブラウザを担当。
入室した証跡としてスクショも残すので、後から「ほんとに入ったな」と確認できるようになっています。
以下にタスク起動時に実行する rake タスクのコードを記載します。かなり抽象化してますが、やりたいことは指定した参加人数に応じた ECS タスクの起動処理です。
require 'aws-sdk-ecs'
namespace :stress_test do
  # ex) bundle exec rake stress_test:run[room_id, attendee_count]
  desc 'stress test'
  task :run, %i[room_id max_attendee_count] => :environment do |_t, args|
    ...
    ecs_client = Aws::ECS::Client.new(region: 'ap-northeast-1')
    attendees.each_with_index do |attendee, i|
      params = {
        cluster: "cluster-name",
        task_definition: "task-definition-name",
        capacity_provider_strategy: [
          {
            capacity_provider: 'FARGATE_SPOT',
            weight: 1
          }
        ],
        network_configuration: {
          ...
        },
        enable_execute_command: true,
        overrides: {
          ...
        }
      }
      ecs_client.run_task(params)
      sleep 5 # 一気に起動すると、Fargate、Chimeのレートリミットに引っかかるため、少し間隔をあける
    end
  end
end
実行時はこんな感じで rake タスクを実行すれば参加人数に応じて ECS タスクが立ち上がります
$ bundle exec rake stress_test:run[room_id, 3000]
k6 による入室処理を実行するタスクのコンテナイメージはこんな感じ(Dockerfileから一部抜粋)
# 1. xk6-browser入りk6をビルドするステージ
FROM  golang:1.24-alpine as builder
RUN apk --update add --no-cache git make bash curl tar gzip chromium
# xk6本体をインストール
RUN go install go.k6.io/xk6/cmd/xk6@latest
# xk6-browser入りk6をビルド(バージョンは必要に応じて調整)
RUN /go/bin/xk6 build v0.51.0 --with github.com/grafana/xk6-browser --output /k6
# 2. 実行用の軽量イメージ
FROM  alpine:latest
RUN addgroup -g 12345 k6group \
    && adduser -D -u 12345 -G k6group k6
...
CMD ["k6", "run", "/scripts/attendEvent.js"]
実際にウェビナー視聴画面に入室する attendEvent.js
k6 だと簡単にブラウザ操作ができるので今回のケース以外でも使えそうです。
import { browser } from 'k6/browser';
import { sleep } from 'k6';
...
export const options = {
  ...
}
export default async function () {
  ...
  const page = await browser.newPage();
  const index = __VU - 1;
  const maxRetry = 3;
  try {
    ...
    for (let attempt = 1; attempt <= maxRetry; attempt++) {
      try {
        await page.goto(sessionUrl);
        sleep(10);
        // ボタンが表示されていれば成功
        await page.waitForSelector('button', { timeout: 5 });
        // スクリーンショット取得
        await page.screenshot({ path: `/images/attendee-${index}.png` });
        break;
      } catch (error) {
        ...
      }
    }
    if (success) {
      // 待機
      sleep(duration);
    }
  } finally {
    await page.close();
  }
}
構築時に直面した課題と解決策
- 
1ブラウザ=1人問題
Cookie でセッションが共有されるため、1つのブラウザプロセスでは複数人を同時に入室させることができません。そのため、仮想ユーザーごとに独立したブラウザプロセスを起動し、それぞれを別参加者として入室させるようにしました。 - 
OOM(Out Of Memory)エラー
1つのコンテナで無邪気に大量ブラウザプロセスを立ち上げると当然リソース不足になるので、1 ECS タスクにつき参加者は5人までに制限しました。 - 
入室操作が手間
入室時には「入室チェッカー」や「入室ボタン」をクリックする必要があり、k6 でブラウザ操作して良かったのですが面倒だったので、URL パラメータに特定のタグが付与されている場合には入室の前段処理をスキップするようにアプリ側を改修しました。 - 
コスト問題
3000人規模を想定するとリソースコストが爆増しますので、Fargate Spot でコスト削減を行いました。さらに、タスクの最大稼働時間も1時間に制限してタスクの立ちっぱなしが起きないようにしました。 - 
ECSのタスク起動レートリミット
いっきに数百タスクを立ち上げるとAPI制限に引っかかるため、起動間隔を空けて回避しました。 
結果
最終的に、3000人の同時接続試験に成功しました。(ECS タスクでいうと 600 台が同時稼働)
コマンドを1発叩くだけで複雑な手順は不要なので、今後も継続的に試験をするのに適した機構が構築できました。
とはいえ課題もまだ残っています。
たとえば、600 台のECSタスクが立ち上がるまでにはどうしても時間を要しますし、音声の疎通までは今回の仕組みでは試せていません。
また、Fargate Spot を使っているとはいえ、数千人規模を実施するとコストもそれなり(1発1万弱)になるので、「気軽にポンポン実行するものではないな」というのも正直な感想です。
それでも、現実に近い形で数千人規模のウェビナーを検証できる手段を得られたのは良かった点です。
Bizibl では開発エンジニアを絶賛採用しています!カジュアル面談に興味がある方はこちらから!
Discussion