大服遊戲架構
約定:
假定遊戲是開房間/匹配進戰鬥的類型,是Statefull的,不借用資料庫和微服務實現業務。
每個玩家同時最多存在兩個後端邏輯伺服器(接管)處理其業務。
其中一個"通常"保持不變(且在玩家鏈接到網關時立即進行分配),用於處理遊戲房間外的遊戲邏輯(譬如抽寶箱,購買道具等),記作 PrimaryServer;另外一個作為遊戲房間內的業務處理,會隨著每次匹配而變動/重新分配,記作SlaveServer。
主要架構:
1:網關伺服器
網關用於轉發玩家和邏輯伺服器之間的通信。網關和邏輯伺服器是一套消息定義ProtoGateLanServer,玩家和"伺服器"則是另外一套消息定義,彼此獨立;網關會把玩家發送的遊戲消息通過ProtoGateLanServer中的一個UpMsg轉發給邏輯伺服器。(也即,修改玩家遊戲相關協議不會改動網關)
這裡只介紹比較重要的一個類型,玩家在網關上的設計:
struct ClientSession{ SocketSession netSession; int64_t clientID; SocketSession primaryServer; SocketSession slaveServer;}
其中 netSession是玩家在網關上的網路會話對象;clientID 則作為玩家在整個集群中的運行時ID,需要保證每個網關上不重複(譬如採用 [網關ID:自增ID:時間] 來確定)
primaryServer 是接管此玩家遊戲房間外/非戰鬥邏輯的邏輯伺服器鏈接,slaveServer則是房間內/戰鬥業務邏輯伺服器(後者可為null,譬如玩家在沒有戰鬥的時候)
網關接收到玩家的消息後優先發送給slaveServer,如果不存在則發送給primaryServer。
2:邏輯伺服器
根據前面所述,邏輯伺服器分為兩種類型:Primary和Slave。
玩家在邏輯伺服器上的會話對象結構:
struct PlayerSession{ SocketSession connectionServerSession; int64_t clientID; Any userData; // 邏輯層對象}
connectionServerSession 是此玩家所在網關在邏輯伺服器上的網路會話, clientID 則就是玩家在網關上分配的運行時ID。 邏輯伺服器上採用 map<int64_t, PlayerSession> 管理會話列表,當收到網關轉發的玩家消息時,根據clientID查找PlayerSession ,然後交給其 userData進行處理。
同樣,玩家的邏輯對象也需要包含一個 PlayerObject 對象,用於給玩家發送消息(通過ProtoGateLanServer中一個DownMsg消息,攜帶clientID 即可,網關通過clientID查詢玩家session即可發送給玩家;根據網路庫差異則實現有所不同,譬如用我的網路庫中,DownMsg可攜帶玩家在網關上的SessionID,邏輯伺服器下發消息時攜帶它,網關收到後直接調用send介面,不用進行一次map查詢)。
整個集群中,通常只有Slave類型的邏輯伺服器會鏈接/讀寫DB。
3:全局伺服器
邏輯伺服器之間的通信全部通過全局伺服器進行中轉,一般而言消息量不會特別高;比如玩家大部分時間時在戰鬥中,也只有戰鬥匹配時和結束時才通過全局伺服器轉發相關消息而已。
全局伺服器也維護架構中的全局業務邏輯。
難點問題及其處理:
1:定位/標識玩家的問題
邏輯伺服器發送消息給網關,讓其轉發消息給玩家時,只允許攜帶玩家的clientID,不能攜帶任何業務上的ID(以讓網關去定位玩家),這樣保證網關與邏輯無關。
內部邏輯伺服器之間在業務通信時,若需要定位玩家,則只允許在消息中攜帶玩家的Name或資料庫UID等邏輯唯一標識,在邏輯層面通過它們去查找玩家。 切記不能通過clientID去定位玩家。
2:登陸上下線
登陸消息通過 網關->Primary邏輯伺服器,後者處理成功後告訴全局伺服器進行登記,全局伺服器判斷玩家是否已經在線。若已經在線,則返回失敗,並踢掉先前的賬號,踢號流程務必按照:
全局伺服器->邏輯伺服器->網關伺服器—網關踢掉玩家網路鏈接—然後再 => 通知邏輯伺服器某玩家下線 => 通知全局伺服器移除玩家。
所有的 -> (注意不是 =>) 僅做轉發,不做業務/數據/狀態上的任何處理。
玩家下線流程與上面斜線部分基本一致(只不過會延遲執行下線流程--給予斷線重連機會)。 也就是說踢號流程是內部伺服器通知網關伺服器:1、主動斷開玩家網路。2、立即走一次下線流程。
3:斷線重連
首先需要網關檢測玩家網路斷開時,延遲n秒後才通知內部伺服器某clientID的玩家掉線,以此延遲回收玩家邏輯對象。斷線重連流程是,玩家新建鏈接,並發送重連消息包到任意網關(即允許跟之前所鏈接的網關不一樣)(理所當然,客戶端需要上傳自己的UID和Password之類),網關廣播給所有內部邏輯伺服器。邏輯伺服器通過UID查詢玩家,如果找到則根據新的鏈接信息重寫其 (本文上面有定義)PlayerSession 的值(業務數據不需要任何變動)。那麼當網關延遲到期,通知舊的clientID掉線時,在邏輯伺服器里就不會/能再做任何操作了,因為這個clientID已經沒有關聯到邏輯對象了。
然後再進行兩個操作:1、成功處理斷線重連請求的LogicServer告訴網關,請求將玩家的primaryServer設置成它。2、通知全局伺服器修改玩家會話的ClientID和所屬CSID等(並非具體業務上需要, 主要用於玩家匹配進入戰鬥後,全局伺服器需要告訴戰鬥邏輯伺服器構建玩家 PlayerSession)。
各類型服務宕機和恢復:
1: 網關伺服器宕機:
PrimaryServer延遲執行此網關上的所有玩家掉線處理(等待玩家通過其他網關進行重連) GameServer延遲執行此網關上的所有玩家掉線處理(退出戰鬥)(等待玩家通過其他網關進行重連)2:非戰鬥邏輯伺服器(PrimaryServer)宕機:
所有伺服器執行此伺服器上的玩家斷線處理(通常會有回檔問題,無可避免,當然你也可以在重要操作後立即寫資料庫,減小損失)
3:戰鬥邏輯伺服器斷開(SlaveSrever)宕機: 所有伺服器執行此伺服器上的玩家退出/結束戰鬥處理(無可避免)4:全局伺服器宕機:PrimaryServer在再次重連全局伺服器成功後,嘗試發送當前所有在線的玩家數據全局伺服器會基於此重建在線玩家表,即可恢復服務SlaveSrever則不作處理,只需定時重連全局伺服器即可。(譬如待玩家戰鬥結束,直接發送戰鬥結果給全局伺服器就行)PS:額外說兩點,各個伺服器之間的TCP Srvice具備自動重連功能。集群的開啟,與各個伺服器的啟動順序無關。
單點全局伺服器的處理/改進辦法:
1:所有採用分散式資料庫做數據持久化,從而將全局邏輯直接放到各個LogicServer(可能還需要事務)去做的情況(從來去掉全局伺服器),本文不做討論,因為某些業務無法(方便/高效的)用資料庫的數據結構/類型去表示,所以本文假設必然存在一個全局伺服器保持內存狀態去處理全局業務(譬如匹配功能,組隊功能等,當然再額外拆分出多個匹配伺服器什麼的做sharding也是可以的,另談)。
2:先將全局伺服器:邏輯伺服器之間消息轉發業務和全局業務分離
2:將消息轉發業務改成邏輯伺服器互聯;或者通過第三方分散式消息中間件(但一般都不適合遊戲),主要擔心消息的亂序問題。(為啥沒有通過網關轉發,還是擔心亂序問題,除非你的應用無所謂消息順序)
4:再將全局業務通過多個全局伺服器,採用Raft管理起來,完成高可用的全局業務處理。(因為全局伺服器可能含有一些狀態,所以要考慮高可用,譬如全局匹配隊列信息,組隊信息等,如果沒有它們這些狀態,那直接將按照上一小結所描述的服務快速恢復即可。
網關和邏輯伺服器的動態增添(提高系統承載):
採用etcd維護各個伺服器信息,且邏輯伺服器伺服器定期輪詢它們;譬如增加一個新網關,它告訴etcd。 邏輯伺服器拿到最新的網關列表,篩選出新的網關伺服器,並進行鏈接。
資料庫寫入問題的處理辦法:
在需要寫入數據的業務處理中,先判斷玩家是否在線,在線則將消息發送給玩家所在LogicServer進行處理,以盡量確保一個玩家的數據總是在一個節點去修改(假定伺服器都是有狀態的,不是stateless那種借用資料庫和微服務的)。若不在線,盡量採用事務,或者採用list等操作去增添(修改)數據(避免直接update set)。
最後,關於弱聯網/Stateless的大服架構,譬如COC等,可參考《大型網站技術架構》一書。
推薦閱讀:
※遊戲數據存儲方案?
※互聯網中TCP Socket伺服器的實現過程需要考慮哪些安全問題?
※如何評價微軟的orleans框架?
※作為遊戲客戶端開發,需要掌握哪些伺服器方面的知識,以及如何學習?
TAG:游戏服务器 |