當我設計遊戲服務器時,我在想些什麼?(1)

機緣巧合的機會,我有幸能夠從頭開始設計一個遊戲的服務器。中間遇到很多歡聲笑語和悲傷淚水,這裏分享一下。

我之前所在項目組的遊戲服務器架構如下圖:

這款遊戲是一款MMO的端遊,GateWay網關的任務是接受客戶端的連接,然後通過分發策略,把玩家丟進GameSvr上去,之後玩家的所有請求都直接發給GameSvr,由GameSvr處理了。當然這裏的分發策略跟一般的web服務器是不同的,web服務器一般會做成無狀態的服務器,也就是對於客戶端來說請求到達哪一個服務器都沒有關係,都能夠被處理,但是遊戲服務器大多都是有狀態的,web的一般分發策略是做一個負載均衡,只要保證服務器的整體壓力沒問題就已經是一個好的架構了,但是遊戲服務器由於存在狀態需要維護,所以通常都沒辦法做簡單的負載均衡。舉個例子,由於我們是MMO遊戲,所以在服務器邏輯上存在地圖的概念,好比魔獸世界的遊戲裏不同的玩家出生的地圖是不一樣的,而且你從一張地圖下線後,下次上線的地點一般來說都會是你上次下線的地點,而根據遊戲歷程和策劃想要營造的遊戲效果來看,遊戲裏不同地圖之間的壓力通常來說是不一樣的,比如主城的玩家就通常比一張偏遠的野外地圖的玩家要多,而遊戲服務器的壓力通常都出現在玩家聚集的時候,例如大量IO的壓力。所以從設計人員的角度來看,一般會把不同的地圖分配到單獨的進程中去,所以GameSvr通常會按照地圖來進行劃分,那麼GameSvr進程天生就具備了自己的狀態,因爲兩個GameSvr進程已經就不一樣了,玩家是在哪個地圖,就只能把玩家丟到哪個GameSvr上去。當然,這只是我們採取的一種做法,其他人還有不同的做法,不過大致是類似的。


我曾仔細考量過上面那個遊戲單個GameSvr的承載能力,通常在1000人左右就已經開始吃緊了,這點也比較好理解,根據這個服務器的架構,幾乎所有的玩家邏輯都在同一個進程內被處理,只有一些公共的邏輯被分配給Relay進程處理,這裏的Relay進程承擔兩個作用,一個是做不同的GameSvr間的通信轉發,另一個是它自身也會維護遊戲裏的公共邏輯,什麼叫公共邏輯呢?比如聊天、幫會、排行榜等等,這些不跟地圖相關的遊戲邏輯,又被多個GameSvr共享的部分就被抽離到Relay進程上,其實這裏有兩種做法,一種是讓每個GameSvr進程自己去維護這份公共邏輯,但顯然這會浪費GameSvr進程僅剩的不多的壓力負載。但是把邏輯放入Relay進程也帶來一些問題,比如在遊戲研發中會發現,越來越多的邏輯需要放進Relay進程,導致Relay進程越來越龐大,比如我們遊戲中的幫會數據和全局定時器的維護就由Relay進程來維護的,但是程序員在寫功能的時候經常就直接一個RPC調用來使用Relay的功能,因爲這很方便,而且從現有架構的設計來看也是合理的,我們當時的relay的rpc接口設計的是同步的接口,於是當rpc激增的時候,整個遊戲邏輯就變卡了。


Relay的龐大導致兩個問題,一個是單點故障的概率變高了,另一個就是Relay的性能低下會導致整個遊戲服務器變卡。我們的服務器有段時間在每天晚上10點左右就開始卡,只要涉及到公共邏輯的部分就幾乎無法使用,例如無法聊天、無法進行幫會操作等等,最後排查問題的結果就發現是遊戲中的某些功能把邏輯寫在Relay上,到10點進行了一個十分複雜的計算,導致Relay阻塞在那裏,而無法響應GameSvr發來的請求。我是在遊戲的開發末期加入這個項目的,當時Relay的體積還是比較小的,大家都刻意的去儘量避免遊戲邏輯放在Relay上,但是當遊戲真正進入運營期之後,deadline的限定,大家不得不把邏輯放Relay上,因爲這是最快的實現方式。我眼看着Relay一步步的胖起來,在我離開這個項目的時候,Relay上的代碼已經滿目蒼夷了。


再來談談這個架構的其他部分,比如Bishop是用來進行我們遊戲內交易數據的統計和驗證的模塊,主要是和公司的交易系統進行對接,AccoutSvr的作用是處理遊戲內賬戶的相關操作,比如建立賬戶,新建角色等等,它的主要作用其實是爲了保證遊戲數據的唯一性,例如角色名的唯一、寵物名的唯一等等。接着是Mysql的部分,雲風曾經在博客裏說過遊戲服務器中數據庫的作用應該只是一種備選方案,http://blog.codingnow.com/2014/03/mmzb_db.html 也就是說如果存在理想的遊戲服務器,永不宕機,永不維護,那麼實際上數據庫是不需要的部分,數據庫承擔的是一個遊戲恢復和容錯的機制,我很贊同雲風的這種說法,而一旦不需要去考慮數據庫的部分,那麼遊戲設計其實減掉了很多容錯的做法。關於數據庫的部分我後面還會談到。


然後是這個架構的擴展性,對於遊戲服務器來說,擴展通常有三種,一種是開新區、另一種是合服、最後纔是由於服務器壓力頂不住而擴展邏輯進程。上面我們這個端遊的服務器模塊從設計上就不去考慮開新服的機制,單套服務器架構基本只支持一個區,所以在開新區的時候,做法是部署同一套服務器架構,利用客戶端更新服務器的地址來實現不同服的架構;合服的情況要更加複雜,因爲在設計上沒有考慮多服的設計,所以不同服之間的數據可以說是毫不相關,那麼在合併服務器的時候必然存在一些衝突數據需要處理,不光是一些玩家數據,甚至還有一些遊戲邏輯數據,例如我之前做過一個功能,是實現一個全服的玩家聯賽,最後每個區會產生一個冠軍,然後冠軍的雕像會被放置到遊戲中的主城中展示一個月,但是後來有兩個區合併了,那麼到底展示誰的雕像,因爲無論你展示哪一個區的,玩家都會不滿,當然,這種事情一般需要策劃去考慮然後解決,但是從程序上來說,確實存在這種需要特別處理的地方。在合服的時候,另一個問題是之前保證的角色名不重複是利用的各自區的accsvr,通常accsvr保證惟一性的做法是使用同一個數據庫來保存需要排重的信息,但是對於不同的網絡運營商,這個數據庫很可能沒辦法統一起來,這樣一來,當兩個處於不同網絡中的區需要合併的時候就出現了難以解決的問題,比如我們有一次把電信和網通的兩個區進行合併,本來這兩個區各自確實能夠保證角色名唯一,但是當跨運營商合併的時候卻會發現衝突。


然後是服務器壓力,不知道是由於當年設計這個架構的人沒有考慮到還是當時不存在這樣的問題,所以服務器橫向擴展的能力是十分弱的,對於新開的副本,relay進程會根據gamesvr的壓力來選擇把新副本開在哪個gamesvr進程上,但對於常駐地圖,卻是寫死在配置表裏,綁定固定的gamesvr,而relay又沒有提供動態添加gamesvr的功能,這導致瞭如果在服務器運行期間出現了單個服務器進程壓力,除了重啓服務器,幾乎沒有任何辦法。這個在運營期暴露的很明顯,由於GameSvr按照地圖進行劃分,當開新區的時候,玩家大量涌入同一張地圖,導致單個GameSvr壓力出現峯值,但這個時候卻沒辦法動態的擴展進程進行分壓,而導致服務器宕掉。在後期的運營中這樣的情況屢見不鮮。


後來我還聽到過很多關於頁遊的架構,頁遊服務器整體上跟端遊思路是類似的,由於客戶端的通常通信方式不再像端遊那樣採用原生tcp連接,而是使用http等短連接的協議,所以在客戶端連接的部分設計上更加靈活,而且大部分頁遊的遊戲邏輯沒有端遊那麼複雜, 在客戶端的表現力有限的情況下,基本上整體的遊戲服務器設計要更加精簡,所以我看到通常服務器後端會更加偏重於如何進行橫向擴展。上面我提到的架構擴展性差的問題,對比頁遊的滾服方式,體現的最明顯了。


這就是關於我上一個遊戲服務器的樣子了,下一篇開始我自己的服務器設計。


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