網絡遊戲中玩家數據的處理

背景

網絡遊戲中最重要的數據莫過於玩家在遊戲的過程中產生的數據。

可以簡單的分成兩類:

  1. 存檔數據
  2. 過程記錄數據

第一類數據主要是類似角色『基礎』信息,揹包、技能、任務,以及所有(或者部分)玩家共有的王國、地圖、聯盟等信息。
第二類主要是類似『日誌』信息一樣的,比如「某個地方某角色使用了某道具」這樣的操作記錄。

這篇文章中我們主要討論第一種數據的處理,關於後一種用作記錄和分析的數據,可能會在後面寫一個專門的Blog介紹。

存儲結構

首先我們需要決定數據的形態,和描述的方法。
這裏有一個選擇:以『條目格式』爲核心,還是以『文檔格式』爲核心。

『條目格式』的優點:看起來較爲平坦,方便實時落地,和各種數據庫存儲模式搭配方便,外部工具修改方便,便於批量處理。
『文檔格式』的有點:通常是樹形結構,和大部分腦中的角色模型更加匹配,在內存中處理的時候,通常來說效率更高。

採用不同的數據結構通常會影響到項目使用的技術,甚至直接影響到整體架構和處理模型。如果使用條目,可能就會用memcache/redis存儲,使用更多的無狀態業務邏輯處理方式。

我們團隊習慣的,是以 struct 爲核心,配合 bson/protobuf 實現接口和序列化,主要操作都在內存中處理的方式。
使用的時候,內存中的數據結構爲 Go 的結構體;落地時使用MongoDB數據庫的保存的bson格式的文檔(Document);在和移動端交互以及某些內部RPC接口調用的時候,使用protobuf作爲傳輸格式;配置方面,使用etcd同步的csv條目數據。

不論是bsonprotobuf還是 csv 條目數據,都可以使用的是 Go 裏面的擴展性極強的 struct tag 來完成自動轉換。
1550139780426.jpg

數據兼容和升級

在長時間的線上業務維護和開發過程中,玩家的數據結構總是會修改、更新的。
如果每次都停止系統,把幾千萬甚至上億的存放在數據庫中的數據都升級準備好肯定是不可能的。通常情況下會在數據載入的過程中進行數據兼容檢查和更新。

此時,在go程序裏面使用bson作爲數據的最終存儲方式會給我們帶來非常多的便利。bson unmarshal的自動兼容處理可以把大部分『新增』、『刪除』操作都完美處理好,對於那些需要修改的字段或者複合結構,我們完全可以使用新增操作來替代,升級之後刪除舊數據即可。

使用二進制數據的那種痛苦 v0.01 -> v0.02 -> v0.0N 升級過程再也不需要見到了。而我們也不需要向使用條目存儲數據的項目一樣,等待一個巨大的庫執行 alter table 的操作。

序列化的效率

有很多人都做過測試,有不同的結果。我們實際測試的結果是:

gob 最好,bson,protobuf 和 json 較差。

LRU 的選擇

上面提到,需要操作的數據的,都是以 struct 的形式存放在內存中的。自然不能將所有的數據都載入,通常我們需要維護一個數據結構,將那些很久沒有使用過的數據逐步淘汰出內存。這裏使用一個LRU表就可以恰當且方便的完成。
Go的一個LRU實現

獲取到需要淘汰的數據,通常會將其持久化到數據庫中,線上我們遇到過這樣的一個場景:

  1. 更新啓動服務器之後,停服期間玩家完成的大量操作在啓動之後統一處理(建築、戰鬥、科研等),導致大量玩家載入內存。同時觸發聯盟操作,導致大量聯盟數據也載入到相應服務進城的內存中。
  2. 由於大部分需要持久化的數據都是使用的一個接口,裏面定義了相同的存盤間隔(或者LRU閾值)。
  3. 半小時後,巨量在啓動的時候載入的玩家和聯盟數據超過存盤閾值,被LRU算法篩選出來。
  4. 併發的序列化以及db操作導致MongoDB卡死……

所以,做了LRU,最好配合限流或者hash操作,防止同時間大量數據一起淘汰。

內存數據的丟失

數據放在內存中,總是給人一種『不靠譜』的感覺,至少是有不少人給我提起過的。

然而遊戲的業務中最重要的是什麼?我個人認爲是響應速度和可用性。

實際上,每個半年左右總會有一些雲主機無辜掛掉(該死的阿里雲),實踐證明,通過一些常規的手段,可以在上層避免掉軟硬件crash帶來的災難,至於內存中較新的未持久化的數據,丟了就丟了吧

  • 使用適當的保底存盤策略(每個玩家半小時至少存盤一次),加上登出或者某些特殊操作(例如充值)的即時存盤,保證數據絕大部分數據的正確。
  • 敏感操作全部記錄track日誌(通過kafka一類的方式保存),必要的時候從日誌中恢復丟失的操作。
  • 使用雲服務器自帶的備份功能,將所有數據庫做1小時間隔的備份。
  • 多處引用的數據,只在一個地方保存(或者仲裁),防止部分節點數據損壞帶來的一致性問題(那怕出錯也不要衝突)。

動態讀寫內存

Go的動態特性(其實就是Reflect啦),給我們帶來了一些運行時調試的便利。起碼有兩種方式可以實現不停止服務器修改數據:

  • 自己實現一套get/set/del的操作,讀寫內存中的struct object。
  • 引入lua,動態的載入自定義lua script讀寫內存中的數據。

由於Go到目前爲止(1.11)仍然沒有靠譜的動態更新方案,我們開始考慮,在某些運營業務爲主的服務中,嘗試用lua來編寫業務邏輯,這樣可以達到不停機修復bug甚至更新程序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章