天天看點

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

接上篇 經典遊戲伺服器端架構概述(1)。

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

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

[圖-全區分線模型]

1、需求:擴容和容災

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

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

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

2、分區分服的關系型資料庫

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

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

[圖-分表分庫]

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

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

3、NoSQL

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

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

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

[圖-NoSQL]

NoSQL的确是非常适合用來存儲遊戲資料。特别是有些伺服器如Redis還帶有豐富的字段值類型。但是,NoSQL本身往往不帶很複雜的容災熱備機制,這是需要額外注意的。而且NoSQL的通路延遲雖然比關系型資料庫快很多,但是畢竟要經過一層網絡。這對于那些發展了很多年的ORM庫來說,缺乏了一個本地緩存的功能。這就導緻了NoSQL還不能簡單的取代掉所有伺服器上的“狀态”。而這些正是分布式緩存所希望達成的目标。

4、分布式緩存

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

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

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

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

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

[圖-分布式緩存]

5、內建緩存的NoSQL

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

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

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

[圖-內建緩存的NOSQL]

6、開房間型遊戲模型

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

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

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

[圖-開房間型遊戲]

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

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

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

7、專用聊天伺服器

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

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

是以遊戲開發者們常常會專門的針對聊天功能來開發一系列的服務程序,以便能讓遊戲的聊天功能獨立出來,做到負載分流和代碼重用的邏輯。很多網遊系統,其聊天系統從用戶端來說就是和主遊戲程序分開的。

聊天伺服器的本質是對用戶端資料做廣播,進而讓玩家可以互動,是以有很多遊戲開發者也直接拿聊天伺服器來做棋牌遊戲的房間伺服器,或者反過來用。由于在遊戲“分服”裡面單獨部署了聊天伺服器,這類伺服器也往往被用來承擔做“跨服玩法”的程序。比如跨服團隊戰、跨服副本等等。不管這些伺服器最終叫什麼名字,實際上他們承擔的主要功能還是廣播,而且是運作玩家“二次登入”的廣播伺服器。以至于後來,有部分遊戲直接全部都用聊天伺服器來代替原始的“遊戲伺服器”,這樣還能實作一個叫“跳線”的功能,也就是玩家從一個“線上環境”跳到另外一個“線上環境”去。——這些都是對于“廣播”功能的靈活運用。

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

[圖-專用聊天伺服器]

盡管分服的遊戲模型已經營運了很多年,但是有一些遊戲營運商還是希望能讓盡量多的玩家一起玩。因為網遊的人氣越活躍,産生的互動越多,遊戲的樂趣也可能越多。這一點最突出表現在棋牌類網遊上。如聯衆、QQ遊戲這類産品,無不是希望更多玩家能同時線上接入一個“大”伺服器,進而找到可以一起玩的夥伴。在手遊時代,由于手機本身線上時間不穩定,是以想要和朋友一起玩本來就比較困難,如果再以“伺服器”劃分區域,互動的樂趣就更少了,是以同樣也呼喚這一個“大”伺服器,能容納下所有此款遊戲的玩家。是以,開發者們在以前積累的分服模型和分線模型基礎上,開發出滿足海量線上互動需求的一系列遊戲伺服器模型——全服全線模型。

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

[圖-全服全線模型]

靜态配置

全服全線模型的本質是一個各種不同功能程序組成的分布式系統,是以這些程序間的關系是在運維部署期間必須關注的資訊。最簡單的處理方法,就是預先規劃出具體的程序數量、以及程序部署的實體位置,然後通過一套配置檔案來描述這個規劃的内容。對于每個程序,需要配置列明每個程序的pid檔案位置;内部通訊用的位址,如IP+端口或者消息隊列ID;啟動和停止腳本路徑;日志路徑等等……由于有了一套這樣的配置檔案,我們還可以編寫工具對所有的這些程序進行監控和操作批量啟停。

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

[圖-靜态配置]

雖然我們可以以靜态配置為基礎做很豐富的管理工具,但是這種做法還是有可以改進的空間:每次擴容、更換故障伺服器或者搬遷伺服器(這在營運中很常見),我們都必須手工修改靜态配置資料,由于是人工操作,就總會産生很多錯誤,根據個人經驗,遊戲營運事故中的70%以上,是跟運維操作有關;由于整個分布式系統被切分成大量的程序,對于新進入此項目的程式員來說,要完整的了解這個系統,需要在思想上跨越層層阻隔:每個程序的功能、它們部署的關聯、每個程序間的協定報的含義、每個業務流程具體的跨程序過程……這要花費很多時間才能搞明白的。而且大部分遊戲的這種架構并不統一,每個遊戲都可能需要重新了解一次,知識無法重用;在開發測試上,由于分布式系統的複雜性,要多搭幾個開發、測試環境也是很費時間的,以至于這項工作甚至要安排專人來負責,這對于小型遊戲開發團隊來說幾乎是不可承擔的成本。是以我們還需要一些更加自動化,更加容易了解的全服全線遊戲伺服器模型。

基于中心點的動态組織

SOA架構模式是業界一個比較經典的分布式軟體架構模式,這個架構的特點是能動态的組織一個非常複雜的分布式服務系統。這個系統可以包含提供各種各樣供的服務程式,而這些服務程式都以同一個标準接口來使用,并且服務自己會注冊自己到叢集中,以便請求方能找到自己。這種架構使用Web Serivce來作為服務接口标準,通過釋出WSDL來提供接口API,這極大的降低了開發者對這些服務的使用成本。在遊戲領域,伺服器端提供的功能程式,實際上也是非常多樣的,如果要建構一個分布式的系統,在這個方面是非常适合SOA架構的思想的;然而,遊戲卻很少使用HTTP協定及其之上的Web Service做通訊層,因為這個協定性能太低。不過,類似SOA的,基于中心節點的動态組織的服務管理思路,卻依然适用。

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

[圖-基于中心點的動态組織]

一般來說我們會使用一組目錄伺服器來充當“中心點”,代表整個叢集。開源産品中最好的産品就是ZooKeeper了。當然也有一些開發者自己編寫這樣的目錄伺服器。由于每個服務程序會自己上報負載和狀态,是以每個程序隻需要配置自己提供的服務即可:服務名字、服務接口。對于請求方來說,一般都可以預先編寫目标服務接口的類庫,用來程式設計,有些項目還使用RPC功能,使用IDL語言配置直接生成這些接口類庫。當需要請求的時候,執行“名字查找”-“路由選擇”-“發起請求”就可以完成整個過程。由于有“查找”-“路由”的過程,是以如果目标服務故障、或者新增了服務提供者,請求方就能自動獲得這些資訊,進而達到自動動态擴容或容災的效果,這些都是無需專門去做配置的。

服務化與雲

盡管動态組織的架構有如此多優點,但是開發者還是需要自己部署和維護中心節點。對于一些常用的服務,如網絡代理服務、資料存儲服務,使用者還是要自己去安裝,以及想辦法接入到這套體系中去。這對于開發、測試還是有一定的運維工作壓力的。于是一些開發團隊就把這類工作集中起來,預先部署一套大的叢集中心系統,所有開發者都直接使用,而不是自己去安裝部署,這就成為了服務化,或者雲服務。

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

[圖-服務化、雲]

使用專人維護的服務化叢集确實是一個輕松愉快的過程。但是遊戲開發和營運過程中,往往需要多套環境,如各個不同版本的測試環境、給不同營運平台搭建的環境、海外營運的環境等等……這些環境會大大增加維護服務化叢集的工作量,對于解決這個問題,建立高度自動化運維的私有雲,成為一個需要解決的問題放上了桌面。提高叢集的運維效率,降低工作複雜程度,需要一些特别的技術,而虛拟化技術正式解決這些問題的最新突破。

1、使用RPC提高網絡接口編寫效率

在分布式系統中,如果所有的接口都需要自己定義資料協定報來做互動,這個網絡程式設計的工作量将會非常的大,因為對于一個普通的通信接口來說,至少包括了:一個請求包結構、一個響應包結構、四段代碼,包括請求響應包的編碼和解碼、一個接收資料做分發的代碼分支、一個發送回應的調用。由于分布式的遊戲伺服器程序非常多,一個類似登入這樣的操作,可能需要曆經三、四個程序的合作處理,這就導緻了接近十個資料結構的定義和無數段類似的代碼。而這些代碼,如果在單程序的環境下,僅僅隻是三、四個函數定義而已。

是以很多開發者投入很大精力,讓網絡通信的編寫過程,盡量簡化成類似函數的編寫一樣。這就是前文所述的遠端調用的方法。在全區全線的遊戲中,如果是比較重度的遊戲,采用RPC方式做開發,會大大降低開發的複雜程度。當然也有一些比較輕度的遊戲,還是采用傳統的協定包編解碼、分發邏輯調用的做法。

2、簡化資料處理

在分布式系統中,對于避免單點、容災、擴容中最複雜的問題,就是在記憶體中的資料。由于記憶體中有遊戲業務的資料,是以一般我們不敢随便停止程序,也難以把一個程序的服務替換為另外一個程序。然而,遊戲資料對比其他業務,還是非常有特點的:

寫入越不頻繁的資料,價值越高。比如過關、更新、獲得重要裝備。

大量資料都是讀非常頻繁,而寫非常不頻繁的,如玩家的等級、經驗。

大量寫入頻繁的資料,實際上是不太重要,可以有一定損失,比如玩家位置,在某個關卡内的HP/MP等……

是以,隻要我們能按資料的特性,對遊戲中需要處理的資料做一定分類,就能很好的解決分布式中的這些問題。

首先我們要對資料的分布做規劃,一般來說采用按玩家ID做分布,這樣能讓服務程序中記憶體的資料緩存高度命中。常用的手法有用一緻性哈希來選擇路由,調用相關的服務程序。

其次對于讀頻繁而寫不頻繁的資料,我們采用讀緩存而寫不緩存的政策。每個服務程序都保留其讀緩存資料,如果需要擴容和容災,僅僅需要修改服務通路的路由即可。

再次對于讀不頻繁而寫頻繁的資料,我們采用寫緩存和讀不緩存的政策。由于這些資料丢失掉一些是不要緊的,是以容災處理就直接忽略即可,對于擴容,隻需要對所有服務程序都做一次回寫即可。

最後,有一些資料是讀和寫都頻繁的資料,比如玩家位置,HP/MP這類,我們采用讀寫都緩存,由于資料重要性不高,隻要我們多分幾個服務程序即可降低故障時影響的範圍;在擴容的時候調用全節點清理讀緩存和回寫髒資料即可。

在和持久化裝置打交道的時候,傳統的ORM類庫往往能幫我們把資料存入關系型資料庫,然而,使用一個自帶資料熱備的NOSQL也是很好的選擇。因為這樣能節省大量的分庫分表邏輯代碼。

3、自動化部署叢集環境

最新的虛拟化技術給分布式系統提供能更好的部署手段,以Docker為标志的虛拟化平台,可以很好的提高服務化叢集的管理。我們可以把每個服務程序打包成一個映像檔案,放入Docker虛拟機中運作,也可以把一組互相關聯的服務程序打包運作。這些環境問題都由Docker處理了。

但是,我們同時需要注意的是,如果我們的程序的資源是靜态配置設定的(前文提到),在Docker的虛拟機中可能因為記憶體不足等原因直接無法啟動。這就需要我們把完全靜态配置設定資源的程式,修改為有資源限制,但是動态配置設定的程式。這樣我們才能在任何可以部署Docker的機器上部署我們的遊戲伺服器。

1、分布式接入層

一般來說,我們全線伺服器系統碰到的第一個問題,就是大量并發的網絡請求。特别是大量玩家都在一起互動,産生了大量由于狀态同步而需要廣播的資料包。這些網絡請求的處理,顯然應該獨立出來成為單獨的程序。同時這些網絡接入程序,還應該是一個叢集中的成員。這就誕生了分布式接入服務層。

這些網路接入程序的第一個功能,就是把并發的連接配接,代理成為後端一個串行的連接配接,這可以讓後端服務程序的處理邏輯更簡單,而且網絡處理消耗變得更小。

其次,網絡接入程序需要支援廣播功能。如果隻是普通的廣播實作,很多人會需要拷貝很多次需要廣播的内容,然後挨個對Socket做發送。這其實是一個消耗很高的操作。而單獨的網絡接入程序,可以善用“零拷貝”等技術,大大降低廣播的性能開銷。而且還可以通過多個程序一起做廣播操作,以達到更大的線上同步區域。

最後,網絡接入程序需要支援一些額外的有用功能,包括通訊的加密、壓縮、流量控制、過載保護等等。有些團隊還把使用者的登入鑒權也加入網絡接入功能中。

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

[圖-分布式接入層]

2、使用P2P

網絡狀态同步産生的廣播請求中,絕大多數都是用戶端之間的網絡狀态,是以我們在可以使用P2P的用戶端之間,直接建立P2P的UDP資料連接配接,會比通過伺服器轉發降低非常多的負載。在一些如賽車、音樂、武打類型的著名遊戲中,都有使用P2P技術。而接入程序天然的就是一個P2P撮合伺服器。

有些遊戲為了進一步降低延遲,還對所有的玩家狀态,隻同步輸入動作,以及死亡、技能等重要狀态,讓怪物和一般狀态通過計算獲得,這樣就更能節省玩家的帶寬,提高及時性。加上一些動作預測技術,在用戶端上能表現的非常流暢。

遊戲服務端的各種架構中,以前往往比較關注那些非功能性的需求:容災性、擴容、承載量,延遲。而在現在手遊時代,開發效率越來越重要,有些團隊甚至不設專門的伺服器端程式員。是以遊戲服務端架構應該更多的關注業務開發的效率。

現代遊戲中,隻要是帶RPG元素的,角色系統、物品系統、技能系統、任務系統就都會具備,而且都有一批比較穩定的核心邏輯。隻要是能線上互動的,就有好友系統、郵件系統、聊天系統、公會系統等。另外商城系統、活動系統、公告系統更是每個遊戲都似乎要重複發明的輪子。

遊戲的後端應用也有很多可重用的部分,比如客服系統、資料統計平台、官網資料接口等等。這些在遊戲服務端架構中往往是最後再添加進去的。

如果把以上的問題都統一考慮起來,我們實際上是可以在一個穩定的底層架構上,構造出一整套常用的遊戲業務邏輯模闆,用來減少遊戲領域的業務代碼開發。是以這樣一套可以運作各種業務邏輯模版的底層架構,正是遊戲服務端架構發展的方向。

現在有的團隊已經在搭建自己的Docker雲,這可以讓遊戲伺服器在虛拟雲上動态的生長,進而達到真正的動态擴容和動态容災。加上如果遊戲伺服器不再是一個個服務程序,而是真正意義上的一個個服務,可以動态的加入或者離開雲環境,那麼這就是一個遊戲領域的PaaS系統。我熱切的希望能看到,可以用一套SDK,開發或重用那些成型的業務模版,然後動态注冊到服務雲中就能運作,這樣一種遊戲伺服器架構。