Switch 2 GameChat バックエンドを分解する:マルチリージョン SFU と DynamoDB 一貫性の設計判断

2026-06-26T20:30:00+09:00 | 29分で読めます | 15回閲覧

@
Switch 2 GameChat バックエンドを分解する:マルチリージョン SFU と DynamoDB 一貫性の設計判断

背景

Nintendo がバックエンドの話を公開するのは珍しい。AWS Summit Japan 2026 の CDN221「ゲームチャットを支える技術」 というセッションで、Nintendo Switch 2 の本体機能である GameChat のサービス設計が紹介された。登壇者はニンテンドーシステムズ株式会社のシステム開発部の方たちで、このセッションの背景自体が興味深い。ニンテンドーシステムズは任天堂本体ではなく、任天堂と DeNA が 2023 年に設立した合弁会社で、任天堂の Online Service 関連システムを担当しているらしい。

発表でとくに新しい技術名詞は出てこなかった。WebRTC、EC2、Fargate、DynamoDB、SQS、Terraform、OpenTelemetry といった、聞けば誰でも知っているコンポーネントが並ぶ。にもかかわらず、後から確認したらほぼすべての LLM が「情報密度が高いセッション」と評価していた。以下、このセッションを振り返って、三つの層で整理された設計判断を見ていく。

GameChat って何?

GameChat は Nintendo Switch 2 の本体機能で、ユーザー同士でボイスチャット、ビデオチャット、ゲーム画面の共有ができる。Joy-Con 2 に追加された C ボタンがシステムレベルの入口になっていて、どのゲームを遊んでいる最中でもチャットに入れる。

重要なのは画面共有がデフォルトの機能として組み込まれている点だ。軽量なボイスチャンネルではなく、音声・カメラ映像・ゲーム画面共有を同時に扱うリアルタイム通信システムということになる。ゲームを動かしながら並走させるので、CPU/GPU・メモリ・ハードウェアエンコーダー・ネットワーク上り帯域はどれも無限ではない。サーバー側がクライアントの複雑さを引き取る設計が前提になる。

ユーザー視点では単純な流れに見える。C ボタンを押して、フレンドを選んで、チャット開始、必要なら画面共有と消音の設定をするだけ。ただバックエンドから見ると、各ステップに対応するサービス呼び出しがある。

ボタンを押した時点で session bootstrap、認証トークンの確認か更新、チャットのコンテキスト初期化が走る。フレンドを選ぶ段階ではフレンドリストとプレゼンス情報が必要になる。チャット開始で初めて 招待・ルーム作成・メディアサーバー割り当て・WebRTC 接続の準備に入る。画面共有の設定は SFU 側の publisher/subscriber 関係に影響する。

あとこの流れには重要な構造が含まれている。チャットは完全にピアツーピアの mesh ではなく、発起側がルームを作って他のメンバーを招待する形になっている。後から出てくるステートマシン・一貫性の問題・追加招待のロジックは全部このルーム概念を中心に展開する。

制約:12 人ルームとゲーム並走

GameChat のサービス要件は、リアルタイム映像・音声通信、1 グループ最大 12 人、ゲームプレイ中の並走の三つだ。12 という数字が効いてくる。

P2P の mesh 構成にすると、12 人ルームでは各クライアントが他の 11 人に向けてメディアストリームを上げ続ける必要がある。家庭用ネットワーク・NAT 環境・Switch 2 のエンコード予算を考えると、これは現実的でない。音声だけでも mesh のコネクション管理はすぐ膨らむし、ゲーム画面の共有が加わればP2P は選択肢から消える。

「ゲーム並走」は一番きつい制約だと思う。一般的なビデオ会議アプリならフォアグラウンドリソースを使い切れるが、GameChat はシステム層に埋め込まれたバックグラウンドの通信プロセスに近い。ゲームのレンダリングやオンライン対戦に必要なリソースを奪えないので、サーバー側がより多くの転送・選択・状態管理を担う必要がある。

セッションでは GameChat がリアルタイム音声映像に WebRTC を使っていると説明があり、UDP パケット通信が強調されていた。聞くと単純に思えるが、「WebRTC を使った」と言うだけでは実際の複雑さはあまり伝わらない。

WebRTC には ICE・STUN・TURN・DTLS-SRTP・RTCP・輻輳制御など一式のメディアプロトコルスタックが含まれている。それより重要なのは、WebRTC はビジネスシグナリングを規定しないという点だ。ルームの作り方、招待の送り方、ユーザー認証、SFU エンドポイントの発見方法は、全部アプリケーション側で実装する必要がある。

Switch 2 のような組み込みゲーム機では、ハードウェアのコーデック・システムリソースの隔離・NAT traversal・ネットワーク品質も制約になる。WebRTC はメディア転送を標準化した上で、ルーム・認証・スケジューリングの問題をサービス側に残す技術だ。

P2P は 2 人通信なら理論遅延が最短だが、多人数 GameChat で重要なのは理論最短ルートではなく、安定性とクライアント負荷だ。

P2P mesh の複雑さは $O(N^2)$ で、12 人ルームの総接続数は $C(12,2)=66$ になり、各クライアントが複数の上りストリームを維持しなければならない。SFU はクライアントの上りを 1 本のメディアストリームに集約して、サーバーが選択的に他のメンバーに転送する。サーバー中継の遅延は増えるが、帯域・負荷・障害処理がずっと扱いやすくなる。

実際のネットワークでは NAT 穴あけの失敗・TURN フォールバック・国際回線の不安定さ・弱回線のメンバーが部屋全体を引っ張る問題もある。世界中の家庭ネットワークで安定動作が必要な Nintendo のシステムなら、SFU のほうが現実的な選択だと思う。

システム構成

アーキテクチャ図はシステムを二つの層に分けている。

コントロールプレーンは Admin Region に集中している。CloudFront・ALB・Fargate・SQS・API Gateway・DynamoDB がグループサーバー、非同期ワーカー、SFU インスタンスマネージャー、状態ストレージを担う。ルーム・招待・認証・状態同期・インスタンスディスカバリーなどのビジネスロジックと制御ロジックがここに集まる。

メディアプレーンは複数リージョンに展開されている。各リージョンに EC2 上の SFU サーバーと Amazon Transcribe が置かれていて、SFU が実際の WebRTC 音声映像接続と転送を処理する。低遅延メディアパスはユーザーの近くに置く必要があるが、コントロールプレーンの状態もマルチリージョンにしようとすると、データ同期・競合解決・複雑な障害パターンが増える。

機能で整理すると三つに分かれる。

グループサーバーはルーム作成・招待・メンバーシップ・認証 webhook と DynamoDB 更新を担当する。ビジネス状態の主な入口だ。

SFU インスタンスマネージャーは各リージョンの SFU インスタンス状態を監視して、接続数と負荷情報を集中管理し DynamoDB に書き込む。グループサーバーがこれを読んで接続先を選ぶ。

SFU サーバーは EC2 上で動いて WebRTC 接続とメディア転送を処理し、接続イベントをコントロールプレーンに戻す。UDP の扱い・高 PPS・長期接続・インスタンスレベルのネットワーク性能・状態を持つルームを考えると、Fargate より EC2 が自然な選択だろう。

セッションではシングルリージョン 500ms 対マルチリージョン 50ms の比較が示されていたが、地図と数値は図解用だと明示されていた。この数字を実測値として受け取る必要はない。大事なのは、リアルタイム通話が RTT の閾値に敏感だという点だ。

音声通話の体験は線形に悪化しない。50ms 以下はほとんど気にならず、50〜150ms はまだ許容範囲で、150ms を超えると会話のリズムが崩れ始め、300ms を超えると「相手が話し終わるのを待つ」状態になる。ゲームを遊びながら通話する GameChat のシナリオでは遅延がより目立ちやすい。

マルチリージョン SFU の価値は、ユーザーをなるべく近くのメディアノードに繋いで、クロスリージョンの複雑さをサーバー側に押し込めることにある。ただ構成図では SFU 間のカスケーディングについては触れておらず、具体的なリージョンも公開されていなかった。この辺りはセッションの情報の空白部分だと思う。

Nintendo Systems はリージョンを選択的に使う方針をとっている。SFU サーバーはマルチリージョン、グループサーバーと SFU インスタンスマネージャーはシングルリージョンだ。

典型的なコントロールプレーン/データプレーン分離の構成だ。メディアプレーンは通話遅延に直結するのでユーザーの近くに置く必要があるが、コントロールプレーンが主に担うのはルーム作成・参加・状態管理で、遅延への感度は相対的に低い。コントロールプレーンをシングルリージョンに保てば、Global Tables・クロスリージョン書き込み競合・強一貫性の協調といった複雑さを避けられる。

デメリットははっきりしている。Admin Region 全体が障害になると、世界中のユーザーが新しい GameChat を作れなくなる可能性がある。ただすでに確立したメディア接続はそのまま継続できるかもしれない。ここでのトレードオフの核心は、最も頻繁に発生しユーザー体験に直結するメディアパスを近端化して、低頻度かつ複雑なコントロール状態は集中管理するという判断だ。

クライアントはどの SFU に繋ぐかを知る必要がある。一見 service discovery に見えるが、普通の DNS や負荷分散より複雑だ。

SFU の選択には少なくとも、リージョンのレイテンシ・インスタンスの接続数・CPU/帯域の負荷・ヘルス状態・ルームのアフィニティを同時に考慮する必要がある。同じルームのメンバーが複数の SFU に散らばると SFU 間の転送が必要になり、全員が同じ SFU に繋がると遠方ユーザーの遅延が増える。

SFU のディスカバリーは実質的には placement スケジューラー、つまり特定のユーザー・ルーム・時刻に最適なメディアインスタンスを選ぶ仕組みだ。

解決策は、SFU インスタンスマネージャーが全インスタンスの状態を監視して接続数などを DynamoDB に書き込み、グループサーバーが同じ DynamoDB を読んで適切なインスタンスを選んでエンドポイントをクライアントに返す構成だ。

Consul・etcd・ZooKeeper・Cloud Map を別途入れずに、コントロールプレーンの状態ストアとして使っている DynamoDB を流用している。AWS マネージド環境では、基盤を一つ増やさない分運用が楽になる。

ただこの選択はインスタンス状態がほぼ最終一致になることを意味する。接続数が変わるたびに DynamoDB に同期書き込みすると WCU とコストが跳ね上がる。現実的にはマネージャーがメモリ上で状態を持ち、定期的か閾値ベースで DynamoDB にフラッシュすると思われる。グループサーバーが参照する負荷情報は数秒遅れる可能性があるので、クライアントが接続失敗したときのリトライ/フォールバックも当然必要になる。

入室の流れ

入室時には三つの条件を確認する必要がある。ユーザーが正しい SFU インスタンスに繋いでいるか、対応するグループに所属しているか、アクセストークンが有効か。ユーザーの状態変化、たとえば入室・退室・瞬断・復帰も、グループサーバーに同期されて DynamoDB に書き込まれる。

つまり SFU はビジネス状態の source of truth ではない。メディア接続とイベントを処理するが、ユーザーが入る権限を持つかどうか・ルームの現在状態は何かは、グループサーバーと DynamoDB が管理する。

この役割分担で SFU の blast radius が抑えられている。SFU は公衆ネットワークの UDP と WebRTC の入口に晒されているが、ユーザーテーブル・メンバー関係・認証シークレットを持っていなければ、メディアインスタンスに問題が起きてもビジネスデータとアクセス制御はコントロールプレーンにそのまま残る。

SFU が WebRTC 接続リクエストを受けると、認証 webhook でグループサーバーを呼ぶ。リクエストにはユーザー情報・接続先インスタンス・タイムスタンプ・アクセストークン・WebRTC 設定が含まれていて、グループサーバーが確認して 200 OK を返してから SFU が接続を完了する。

よくあるメディアサーバーの auth callback パターンに近い。SFU 自体が DynamoDB に直アクセスせず、完全なビジネス認証を内包せず、認可の判断を外部のマネージャーサーバーに委ねる形だ。

コストはある。認証 webhook が入室の critical path に入っている。クロスリージョンのユーザーが近くの SFU に繋いでも、SFU が Admin Region のグループサーバーに認証を問い合わせに行く可能性がある。ルームに参加するような低頻度の操作なら数百ミリ秒の遅延は許容できるが、メディアフレームの転送ごとにこれをやると完全にアウトだ。

セッションでは webhook を二種類に分けていた。

認証 webhook は同期パスで、入室体験に直接影響する。低遅延・独立スケールのグループサーバーパスで処理する。

イベント webhook は非同期処理でいい。SFU がイベントを API Gateway に送り、SQS に流れて独立ワーカーが DynamoDB を更新する。入室・退室・瞬断・復帰といったイベントは最終的に同期する必要があるが、必ずしもユーザーの今の操作をブロックしなくていい。

この分離は成熟したシステム設計で重要なポイントだと思う。SLA の違うワークロードを同じワーカーグループに混ぜると、高頻度の非同期イベントが同期認証リソースを圧迫して、最終的にユーザーが C ボタンを押してもなかなかチャットに入れない状況を生む。

オブザーバビリティと Terraform

オブザーバビリティのセクションから技術スタックの情報がいくつか出てきた。グループサーバーは Go で書かれていて、AWS Distro for OpenTelemetry SDK でトレースを生成する。SFU サーバー側では Envoy proxy がトレース ID を生成・伝播して、OtelCollector コンテナが収集して AWS X-Ray に送る。

X-Ray native SDK には直接バインドせず、OpenTelemetry を抽象層として使っている。X-Ray は今の exporter にすぎないので、Tempo・Honeycomb・Datadog に切り替えたければ collector の設定を変えれば済む構成だ。

SFU 側で Envoy を使っているのも面白い。メディアサーバー本体に計装を埋め込むのが難しい場合、とくに OSS の SFU や内部 fork の場合、Envoy sidecar で webhook の HTTP トラフィックを捕捉してトレースを注入するのは、外部から可観測性を足す方法として理にかなっている。

SFU のリージョンリソースは Terraform モジュールで管理している。各リージョンに異なる設定を注入するだけで、EC2・ネットワーク・Transcribe のリソースを作れる構成だ。

「一つのリージョンのメディアプレーンリソース一式」を繰り返しデプロイできる単位に抽象化している点が価値だ。リージョンを増減するとき、インフラ定義をコピペしなくていい。

ただ「即座に対応できる」という表現は少し言いすぎな気がする。新しいリージョンを本番に追加するには、イメージの複製・クライアントのリージョンリスト更新・インスタンスマネージャーの設定・監視アラート・段階的なトラフィック切り替え・障害演練など、Terraform が解決しない作業がまだたくさんある。Terraform が担うのはプロビジョニングであって、完全な本番導入プロセスではない。

DynamoDB の設計

後半は DynamoDB 開発事例に入る。コスト効率・ユースケース駆動設計・非正規化・GameChat 開始フロー・状態遷移と最終一致性・追加招待の不整合処理がテーマだった。

前半をひとことで言えばコントロールプレーン/メディアプレーン分離の話だが、後半はより踏み込んでいる。状態が DynamoDB に入ったとき、読み書きコスト・一貫性ウィンドウ・UX のステートマシンをどう一緒に設計するか、という話だ。

DynamoDB は key-value/wide-column スタイルのマネージド DB で、パーティションキー・ソートキー・アイテムサイズが読み書きコストに直接響く。セッションでは書き込みのほうが読み込みよりコストに注意が必要だと強調していた。WCU 自体だけでなく、書き込みが GSI・Streams・レプリケーションパスに与える増幅も理由だ。

GameChat のような頻繁に状態が変わるシステムでは、コストはリリース後に調整するものではない。入室・退室・瞬断・復帰のたびに書き込みが発生する可能性があり、データモデルの設計によって同じ状態変化が複数の GSI や複数のアイテム行を更新することになれば、コストが何倍にもなる。

DynamoDB のコスト効率は「クエリ回数を減らす」だけの話ではなく、アクセスパターン・アイテム形状・GSI の数・一貫性要件を一緒に設計するところから来る。

Nintendo Systems の設計ステップは、まず仮の RDB データモデルを作り、API 設計、ユースケースの列挙、アクセスパターンの抽出、最後にキーとファセットの決定という順序だった。

どう読み取るかを先に決めてから、どう保存するかを設計する。RDB モデルでエンティティと関係(ユーザー・グループ・ルーム・招待)を整理して、実際に DynamoDB に落とすときにクエリパターンからパーティションキー・ソートキー・ファセットを逆引きする。

これは Alex DeBrie(『The DynamoDB Book』)が推奨する標準フローだ。

セッションではリスト情報をアイテム内にそのまま持つ例で非正規化が説明されていた。GameChat の文脈では、ルームアイテムがメンバーリストを直接含んでいる形が当てはまる。ユーザー ID と表示名まで含む場合もあるだろう。

こうするとルームの状態を読み取るのに GetItem か Query の 1 回で済む。user テーブルの join が不要になる。コストは、ユーザーが名前を変えたような低頻度のイベントで fan-out 更新が必要になるか、短期的な stale を受け入れることだ。

GameChat ではこのトレードオフは理にかなっていると思う。ルームは短命で、メンバーリストの読み取りは高頻度で、ユーザーの改名は低頻度だ。高頻度の読み取りパスを 1 回の読み取りに圧縮するほうが、完全な正規化を追求するよりリアルタイムシステムのコストと遅延目標に合っている。

ステートマシン

GameChat 開始フローは、複数システムにまたがるステートマシンとして抽象化できる。フレンド選択、予約済み、認証済み、接続済み、通知送信というステップだ。単純なバックエンドの列挙値ではなく、UI 操作・DynamoDB のルーム状態・SFU のメディア接続の事実・通知チャネルを串刺しにしている。フレンド選択はクライアント側で起きる。予約済みはコントロールプレーンがチャット意図を記録した状態。認証済みは SFU webhook の認可結果から来る。接続済みは WebRTC 接続の事実が根拠になるべきで、通知送信は後回しにできる非同期の副作用だ。

「予約済み」が特に面白い。完全なメディアルームをすぐ作るのではなく、まず安価なコントロールプレーンストレージにチャットの意図を記録する。ユーザーがフレンドを選んだだけで相手がまだ応答していない段階で SFU リソースを確保しなくて済む。DynamoDB のルーム状態は軽量で、EC2 上の SFU ルームが高価なメディアリソースだ。要するに lazy resource allocation で、論理的な予約を先に行い、実際にメディア接続が必要になってから物理リソースを確保する。

接続済みに入るとき、事実の根拠はクライアントの UI でも DynamoDB 自体でもなく、SFU になる。SFU がグループサーバーに接続完了を通知して、コントロールプレーンがそのメディアプレーンの事実を状態ストアに書き戻す。セッションでは展開されていなかったが実際には厄介な問題がある。WebRTC 接続完了が SDP offer/answer 完了を指すのか、ICE 成功か、DTLS ハンドシェイク完了か、最初のメディアフレームの受信かで、ユーザーの待ち時間・失敗時のリトライ・状態タイムアウトの設計が変わってくる。

接続完了後の通知はホストの待ち経路と同じに束ねる必要はない。ホストはすぐフィードバックをもらってチャット画面に入る必要があり、これはユーザーが知覚するパスだ。招待相手はまだ知らないので、通知が数秒遅れても通常は許容できるし、失敗してもリトライできる。ここでも同期/非同期の分流が出てくる。

UI の状態・SFU のイベント・DynamoDB の状態・通知チャネルが同じシステムでない場合、一貫性の問題はデータベースの内部詳細ではなくユーザーに見えるフロー上の問題になる。意図を先に記録してメディアの事実を後で確認し、最後に非同期で通知するという分解が、コストと SLA で異なるステップに対応していて、高価なリソース確保・メディア接続確認・通知送信を同じ同期パスに詰め込まずに済むようにしている。

一貫性の扱い

セッションで示された実際の問題がある。クライアントが認証済み状態のときに特定の API を呼ぶと「API アクセス不可」が返ってくる。接続済み状態に入ってから初めて API が許可される。

この問題の本質はクライアントが認識している状態とサービス側の状態のズレだ。UI がユーザーを「チャットに入った」と思わせていても、コントロールプレーンがルームを authenticated のままで connected ではないと判断していることがある。あるいは DynamoDB への connected 書き込みは終わっているが、最終一致読み取りをすると古い状態が返ってくることもある。

解決策に銀の弾丸はない。読み取り一貫性を高める、UI がより厳格な server confirmed state を待つ、API の事前条件を緩めてより多くの操作を authenticated 段階で実行可能にする、などの選択肢があって、成熟したシステムでの対処は組み合わせになる。全読み取りを strong consistent に変えるだけで解決しようとするのは、だいたい間違いだと思う。

DynamoDB は strongly consistent read をサポートしていて、確認済みの書き込みを読み取れることが保証される。多くの一貫性問題への直接解法だが、コストが高く読み取り負荷の分布も変わる。

また strongly consistent read には適用範囲の制限がある。シングルリージョンのベーステーブル読み取りには有効だが、GSI は常に eventually consistent のままだ。高頻度読み取りパスで全読み取りを strong read に変えると、コストが増えるだけでなくデータモデル自体の問題を隠してしまう可能性がある。

strong read は重要な制約に使うツールであって、状態の不整合が出るたびに全面的に有効にするスイッチではない。

Nintendo Systems はっきりと、設計上の工夫で最終一致性を受け入れたため、strongly consistent read は採用しなかったと述べていた。

セッション全体を通じて最も判断が見えた部分だと思う。すぐ思いつく解決策は読み取りを strong consistent に変えることで、それで問題は和らぐ。でもコストが倍になり、DynamoDB を RDBMS のように使い始める入口になりがちだ。より成熟したアプローチは、具体的なアクセスパターンを分析することだ。本当に強い一貫性が必要な操作はどれか、ビジネスフローによって read-modify-write を回避できる操作はどれか。

単なるコスト削減ではなく、DynamoDB のコストとスケールの優位性を保つための前提条件の話だ。

このスライドがセッションで一番「なるほど」と思った瞬間だったので、先に解説する。

追加招待の不整合問題は、よくあるパターンから来ている。サーバーが「D を招待する」リクエストを受けて DynamoDB の現在のメンバーを読み取り、[A, B, C, D] を計算して書き戻す。この読み取りが eventually consistent なら、古い [A] を読む可能性があって、最終的にメンバーリストが [A, D] になり、B と C が消えてしまう。

標準的な解決策は strong read・conditional write・バージョン管理・トランザクションだ。Nintendo Systems のアプローチはもう少し面白い。追加招待は「通話中のチャットにフレンドを選ぶ」という文脈で発生する。ホストクライアントはリアルタイムチャット中にいて、SFU 経由で同期された最新のルーム view を持っている。だから追加招待のリクエストに現在の view と新しいフレンドを一緒に乗せれば、サーバーが古くなりうる読み取りをもう一度やる必要がなくなる。

この設計のポイントは「クライアントを信頼する」というだけじゃない。サーバー側はホスト身元・フレンド関係・権限を検証する。本当の洞察は、追加招待という操作において、プロダクトの制約がホストだけが操作でき・ホストは必ずオンラインで・ホストクライアントがリアルタイムチャネル経由で最新状態を持っていることを保証しているという点だ。だからシステムは既存のリアルタイムチャネルを流用して、一貫性の責任を DynamoDB 読み取りから操作コンテキストに移せる。

このセッションで一番注目すべき点だと思う。エンジニアリングの問題は必ずしもデータベース層だけで解決する必要はなく、プロダクトフロー自体がより安くてユーザー体験に近い制約を提供することがある。

最後の総括はかなり抑制されていた。DynamoDB は適切に設計されれば最初から期待どおりのパフォーマンスとスケーラビリティが得られる。でもユースケースが変わり、処理順序が変わり、表示情報が変わると再設計が必要になる可能性がある。UX を頻繁に試行錯誤している段階にも向かない。

この言葉は重要だと思う。AWS Summit という場で、登壇者は DynamoDB を万能データベースとして語らず、アクセスパターンが安定していて規模と遅延の要件が明確なシステムに向いていると明示した。GameChat は Switch 2 の本体機能で、コアの UX は発売前にほぼ固まっていたはずなので、DynamoDB は合理的な選択だ。

逆に言えば、プロダクトの形がまだ頻繁に変わっているなら、DynamoDB の single-table design とファセット設計がイテレーションを重くする。データベース選択は技術への信仰ではなく、システムの特性と開発フェーズの照合だ。

Q1「なるほど」ポイント

追加招待の不整合問題の解法は、最初に聞いたときよく分からなかった。ホストクライアントが見えているリストをそのままサーバーに送ると、サーバーが DB を読まなくていい?ということなので、改めて GPT に聞いた。

Ask
追加招待の「なるほど」ポイントが理解できなかった。なぜ「プロダクトのロジックで性能問題を回避する」というのが本当の「なるほど」なのか?
ChatGPT

この「なるほど」は、「クライアントがメンバーリストを送ってくる」という動作自体にあるのではなく、エンジニアが別の問いを立て直したところにある。

追加招待が発生するこの瞬間、誰が一番正確なルーム view を持っているか?

この問いで展開するとロジックが明確になる。

1. この操作は普通に見える

追加招待は append にしか見えない。ホストが通話中に「+」を押してフレンド D を選んで、D を今のルームに加える。

素直に server-side read-modify-write で実装すると:

  1. server が現在の members(たとえば [A, B, C])を読む
  2. 新しいリスト [A, B, C, D] を計算する
  3. DynamoDB に書き戻す

バックエンドがトランザクション型 RDB なら自然なモデルだ。

2. DynamoDB の eventually consistent read がこれを lost update に変える

GameChat の状態は DynamoDB にあり、デフォルトの読み取りは eventually consistent かもしれない。書き込みが ack されても、次の通常読み取りで必ずすぐ見えるとは限らない。

タイムラインを展開すると問題が見える:

時刻 起きたこと DynamoDB の通常読み取りで見えるもの
T1 B がルームに加わり、[A, B] の書き込みが ack [A]
T2 ホストが続けて C を招待 [A]
T3 server が古い値を元に計算して [A, C] を書き込む B が消える

「少し読み取りが遅い」という話ではなく、典型的な lost update だ。server は append しているつもりで、古いスナップショットで新しい状態を上書きしている。

3. 標準解法は正しいが、どれもコストを払う

この問題はデータベース層で解決できる:

解法 解決できること コスト
Strongly consistent read ベーステーブルの最新データを読む 読み取りコストが高く、GSI は依然として eventually consistent
Conditional write / バージョン管理 旧バージョンによる上書きを防ぐ 失敗時のリトライが必要で、遅延と実装の複雑さが増す
Transaction 読み書きの制約をトランザクションに入れる コストが高く、DynamoDB トランザクション自身の制限もある

これらはどれも間違いではない。本当の問題は、追加招待がユーザーの待ちパスで頻繁に起きうる操作だという点だ。毎回より重いデータベース一貫性メカニズムに頼ると、正確には動くがコストと遅延が増える。

4. Nintendo の転換点:DB に先に聞くな、誰がすでに最新の状態を知っているか聞け

ここでの重要な観察は、追加招待が任意のコンテキストで起きないという点だ。

これが起きるのは必ず:

  • ホストがルームにいる
  • ホストが SFU 経由でリアルタイムチャットに参加中
  • ルームのメンバー変化がリアルタイムチャネルでオンラインメンバーに同期されている
  • 追加招待はホストだけが開始できる

つまり B がルームに加わった時点で、SFU はすでにメンバー変化をホストクライアントにプッシュしている。そのときホストクライアントが見るルーム view はすでに [A, B, C] かもしれないが、グループサーバーが DynamoDB から通常読み取りすると数百ミリ秒前の [A] が返ってくることがある。

言い換えると:

追加招待というこの具体的な操作では、ホストクライアントが持つ view のほうがサーバーの DynamoDB 通常読み取り結果より新鮮な可能性がある。

これが「なるほど」の位置だ。

5. リクエストが「D を招待する」から「この view を前提に D を招待する」に変わる

Nintendo のアプローチはリクエストのセマンティクスを変えるものとして理解できる:

1以前:invite(D)
2以後:invite(D, based_on_members = [A, B, C])

サーバーは受け取ってから、DynamoDB の現在のメンバーを読まずに、ホストが送ってきた現在 view を元に検証後 [A, B, C, D] を書き込む。

クライアントを無条件で信頼しているわけではない。サーバーは引き続き検証する:

  • 発起者がホストか
  • D を招待できるか
  • ホストにこのルームへの権限があるか
  • リクエストのルーム/user/token が一致しているか

サーバーが古くなりうる read-modify-write をやめて、一貫性の責任を「サーバーがメインストレージを再読み取りする」から「サーバーがクライアントの提供した基準 view を検証する」に移している。

6. プロダクトの制約が十分に硬いからこの解法が成立する

この設計には明確な境界がある:

プロダクト制約 それが持つエンジニアリングの含意
ホストだけが追加招待できる 複数メンバーが members を同時に書く競合を避ける
ホストは必ずルームにいる ホストは必ずリアルタイム接続と現在のルーム view を持つ
メンバー状態は SFU 経由でリアルタイム同期される ホストの view が DynamoDB 通常読み取りより新鮮になりうる
追加招待は通話中に発生する 既存のリアルタイム状態チャネルを流用できる

「どのメンバーでも招待できる」とプロダクトが変わると、この解法はそのまま使えなくなる。writer が一人でなくなり、各メンバーの view が非同期になり、弱回線・バックグラウンド・別デバイスからの操作が発生して、conditional write・バージョン管理・トランザクションが必要になる。

7. より汎用的なパターンに抽象化すると

エンジニアリングで似た例は多い:

  • git push:クライアントがローカルのコミットグラフと自分が認識するリモート ref を送り、サーバーが fast-forward できるかを確認する。サーバーがクライアントの意図をゼロから推測するのではなく。
  • HTTP If-Match: <etag>:クライアントが「etag X を元に変更した」と明示して、サーバーは etag が一致する場合のみ更新を受け入れる。

システムにすでにあるリアルタイム状態チャネルを一貫性設計に組み込む。難しいのは、各コンポーネントが実際に何を提供しているかを見抜くことだ。DynamoDB は永続状態を提供し、SFU はリアルタイムのメンバー変化を提供し、ホストクライアントはこのプロダクトフローにおいて唯一・オンライン・最新 view を持つ発起点になっている。

Q2 積み木に見える理由

このセッションを聞いて自分が最初に感じたのは、技術的な深さはあるのに、聞いていると既知のコンポーネントを並べているだけに感じられるということだった。最初に WebRTC の解説があって、後で DynamoDB の話になって、「Nintendo ってこんな基礎的なものだけ使ってるの?どこがすごいの?」という感想になった。この疑問を Claude に投げた。

Ask
あなたの分析では技術的な深さがあると言っているが、なぜ聞いていると積み木みたいに既知の技術を並べているだけに感じるのか?難しさはどこにあるのか?
Claude

その感覚は正しい。このセッションは表面的には確かに「積み木」だ。でもそれがこのセッションの隠れた価値になっている。

エンジニアリングでよくある誤解がある。新しい技術を使った = 難しい、見慣れた技術を使った = 簡単、という等式だ。これは大きく間違っている。

「技術の新しさ」と「エンジニアリングの難しさ」を二つの軸にすると、より率直な地図が見えてくる:

技術の新しさ 低 技術の新しさ 高
難しさ 高 GameChat:成熟技術で高制約問題を解く ← このtalkはここ 自作 SFU、自作分散 KV、自作 consensus
難しさ 低 普通の社内 CRUD Kubernetes が不要なのに無理やり入れる

聴衆の本能的な反応は右上にある。名前が新しければ難しいと感じる。でも一番難しく、一番過小評価されやすいのは実は左上、つまり fancy な技術が「無料でかっこいい」を提供してくれない分、難しさのすべてが「どう組み合わせるか」と「トレードオフをどう消化するか」に集中する場所だ。右下は逆に、イノベーションの予算があっても使いどころを間違えた典型的な落とし穴だ。

「DynamoDB と SQS を使います」と言えば聴衆は平静になる。でも実際に業務で使うには先に分かっていないといけないことがある:

  • DynamoDB のどの書き込みパターンが WCU を爆発させるか
  • どのアクセスパターンが GSI を制御不能にするか
  • strong read が本当の解になる場面はいつか
  • それが単にコストを倍にするだけのことはいつか

これは Choose Boring Technology という考え方に繋がる(McKinley, 2015):

各チームの「イノベーション予算」は有限で、プロダクトが本当に差別化できる部分に使うべきで、インフラに使うべきではない。

Nintendo Systems の予算がどこに使われているかは明確だ。C ボタンの物理インタラクション・画面共有とゲーム並走でのフレームドロップなし・Switch 2 クライアントの WebRTC 実装・12 人 SFU の UX 調整。

一方、グループサーバーは Go + Fargate・状態ストレージは DynamoDB・IaC は Terraform・オブザーバビリティは OpenTelemetry、これは全部既存のソリューションだ。もし SFU・KV・サービスメッシュを同時に自作していたら、Switch 2 の発売日はたぶんもう 1 年ずれていた。

チームのエンジニアリング水準を判断するには、何を「やらない」かを見るほうが、何を「やった」かを見るより情報量が多い。 このセッション全体で fancy な技術名詞がなかったのは意識的な選択だ。

振り返り

GameChat の難しさは、成熟した技術で高制約のシステムを作ることに集中している。ゲーム動作中に音声・カメラ・画面共有・多人数リアルタイム接続を並走させる。メディアパスを世界中のユーザーの近くに置く。コントロールプレーンの状態は単純に保つ。認証・イベント・通知・状態遷移を異なる SLA のパスに分ける。そして DynamoDB の最終一致性の下でユーザーに見える競合条件を避ける。

その中で特に注目すべき設計点が三つある。

一つ目はコントロールプレーン/メディアプレーン分離。SFU はマルチリージョン、コントロールプレーンはシングルリージョン。全コンポーネントをグローバルに分散させるのではなく、低遅延の必要性と状態の複雑さを別々に扱う。

二つ目は同期/非同期 webhook の分流。認証は入室の critical path にある。イベント同期は API Gateway + SQS + worker でバッファリングできる。SLA の異なるワークロードを物理的に分離して、お互いを干渉させない。

三つ目は追加招待での stale read 回避。全読み取りを strong consistent に変えるのではなく、「ホストが通話中でリアルタイムチャネル経由で最新の view を持っている」というプロダクト制約を使って、server-side read-modify-write を回避する。

この種の設計に新しい技術名詞はない。ただエンジニアがプロダクトフロー・クライアント状態・メディアサーバー・データベース一貫性・コストモデルを同時に理解している必要がある。その工程の面白さは技術の派手さではなく、どこに boring technology を使うか・どこでプロダクトの制約がシステムの複雑さを代わりに引き取るかを知っているところにある。

© 2026 CheerChen's Blog

🌱 Powered by Hugo with theme Dream.

About

hi CheerChen です。