1. 程式人生 > >經典遊戲伺服器端架構概述 (1)

經典遊戲伺服器端架構概述 (1)

架構的分析模型

一. 討論的背景

現代電子遊戲,基本上都會使用一定的網路功能。從驗證正版,到多人互動等等,都需要架設一些專用的伺服器,以及編寫在伺服器上的程式。因此,遊戲伺服器端軟體的架構,本質上也是遊戲伺服器這個特定領域的軟體架構。

軟體架構的分析,可以通過不同的層面入手。比較經典的軟體架構描述,包含了以下幾種架構:

  • 執行時架構——這種架構關心如何解決執行效率問題,通常以程式程序圖、資料流圖為表達方式。在大多數開發團隊的架構設計文件中,都會包含執行時架構,說明這是一種非常重要的設計方面。這種架構也會顯著的影響軟體程式碼的開發效率和部署效率。本文主要討論的是這種架構。

  • 邏輯架構

    ——這種架構關心軟體程式碼之間的關係,主要目的是為了提高軟體應對需求變更的便利性。人們往往會以類圖、模組圖來表達這種架構。這種架構設計在需要長期運營和重用性高的專案中,有至關重要的作用。因為軟體的可擴充套件性和可重用度基本是由這個方面的設計決定的。特別是在遊戲領域,需求變更的頻繁程度,在多個網際網路產業領域裡可以說是最高的。本文會涉及一部分這種架構的內容,但不是本文的討論重點。

  • 物理架構——關心軟體如何部署,以機房、伺服器、網路裝置為主要描述物件。

  • 資料架構——關心軟體涉及的資料結構的設計,對於資料分析挖掘,多系統協作有較大的意義。

  • 開發架構——關心軟體開發庫之間的關係,以及版本管理、開發工具、編譯構建的設計,主要為了提高多人協作開發,以及複雜軟體庫引用的開發效率。現在流行的整合構建系統就是一種開發架構的理論。

二. 遊戲伺服器架構的要素

伺服器端軟體的本質,是一個會長期執行的程式,並且它還要服務於多個不定時,不定地點的網路請求。所以這類軟體的特點是要非常關注穩定性和效能。這類程式如果需要多個協作來提高承載能力,則還要關注部署和擴容的便利性;同時,還需要考慮如何實現某種程度容災需求。由於多程序協同工作,也帶來了開發的複雜度,這也是需要關注的問題。

功能約束,是架構設計決定性因素。一個萬能的架構,必定是無能的架構。一個優秀的架構,則是正好把握了對應業務領域的核心功能產生的。遊戲領域的功能特徵,於伺服器端系統來說,非常明顯的表現為幾個功能的需求:

  • 對於遊戲資料和玩家資料的儲存

  • 對玩家客戶端進行資料廣播

  • 把一部分遊戲邏輯在伺服器上運算,便於遊戲更新內容,以及防止外掛。

針對以上的需求特徵,在伺服器端軟體開發上,我們往往會關注軟體對電腦記憶體和CPU的使用,以求在特定業務程式碼下,能儘量滿足承載量和響應延遲的需求。最基本的做法就是“時空轉換”,用各種快取的方式來開發程式,以求在CPU時間和記憶體空間上取得合適的平衡。在CPU和記憶體之上,是另外一個約束因素:網絡卡。網路頻寬直接限制了伺服器的處理能力,所以遊戲伺服器架構也必定要考慮這個因素。

對於遊戲伺服器架構設計來說,最重要的是利用遊戲產品的需求約束,從而優化出對此特定功能最合適的“時-空”架構。並且最小化對網路頻寬的佔用。

[圖:遊戲伺服器的分析模型]

三. 核心的三個架構

基於上述的分析模型,對於遊戲服務端架構,最重要的三個部分就是,如何使用CPU、記憶體、網絡卡的設計:

  • 記憶體架構:主要決定伺服器如何使用記憶體,以保證儘量少的記憶體洩漏的可能,以及最大化利用伺服器端記憶體來提高承載量,降低服務延遲。

  • 排程架構:設計如何使用程序、執行緒、協程這些對於CPU排程的方案。選擇同步、非同步等不同的程式設計模型,以提高伺服器的穩定性和承載量。同時也要考慮對於開發帶來的複雜度問題。現在出現的虛擬化技術,如虛擬機器、docker、雲伺服器等,都為排程架構提供了更多的選擇。

  • 通訊模式:決定使用何種方式通訊。網路通訊包含有傳輸層的選擇,如TCP/UDP;據表達層的選擇,如定義協議;以及應用層的介面設計,如訊息佇列、事件分發、遠端呼叫等。

本文的討論,也主要是集中於對以上三個架構的分析。

四. 遊戲伺服器模型的進化歷程

最早的遊戲伺服器是比較簡單的,如UO《網路創世紀》的服務端一張3.5寸軟盤就能存下。基本上只是一個廣播和儲存檔案的伺服器程式。後來由於國內的外掛、盜版流行,各遊戲廠商開始以MUD為模型,建立主要執行邏輯在伺服器端的架構。這種架構在MMORPG類產品的不斷更新中發揚光大,從而出現了以地圖、視野等分佈要素設計的分散式遊戲伺服器。而在另外一個領域,休閒遊戲,天然的需要集中超高的線上使用者,所以全區型架構開始出現。現代的遊戲伺服器架構,基本上都希望能結合承載量和擴充套件性的有點來設計,從而形成了更加豐富多樣的形態。

本文的討論主要是選取這些比較典型的遊戲伺服器模型,分析其底層各種選擇的優點和缺點,希望能探討出更具廣泛性,更高開發效率的伺服器模型。

分服模型

一. 模型描述

分服模型是遊戲伺服器中最典型,也是歷久最悠久的模型。其特徵是遊戲伺服器是一個個單獨的世界。每個伺服器的帳號是獨立的,而且只用同一伺服器的帳號才能產生線上互動。在早期伺服器的承載量達到上限的時候,遊戲開發者就通過架設更多的伺服器來解決。這樣提供了很多個遊戲的“平行世界”,讓遊戲中的人人之間的比較,產生了更多的空間。所以後來以伺服器的開放、合併形成了一套成熟的運營手段。一個技術上的選擇最後導致了遊戲運營方式的模式,是一個非常有趣的現象。

[圖:分服模型]

二. 排程架構

1 . 單程序遊戲伺服器

最簡單的遊戲伺服器只有一個程序,是一個單點。這個程序如果退出,則整個遊戲世界消失。在此程序中,由於需要處理併發的客戶端的資料包,因此產生了多種選擇方法:


[圖:單程序排程模型]

  • 同步-動態多執行緒:每接收一個使用者會話,就建立一個執行緒。這個使用者會話往往就是由客戶端的TCP連線來代表,這樣每次從socket中呼叫讀取或寫出資料包的時候,都可以使用阻塞模式,編碼直觀而簡單。有多少個遊戲客戶端的連線,就有多少個執行緒。但是這個方案也有很明顯的缺點,就是伺服器容易產生大量的執行緒,這對於記憶體佔用不好控制,同時執行緒切換也會造成CPU的效能損失。更重要的多執行緒下對同一塊資料的讀寫,需要處理鎖的問題,這可能讓程式碼變的非常複雜,造成各種死鎖的BUG,影響伺服器的穩定性。

  • 同步-多執行緒池:為了節約執行緒的建立和釋放,建立了一個執行緒池。每個使用者會話建立的時候,向執行緒池申請處理執行緒的使用。在使用者會話結束的時候,執行緒不退出,而是向執行緒池“釋放”對此執行緒的使用。執行緒池能很好的控制執行緒數量,可以防止使用者暴漲下對伺服器造成的連線衝擊,形成一種排隊進入的機制。但是執行緒池本身的實現比較複雜,而“申請”、“施放”執行緒的呼叫規則需要嚴格遵守,否則會出現執行緒洩露,耗盡執行緒池。

  • 非同步-單執行緒/協程:在遊戲行業中,採用Linux的epoll作為網路API,以期得到高效能,是一個常見的選擇。遊戲伺服器程序中最常見的阻塞呼叫就是網路IO,因此在採用epoll之後,整個伺服器程序就可能變得完全沒有阻塞呼叫,這樣只需要一個執行緒即可。這徹底解決了多執行緒的鎖問題,而且也簡化了對於併發程式設計的難度。但是,“所有呼叫都不得阻塞”的約束,並不是那麼容易遵守的,比如有些資料庫的API就是阻塞的;另外單程序單執行緒只能使用一個CPU,在現在多核多CPU的伺服器情況下,不能充分利用CPU資源。非同步程式設計由於是基於“回撥”的方式,會導致要定義很多回調函式,並且把一個流程裡面的邏輯,分別寫在多個不同的回撥函式裡面,對於程式碼閱讀非常不理。——針對這種編碼問題,協程(Coroutine)能較好的幫忙,所以現在比較流行使用非同步+協程的組合。不管怎樣,非同步-單執行緒模型由於效能好,無需併發思維,依然是現在很多團隊的首選。

  • 非同步-固定多執行緒:這是基於非同步-單執行緒模型進化出來的一種模型。這種模型一般有三類執行緒:主執行緒、IO執行緒、邏輯執行緒。這些執行緒都在內部以全非同步的方式執行,而他們之間通過無鎖訊息佇列通訊。

2 . 多程序遊戲伺服器

多程序的遊戲伺服器系統,最早起源於對於效能問題需求。由於單程序架構下,總會存在承載量的極限,越是複雜的遊戲,其單程序承載量就越低,因此開發者們一定要突破程序的限制,才能支撐更復雜的遊戲。

一旦走上多程序之路,開發者們還發現了多程序系統的其他一些好處:能夠利用上多核CPU能力;利用作業系統的工具能更仔細的監控到執行狀態、更容易進行容災處理。多程序系統比較經典的模型是“三層架構”:

在多程序架構下,開發者一般傾向於把每個模組的功能,都單獨開發成一個程序,然後以使用程序間通訊來協調處理完整的邏輯。這種思想是典型的“管道與過濾器”架構模式思想——把每個程序看成是一個過濾器,使用者發來的資料包,流經多個過濾器銜接而成的管道,最後被完整的處理完。由於使用了多程序,所以首選使用單程序單執行緒來構造其中的每個程序。這樣對於程式開發來說,結構清晰簡單很多,也能獲得更高的效能。


[圖:經典的三層模型]

儘管有很多好處,但是多程序系統還有一個需要特別注意的問題——資料儲存。由於要保證資料的一致性,所以儲存程序一般都難以切分成多個程序。就算對關係型資料做分庫分表處理,也是非常複雜的,對業務型別有依賴的。而且如果單個邏輯處理程序承載不了,由於其記憶體中的資料難以分割和同步,開發者很難去平行的擴充套件某個特定業務邏輯。他們可能會選擇把業務邏輯程序做成無狀態的,但是這更加加重了儲存程序的效能壓力,因為每次業務處理都要去儲存程序處拉取或寫入資料。

除了資料的問題,多程序也架構也帶來了一系列運維和開發上的問題:首先就是整個系統的部署更為複雜了,因為需要對多個不同型別程序進行連線配置,造成大量的配置檔案需要管理;其次是由於程序間通訊很多,所以需要定義的協議也數量龐大,在單程序下一個函式呼叫解決的問題,在多程序下就要定義一套請求、應答的協議,這造成整個原始碼規模的數量級的增大;最後是整個系統被肢解為很多個功能短小的程式碼片段,如果不瞭解整體結構,是很難理解一個完整的業務流程是如何被處理的,這讓程式碼的閱讀和交接成本巨高無比,特別是在遊戲領域,由於業務流程變化非常快,幾經修改後的系統,幾乎沒有人能完全掌握其內容。

三. 記憶體架構

由於伺服器程序需要長期自動化執行,所以記憶體使用的穩定是首要大事。在伺服器程序中,就算一個觸發機率很小的記憶體洩露,都會積累起來變成嚴重的運營事故。需要注意的是,不管你的執行緒和程序結構如何,記憶體架構都是需要的,除非是Erlang這種不使用堆的函式式語言。

1 . 動態記憶體

在需要的時候申請記憶體來處理問題,是每個程式設計師入門的時候必然要學會的技能。但是,如何控制記憶體釋放卻是一個大問題。在C/C++語言中,對於堆的控制至關重要。有一些開發者會以樹狀來規劃記憶體使用,就是一般只new/delete一個主要的型別的物件,其他物件都是此物件的成員(或者指標成員),只要這棵樹上所有的物件都管理好自己的成員,就不會出現記憶體漏洞,整個結構也比較清晰簡單。

[圖:物件樹架構]

在Objective C語言中,有所謂autorealse的特性,這種特性實際上是一種引用計數的技術。由於能配合在某個排程模型下,所以使用起來會比較簡單。同樣的思想,有些開發者會使用一些智慧指標,配合自己寫的框架,在完整的業務邏輯呼叫後一次性清理相關記憶體。


[圖:根據業務處理排程管理記憶體池]

在帶虛擬機器的語言中,最常見的是JAVA,這個問題一般會簡單一些,因為有自動垃圾回收機制。但是,JAVA中的容器型別、以及static變數依然是可能造成記憶體洩露的原因。加上無規劃的使用執行緒,也有可能造成記憶體的洩露——有些執行緒不會退出,而且在不斷增加,最後耗盡記憶體。所以這些問題都要求開發者專門針對static變數以及執行緒結構做統一設計、嚴格規範。

2 . 預分配記憶體

動態分配記憶體在小心謹慎的程式設計師手上,是能發揮很好的效果的。但是遊戲業務往往需要用到的資料結構非常多,變化非常大,這導致了記憶體管理的風險很高。為了比較徹底的解決記憶體漏洞的問題,很多團隊採用了預先分配記憶體的結構。在伺服器啟動的時候分配所有的變數,在執行過程中不呼叫任何new關鍵字的程式碼。

這樣做的好處除了可以有效減少記憶體漏洞的出現概率,也能降低動態分配記憶體所消耗的效能。同時由於啟動時分配記憶體,如果硬體資源不夠的話,程序就會在啟動時失敗,而不是像動態分配記憶體的程式一樣,可能在任何一個分配記憶體的時候崩潰。然而,要獲得這些好處,在編碼上首先還是要遵循“動態分配架構”中物件樹的原則,把一類物件構造為“根”物件,然後用一個記憶體池來管理這些根物件。而這個記憶體池能存放的根物件的數目,就是此服務程序的最大承載能力。一切都是在啟動的時候決定,非常的穩妥可靠。


[圖:預分配記憶體池]

不過這樣做,同樣有一些缺點:首先是不太好部署,比如你想在某個資源較小的虛擬機器上部署一套用來測試,可能一位內沒改記憶體池的大小,導致啟動不成功。每次更換環境都需要修改這個配置。其次,是所有的用到的類物件,都要在根節點物件那裡有個指標或者引用,否則就可能洩漏記憶體。由於對於非基本型別的物件,我們一般不喜歡用拷貝的方式來作為函式的引數和返回值,而指標和應用所指向的記憶體,如果不能new的話,只能是現成的某個物件的成員屬性。這回導致程式越複雜,這類的成員屬性就越多,這些屬性在程式碼維護是一個不小的負擔。

要解決以上的缺點,可以修改記憶體池的實現,為動態增長,但是具備上限的模型,每次從記憶體池中“獲取”物件的時候才new。這樣就能避免在小記憶體機器上啟動不了的問題。對於物件屬性複雜的問題,一般上需要好好的按面向物件的原則規劃程式碼,做到儘量少用僅僅表示函式引數和返回值的屬性,而是主要是記錄物件的“業務狀態”屬性為主,多花點功夫在構建遊戲的資料模型上。

四. 程序間通訊手段

在多程序的系統中,程序間如何通訊是一個至關重要的問題,其效能和使用便利性,直接決定了多程序系統的技術效能。

1 . Socket通訊

TCP/IP協議是一種通用的、跨語言、跨作業系統、跨機器的通訊方案。這也是開發者首先想到的一種手段。在使用上,有使用TCP和UDP兩個選擇。一般我們傾向在遊戲系統中使用TCP,因為遊戲資料的邏輯相關性比較強,UDP由於可能存在的丟包和重發處理,在遊戲邏輯上的處理一般比較複雜。由於多程序系統的程序間網路一般情況較好,UDP的效能優勢不會特別明顯。

要使用TCP做跨程序通訊,首先就是要寫一個TCP Server,做埠監聽和連線管理;其次需要對可能用到的通訊內容做協議定製;最後是要編寫編解碼和業務邏輯轉發的邏輯。這些都完成了之後,才能真正的開始用來作為程序間通訊手段。

使用Socket程式設計的好處是通用性廣,你可以用來實現任何的功能,和任何的程序進行協作。但是其缺點也異常明顯,就是開發量很大。雖然現在有一些開源元件,可以幫你簡化Socket Server的編寫工作,簡化連線管理和訊息分發的處理,但是選擇目標建立連線、定製協議編解碼這兩個工作往往還是要自己去做。遊戲的特點是業務邏輯變化很多,導致協議修改的工作量非常大。因此我們除了直接使用TCP/IP socket以外,還有很多其他的方案可以嘗試。


[圖:TCP通訊]

2 . 訊息佇列

在多程序系統中,如果程序的種類比較多,而且變化比較快,大量編寫和配置程序之間的連線是一件非常繁瑣的工作,所以開發者就發明了一種簡易的通訊方法——訊息佇列。這種方法的底層還是Socket通訊實現,但是使用者只需要好像投遞信件一樣,把訊息包投遞到某個“信箱”,也就是佇列裡,目標程序則自動不斷去“收取”屬於自己的“信件”,然後觸發業務處理。

這種模型的好處是非常簡單易懂,使用者只需要處理“投遞”和“收取”兩個操作即可,對於訊息也只需要處理“編碼”和“解碼”兩個部分。在J2EE規範中,就有定義一套訊息佇列的規範,叫JMS,Apache ActiveMQ就是一個應用廣泛的實現者。在Linux環境下,我們還可以利用共享記憶體,來承擔訊息佇列的儲存器,這樣不但效能很高,而且還不怕程序崩潰導致未處理訊息丟失。


[圖:訊息佇列]

需要注意的是,有些開發者缺乏經驗,使用了資料庫,如MySQL,或者是NFS這類執行效率比較低的媒介作為佇列的儲存者。這在功能上雖然可以行得通,但是操作一頻繁,就難以發揮作用了。如以前有一些手機簡訊應用系統,就用MySQL來儲存“待發送”的簡訊。

訊息佇列雖然非常好用,但是我們還是要自己對訊息進行編解碼,並且分發給所需要的處理程式。在訊息到處理程式之間,存在著一個轉換和對應的工作。由於遊戲邏輯的繁多,這種對應工作完全靠手工編碼,是比較容易出錯的。所以這裡還有進一步的改進空間。

3 . 遠端呼叫

有一些開發者會希望,在編碼的時候完全遮蔽是否跨程序在進行呼叫,完全可以好像呼叫本地函式或者本地物件的方法一樣。於是誕生了很多遠端呼叫的方案,最經典的有Corba方案,它試圖實現能在不同語言的程式碼直接,實現遠端呼叫。JAVA虛擬機器自帶了RMI方案的支援,在JAVA程序之間遠端呼叫是比較方便的。在網際網路的環境下,還有各種Web Service方案,以HTTP協議作為承載,WSDL作為介面描述。

使用遠端呼叫的方案,最大好處是開發的便捷,你只需要寫一個函式,就能在任何一個其他程序上對此函式進行呼叫。這對遊戲開發來說,就解決了多程序方案最大的一個開發效率問題。但是這種便捷是有成本的:一般來說,遠端呼叫的效能會稍微差一點,因為需要用一套統一的編解碼方案。如果你使用的是C/C++這類靜態語言,還需要使用一種IDL語言來先描述這種遠端函式的介面。但是這些困難帶來的好處,在遊戲開發領域還是非常值得的。


[圖:遠端呼叫]

五. 容災和擴容手段

在多程序模型中,由於可以採用多臺物理伺服器來部署服務程序,所以為容災和擴容提供了基礎條件。

在單程序模型下,容災常常使用的熱備伺服器,依然可以在多程序模型中使用,但是開著一臺什麼都不做的伺服器完全是為了做容災,多少有點浪費。所以在多程序環境下,我們會啟動多個相同功能的伺服器程序,在請求的時候,根據某種規則來確定對哪個服務程序發起請求。如果這種規則能規避訪問那些“失效”了的服務程序,就自動實現了容災,如果這個規則還包括了“更新新增服務程序”的邏輯,就可以做到很方便的擴容了。而這兩個規則,統一起來就是一條:對服務程序狀態的集中儲存和更新。

為了實現上面的方案,常常會架設一個“目錄”伺服器程序。這個程序專門負責蒐集伺服器程序的狀態,並且提供查詢。ZooKeeper就是實現這種目錄伺服器的一個優秀工具。


[圖:伺服器狀態管理]

儘管用簡單的目錄伺服器可以實現大部分容災和擴容的需求,但是如果被訪問程序的記憶體中有資料存在,那麼問題就比較複雜了。對於容災來說,新的程序必須要有辦法重建那個“失效”了的程序記憶體中的資料,才可能完成容災功能;對於擴容功能來說,新加入的程序,也必須能把需要的資料載入到自己的記憶體中才行,而這些資料,可能已經存在於其他平行的程序中,如何把這部分資料轉移過來,是一個比較耗費效能和需要編寫相當多程式碼的工作。——所以一般我們喜歡對“無狀態”的程序來做擴容和容災。

全服分線模型

一. 模型描述

由於多程序伺服器模型的發展,遊戲開發者們首先發現,由於遊戲業務的特點,那些需要持久化的資料,一般都是玩家的存檔,以及一些遊戲本身需要用的,在執行期只讀的資料。這對於儲存程序的分佈,提供了非常有利的條件。於是玩家資料可以存放於同一個叢集中,可以不再和遊戲伺服器繫結在一起,因為登入的時候便可根據玩家的ID去儲存叢集中定位想要存取的儲存程序。


[圖-全區分線模型]

二. 儲存的挑戰

1、需求:擴容和容災

在全區分線模型下,遊戲玩家可以隨便選擇任何一個伺服器登入,自己的帳號資料都可以提取出來玩。這種顯然比每個伺服器重新“練”一個號要省事的多。而且這樣也可以和朋友們約定去一個負載較低的伺服器一起玩,而不用苦苦等待某一個特定的伺服器變得空閒。然而,這些好處所需要付出的代價,是在儲存層的分散式設計。這種設計有一個最需要解決的問題,就是遊戲伺服器系統的擴容和容災。

從模型上說,擴容是加入新的伺服器,容災是減掉失效的伺服器。這兩個操作在無狀態的伺服器程序上操作,都只是更新一下連線配置表,然後重啟一下即可。但是,由於遊戲存在大量的狀態,包括執行時記憶體中的狀態,以及持久化的儲存狀態,這就讓擴容和容災需要更多的處理才能成功。

最普通的情況下,在擴容和容災的時候,首先需要通知所有玩家下線,把記憶體中的狀態資料寫入持久化資料程序;然後根據需要的配置,把持久化資料重新“搬遷”到新的變化後的伺服器上。——如果一個遊戲有幾千萬使用者,這樣的資料搬遷將會耗時非常長,玩家也被迫等待很長的時間才能重新登入遊戲。所以在這種模型下,對於資料儲存的設計是最關鍵的地方。

2、分割槽分服的關係型資料庫

我們常常會使用MySQL這種關係型資料庫來存放遊戲資料。由於SQL能夠表述非常複雜的資料操作,這對於遊戲資料的一些後期處理有非常好的支援:如客服需要發獎勵,需要撤銷某些錯誤的運營資料,需要封停某些特徵的玩家……但是,分散式資料庫也是最難做分佈的。一般來說我們都需要通過某一主鍵欄位做分庫和分表;而另外一些如唯一關鍵字等資料,就需要一些技巧來處理。

[圖-分表分庫]

以玩家ID作為分表分庫是一個非常自然的選擇,但是這種方案,往往需要在邏輯程式碼中,對玩家資料按照自定義的規則,做儲存程序的選擇。但是如果發現這個分表分庫的演算法(原則)不符合需求,就需要把大量的資料做搬遷。如上圖是按玩家ID做奇偶規則分佈到兩個表中,一旦需要增加第三臺伺服器,資料儲存的目的伺服器編號就變成了id%3,這樣就需要把好多資料需要從原來的第一、二臺資料庫中拷貝出來,非常麻煩。

有的開發者會預先建立幾十個表(如120個表=2x3x4x5),一開始是全部都放在一個伺服器上,然後在增加資料庫伺服器的時候,把對應的整個表搬遷出來。這樣能減輕在搬遷資料的時候造成的複雜度,但還是需要搬遷資料的。最後如果與建立的表還是放不下了,依然還是需要很複雜和耗時的重新拷貝資料。

3、NoSQL

在很多開發者絞盡腦汁折騰MySQL的時候,NoSQL橫空出世了。實際上在很早,目錄型儲存程序就在DNS等特定領域默默工作了。NoSQL系統最大的好處正是關係型資料庫最大的弱點——分佈。

由於主鍵只有一個,因此內建的分佈功能使用起來非常簡便。而且遊戲玩家資料,絕大多數的操作都是根據主鍵來讀寫的。“自古以來”遊戲就有“SL大法”之稱,其本質就是對存檔資料的簡單讀、寫。在網遊的早期版本MUD遊戲時代,玩家存檔只是簡單的放在硬碟的檔案上,檔名就是玩家的ID。這些,都說明了遊戲中的玩家資料,其讀寫都是有明顯約束的——玩家ID。這和NoSQL簡直是天作之合。


[圖-NoSQL]

NoSQL的確是非常適合用來儲存遊戲資料。特別是有些伺服器如Redis還帶有豐富的欄位值型別。但是,NoSQL本身往往不帶很複雜的容災熱備機制,這是需要額外注意的。而且NoSQL的訪問延遲雖然比關係型資料庫快很多,但是畢竟要經過一層網路。這對於那些發展了很多年的ORM庫來說,缺乏了一個本地快取的功能。這就導致了NoSQL還不能簡單的取代掉所有伺服器上的“狀態”。而這些正是分散式快取所希望達成的目標。

4、分散式快取

在業界用的比較多的快取系統有memcached,開發者有時候也會使用諸如Hibernate這樣的ROM庫提供的cache功能。但是這些快取系統在使用上往往會有一些限制,最主要的限制是“無法分散式使用”,也就是說快取系統本身成為效能瓶頸後,就沒有辦法擴容了。或者在容災的情景下,快取系統往往容易變成致命的單點。

Orcale公司有一款叫Coherence的產品,就是一種能很好解決以上問題的“能分散式使用”的產品。他利用區域網的組播功能來做節點間的狀態同步,同時採用節點互相備份的方案來分佈資料。這款產品還使用Map介面來提供功能。這讓整個快取系統既使用簡單又功能強大。更重要的是,它能讓使用者對於資料的存取特性做配置,從而提供使用者可接受的資料風險下的更高效能——本地快取。

由於遊戲的資料,真正變化頻繁的,往往不是“關鍵”的需要安全保障資料,如玩家的位置、玩家在某次戰鬥中的HP、子彈怪物的位置等等。而那些非常重要的資料,如等級、裝備,又變化的不頻繁。這就給了開發者針對資料特性做優化以很大的空間。而且,大部分資料的讀、寫頻率都有典型的不平衡狀態。普遍遊戲資料都是讀多寫少。少量的日誌、上報資料是寫多、幾乎不讀。

對於快取系統來說,有三個重要的因數決定了在遊戲開發中的地位。首先是其使用的便利性,因為遊戲的資料結構變化非常頻繁,如果要很繁瑣的配置資料結構,則不會適合遊戲開發;其次是要能提供近似本地記憶體的效能,由於遊戲伺服器邏輯基本上都是在頻繁的讀寫某一特定資料塊,如玩家位置、經驗、HP等等,而且遊戲對於處理延遲也有較高的需求(WEB應用在2秒以內都可以忍受,遊戲則要求最好能在20ms以內完成)。要能同時滿足這兩點,是不太容易的。


[圖-分散式快取]

5、整合快取的NoSQL

根據上面的描述,讀者應該也會想到,如果資料庫系統,或者叫持久化系統,自帶了快取,是否更好呢?這樣確實是會更好的,而且特別是對於NOSQL系統來說,能以一些內部的演算法策略,來降低前端邏輯開發的複雜程度。一般來說,我們需要對整合快取的NOSQL系統有以下幾方面的需求:首先是冷熱資料自動交換,就是對於常用資料有演算法來判別其冷熱,然後換入到記憶體以提高存取性;其次是分散式擴容和容災功能,由於NOSQL是可以知道資料的主關鍵字的,所以自然就可以自動的去劃分資料所在的分段,從而可以自動化的尋找到目標儲存位置來做操作;最後是資料匯出功能,由於NOSQL支援的查詢索引只能是主鍵,對於很多後臺遊戲操作來說是不夠的,所以一定要能夠到處到傳統的SQL伺服器上去。

在這方面,有很多產品都做過一定的嘗試,比如在Redis或者MangoDB上做外掛修改,或者以ORM系統封裝MySQL以試圖構造這種系統等等。


[圖-整合快取的NOSQL]

三. 跳線和開房間

6、開房間型遊戲模型

在全區分線伺服器模型中,最早出現在開房間型別的遊戲中。因為海量玩家需要臨時聚合到一個個小的線上服務單元上互動。比如一起下棋、打牌等。這類遊戲玩法和MMORPG有很大的不同,在於其線上廣播單元的不確定性和廣播數量很小。

這一類遊戲最重要的是其“遊戲大廳”的承載量,每個“遊戲房間”受邏輯所限,需要維持和廣播的玩家資料是有限的,但是“遊戲大廳”需要維持相當高的線上使用者數,所以一般來說,這種遊戲還是需要做“分服”的。典型的遊戲就是《英雄聯盟》《穿越火線》這一類遊戲了。而“遊戲大廳”裡面最有挑戰性的任務,就是“自動匹配”玩家進入一個“遊戲房間”,這需要對所有線上玩家做搜尋和過濾。


[圖-開房間型遊戲]

這類遊戲伺服器,玩家先登入“大廳伺服器”,然後選擇組隊遊戲的功能,伺服器會通知參與的所有遊戲客戶端,新開一條連線到房間伺服器上,這樣所有參與的使用者就能在房間伺服器裡進行遊戲互動了。

由於“大廳伺服器”只負責“組隊”,所以其承載力會比具體的房間伺服器更高一些,但這裡仍然會是效能瓶頸。所以一般我們需要儘量減少大廳伺服器的功能,比如把登入功能單獨列出來、把玩家的購買物品商城功能也單獨出來等等。最後,我們也可以直接想辦法把“組隊”功能也按組隊邏輯做一定劃分,比如不同的組隊玩法、副本型別、組隊使用者等級等等。

雖然這種模型已經可以對很多遊戲做很好的承載了,但是在大廳伺服器這裡依然無法做到平行擴充套件,原因是玩家的線上資料比較難分佈到不同的服務程序上去,而且還帶有大量複雜的資料查詢邏輯。

7、專用聊天伺服器

不管是MMORPG還是開房間類遊戲,聊天一直都是網路遊戲中一個重要的功能。而這個功能在“線上人數”很多,“聊天頻道”很多的情況下,會給效能帶來非常大的挑戰。在很多型別的頁遊和少部分手機遊戲裡面,線上聊天甚至是唯一的“帶公共狀態”的服務。

聊天服務處理點對點的聊天,還有群聊。使用者可能會新增好友、建立好友群組等各種功能。這些功能,都是和一般的遊戲邏輯有一定差別的功能。這些功能往往並不是非常容易實現。很多遊戲都期望建立類似騰訊QQ的遊戲聊天功能,但是QQ是一整個公司在做開發,要用僅僅一個遊戲團隊做成這麼完