実際に解決しようとしていた問題
Hytaleエンジンは、イベントマネージャーと呼ばれるシンプルなパブ/サブシステムを通じてイベントをトリガーします。しかし、Veltrixを2,500人の同時プレイヤーにスケールアップしたとき、金曜日の宝探しは同時参加者数が1,200人を超えると停止してしまいました。症状は目立ちました:
- イベントマネージャーのブロックキューがRedisストリームで89%に達した
- 宝箱スポーンごとに2.4秒の遅延の増加
- クライアント側の宝箱アクティベーションタイムアウト、NRE-7280: 宝箱アクティベーションタイムアウト—リージョン53が応答しない
- Redisのメモリ使用量が15分未満で2.1GBから11.2GBに急増し、キャッシュ層のOOMキラーがトリガーされた
根本原因は論理ではありませんでした。設定でした:すべての地域に一つのグローバルイベントチャネルがあり、すべての宝物タイプに一つのRedisストリームがあり、バックプレッシャーがありませんでした。EventManagerは制御された灌漑システムではなく、水龍頭のように扱われていました
最初に試したこと(そしてなぜ失敗した)
最初の試みは単純なスケーリングでした:Redisのシャードを増やし、コンシューマーを増やし、より高速なハードウェアを使用しました。私たちはRedis 7.2のシャード3つを問題に投げかけ、それぞれに4つの地域を跨いで8つのコンシューマーグループを持たせました。それにより、負荷がある状態でキューがまだ積み重なるまで40分の安定性を得ました。なぜでしょうか?
- パブ/サブチャネルはまだグローバルでした。ハーバーメアの宝物が、ブライトフェンの1つにまだ並んでいました。
- 消費者の流動化:プレイヤーが地域を移動する際に、消費者グループを完全に切り替えなかったため、重複したスポーンと幻のチェストが発生した。
- カットバックはありません。Redisメモリが急増した時、OOMキラーはプロセスをただ殺さなかった—全体のキャッシュレイヤーを殺し、すべてのアクティブなプレイヤーセッションを落とした。
- OpenRestyをレートリミッターとして導入しましたが、それによりスパウンジョブごとに追加で400msの遅延が生じ、プレイヤーが動きにカクつきを報告し始めました
厳しい現実は?我々は処理能力の最適化を信号整合性の最適化の代わりに行いました。イベントストリームを境界が明確な閉じた文脈ではなく、生データパイプラインとして扱っていました
アーキテクチャ上の決定
厳格な地域イベントバスモデルに転換しました:
- 6つの地域それぞれに独自のRedisストリーム(単一方向のストリーム、シャードではありません)を割り当てました
- チャンネル名をBiome IDに合わせて再命名しました:Harbormere用のEventStream_53、Blightfen用のEventStream_71
- 宝箱スポーンルールは地域ごとに整理されました:明示的に許可されていない限り、異なる地域でのスポーンはできません(クロス地域の幽霊宝箱のデバッグ後に完全に無効化しました)
- Goで書かれた軽量なイベントバスゲートウェイを導入し、2つのvCPU/4GBあたりの専用k3sノード上で実行しています。これはファンアウトルーターとして機能し、コンシューマーではありません
- 各リージョンのコンシューマーグループは最大32件のメッセージを同時に処理し、Redis NACKに対して指数的バックオフを適用しています
- Redisのmaxmemory-policyをallkeys-lruに設定し、8GBのハードリミットを設定し、メモリが6GBを超えた際にLuaスクリプトで強制的にGCを実行するように追加しました
- 私たちは、宝のアクティベーションロジックをクライアントサイドから、Fly.io上でPostgres 16とpgbouncerを使用して実行されている地域マイクロサービスであるTreasureCoreに移動しました。それにはRESTエンドポイントを公開しました:POST /treasure/{biomeId}/activate、ETagロックを使用してダブルスプーンを防ぐために。
トレードオフは明らかだった:より多くの運用オーバーヘッド、地域ごとのコスト上昇、そして地域間のテレポート時の遅延。しかし、私たちは利便性よりも正確さを選んだ。地域モデルでは、プレイヤーがイベント中にテレポートした場合でも、ハーバーメアの宝箱スポーンがブライトフェンのチェスト生成を妨げない。
数字が示した結果
3週間の安定した運用後:
- Redisのメモリは全ストリームで3.2GBに安定(旧グローバルモデルから71%削減)
- 宝箱のスポーン遅延は2.4秒から180ms p99に低下
- 負荷下でNRE-7280エラーは発生しなくなり、アクティベーション失敗率は12%から<0.1%
- プレイヤー体験が向上:画面上の宝箱のフリッカーはなくなり、テレポートによるデシンクもなくなった
- コスト:Redisは月$47($189から下がった)に、加えてTreasureCoreインスタンス6個は月$112です。地域間の遅延を14msトレードインして安定性を得ました.
指標は私たちがすでに疑っていたことを教えてくれました:イベントエンジンをグローバルシステムとして扱うのは反パターンでした。地域化は早すぎる最適化ではなく、損傷制御でした.
私が違う方法で行うべきだったこと
私はもう一度、単一のグローバルチャネルを持つイベントシステムを設計しないでしょう。Hytaleのために、他のゲームのために。強い地域化があったとしても、プレイヤーがピーク負荷時に地域をまとめてテレポートすると、イベントゲートウェイがルート更新で圧倒される問題に直面しました。私たちの解決策は、テレポートによる地域切り替えにクールダウンを導入することでしたが、それはUXに影響しました。
次回はイベントバスをさらに分割する:静的イベント(固定の宝箱)用のストリームと、ダイナミックイベント(モブ、天候、タイムドスポーン)用のストリームを分ける。学習曲線を乗り越えられるなら、Redis Streamsの代わりにNATS JetStreamを使うと良い——これには既にストリームレベルのバックプレッシャーが組み込まれている。
そしてもう一度クライアント側のアクティベーションを信じることはない。HytaleクライアントはまだJavaScriptとWebGLで、物理のデシンクは避けられない。そのロジックをそれが属するサーバーに押し付ける。
その金曜日、私たちはVeltrixの崩壊からそれを救いました。より多くのハードウェアを問題に投げかけたのではなく、私たちが実際に構築しているシステムの境界を尊重することで。そしてその教訓はしっかりと残ります:イベントはただの機能ではありません—theyre契約です。契約を破れば、システムもそれと共に壊れます。












