在游戲開發(fā)中,,我們經常會遇到一些技術難題,而其引發(fā)的bug則會影響整個游戲的品質,。女性向手游《食物語》就曾遇到過一些開發(fā)上的難題,,騰訊游戲學院專家團Wade、Zc,、Jovi等專家為其提供了指導和幫助,。過載保護、集群,、服務器通信,、并發(fā)選型等方面的問題,是中小團隊常常的技術難題,,本文分享了一些專家在坐診過程中提到的游戲服務器常見問題的解決方法,,希望對大家有所幫助。
問題一:玩家登錄時拉取好友信息,,但好友服務繁忙導致登錄失敗,。
解決方法:
1,、分離關鍵路徑上非關鍵調用,,縮短事務流程,避免周邊服務異常阻塞登錄,。
2,、服務熔斷機制,超出處理能力快速失敗,,防止雪崩,。
3,、按用戶隔離事務,避兔單個用戶請求阻塞影響到其他用戶,。
問題二:壓測并發(fā)登錄對redis產生很大壓力,。
解決方法:redis數據表數量多,一次事務會產生多個 redis請求,,小表合并為大表,。
Wade:服務器進程的管理一般比較簡單,有很多還是用配置文件靜態(tài)組織的,。同時往往進程間通信的手段比較缺乏,,沒有使用消息隊列中間件,甚至還有用 Redis 來做通信組件使用的,。為了提高集群管理的自動化水平,,使用 ZooKeeper 是一個比較常見的方法。
Zc:redis一般做為內存緩存來使用,不宜將關鍵數據存放在redis中.其數據安全性并不如一般的DB,。在使用過程中也需要參考性能基線,,控制訪問頻率和流量。
問題三:外部服務有延遲,,調用到的業(yè)務流程中產生卡頓,。
解決方法:業(yè)務側增加緩存:同玩好友msdk+最近角色id+角色信息。
Wade:很多團隊對于過載保護不夠重視,,往往只在最外層接入客戶端一側有最大連接數或者最大會話數的限制,。而對于內部的多個進程,比如訪問數據庫的進程,,就沒有太多的負載保護,。由于游戲是帶狀態(tài)的進程比較多,所以負載均衡往往也做的不多,,基本上是按狀態(tài)所在進程去轉發(fā)處理請求,。
Zc:注意緩存和降級處理。外部平臺數據,,盡量緩存,,提高訪問體驗。當發(fā)現(xiàn)外部服務出現(xiàn)故障,,或本身出現(xiàn)負載風險時,,應降級服務。
Jovi:msdk midas平臺特權等api接入工作,,游戲業(yè)務可以建立一個隔離層專門處理這塊需求,,避兔過分侵入游戲邏輯,更容易控制。
問題四:運營和客服接口修改玩家數據,,會與正常游戲的數據回寫產生競爭,。
解決方法:使用類似郵件機制去修改數據。
Zc:多線程開發(fā)中,,經常會有線程池用盡或線程死鎖導致服務質量下降,。建議將線程池根據業(yè)務需求合理分類,不同業(yè)務間有合理的負載配比,,不會相互影響,。非關鍵流程需要延后或者異步化處理,避免卡死關鍵流程,。
同時,,合理的線程模型可以有效減少線程間競爭。對確實需要競爭的資源在流程入口處統(tǒng)一有序加鎖,,避免在邏輯過程中,,隨意嵌套取鎖競爭。并且,,給鎖加個超時時間,,避免業(yè)務中斷。
Jovi:確保同一時刻只有單個數據修改點,,有助于避免數據競爭,。建議設計時采用CQRS方式,采用獨立的數據表和服務記錄事件,,匯總到單一修改服務上執(zhí)行,。
Wade:并發(fā)編程是服務器端最常見的問題,一般會用多線程或者非阻塞兩種方法之一解決,。對于天然支持多線程的語言,,如JAVA,很多開發(fā)者傾向多線程,,好處是代碼編寫起來比較方便,,但是這就要很清醒的對各種對象進行鎖的操作,或者熟練使用類似 java.util.concurrent 這種多線程工具庫,。而如果使用非阻塞,,好處是不會有鎖的問題,但代碼被分割到各個回調函數中,,可讀性非常糟糕,,所以有的團隊會使用“協(xié)程”或者 Promise 之類的工具來緩解這個問題,但這也引入了更多的復雜性,。
下面詳細介紹一下游戲服務器端架構中的調度架構,,方便大家理解,。
a) 單進程游戲服務器
最簡單的游戲服務器只有一個進程,,是一個單點,。這個進程如果退出,則整個游戲世界消失,。在此進程中,,由于需要處理并發(fā)的客戶端的數據包,因此產生了多種選擇方法:
1.webp.jpg (176.04 KB)
下載附件
2020-8-31 13:57 上傳
同步-動態(tài)多線程
每接收一個用戶會話,,就建立一個線程,。這個用戶會話往往就是由客戶端的TCP連接來代表,這樣每次從socket中調用讀取或寫出數據包的時候,,都可以使用阻塞模式,,編碼直觀而簡單。有多少個游戲客戶端的連接,,就有多少個線程,。但是這個方案也有很明顯的缺點,就是服務器容易產生大量的線程,,這對于內存占用不好控制,,同時線程切換也會造成CPU的性能損失。更重要的多線程下對同一塊數據的讀寫,,需要處理鎖的問題,,這可能讓代碼變的非常復雜,造成各種死鎖的BUG,,影響服務器的穩(wěn)定性,。
同步-多線程池
為了節(jié)約線程的建立和釋放,建立了一個線程池,。每個用戶會話建立的時候,,向線程池申請?zhí)幚砭€程的使用。在用戶會話結束的時候,,線程不退出,,而是向線程池“釋放”對此線程的使用。線程池能很好的控制線程數量,,可以防止用戶暴漲下對服務器造成的連接沖擊,,形成一種排隊進入的機制。但是線程池本身的實現(xiàn)比較復雜,,而“申請”,、“釋放”線程的調用規(guī)則需要嚴格遵守,否則會出現(xiàn)線程泄露,,耗盡線程池,。
異步-單線程/協(xié)程
在游戲行業(yè)中,采用Linux的epoll作為網絡API,以期得到高性能,,是一個常見的選擇,。游戲服務器進程中最常見的阻塞調用就是網路IO,因此在采用epoll之后,,整個服務器進程就可能變得完全沒有阻塞調用,,這樣只需要一個線程即可。這徹底解決了多線程的鎖問題,,而且也簡化了對于并發(fā)編程的難度,。但是,“所有調用都不得阻塞”的約束,,并不是那么容易遵守的,,比如有些數據庫的API就是阻塞的;另外單進程單線程只能使用一個CPU,,在現(xiàn)在多核多CPU的服務器情況下,,不能充分利用CPU資源。異步編程由于是基于“回調”的方式,,會導致要定義很多回調函數,,并且把一個流程里面的邏輯,分別寫在多個不同的回調函數里面,,對于代碼閱讀非常不利,。——針對這種編碼問題,,協(xié)程(Coroutine)能較好的幫忙,,所以現(xiàn)在比較流行使用異步+協(xié)程的組合。不管怎樣,,異步-單線程模型由于性能好,,無需并發(fā)思維,依然是現(xiàn)在很多團隊的首選,。
異步-固定多線程
這是基于異步-單線程模型進化出來的一種模型,。這種模型一般有三類線程:主線程、IO線程,、邏輯線程,。這些線程都在內部以全異步的方式運行,而他們之間通過無鎖消息隊列通信,。
b) 多進程游戲服務器
多進程的游戲服務器系統(tǒng),,最早起源于對于性能問題需求。由于單進程架構下,,總會存在承載量的極限,,越是復雜的游戲制作其單進程承載量就越低,,因此開發(fā)者們一定要突破進程的限制,才能支撐更復雜的游戲,。
一旦走上多進程之路,,開發(fā)者們還發(fā)現(xiàn)了多進程系統(tǒng)的其他一些好處:能夠利用上多核CPU能力;利用操作系統(tǒng)的工具能更仔細的監(jiān)控到運行狀態(tài),、更容易進行容災處理,。多進程系統(tǒng)比較經典的模型是“三層架構”:
在多進程架構下,,開發(fā)者一般傾向于把每個模塊的功能,,都單獨開發(fā)成一個進程,然后以使用進程間通信來協(xié)調處理完整的邏輯,。這種思想是典型的“管道與過濾器”架構模式思想——把每個進程看成是一個過濾器,,用戶發(fā)來的數據包,流經多個過濾器銜接而成的管道,,最后被完整的處理完,。由于使用了多進程,所以首選使用單進程單線程來構造其中的每個進程,。這樣對于程序開發(fā)來說,,結構清晰簡單很多,也能獲得更高的性能,。
2.webp.jpg (86.81 KB)
下載附件
2020-8-31 13:57 上傳
盡管有很多好處,,但是多進程系統(tǒng)還有一個需要特別注意的問題——數據存儲。由于要保證數據的一致性,,所以存儲進程一般都難以切分成多個進程,。就算對關系型數據做分庫分表處理,也是非常復雜的,,對業(yè)務類型有依賴的,。而且如果單個邏輯處理進程承載不了,由于其內存中的數據難以分割和同步,,開發(fā)者很難去平行的擴展某個特定業(yè)務邏輯,。他們可能會選擇把業(yè)務邏輯進程做成無狀態(tài)的,但是這更加加重了存儲進程的性能壓力,,因為每次業(yè)務處理都要去存儲進程處拉取或寫入數據,。
除了數據的問題,多進程架構也帶來了一系列運維和開發(fā)上的問題:首先就是整個系統(tǒng)的部署更為復雜了,,因為需要對多個不同類型進程進行連接配置,,造成大量的配置文件需要管理;其次是由于進程間通訊很多,,所以需要定義的協(xié)議也數量龐大,,在單進程下一個函數調用解決的問題,,在多進程下就要定義一套請求、應答的協(xié)議,,這造成整個源代碼規(guī)模的數量級的增大,;最后是整個系統(tǒng)被肢解為很多個功能短小的代碼片段,如果不了解整體結構,,是很難理解一個完整的業(yè)務流程是如何被處理的,,這讓代碼的閱讀和交接成本巨高無比,特別是在游戲領域,,由于業(yè)務流程變化非??欤瑤捉浶薷暮蟮南到y(tǒng),,幾乎沒有人能完全掌握其內容,。
|