Cloud RunとFirestoreでゲーム部屋の状態を管理する設計
Cloudflare Durable Objectsによる1部屋1インスタンスの一貫性を、Cloud Runの複数インスタンス環境でどう再現したかをまとめた移行記録です。
Codename LOLはもともとCloudflare Workers上で動いており、部屋ごとの状態管理にはDurable Objectsを使っていました。運用をGoogle Cloud Runへ移すにあたり、Durable Objectsが暗黙に保証していた性質を、Cloud Run側でどう作り直すかが最大の設計課題になりました。
Durable Objectsが保証していた一貫性
Durable Objectsは部屋IDごとに1つのインスタンスだけが存在することをランタイムが保証してくれます。同じ部屋への操作は必ず同じオブジェクトに届くため、複数の参加者がほぼ同時にカードを選んでも、状態更新の順序を気にせず素直に実装できていました。Cloud Runにはこの「1キーにつき1インスタンス」という保証は存在しません。
スプリットブレインを避けるためのmaxInstances=1
Cloud Runは負荷に応じて自動的にインスタンス数を増やします。ゲーム部屋の状態をメモリ上に持つ実装のまま複数インスタンスが同時に立ち上がると、同じ部屋への操作が別々のインスタンスに振り分けられ、部屋の状態がインスタンスごとに分岐する「スプリットブレイン」が発生します。これを避けるため、このサービスはmaxInstances=1に固定し、常に単一インスタンスだけが全リクエストを処理する構成にしました。スケールしない代わりに、Durable Objects時代と同じ「1部屋=1つの処理主体」という前提を維持できます。
ゼロスケールとFirestoreへの永続化
個人運用でコストを抑えるため、minInstances=0のままアイドル時にはインスタンスをゼロまでスケールインさせています。ここで問題になるのが、インスタンスが消えるとメモリ上に持っていた部屋の状態も消えてしまうことです。そこで、部屋の状態を変更するたびにFirestoreのコレクションrooms(ドキュメントIDを部屋IDとする)へwrite-throughで書き込み、インスタンスが新しく起動した際は該当ドキュメントを読み戻してメモリ上にハイドレートする方式にしました。通常時の読み書きはメモリ上で完結するため、Durable Objects時代と近い速度感を維持しつつ、インスタンスの再起動にも耐えられます。
掃除処理をalarmからプロセス内定期実行へ
Durable Objectsでは、12時間操作のない部屋を掃除する処理をalarm APIで実装していました。Cloud Runにはalarmに相当する仕組みがないため、単一インスタンス内でsetIntervalによる定期実行タスクを回し、最終更新時刻が12時間を超えた部屋をメモリとFirestoreの両方から削除する処理に置き換えています。maxInstances=1で単一インスタンスに固定しているため、複数のインスタンスが同時に同じ部屋を掃除しようとする競合も起きません。
切断プレイヤーの扱いは変えていない
一時的な通信断を永続メンバーとして残すと、開始条件を満たせなくなります。このゲームでは再接続の完全な復元より、短いセッションでロビーを正常に保つことを優先し、切断したプレイヤーはメンバー一覧から取り除く方針を今回の移行後も維持しました。ホストが抜けた場合は、残っている参加者へ権限を移します。この挙動自体はDurable Objects時代から変更していません。