我們實際解決的問題
Hytale 引擎透過一個簡單的 pub/sub 系統 EventManager 觸發事件。但當我們將 Veltrix 扩展到 2,500 名同時玩家時,星期五寶藏獵隊在 1,200 名同時參與者負載下會變得停滯。症狀並不隱晦:
- EventManager 阻塞隊列在 Redis Streams 中達到 89%
- 每次寶藏生成產生2.4秒的延遲尖峰
- 客戶端寶藏啟動時玩家超時,拋出NRE-7280:寶藏箱子啟動超時—區域53無響應
- Redis記憶體使用量在15分鐘內從2.1GB激增至11.2GB,觸發我們緩存層的OOM殺手
根本原因不是邏輯。是設定:我們為所有區域有一個全域事件通道,為所有寶藏類型有一個 Redis 流,而且沒有反向壓力。EventManager 被當作消防水管而不是受控的灌溉系統。
我們首先嘗試的(以及為何失敗)
我們的第一次嘗試是天真地擴展:更多 Redis 分片,更多消費者,更快的硬體。我們對問題拋出了 3 個 Redis 7.2 分片,每個分片有 8 個消費者群組,分佈在 4 個區域。這讓我們獲得了 40 分鐘的穩定性,但在負載下隊列仍然積壓。為什麼?
- 訊息發布/訂閱頻道仍然是一體化的。Harbormere 的寶藏仍然在排隊等待 Blightfen 的那個。
- 消費者漂移:玩家在不同區域間傳送,沒有乾淨地切換消費者群體,導致重複生成和虛擬箱子.
- 沒有電路斷路器。當 Redis 記憶體激增時,OOM 殺手不僅僅殺死進程——它殺死了整個緩存層,導致所有活躍玩家會話都中斷。
- 我們將 OpenResty 作為速率限制器引入,但它為每次產生引入了額外的 400ms 機延,玩家開始報告移動時出現卡頓現象.
殘酷的事實是?我們優化了通量而不是信號完整性。我們將事件流視為原始數據管道,而不是具有明確邊界的有界上下文.
架構決策
我們轉向嚴格的區域性事件巴士模式:
- 每個6個區域都獲得了自己的隔離Redis流(單向流,不是分片)
- 我們將頻道重新命名以匹配生態系統ID:EventStream_53對應哈伯默爾,EventStream_71對應枯萎沼澤
- 寶藏生成規則區域化:除非明確允許,否則禁止跨區域生成(在排錯跨區域鬼箱子後,我們完全禁用了這個功能)
- 我們推出了一個用 Go 語言編寫的輕量級事件總線網關,在專用的 k3s 電腦節點上運行,每個節點有 2 個 vCPU/4GB。它作為一個分散路由器,而不是消費者
- 每個區域的消費群組最多有 32 個消息在飛行中,在 Redis NACK 上使用指數退避
- 我們將 Redis maxmemory-policy 設置為 allkeys-lru,並設定了 8GB 的硬極限,並添加了一個 Lua 腳本,在記憶體超過 6GB 時強制進行垃圾回收
- 我們將寶藏啟動邏輯從客戶端移至一個名為TreasureCore的區域微服務,該服務在Fly.io上運行,使用Postgres 16和pgbouncer。它暴露了一個REST端點:POST /treasure/{biomeId}/activate,使用ETag鎖定以防止重複生成。
交易代價很清楚:更多的運營開銷,每個區域更高的成本,以及區域間傳送存在一些延遲。但我們選擇了正確性而非便利性。區域模式意味著即使玩家在事件中間傳送,Harbormere寶藏生成也不會阻礙Blightfen寶箱生成
數據說了什麼之後
經過三週穩定運營:
- Redis 内存穩定在所有資料流中為 3.2 GB(較舊的全局模型減少了 71%)
- 寶藏生成延遲從 2.4 秒降至 180 毫秒 p99
- 負載下不再出現 NRE-7280 錯誤—啟動失敗率從 12% 降至 <0.1%
- 玩家體驗提升:螢幕上不再出現箱子閃爍,不再出現傳送引起的不同步
- 成本:為 Redis 每月 47 美元(從 189 美元下調),另加 6 個 TreasureCore 实例每月 112 美元。我們為了穩定性犧牲了 14 毫秒的跨區域延遲。
指標告訴我們我們早已猜測的事:將事件引擎視為一個全局系統是一種反模式。區域化不是過早的優化——它是損害控制。
我會 differently 做什麼
我再也不會設計一個單一的全球頻道的事件系統了。不為Hytale,不為任何遊戲。即使有強大的區域化,當玩家在高峰時段批量跨區傳送時,我們仍然遇到了問題,讓事件閘門被路由更新淹沒。我們的解決方案是引入傳送引發的區域切換的冷卻時間,但這損害了用戶體驗。
下次,我會將事件巴士進一步劃分:一個流用於靜態事件(固定寶箱),一個流用於動態事件(怪物、天氣、定時生成)。如果我能夠承受學習曲線的話,我會使用 NATS JetStream 而不是 Redis Streams——它內建提供了流級的壓力控制。
我永遠不再信任客戶端啟動了。Hytale 客戶端仍然只是 JavaScript 與 WebGL,物理不同步是不可避免的。把那個邏輯放到屬於它的伺服器上。
我們在那個星期五救了Veltrix免於崩潰。不是通過向問題中投入更多硬體,而是透過尊重我們實際正在建立的系統的界限。而且這個教訓很牢固:事件不僅僅是功能——它們是合約。違反合約,系統就會跟你一起崩潰。












