技術 約4分で読めます

数万人同時視聴イベントの設計パターン

「19時から全員で一斉に視聴しよう!」系のイベント。公式が告知して、数万人がサイトに集まり、同じタイミングで動画を再生する。再生に合わせてWeb上で演出が変わる。

こういう企画を実現するとき、どう設計するか。

Watch Partyとの違い

NetflixのTelepartyやAmazon Watch Partyは「友達同士で動画を同期視聴」するサービス。これらはホストの再生位置に他のメンバーが追従する仕組み。

一方、大規模同時視聴イベントは:

  • 数万人規模
  • 全員がサーバ時刻という絶対基準に合わせる
  • ホストがいない(全員が対等)
  • 動画再生に連動したWeb演出がある

設計思想が根本的に違う。

よくある失敗パターン

全部を1台のサーバでやろうとすると死ぬ。

[クライアント] → [本体サーバ]
                    ├── 時刻同期 API
                    ├── 演出データ
                    ├── コメント機能
                    └── その他全部

数万人が数秒ごとに時刻同期のためにポーリングすると、毎秒数千〜数万リクエスト。普通のサーバは耐えられない。

推奨アーキテクチャ

時刻サーバを分離するのが鍵。

[クライアント] → [時刻サーバ (Cloudflare Workers等)]

           [CDN] → 演出タイムシート (JSON)

       [本体サーバ] → 動的機能のみ

時刻サーバの役割

超軽量なエンドポイント。やることは2つだけ:

  1. 現在のサーバ時刻を返す
  2. イベントの状態(進行中/停止中)を返す
{
  "serverTime": 1704067200000,
  "status": "running",
  "message": null
}

Cloudflare WorkersやVercel Edge Functionsなら、エッジで動くので世界中どこからでも低レイテンシ。しかも安い。

状態管理の相乗り

時刻サーバにstatusを持たせることで、緊急停止にも対応できる。

事故が発生して「いったん止めます」となったとき、status: "paused"を返せば全クライアントに即座に伝わる。復旧したらrunningに戻す。

本体サーバが死んでも、時刻サーバさえ生きていれば一斉通知できる。

クライアント側の時刻同期

初回アクセス時

  1. 時刻サーバにリクエストを送信
  2. RTT(往復時間)を計測
  3. オフセットを計算
const start = Date.now();
const res = await fetch(TIME_SERVER_URL);
const data = await res.json();
const rtt = Date.now() - start;

// サーバ時刻 = 受信時刻 + RTT/2(片道分を補正)
const serverTime = data.serverTime + (rtt / 2);
const offset = serverTime - Date.now();

以後はDate.now() + offsetで「サーバ基準の現在時刻」が取れる。

再同期は控えめに

初回以降はローカルで計算できるので、サーバアクセスは最小限でいい。

  • 5分に1回程度で補正
  • 時刻サーバにアクセスできなくても進行は止めない
  • 状態チェック(緊急停止の検知)を兼ねる

数万人が5分に1回なら、毎秒100〜200リクエスト程度。余裕。

タイムシートの設計

「何秒にどの演出を出すか」を定義したJSON。CDNで事前配布。

[
  { "time": 0, "action": "showOpening" },
  { "time": 560, "action": "startSong1" },
  { "time": 1200, "action": "showParticles" },
  { "time": 6800, "action": "discChange" }
]

クライアント側は補正済み時刻でタイムシートを走査し、該当する演出を発火。完全ローカル処理なのでサーバ負荷ゼロ。

function checkCue(currentTime) {
  const elapsed = currentTime - eventStartTime;
  for (const cue of timesheet) {
    if (cue.time <= elapsed && !cue.fired) {
      cue.fired = true;
      triggerAnimation(cue.action);
    }
  }
}

演出設計の現実解

完全同期は無理

どう頑張っても±500ms〜数秒のズレは発生する。原因は:

  • ネットワーク遅延のばらつき
  • 動画再生開始タイミングの個人差
  • デバイス性能の差

ズレに強い演出を選ぶ

演出タイプ同期しやすさ
背景色がじわっと変わる
パーティクルが降る
テキストがフェードイン
花火が上がる×
爆発と同時にフラッシュ×

瞬間的な演出は「ズレてる感」が目立つ。「いつ始まってもいい」系の演出を選ぶと破綻しにくい。

スケーリングまとめ

コンポーネント配置理由
時刻サーバエッジ (Workers)超軽量、高可用性
タイムシートCDN静的ファイル、キャッシュ可
演出アセットCDN同上
コメント等本体サーバ動的機能のみここに集約

本体サーバの負荷を極限まで減らすのがポイント。時刻同期とアセット配信をエッジに逃がせば、本体はコメント等のリアルタイム機能だけに集中できる。

まとめ

  1. 時刻同期を別サーバに分離する
  2. 初回で時刻取得、以後はローカル計算で進行
  3. タイムシートはCDNで事前配布
  4. 演出はズレに強いものを設計
  5. 状態管理を時刻サーバに相乗りさせて緊急対応も可能に

「全部1台でやる」から「役割ごとに分離する」への発想転換が重要。