來源:高效能伺服器開發
ID:easyserverdev
騰訊QQGame遊戲同時線上的玩家數量極其龐大,為了方便組織玩家組隊遊戲,騰訊設定了大量遊戲室(房間),玩家可以選擇進入屬意的房間,併在此房間內找到可以加入的遊戲組(牌桌、棋盤等)。玩家選擇進入某個房間時,必須確保此房間當前人數未滿(通常上限為400),否則進入步驟將會失敗。玩家在登入QQGame後,會從伺服器端獲取某類遊戲下所有房間的當前人數資料,玩家可以據此找到未滿的房間以便進入。
如上篇所述的原因,如果待進入房間的人數接近上限時,玩家的進入請求可能失敗,這是因為伺服器在收到此進入請求之前可能有若干其他玩家也請求進入這個房間,造成房間人數達到上限。
這一問題是無法透過上篇所述調整協作分配的方法來解決的,這是因為:要進入的房間是由玩家來指定的,無法在伺服器端完成此項工作,遊戲軟體必須將伺服器端所維護的所有房間人數資料複製到玩家的客戶端,並讓玩家在介面上看到這些資料,以便進行選擇。
這樣,上篇所述的客戶端與伺服器端協作分配原則(誰掌握資料,誰幹活),還得加上一些限制條件,並讓位於另一個所謂”使用者驅動客戶端行為”原則–如果某個功能的執行是由使用者來推動的,則這個功能的實現應當放在客戶端(或者至少由客戶端來控制整個協作),並且客戶端必須持有此功能所依賴相關資料的副本,這個副本應當儘量與伺服器端的源保持同步。
註意:點選圖片可以放大觀看
圖一”進入房間”失敗示意
QQGame還存在一個明顯的不足,就是:玩家如果在遊戲一段時間後,離開了某個房間,並且想進入其它房間,這時QQGame並不會掃清所有房間的當前人數,造成玩家據此資訊所選的待進入房間往往實際上人數已滿,使得進入步驟失敗。筆者碰到的最糟情形是重覆3、4次以上,才最後成功進入另外某個房間。此缺陷其實質是完全放棄了客戶端資料副本與伺服器端的源保持同步的原則。
實際上,QQGame的開發者有非常充分的理由來為此缺陷的存在進行辯護:QQGame同時線上的使用者數超過百萬甚至千萬數量級,如果所有客戶端要實時(所謂實時,就玩家的體驗容忍度而言,可以定為不超過1秒的延遲)地從伺服器端獲取更新資料,那麼最終只有一個結果–系統徹底崩潰。
設想一下每秒千萬次請求的吞吐量,以普通伺服器每秒上百個請求的處理能力(這個資料是根據服務請求處理過程可能涉及到I/O操作來估值的,純記憶體處理的情形可能提高若干數量級),需要成千上萬臺伺服器組成叢集方能承受(高可用性挑戰);而隨著玩家不斷地進入或退出遊戲房間,相關資料一直在快速變化中,
-
正向來看,假設有一臺中心伺服器持有這些資料,那麼需要讓成千上萬臺伺服器與中心保持這些動態資料的實時同步(資料一致性挑戰);
-
相對應的,逆向來看,玩家進入房間等請求被分配給不同的伺服器來處理,一旦玩家進入房間成功則對應伺服器內的相關資料被改變,那麼假定中的中心伺服器就需要實時彙集所有工作伺服器內發生的資料變動(資料完整性挑戰)。
同時處理上萬臺伺服器的資料同步,這需要什麼樣的中心伺服器呢?即使有這樣的超級伺服器存在,那麼Internet網較大的(而且不穩定的)網路通訊延遲又怎麼解決呢?
對於軟體缺陷而言,可以在不同的層面來加以解決–從設計、到需求、甚至是直接在業務層面來解決(例如,08年北京奧運會網上購票系統,為瞭解決訂票請求擁塞而至系統崩潰的缺陷,最後放棄了原先”先到先得”的購票業務流程,改為:使用者先向系統發訂票申請,系統只是記錄下來而不進行處理,而到了空閑時,在後臺隨機抽選幸運者,為他們一一完成訂票業務)。當然解決方案所處的層面越高,可能就越讓人不滿意。
就上述進入房間可能遭遇失敗的缺陷而言,最簡便的解決方案就是:在需求層面調整系統的操作方式,即增加一個類似上篇所述”自動快速加入遊戲”的功能–“自動進入房間”功能。系統在伺服器端為玩家找到一個人數較多又未滿的房間,並嘗試進入(註意,軟體需求是由使用者的操作標的所驅動的,玩家在此的標的就是儘快加入一個滿意的遊戲組,因此由系統來替代玩家選擇標的房間同樣符合相關標的)。而為了方便玩家手工選擇要進入的房間,則應當增加一個”掃清當前各房間人數”的功能。另外,還可以調整房間的組織樣式,例如以地域為單位來劃分房間,像深圳(長城寬頻)區房間1、四川(電信)房間3、北美區房間1等,在深圳上網的玩家將被系統引導而優先進入深圳區的房間。
不管怎樣,解決軟體缺陷的王道還是在設計層面。要解決上述缺陷,架構設計師就必須同時面對高可用、資料一致性、完整性等方面的嚴峻挑戰。
在思考相關解決方案時,我們將應用若干與高效能伺服器叢集架構設計相關的一些重要原則。首先是”分而治之”原則,即將大量客戶端發出的服務請求進行適當的劃分(例如,所有從深圳長城寬頻上網的玩家所發出的服務請求分為一組),分別分配給不同的伺服器(例如,將前述服務請求分組分配給放置於深圳資料中心的伺服器)來加以處理。對於QQGame千萬級的併發服務請求數而言,採用Scale Up向上擴充套件,即升級單個伺服器處理能力的方式基本上不予考慮(沒有常規的主機能處理每秒上千萬的請求)。唯一可行的,只有Scale Out向外擴充套件,即利用大量伺服器叢集做負載均衡的方式,這實質上就是”分而治之”原則的具體應用。
點選圖片可以放大
圖二 分而治之”下的QQGame遊戲服務叢集部署
然而,要應用”分而治之”原則進行Scale Out向外擴充套件,還依賴於其它的條件。如果各伺服器在處理被分配的服務請求時,其行為與其它伺服器的行為結果產生交叉(迴圈)依賴,換句話講就是共享了某些資料(例如,伺服器A處理客戶端a發來的進入房間#n請求,而同時,伺服器B也在處理客戶端b發來的進入房間#n請求,此時伺服器A與B的行為存在迴圈依賴–因為兩者要同時訪問房間#n的資料,這一共享資料會造成兩者間的迴圈依賴),則各伺服器之間必須確保這些共享資料的一致完整性,否則就可能發生邏輯錯誤(例如,假定房間#n的人數差一個就滿了,伺服器A與B在獨自處理的情況下,將同時讓客戶端a與b的進入請求成功,於是房間#n的最終人數將超出上限)。
而要做到此點,各伺服器的處理行程之間就必須保持同步(實際上就是排隊按先後順序訪問共享資料,例如:伺服器A先處理,讓客戶端a進入房間成功,此時房間#n滿員;此後伺服器B更新到房間#n滿的資料,於是客戶端b的進入請求處理結果失敗),這樣,原來將海量請求做負載均衡的意圖就徹底失敗了,多臺伺服器的併發處理能力在此與一臺實質上並沒有區別。
由此,我們匯出了另外一個所謂”處理自治”(或稱”行為獨立”)的原則,即所有參與負載均衡的伺服器,其處理對應服務請求的行為應當不迴圈依賴於其它伺服器,換句話講,就是各伺服器的行為相對獨立(註意:在這裡,非迴圈依賴是允許的,下文中我們來分析為什麼)。
由此可見,簡單的負載均衡策略對於QQGame而言是解決不了問題的。我們必須找到一種途徑,使得在使用大量伺服器進行”分而治之”的同時,同時有確保各個伺服器”處理自治”。此間的關鍵就在於”分而治之”的”分”字上。前述將某個地域網段內上網的玩家所發出的服務請求分到一組,並分配給同一伺服器的做法,其目的不外乎是盡可能地減少網路通訊延遲帶來的負面影響。但它不能滿足”處理自治”的要求,為了確保自治,應當讓同一臺伺服器所處理的請求本身是”自治”(準確的說法是”自閉包”Closure)的。同一臺伺服器所處理的所有請求組成一個服務請求集合,這個集合如果與其它任何與其無交集的(請求)集合(包含此集合的父集合除外)不迴圈依賴,則此服務請求集合是”自閉包”的,而處理此請求集合的伺服器,其”行為獨立”。
我們可以將針對同一房間的進入請求劃分到同一服務請求分組,這些請求相互之間當然是存在迴圈依賴的,但與其它分組中的請求卻不存在迴圈依賴(本房間內人數的變化不會影響到其它房間),而將它們都分配給同一伺服器(不妨命名為”房間管理伺服器”,簡稱”房間伺服器”)後,那個伺服器將是”處理自治”的。
點選圖片可以放大
圖三 滿足”處理自治”條件的QQ遊戲區域”房間管理”服務部署
那麼接下來要解決的問題,就是玩家所關註的某個遊戲區內,所有房間當前人數資料的實時更新問題。其解決途徑與上述的方法類似,我們還是將所有獲取同一區內房間資料的服務請求歸為一組,並交給同一伺服器處理。與上文所述場景不同的是,這個伺服器需要實時彙集本區內所有房間伺服器的房間人數資料。我們可以讓每個房間伺服器一旦發生資料變更時,就向此伺服器(不妨命名為”遊戲區域管理伺服器”,簡稱”區伺服器”)推送一個變更資料記錄,而推送的資料只需包含房間Id和所有進入的玩家Id(房間伺服器還包含其它細節資料,例如牌桌佔位資料)便可。
另外,由於一個區內的玩家數可能是上十萬數量級,一個伺服器根本承擔不了此種負荷,那麼怎麼解決這一矛盾呢?如果深入分析,我們會發現,更新區內房間資料的請求是一種資料只讀類請求,它不會對伺服器狀態造成變更影響,因此這些請求相互間不存在依賴關係;這樣,我們可以將它們再任意劃分為更小的分組,而同時這些分組仍然保持”自閉包”特性,然後分配給不同的區伺服器。多臺區伺服器來負責同一區的資料更新請求,負載瓶頸被解決。
當然,此前,還需將這些區伺服器分為1臺主區伺服器和n臺從屬區伺服器;主區伺服器負責彙集本區內所有房間伺服器的房間人數資料,從屬區伺服器則從主區伺服器實時同步區房間資料副本。
更好的做法,則是如『圖五』所示,由房間伺服器來充當從屬區伺服器的角色,玩家進入某個房間後,在玩家進入另外一個房間之前,其客戶端都將從此房間對應的房間伺服器來更新區內房間資料。要註意的是,圖中房間伺服器的資料更新利用了所謂的”分散式物件快取服務”。
玩家進入某個房間後,還要加入某個遊戲組才能玩遊戲。上篇所述的方案,是讓第一個加入某個牌桌的使用者,其主機自動充當本牌桌的遊戲伺服器;而其它玩家要加入此牌桌,其加入請求應當發往第一個加入的使用者主機;此後開始遊戲,其對弈過程將由第一個加入使用者的主機來主導執行。
那麼此途徑是否同樣也符合上述的前兩個設計原則呢?遊戲在執行的過程中,根據輸贏結果,玩家要加分或減分,同時還要記錄勝負場數。這些資料必須被持久化(比如在資料庫中儲存下來),因此遊戲伺服器(『圖六』中的設計,是由4個部署於QQ客戶端的”升級”遊戲前臺邏輯執行服務,加上1個”升級”遊戲後臺邏輯執行服務,共同組成一個牌桌的”升級”遊戲服務)在處理相關遊戲執行請求時,將依賴於玩家遊戲賬戶資料服務(『圖六』中的所謂”QQGame會話服務”);
不過這種依賴是非迴圈的,即玩家遊戲賬戶資料伺服器的行為反過來並不依賴於遊戲伺服器。上文中曾提到,”處理自治”原則中非迴圈依賴是允許的。這裡遊戲伺服器在處理遊戲收盤請求時,要呼叫玩家遊戲賬戶資料伺服器來更新相關資料;因為不同玩家的遊戲賬戶資料是相互獨立的,此遊戲伺服器在呼叫遊戲賬戶資料伺服器時,邏輯上不受其它遊戲伺服器呼叫遊戲賬戶資料伺服器的影響,不存在同步等待問題;所以,遊戲伺服器在此能夠達成負載均衡的意圖。
點選圖片可以放大
圖四 存在”非迴圈依賴”的QQ遊戲客戶端P2P服務與互動邏輯部署
不過,在上述場景中,雖然不存在同步依賴,但是性能依賴還是存在的,遊戲賬戶資料伺服器的處理效能不夠時,會造成遊戲伺服器長時間等待。為此,我們可以應用分散式資料庫表水平分割的技術,將QQ玩家使用者以其登記的行政區來加以分組,並部署於對應區域的資料庫中(例如,深圳的玩家資料都在深圳的遊戲賬戶資料庫中)。
點選圖片可以放大
圖五 滿足”自閉包”條件的QQ分散式資料庫(叢集)部署
實際上,我們由此還可以推論出一個資料庫表水平分割的原則–任何資料庫表水平分割的方式,必須確保同一資料庫實體中的資料記錄是”自閉包”的,即不同資料庫實體中的資料記錄相互間不存在迴圈依賴。
總之,初步滿足QQGame之苛刻效能要求的分散式架構現在已經是初具雛形了,但仍然有很多涉及效能方面的細節問題有待解決。例如,Internet網路通訊延遲的問題、伺服器之間協作產生的效能瓶頸問題等等。筆者將在下篇中繼續深入探討這些話題。