Wind:一款面向雲的分佈式遊戲服務器引擎


3樓貓 發佈時間:2022-05-15 14:44:42 作者:風揚 Language

Wind是一款面向雲的高性能、高效率以及高擴展性的大型分佈式遊戲服務器引擎。Wind利用Python語言的簡潔語法以及豐富的生態庫來提高遊戲業務的開發效率,針對一些對性能有要求的遊戲業務功能(如實時戰鬥功能),Wind利用Golang的高併發特性來保證服務的高性能,同時Wind接入雲的組件來保證遊戲服務的動態擴展性,提高服務資源的利用率。
點擊跳轉
Wind是遊戲服務器界首次結合go與python優點的服務器,如果Wind能解決你的問題的話,希望能幫Wind點個Star,如果不能解決大家的問題的話,也歡迎大家提Issue和Request,我會持續開發和完善Wind。
本文是Wind服務器引擎設計與實現系列的第一篇
  1. Wind單服務引擎功能的設計與實現
  2. Wind分佈式集群功能的設計與實現(待更新)
  3. Wind服務雲部署功能的設計與實現(待更新)

Wind單機引擎功能的設計與實現

本篇文章主要介紹Wind的出現背景,Wind要解決什麼問題,以及Wind的設計和服務器引擎實現方案。從遊戲業務需求出發,介紹Wind擁有的特性以及引擎各個功能如何實現這些特性。

Wind出現背景

得益於雲計算的低成本、按需靈活配置和高資源利用率的特性,大量的互聯網應用已經在雲上部署。但遊戲服務上雲的進度卻很緩慢,這其中的原因跟遊戲產品特性有關係。相對於互聯網產品,遊戲產品對延時更敏感,尤其是一些強競技性遊戲,如果將遊戲服務部署上雲的話,會有一些額外運算與路由導致延時增加,這也導致遊戲服務上雲進程比較緩慢。
雖然一些強競技性遊戲上雲後可能影響遊戲體驗,但是對於一些休閒類、放置類、弱競技類遊戲來說,這些遊戲對時延並不敏感,還是非常適合上雲的。而且隨著雲遊戲的發展和遊戲產品的國際化,遊戲上雲是一個必然趨勢。遊戲上雲可以很好運用雲上資源,更靈活的配置服務資源以及更高效的管理服務,降低遊戲服務的成本。
早前開源服務器更多關注的是單服務器內部的設計,如雲風的Skynet,雖然單服務器性能很高,但沒有提供一個很好的服務集群化方案,集群的配置、集群的監控管理以及集群間的通信交互都很麻煩,集群服務的動態擴展性也不強,導致搭建服務集群困難以及整體服務資源利用率低下。
因此Wind致力於解決上述問題,簡化遊戲服務集群方案,提高服務資源利用率,同時也保證遊戲服務的開發效率以及運行性能。Wind是一款面向雲的高性能、高效率以及高擴展性的大型分佈式遊戲服務器引擎。Wind利用Python語言的簡潔語法以及豐富的生態庫來提高遊戲業務的開發效率,針對一些對性能有要求的遊戲業務功能(如實時戰鬥功能),Wind利用Golang的高併發特性來保證服務的高性能,同時Wind接入雲的組件來保證遊戲服務動態擴展性,提高服務資源的利用率。
遊戲界有Unity和Unreal這樣完善並且開箱即用的客戶端引擎,這樣的引擎大大縮短了遊戲的開發週期,基本上一天就能做一個能跑的遊戲。但是卻並不存在一款大家熟知分佈式服務器引擎,這樣的服務器引擎可以快速上手並且能滿足遊戲各個階段的開發需求。Wind致力於做一款易上手且完善的分佈式服務器引擎,幫助獨立遊戲開發者或者中小企業快速搭建服務器框架並且快速開發遊戲業務,降低遊戲服務器開發難度與成本。

Wind特性

遊戲產品生命週期通常會經歷三個階段,第一個階段是開發階段,第二階段是上線階段,第三個階段是運營階段,不同階段面對場景與需求不一樣,對服務器特性要求也不一樣。產品前期更傾向於開發的高效率,後期更傾向於產品的質量與穩定,Wind針對三個階段的需求有不同的設計。
開發階段
開發階段處在遊戲產品快速試錯與完善階段,通常會有大量需求增加和變更,此時為了應對需求快速變更的情況,主要要求服務器具有便利性,便利性又可分為開發便利性與部署便利性。
  • 開發便利性
開發便利性是指給定一個功能需求,服務器框架能否快速實現邏輯需求,能否降低項目組或者程序員的開發時間和開發難度。一個遊戲服務器功能的開發便利性大體可以由兩部分來保證,一部分是底層引擎功能的抽象複用,如網絡通信、數據存儲、併發模型、遠程函數調用等,抽象是消除複雜的最好工具之一,抽象提供簡潔明瞭的接口,降低程序員上手難度。
開發便利性另一部分保證是遊戲邏輯語言的選擇,通常一個服務器大部分代碼都是產品邏輯代碼,如果選擇一些非常底層的語言來寫邏輯代碼的話,那會非常痛苦。比如用匯編來編寫UI交互邏輯,寫出來的代碼又臭又長,因此邏輯程序語言會選擇一些高級語言來實現,Wind使用Python來編寫遊戲業務代碼,主要利用Python簡潔的語法以及豐富的庫。Python對程序新手也更友好,可以更快的上手服務器開發。
  • 部署便利性
遊戲服務部署主要包括服務參數配置、服務環境安裝以及針對不同類型服務部署在不同物理區域。一般遊戲會有三套運行環境,一個是開發環境、一個是測試環境,一個是運營環境。
比較傳統的部署方式是直接在服務器操作系統中部署多個服務,這種部署方式有個缺點是如果服務環境升級的話,那各個環境也要手動更新,操作重複且容易出錯。考慮到環境的問題,有些運營的比較久的遊戲根本不敢輕易升級。所以現代部署基本上是容器部署,比如Docker容器,只需要寫一個Dockerfile腳本便可以在各個環境使用最新配置運行,降低了不同職能的環境配置成本。
上線階段
遊戲產品上線階段是最重要的時刻,遊戲上線意味著產品投放外部玩家,開始接受外部玩家的考驗。剛上線時是統一時間初次開放,大量玩家同時登陸游戲,這要求服務器需要由高併發能力,同時也要求服務器具有強大的可靠性和擴展性,保證大量玩家登錄時能正常玩遊戲和當人數超出預期時可自動伸縮服務來承載玩家遊戲。
  • 可靠性
服務的可靠性體現在預期負載和數據量下,服務器能否保持良好的性能運行。Wind的單服務性能主要由Golang語言來確保,一些調用頻繁的功能以及對性能有要求的遊戲功能(如網絡通信功能、遊戲戰鬥功能)由Golang這種靜態語言編寫,提升服務運行效率。同時Wind在Golang層採用多線程併發模型,為每一個客戶端開一個線程處理網絡包,提高單服務的併發性。
Wind的多服務可靠性由專用的遊戲負載均衡算法保證,負載均衡包含多種算法(比如最小分配、最大分配),可根據遊戲業務進行切換。同時Wind使用Python動態語言來編寫邏輯,支持函數級別的熱更且可以在不停服的情況下熱更遊戲資源。
  • 擴展性
擴展可以分為功能擴展與資源擴展。遊戲的功能擴展屬於軟件層面上的擴展,Wind使用插件模式來保證遊戲功能高擴展性,每個功能以插件的形式加入引擎,如果你不想用原先的功能,你可以寫一個新的插件替換掉原來的插件,插件模式可以很好擴展遊戲組件。
資源擴展屬於硬件擴展,硬件擴展又可分為縱向擴展和橫向擴展,縱向擴展是將服務器換成性能更強大的服務器,這種方式擴展有限。現代分佈式服務器下更常用的是橫向擴展,也就是添加更多的機器。Wind使用服務發現和負載均衡來保證硬件的擴展性,每當新增服務時,原有的服務集群可以很快發現服務並調度服務,服務的橫向擴展能力主要由服務發現的能力來決定。遊戲服務上雲後又可以讓服務擴展性又進一步,服務上雲後可利用雲上公共資源,動態伸縮服務,提高服務的利用率,降低冗餘,從而降低資源成本。
運營階段
運營階段處於遊戲的穩定階段,玩家數量會保持在一定數量上,服務器會長期的運行,此時穩定性是服務器最重要的一個特性。又因為運營階段是長期的一個維護階段,因此可維護性也是一個非常重要的特性。
  • 可維護性
軟件的大部分開銷並不在最初的開發階段,而是在持續的維護階段,維護包括漏洞修復、保持系統正常運行、調查失效、適配新的平臺、為新的場景進行修改、添加新的功能等等。不幸的是,大部分程序員都不喜歡維護所謂的遺留系統,可能因為涉及修復其他人的錯誤、和過時的平臺打交道是份額外工作。服務的維護主要是服務監控,良好服務監控可以在一定程度上預防服務問題的大規模擴散。

Wind引擎實現

大型分佈式服務器主要由早前單服務引擎發展而來,早前服務器服務玩家數量較少,基本上單進程服務器便能服務玩家。但由於互聯網技術的發展,玩家越來越多,單進程服務器服務不了更多的玩家,因此發展分佈式服務器來服務更多的玩家。得益於雲的發展,遊戲服務上雲提高了資源利用率,降低了服務維護成本。Wind分佈式引擎主要由這三個部分組成,第一部分是單服務器引擎,第二部分是分佈式集群,第三部分是服務雲部署。
單服務引擎包含一個服務器能運轉的所有功能,遊戲客戶端發送請求給單服務引擎,單服務引擎處理請求後並回包給客戶端。單服務器引擎主要包括程序語言、網絡通信、併發模型、遠程函數調用這些主要功能。
分佈式集群由每個運行的單服務引擎組成,分佈式集群主要是為了解決單服務器引擎只能服務少量玩家的問題,通過橫向擴展服務器來解決單服務器壓力過大的問題,分佈式集群功能主要包含服務發現、負載均衡、消息隊列和數據存儲等功能。
對於一些只有幾十個人的遊戲,你可能就只需要起一個服務就行了,但是對於上百萬玩家的遊戲,這時就需要起上千或者上萬個服,而且針對不同地區有不同的部署方案,面對這麼龐大的部署,如果純手動管理,那會非常耗時耗力,這時就需要一些雲部署工具來支持。雲部署主要包含容器部署、K8S編排、服務治理與監控。

單服務器引擎

單服務引擎包含一個服務器運行的所有功能,能單獨運行並服務一部分玩家。單服務引擎運行後,客戶端通過網絡通信將請求發送到服務器中,服務器通過併發模型將請求交給邏輯模塊處理,邏輯模塊通過序列化解碼參數數據並將請求數據交給服務註冊的RPC函數處理。

程序語言

目前遊戲服務器開發語言使用比較廣泛的組合是C/C++和Lua。C/C++屬於靜態語言,擁有很高的運行性能,但因為C/C++語法更傾向於計算機的理解方式,對程序員編寫業務邏輯並不夠友好,降低了產品的開發效率,而且C/C++熱更方法比較有限,線上出問題時不能快速且方便的修復,可能要時不時的停服關機修復,比較影響遊戲體驗,因此引入語法更簡潔、更方便而且支持打補丁的高級動態解釋語言Lua。
引入Lua後,C/C++的分工有了變化,一些要求高性能的服務器模塊用C/C++編寫,比如網絡庫、數學計算庫以及局內實時戰鬥邏輯等,Lua負責一些對性能要求不高的模塊,但業務邏輯量比較大的模塊,這樣的模塊其實佔遊戲業務的很大一部分,比如遊戲的一些外圍系統:等級系統,揹包系統,聊天系統等等。相比於Lua,其實個人更喜歡Python,Python比Lua擁有更簡潔的語法、更高的容錯以及更完善的函數庫,在開發產品業務時,擁有更高的開發效率,所以Wind的遊戲業務邏輯語言使用Python開發。
使用C/C++編寫一些模塊,可以很好解決服務運行效率問題,但C/C++沒有自動內存管理,寫邏輯時很容易發生內存洩漏,而且C/C++語法複雜,程序員上手難度較高,開發成本大,因此引入Golang語言。Golang以高併發著稱,擁有比C++更簡明的語法特性,可提升開發效率,同時Golang提供自動內存管理機制,極大的簡化了程序員開發的難度。因此Wind使用Golang來開發一些對效率有要求的模塊,目前Wind的網絡庫是Golang寫的。一些對性能要求高的遊戲業務你也可以使用Golang來開發,比如戰鬥功能。

Python與Golang的交互

Wind的網絡庫由Golang編寫,目前支持TCP,之後會支持KCP、Udp和Quic這些協議。每個客戶端連接過來後,Golang會開一個線程去處理網絡數據,有數據發過來後,Golang會將數據交給Python邏輯端處理。
這裡有個問題,就是Python線程和Golang線程是怎麼進行交互的?Wind服務器引擎的主線程是在Python端,在起服務器時加載Golang編寫的網絡動態庫(so文件或者DLL文件)並且開啟網絡線程處理客戶端數據。目前Python與Golang的數據交互使用Socket通信交互,Python端啟用一個TCP端口,Golang連接這個端口並且將數據傳送給Python端。
當然你也可以換Python與Golang的交互方式,比如換成ZMQ的zmq_inproc通信,使用zmq_inproc通信時,線程間共享一個ZMQ Context,可以通過共享內存來傳遞數據,不需要使用I/O線程,可以加快Python和Golang的交互。

併發模型

遊戲客戶端所做的工作基本上是將數據按一定邏輯顯示在屏幕上,單個客戶端並不需要與太多的服務器進行交互,多的也就兩三個左右,所以客戶端基本上對併發沒有太多的要求。但是服務器就不一樣了,同一個服務器要服務的客戶端可能是上千個,甚至可能是上萬個,這時候就需要併發模型來合理分配服務器的計算資源並正確的為客戶端服務。
為了最大化利用物理機的多核資源,一般會有兩種併發模型,一種是單進程多線程模型,這種模型通常是單個進程中存在多個遊戲服務,每個服務分配一個核進行計算。遊戲界應用這種併發模型比較成熟的是雲風的Skynet,Skynet啟用多個工作線程來處理服務的事件,各個線程間基於消息傳遞來通信。這種單進程多服務的缺點是,服務之間的隔離比較弱,服務之間共用了進程的數據,單個服務發生問題時可能影響其他服務,這樣的設計也並不適合雲部署,雲部署所滿足的一個基本要求就是服務間相互獨立,每個服務是雲調度的基本單位,每個服務可自由的被調度,因此更適合雲部署的是多進程單線程模型,每個進程是一個服務,同一臺機器可以起多個進程來利用多核優勢,同時單線程中使用異步來處理數據I/O,提高服務器的併發。
那麼Wind使用的是哪個模型了? Wind使用是的混合模型,Wind是單進程單服務,網絡庫利用Golang的高併發特性,每個客戶端連接啟用一個線程來讀取數據,同時為了減低業務邏輯編寫難度,避免多線程鎖問題,Python使用單線程異步協程來編寫業務邏輯。網絡層的多線程消息數據會通過一個隊列來發給Python協程(asyncio)。

Python協程

Python協程本質上是異步,只不過這個協程是語言層面上的支持,編寫遊戲業務時更清晰、更簡單。Python啟動線程時,會啟動一個事件循環,這個循環會一直檢測是否有事件可以處理,如果某個任務要進行I/O(磁盤I/O或者網絡I/O),那麼這個任務會被掛起,直到對應數據到來時,這個任務又會被事件循環處理。
import asyncio async def main(): print('Hello ...') await asyncio.sleep(1) print('... World!') loop = asyncio.get_event_loop() loop.create_task(main()) loop.run_forever()
使用異步協程編寫遊戲業務邏輯時,同一個時刻只能有一個客戶端請求被處理,降低了開發難度,與協程搭配使用的是各個模塊的單例化。

網絡通信

實現遊戲服務器時,主要會接觸到的是傳輸層以上的一些網絡協議,傳輸層協議包括UDP協議和TCP協議。UDP是一種無連接的協議,沒有可靠性保證、順序保證以及流量控制,但正是因為控制項比較少,UDP在數據傳輸過程中延遲小,速率高。遊戲中一些對可靠性要求不高,但要求高速率的業務可以使用UDP傳輸,比如遊戲語音服務
TCP是面向連接的、可靠的、基於字節流的傳輸層通信協議,TCP通過序號確認機制、超時重傳機制、重複累計確認機制和檢驗和機制來實現可靠性傳輸,同時提供流量控制和擁塞控制來控制源端的發送速率,以確保對端能正確接收。相對於UDP啥都沒做來說,TCP什麼事都做了,導致TCP傳輸速率低,延遲大。遊戲是實時性應用,遊戲的一些外圍功能(揹包功能、個人信息)時延要求不高,TCP夠用了,但是對於遊戲戰鬥這類高實時性功能,TCP的延遲太大了,會導致遊戲戰鬥時體驗會非常差。
為了解決TCP傳輸延時大的問題,遊戲界通常會在UDP之上實現一個延時更低的可靠性傳輸協議,比如KCP,Enet。KCP能以比TCP浪費10%-20%的帶寬的代價、換取平均延遲降低30%-40%,且最大延遲降低三倍的傳輸效果。ENet是專門為多人第一人稱射擊遊戲Cube開發的可靠性傳輸協議,Enet提供連接管理,Enet最大的特點是提供多通道機制,每個通道包傳送獨立,單個通道會確保前一個序號消息到達才會發送下一個序號消息,以保證可靠性。
通常遊戲會集成好幾個傳輸協議到網絡層,以便在不同需求場景下切換,比如集成UDP、KCP和TCP到網絡傳輸層。Wind的網絡層也會集成多個網絡協議,以便在不同遊戲場景中進行切換。

遠程函數調用(RPC)

在單機遊戲中,如果你要實現某個遊戲效果,你可以通過函數名字直接調用對應函數來實現效果,但在網絡遊戲中,有些遊戲效果需要向遠端的服務器請求計算或者數據(比如匹配,揹包)來實現,這時就會需要遠程函數調用。遠程函數調用通常發生在同一個共享網絡下的不同地址空間中,比如不同物理機。遠程函數調用通常採用Request-Response通信模式,每個遠程函數名字以Request/Response(也可以用其他結尾,比如Packet)結尾,以此來區分本地函數。
封裝好遠程函數調用庫後,寫代碼時就像寫本地函數調用一樣,程序員並不需要關心與遠端的交互細節。但與本地函數調用不同的是,遠程函數調用需要經過網絡傳輸,網絡傳輸增加了調用的時延與不確定性,為了防止主線程邏輯卡死,遠程函數調用一般設計成異步調用,Request包發出後,不會等待包的返回,而是Response包返回後在處理之前的邏輯。
實現遠程函數調用需要兩個功能支持,一個是序列化功能,一個是協議工廠功能。

序列化

序列化是一個將數據結構和對象信息轉化成可以存儲和傳輸形式的過程。我們在寫代碼時通常以對象的形式讀取數據,因為對象更符合人類思維習慣,我們能更快編寫程序代碼。但是對象信息數據通常是不連續的內存,不能直接進行存儲或者傳輸,所以序列化需要將對象數據轉化成二進制或者連續的字符串。
序列化技術有很多種,比較常見的是Json、Xml、Protobuf等。Json是直接將對象轉化成字符串,擁有很強的可讀性,但缺點也很明顯,那就是序列化後的數據太大了,導致需要的網絡帶寬也會加大,而且Json不安全,沒有錯誤處理機制,你可以將同一個字段值轉化成其他類型的數據。現在比較大型的遊戲通常採用的序列化是Protobuf,Protobuf是協議定義型的,在使用時你需要定義你的數據類型,而且因為Protobuf在序列化時是用ID作為標識符,而不是字段名來標識,所以序列化後的Protobuf數據很小。對於遊戲來說,Protobuf支持函數引用定義,解決了函數重複定義的問題。使用Protobuf後也可以用協議定義文件來生成MD5,以次來標識不同遊戲版本的網絡協議接口,以防老版本的客戶端連接新版本的服務器,造成數據錯誤。

協議工廠

Json序列化時可以將函數名序列化進去,數據包到達服務器後,服務器根據函數名調用註冊的RPC函數,但Protobuf序列化時並不會將函數名的信息帶進去,Protobuf只會序列化協議參數數據,所以要使用Protobuf進行服務器序列化時,還需要一個新字段來標識這些數據是來自哪個協議的數據。通常是用一個ID來表示協議函數,那麼哪個協議ID代表哪個函數了?這時候就會需要協議工廠功能。定義好協議文件後,將協議文件轉化成代碼文件時,通常會給每個協議一個ID,然後生成一個協議工廠代碼文件,這個工廠代碼就是根據ID來調用對應函數。

服務繼承

Wind整個項目的核心代碼是Engine目錄下的SrvEngine.py 其他所有代碼都是以組件的形式附加到這個文件下。Engine擁有一款引擎的所有功能,但並不作具體的邏輯服務使用,具體的邏輯服務需要繼承Engine,然後添加各個邏輯服務的一些特定功能,具體服務在service 目錄下,目前有Gateway服務和Game服務。各個服的具體設計可以參考這邊文章

© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com