


摘要:對游戲服務器架構進行了深入研究,提出了一個基于消息隊列的分布式游戲服務器架構設計。解決了傳統分布式網狀架構在節點多時管理成本增加及節點間消息流量在內網中幾何級增長的問題。并在此基礎上設計了一個游戲服務器通用架構,該架構在多個已經上線的放置掛機類網絡游戲中使用,可以更好地應對玩家需求變化引起的架構調整,以及玩家數量波動時快速擴容或減容的需求。
關鍵詞:分布式;游戲服務器架構;消息中間件;單服架構;分服架構
一、前言
隨著計算機網絡的發展和普及,網絡游戲逐漸成了最常見的娛樂方式。玩家群體的增加對游戲服務器的架構提出了新的要求,傳統的服務器架構已經無法適應海量玩家同時在線的需求。分布式逐漸成為主流,微服務等技術也開始逐漸應用在新的網游架構中。本文提出一種基于消息隊列的分布式網游服務器架構,有效規避了傳統的基于網狀結構的一系列缺點,為后繼服務器的負載均衡,服務能力動態擴容,微服務,以及跨區通訊打下了堅實的基礎。
二、服務器架構演變
(一)集中式服務器架構
1.單服架構
網絡游戲剛剛開始發展時,由于在線人數不多,而且游戲的業務邏輯也較簡單。早期的服務器基本是單服結構,即所有的客戶端都連接到一臺單獨的服務器上,單獨的服務器上運行一個獨立的服務進程,處理客戶端的連接請求,以及所有的游戲邏輯。
這種結構的特點是架構簡單明了,所有的邏輯都在一個進程內,邏輯代碼非常簡單直接。在用戶量較小和邏輯簡單時可以快速開發出想要的功能,調試、測試也容易。但是隨著用戶量的增加,這種架構的缺點開始慢慢顯露出來,單進程的理論處理極限就是進程當前所在服務器的硬件處理能力。當用戶的增加超過當前服務器的硬件極限時,就無法承載更多的玩家。
2.分服架構
為了解決單服架構承載能力不足的問題,人們又提出了分服架構。分服架構的總體思想與集中式架構類似,區別是引入了一個登錄服務器,玩家在登錄的時候先通過登錄服務器確定自己在哪個區服,等確定了區服地址后再登錄到相應的服務器上面,后面的處理流程與單服架構類似。通過分區服的引入,在玩家超過一個服務器的承載范圍后,將后面新加入的玩家加入新的服務器上面去,這樣可以有效規避單服務器承載能力不足的問題[1]。
分服架構雖然解決了玩家增加后單服承載力不足的問題,但是還有一些其他的問題沒有解決。第一個問題是單區服的承載力依然有上限。當游戲類型需要同一個區服的玩家數量多到一定程度才可以玩時,分服架構依然滿足不了這種需求。第二個問題是隨著游戲類型的增加,游戲的復雜度也隨之增加。在傳統的單進程模式下,所有的游戲邏輯都寫在同一個進程內,功能會相互耦合,對軟件的架構能力要求變高。如果設計者沒有很好的架構能力,很容易導致后期邏輯復雜化,耦合度增加,維護成本變高。由于復雜度增加,開發效率也會受到影響,系統的穩定性無法保證。
(二)分布式服務器架構
為了解決上面兩個問題,人們又提出了分布式的網游服務器架構。分布式架構是在單服架構的基礎上演化出來的,它主要針對以下兩個方面做了改進。
一是分布式架構在物理上將單進程拆分成多個進程,并引入了進程間的通信機制。多個進程以某種方式自組織在一起,以進程組的形式為一個區服提供服務。通過這種方式,突破了單區服承載能力有上限的問題,可以提供近乎無限的處理能力。
另外,分布式架構在邏輯上將單進程拆分成多個進程。單服架構中,一個游戲所有的邏輯都在同一個進程中,所以結構會變得很復雜。在分布式架構中,區服中可包含多個不同類型的進程。特定類型的進程處理特定的問題,如客戶端接入進程、聊天服務進程等。不同類型的進程通過進程間通信能力互相協作,最終可以提供完整的游戲服務。由于特定類型的進程只會處理特定的邏輯,所以它本身的邏輯結構也會更清晰簡單,與其他系統的耦合性可以降到最低,后繼的邏輯開發與維護的成本也會下降到最低。
分布式架構在解決了單服架構問題的情況下,又引入了新的復雜度。由于分布式服務是分布在不同硬件上的很多進程協作提供的,所以進程間信息共享以及同步能力就變得非常重要,而這一切的基礎就是進程間的通信能力。
進程間的通信需要兩個要素,第一個是進程必須知道當前區服中還有哪些其他的進程存在,并通過某種方式獲得它的標識符或地址以及它們所能提供的服務。第二個是進程必須由某種安全高效的方法投遞信息到其他的進程。
了解區服中的活動進程信息有多種方式,比如通過共享的數據庫記錄當前的進程信息,然后進程以輪詢的方式查詢這些信息,并維護一個當前的進程列表,包含其他進程需要知道的一切信息(地址,標識符,服務列表等)。也可以通過注冊進程信息到服務發現服務(ETCD,ZooKeeper等),然后再通過監控服務發現框架中的信息變化來獲取類似的信息。
進程間的通信分幾種情況,如果不同進程在同一臺服務器上,可以通過操作系統提供的共享內存或管道等功能通信。如果進程在不同的服務器上,可以通過標準的Socket網絡接口通信,視具體情況可以使用TCP或UDP協議。有些時候,為了統一協議,可以不區分本地或遠程進程,均采用相同的Socket接口通信。在本地情況下,會比原生的共享內存等方式效率低一些,但幾乎可以忽略不計[2]。
(三)傳統分布式服務器架構的拓撲結構
傳統的分布式服務器架構一般采用網狀的拓撲結構,即每個進程都擁有與其他進程通信的通信鏈路。一個典型的進程節點生命周期分為以下幾個階段。
當一個進程啟動時,先注冊自己到服務發現服務上。再從服務發現服務上查詢當前區服中活動的進程節點列表。遍歷所有的列表,然后和列表中的其他節點建立連接。當所有的節點連接都建立完成后,當前節點啟動成功。
節點在正常運行過程中,不斷地給自己關心的節點發送心跳消息,一旦發現鏈路斷開,立即發起重連請求,節點主要保持與其他節點的鏈路正常通信。
當有新的節點啟動時,由于它會主動注冊自己到服務器的發現服務,所以在區服中的其他節點都可以收到通知,然后主要與新加入的節點建立聯系。所以每一對節點都會有兩個鏈路,每個鏈路都是單向的。網狀結構的拓撲關系基本上是現在游戲服務器架構的共識。
(四)傳統分布式網狀結構的問題
網狀結構已經可以滿足大多數的業務需求,但隨著行業的發展,游戲邏輯進一步復雜,分布式節點數量變得越來越多,網狀架構開始出現一些顯著的問題。
首先碰到的第一個問題是復雜度的增加。任意一對節點都會有兩個通信鏈路。當區服中的節點數量為n,單個節點就需要維護2(n-1)條通信鏈路。整個結構中通信鏈路的總數為2n(n-1)。可以看出,隨著節點數量n的增加,通信鏈路的數量會以平方數的速度增加。而現在的游戲,如果游戲類型較復雜的話,很多都會引入微服務的架構,導致節點的類型和數量都快速增加,整個系統的復雜度也很容易失控。由于每個節點都要維護2(n-1)條通信鏈路,而每個通信鏈路一般都會有專有的線程或協程來處理消息、接收邏輯,當鏈路數量多到一定程度時,節點會消耗大量的資源維護鏈路,但實際情況也不是所有的鏈路都會被使用,這樣會造成系統資源的大量浪費。隨著鏈路數量的增加,由于單個節點啟動會創建所有其他節點的鏈路,會導致整個系統的耦合性增加,節點的啟動速度變慢,分布式系統帶來的靈活擴展性也會受到影響。
網狀系統另一個嚴重的問題是可靠性問題。在分布式系統中,維護數據的正確性一定要依賴于完全可靠的通信鏈路,甚至在節點重新拉起后發送的消息也應該能正常收到。而網狀結構依賴的消息鏈路過于原始。首先,它是和進程強綁定的,也就是說當節點進程發生故障需要重新拉起時,消息鏈路就會被重建;其次,消息隊列是進程間維護的,當網絡發生波動時,進程間的消息鏈路有可能斷開,觸發重聯機制。換句話來說,消息鏈路是不可靠的,有可能丟失消息。在分布式系統中,如果無法保證消息的可靠性,會對系統造成嚴重的影響。上層功能需要加入大量的邏輯以確保在消息丟失的情況下,節點的狀態不會發生錯亂。這會極大地增加上層邏輯代碼的復雜度,引入更多不可預料的漏洞。
三、基于消息總線的架構方案
為了應對節點數量增加時網狀結構的問題,本文提出了一種新的基于消息總線的架構方案。該方案主要依賴兩個獨立的組件:服務發現以及消息總線。
服務發現主要解決區服內節點信息的同步問題。服務發現主要提供兩種能力:發現別人、讓別人發現自己。當節點連接到服務發現服務時 ,它可以通過服務發現的查詢能力查詢到和它一樣連接到同一個服務發現服務的其他節點。通過這種機制,節點擁有了發現別人的能力。當一個節點連接到服務發現服務時,所有在同一個服務上的其他節點都可以收到新節點接入的通知。當然,當一個節點從服務發現服務斷開時,其他節點也可以收到老節點離開的通知。通過這種機制,節點就擁有了讓別人發現自己的能力。當任意一個節點都有發現別人和被別人發現的能力時,就相當于掌握了整個分布式系統的全景信息。
服務發現解決的是專有問題,所以可以獨立于整個架構之外去設計,架構只是單向依賴服務發現,而服務發現并不依賴于分布式架構本身。服務發現目前已經有很多成熟的解決方案,如ETCD,ZooKeeper是專業做服務發現的開源項目。Redis新版中的Pub/Sub機制也可以用來做簡單的服務發現。專業的服務發現項目一般支持集群,實現了嚴苛的數據一致性協議,可靠性、穩定性和可擴展性都是可以保證的。
消息總線主要解決的是分布式系統中節點之間的消息傳遞功能。消息總線隔離了需要通信的進程,它們之間不再需要保持單獨的通信鏈路,也不需要知道彼此的存在。節點都只是單向地和消息總線保持連接,所有的消息都通過消息總線來統一傳遞。整體的拓撲架構會變成魚骨狀,相比網狀結構,可以極大地減少消息鏈路的數量。形象點說,消息總線就像一個全自動的包裹投遞系統,每個節點都和系統連接,并擁有一個地址。只要給包裹填好地址,然后提交給消息總線,總線就一定會保證把消息投遞到正確的地方。當然,地址正確是前提。在這個過程中,投遞者完全不用關心對方在哪里,狀況如何,網絡有沒有波動等[3]。
消息總線本質上是把原來節點之間的通信鏈路抽象出來統一管理,所以解決的也是專有問題。消息總線和分布式架構也是單向依賴。消息總線不依賴于分布式架構,而分布式架構依賴消息總線。消息總線目前也有很多成熟的開源項目,如RabbitMQ,NSQ,NATS等。每個項目適合的場景不同,但是解決的問題都是類似的。現代的消息總線不僅可以解決消息投遞的問題,也可以做消息緩存、消息記錄、可靠投遞管理等。在關閉一些高級特性的情況下,高效的消息總線幾乎可以做到和原生Socket類似的投遞效率,完全可以在游戲服務器底層大規模使用。其流程分為節點啟動和節點消息投遞兩個步驟。節點啟動時,將自己注冊到服務發現中,并從服務發現拿到其他節點的地址,更新全局信息,連接到消息總線上;節點需要投遞消息時,從全局信息中拿到目標節點的地址,然后將消息投遞到消息總線上去。從以上流程可以看出,基于消息總線的架構比網狀結構節點的啟動以及消息投遞過程簡化了很多。
基于消息總線的拓撲圖如圖1所示,新架構下節點的通信鏈路大幅減少,每個節點只需要一個消息鏈路,在n個節點的系統中也只存在n個通信鏈路。通信鏈路的減少對應著系統復雜度的降低,當系統需要擴容時,節點增加完全不會對系統的壓力產生影響。
新架構下節點之間都被消息總線隔開,互相完全不知道對方的存在,也就意味著節點之間是完全解耦的,整體的架構更加清晰合理。
消息總線負責了整個系統消息的傳遞,只需要在消息總線中加入可靠性消息投遞保證。對節點而言就可以認為消息投遞是可靠的,節點本身可以不用處理由于消息丟失引起的狀態出錯的問題。可以使節點本身的上層業務邏輯更加清晰簡單,系統的穩定性也會提升[4]。
四、基于消息總線架構的問題及解決
相對于網狀結構,新的架構雖然解決了網狀結構的大多數問題,但有可能引入一些新的問題,下面是一些可能的問題及其解決方案。
主要碰到的是性能問題,相比傳統的直接通信線路,基于消息總線的消息投遞需要經歷消息總線的一次中轉,性能上會有所折損,性能包括吞吐量和延時兩個部分。
首先來討論吞吐量的問題。測試結果顯示設計良好的消息總線,在合理的設置下,基本上可以達到和原生TCP類似的性能。表1為常見消息總線和原生TCP吞吐量的對比。可以看出來,NATS的吞吐量幾乎接近原生的吞吐量。而RabbitMQ差很多,主要原因是RabbitMQ內部做了很多額外的事情,如消息的序列化、防重發等。但是對游戲服務器來說,NATS的默認設置就足夠使用,所以使用NATS基本上不會碰到吞吐量的問題。表1的測試使用4核8G CentOS7.0系統,單個消息包大小為1K。
第二個是消息延時的問題。相比直連的通信鏈路,經過消息總線的延時主要由以下部分組成:消息從節點到消息總線;消到目標節點的時間。由于節點和消息總線一般都在內網,所以它們的延時基本在1ms以內,可以忽略不計。而在消息中間件中停留的時間如表2所示。可以看出NATS的延時時間都在2ms以內,基本可以忽略不計。NATS延時比較低的原因是它采用了特殊的設計來降低延時。表2測試使用4核8G CentOS7.0系統,單個消息包大小為1K,發送10000次取平均值。
由于服務器一般都工作在內網環境,而且節點之間的消息頻率一般都不高,所以使用NATS完全可以滿足節點之間日常的消息投遞需求。另外,消息總線一般都支持集群,當需要的吞吐量沒辦法滿足需求時,還可以通過增加集群的節點來提升消息總線的處理能力。
五、結語
基于消息中間件的新架構不僅解決了傳統網狀架構的種種問題,而且還帶來了一些額外的好處,網絡游戲一般都會有個跨區的功能,傳統架構下開發相關的功能為了實現跨區通信都要引入很多復雜的邏輯。而在新架構中,處理跨區的問題只需要通過配置一個消息總線的網關,把兩個服務區的消息總線打通即可,上層幾乎不需要額外的工作處理跨服邏輯,極大地簡化了相關功能的開發。該架構設計已經在生產環境中成功部署運行,反映良好。
參考文獻
[1]胡棟梁,秦曉軍,王曉鋒.基于消息中間件的分布式網絡掃描[J].計算機工程,2020,46(12):163-170.
[2]姜夢蘭. 于消息中間件服務可靠性保障方案的研究與實現[D].北京:電子科技大學,2010.
[3]李文逍,楊小虎.基于分布式緩存的消息中間件存儲模型[J].計算機工程,2010,36(13):93-95.
[4]馬躍,何雨婷,尹震宇,等.一種面向OPC UA消息通信的多優先級動態調度算法的設計與實現[J].小型微型計算機系統,2021,42(08):1747-1752.
基金項目:蘇州市科技發展規劃項目(項目編號:SYG201803)
作者單位:胡鵬昱,上海農林職業技術學院;王曉軍,上海征途信息技術有限公司中臺研究所
■ 責任編輯:尚丹