天天看點

遊戲伺服器架構:遊戲服務端高并發和高可用,怎樣支援百萬玩家同時線上,不出問題

用通俗的方法來描述一個好的服務端架構,最基礎也是最重要的就兩點: 支援百萬玩家同時線上,不出問題 。這兩點也就分别對應了高并發和高可用。

這篇文章系統的介紹遊戲服務端中的高并發和高可用。

高并發和高可用是一個相輔相成的工作,當我們支援百萬玩家同時線上時卻無法保證伺服器穩定可用,那高并發支援就無從談起;而如果當玩家數量較多時伺服器就常常出問題,那也不能稱為高可用。

一、水準擴充

水準擴充是高并發和高可用的基礎,通過支援水準擴充,我們理論上可以通過增加機器獲得無限的承載上限,進而支援高并發;在此基礎上,若某個程序出現異常,其他程序可以替代其提供服務,進而實作了高可用。

以下圖為例,對于不支援水準擴充的架構,遊戲伺服器中隻有一個戰鬥程序為所有的玩家提供戰鬥服務,這裡存在兩個問題:1.一個程序最多隻能使用一個一台機器的計算資源,存在性能上限。2.若這個程序或者所在機器/網絡發生異常,那麼整個系統就不可用了。

不支援水準擴充

水準擴充有兩種常見的實作模型:

  • 大廳服和所有的戰鬥程序進行全連接配接,需要通路戰鬥服務時去管理器中查詢服務所在的程序位址,然後直接去通路程序。(左圖)
  • 在戰鬥程序前面挂一個路由,路由記錄每個戰鬥所在的戰鬥程序,相關請求會轉發到對應的程序。(右圖)

水準擴充的兩種模型

1.1 有狀态 vs. 無狀态

從程序記憶體中是否儲存狀态的角度可以将服務分為有狀态和無狀态:

  • 有狀态服務:程序記憶體中儲存狀态,比如戰鬥服務将戰鬥資訊(玩家角色狀态、小怪狀态等)儲存在記憶體中,玩家操作或者戰鬥邏輯會改變戰鬥資訊。由于遊戲中狀态比較複雜,業務改變狀态頻率比較高,遊戲大部分的業務都是用有狀态服務的方式提供。
  • 無狀态服務:服務隻處理流程,不儲存資料,一般資料會儲存到後端db中,這種服務的邏輯一般會有很多db操作。這種類型的服務在網際網路web行業用的比較多,遊戲中常見的比如充值、登陸等。(無狀态服務在執行一個流程進行中可能會有一些臨時變量申請記憶體)

無狀态服務本身不儲存狀态,是以程序crash也不會丢失資訊。此外,下文将介紹,由于使用随機配置設定的路由方式,無狀态服務對異常的容忍更加好,是以,從高可用角度,無狀态更加好。

但是,由于無狀态不儲存狀态,所有狀态操作都是資料庫操作,就造成了開發成本更高(代碼寫起來更複雜)、資料庫壓力更大,是以無狀态并不适合所有服務,一般對于狀态簡單明确的服務,可以優先使用無狀态,比如好友服務。

1.2 路由政策

對于有狀态和無狀态服務,他們使用的路由方式也不同。

對于無狀态服務,一般使用 随機配置設定 的路由方式。随機配置設定的路由方式有很大的好處,如果某個程序crash了或者網絡出現了故障,我們隻需要把這個程序從路由中去掉,對後續的請求不會有影響,隻會影響此程序目前正在處理的邏輯。

有狀态服務的路由需要明确每個請求給哪個程序處理,給其他程序其他程序因為沒有相關狀态資訊也無法處理。比如上文提到的戰鬥服務,路由根據戰鬥ID将相關請求發給對應戰鬥所在的程序中才能處理。路由一般使用 取模 或者 一緻性哈希 ,一般會優先使用一緻性哈希而不是取模,防止故障引起抖動。

1.3 舉個例子

下圖是某遊戲架構的簡化版模型,真實的伺服器比這個複雜很多,這裡主要是為了舉個例子。

叢集可以分為三類:支援水準擴充的有狀态服務、支援水準擴充的無狀态服務、不支援水準擴充的單點服務

其中,支援水準擴充的有狀态服務和無狀态服務的程序數量能占到項目的90%,單點服務很少。在這種架構情況下,我們遊戲的承載上限瓶頸在于單點服務,而單點服務邏輯相對比較簡單,承載上限很高。此外,支援水準擴充的服務程序出現異常隻會影響此程序所服務的玩家,具有較高的可用性。

有狀态服務

在我們遊戲的伺服器叢集中,三分之二左右的程序是處理玩家個人邏輯的程序(玩家叢集,很多遊戲項目叫大廳伺服器)。每個程序處理一部分玩家的業務邏輯,通過shading将玩家配置設定在不同的玩家程序中。

可以通過增加個人邏輯程序數量提升伺服器承載量,我們支援不停服增加或減少程序即動态擴容縮容。,這些程序之間就是平等的,不同程序之間沒有強依賴關系。當一個程序crash時,他不會影響其他程序的玩家。

除了玩家程序,還有戰鬥程序、家族程序等類似程序可以這麼設計。

上面提到的都是有狀态服務,我們需要記錄每個玩家/戰鬥/家族在哪個程序中,此外,若程序出現異常,雖然不會影響其他玩家/戰鬥/家族,但目前程序中玩家/戰鬥/家族都會不可用,而且會丢失一些資料。

無狀态服務

我們将部分服務使用無狀态實作,比如登陸、支付、好友、部分排行榜等。由于無狀态服務具相對于有狀态對異常更友好、動态擴容縮容模型 更簡單,是以有對于一個新的服務我們優先考慮使用無狀态,若狀态較複雜才考慮使用有狀态服務實作。

單點服務

遊戲服務中難免出現一些單點服務,比如玩家管理器、叢集管理器、家族管理器等,這類服務不具擴充能力,是遊戲伺服器的承載瓶頸。此外,也不具有高可用性,如果出現異常會導緻導緻整個遊戲叢集不可用。

單點服務邏輯普遍簡單(複雜邏輯我們都要支援水準擴容),性能承載普遍較高。比如,我們遊戲中目前評估的同時線上保守估計應該在50w,此時我們認為我們的一些單點服務會出現滿載,導緻遊戲無法繼續擴容。

此外,單點服務數量較少,出現異常的可能性很低。我們遊戲上線近兩年,我們也隻遇到了兩次機器當機,影響的都是非單點程序,沒有影響整體的遊戲叢集可用性。

當然,單點服務也可以改為支援水準擴充的,隻是工作量的問題。理論是來說,是能完全消除單點的,隻是對于大部分項目來說成本效益不高意義不大。

二、高并發

水準擴充的方案是支援高并發的主要手段(也叫可伸縮Scalability),上文已經介紹過了。

下文主要介紹除了水準擴充高并發的其他方案,以及需要注意的點。

2.1 垂直擴充和性能優化

要提升承載能力,一般有兩個方案:

  • 水準擴充:通過增加機器數量提升承載能力。
  • 垂直擴充:通過增加機器配置提升承載能力。讓一個機器/線程可以承載更多的玩家。

垂直擴充在某些場景也有有用。一般來說,對于上文我們提到的單點,如果不好消除或者消除成本很高,可以通過垂直擴充把這個邏輯放在高配機器上,提升單點邏輯的承載。

此外,我們常常是對戰鬥服進行性能優化,比如使用C++寫高消耗子產品,但對于大廳服一般不将其作為提升承載的首要手段。這個我們不深入讨論,一方面這個總會有上限,難有質變,另一方面不同遊戲優化方案千差萬别,都是代碼級别的優化。

服務端優化和垂直擴充的目标類似,就是讓一台機器能承載更多的玩家/邏輯。

2.2 消除系統單點和邏輯單點

上文介紹的消除單點主要是系統單點,也就是用多個程序而不是一個程序提供服務。

消除系統單點的前提是消除邏輯單點。

舉個例子:我們投放一個武器時,要給這個武器生成一個全服唯一的ID以辨別此武器。這個ID可以使用一個自增的ID,此時就造成邏輯單點。

對于這種情況,如果遊戲中生成武器的頻率很低,那麼這種方案也可以,但如果武器生成頻率很高,因為遊戲中所有邏輯都需要去一個地方去申請這個ID,那就可能産生瓶頸。對于這種情況,我們一般可以使用uuid代替自增ID。(這個場景也常見于DB中的自增列,是以一般建議少使用自增)

2.3 資料庫承載

當玩家線上量達到一定的量級以後,往往對後端的資料庫造成很大的壓力。

一般來說,資料庫本身具有水準擴充能力,加上分庫分表等方案,提高承載能力比較容易。但設計資料庫結構時也需要考慮索引、shadkey等問題,不然嚴重影響資料庫性能。此外,要考慮資料庫并發讀寫能力,比如mongo中的MMAPv1存儲引擎是collection級别鎖,而WiredTiger存儲引擎是doc級别鎖,兩者的并發能力差别極大。

遊戲邏輯普遍比較複雜,資料讀寫量很大,如果每次玩家資訊變更都去讀寫資料庫,會造成較大的資料庫壓力。是以,遊戲的玩家服務一般都是有狀态服務,玩家上線時将資料從資料庫讀到記憶體中,線上期間讀寫資料都是直接操作記憶體,下線時或隔段時間去落地到資料庫。這種方案大大降低了資料庫讀寫操作,對資料庫壓力會小很多。

而一些資料讀寫操作頻率較低的服務,可以考慮将服務做成無狀态然後每次讀寫都去操作資料庫。

2.4 多叢集和跨叢集

當遊戲服務端達到一定的規模後,往往需要分叢集部署,分叢集解決的場景:

  • 單叢集的承載具有上限,比如skynet隻支援256個程序。
  • 多區服需求,每個叢集對應遊戲的一個區服。如果遊戲支援多區服并且完全隔離沒有跨服通信,實作高并發會容易很多。
  • 全球通服,某些叢集希望部署到玩家所在地區。比如美國玩家所用的戰鬥服部署在美國,東南亞的戰鬥服部署在東南亞,而他們共用大廳服部署在某地。

多叢集中需要解決的一個問題是跨叢集通信問題,叢集内一般是程序間全連接配接,但叢集間如果程序全連接配接會造成拓撲混亂連接配接數量爆炸的問題,是以叢集間通信一般使用消息總線,所有的叢集通過消息總線進行通信。

2.5 臨時的高并發

在遊戲業務場景中,玩家的線上和時間、活動等關系很大,不同的時間線上數量可能有幾倍幾十倍的差别。

對于預期内的高流量,可以通過提前做好擴容來進行承載,參考《忍三的服務端優化》中的“動态擴容和縮容”。

對于非預期的瞬間高并發,可以通過排隊系統将流量卡在系統外,動态擴容後再慢慢的進入遊戲中。

2.6 戰鬥場景中的高并發

遊戲還有一個特殊的高并發場景,就是MMO的大規模玩家在某場景聚集,比如國戰。

這種場景沒有完美的解決方案,隻能盡量的提高承載量,常見的提高方案有:

  • 将一個場景切分為cell,不同cell放到不同程序。比如bigworld/kbengine和最新的SpatialOS。
  • 提升單程序承載能力。比如邏輯優化、垂直擴充、使用 C++ 寫遊戲邏輯等, C++ 和python比起來性能有數量級的差别。
  • 服務降級:簡化遊戲邏輯,比如國戰時一般隻要玩家覺得場面熱鬧就差不多了,很多戰鬥邏輯其實都簡化掉了。
  • 分服/分線/分副本:業務上讓玩家隔離。

水風:遊戲的數值系統的實作和演化

三、高可用

高可用追求系統在運作過程中盡量少地出現系統服務不可用的情況。

評價名額是服務在一個周期内的可用時間(SLA, Service Level Agreement),計算公式為服務可用性=(服務周期總分鐘數 - 服務不可用分鐘數)/服務周期總分鐘數×100% 。

一般從兩個次元進行評價:1.系統的完全可用:所有服務對于所有使用者都是可用的。2.系統的整體可用:部分服務或者部分使用者不可用,但系統整體可用。

高可用的目标是争取系統的完全可用,保證整體可用。

大叢集下的異常

由于機器故障、網絡卡頓或斷線等客觀存在的小機率異常情況,服務端也需要考慮這些問題,尤其是在大叢集場景下,小機率事件累加變成了大機率事件,是以在大叢集伺服器場景下,高可用是我們必須要考慮的問題。

高可用,其實就是對各種異常狀況的隔離和處理,不讓小機率異常事件影響遊戲的整體服務。

常見的異常有以下幾種:

  • 機器/程序/網絡異常:阿裡雲機器的可用性承諾是99.975%,大概是每台機器保證一年的不可用時間在1小時以内。若叢集使用一百台機器,理論上來說最差的情況是每三天就有一台機器一小時不可用。當然,真實的可用性比阿裡雲承諾的好很多,我們遊戲大概有100台ECS機器,一年中有兩台機器因為機器故障自動重新開機,并沒有出現過持續的不可用。
  • Saas服務/DB異常:因為我們大數量使用了阿裡雲的mysql、redis等雲服務,這些服務本身也有可用性問題,導緻主從切換等。我們遊戲最常遇到的問題是因為redis主從切換造成網絡閃斷需要網絡重連。
  • 業務BUG:對于業務BUG,盡量有辦法減少bug的影響,不讓某些小BUG導緻系統整體不可用。
  • 突發性能熱點:玩家的正常行為或異常行為導緻的業務突發繁忙,主要是上文所說的高并發問題。此外,對于某些玩家的異常行為(比如外挂\DDOS攻擊),也要保證不會影響系統整體可用。我記得很早之前(傳奇年代),有些外挂能直接讓伺服器重新開機。

3.1 基于水準擴充實作高可用

上文中我們提到了水準擴充可以提高并發承載量,同時可以提高可用性,但側重點不同。對于高并發,水準擴充表示我們可以通過增加機器/程序提高承載量。對于高可用,是說當機器/程序出現異常或者崩潰時,不會影響叢集的整體可用。

在上文水準擴充中已經介紹,對于支援水準擴充的服務,有狀态服務出現異常隻會影響到此程序所提供的服務,其他程序正常運作;對于無狀态服務,影響更小,隻會影響到正在執行的流程。

當然,這需要我們寫一些處理邏輯,包括:

  • 異常監控:通過異常監控,可以快速發現異常,一般使用心跳或者消息逾時機制。
  • 異常處理:比如一個消息逾時後如何處理,是重視還是忽略。如果一個程序不可用,我們需要将此程序踢出叢集。
  • 服務恢複:對于無狀态,直接重新開機即可。對于有狀态,可以将狀态遷移到其他程序提供服務。服務恢複有很多坑(有狀态服務更多),常見的恢複方案比如将所服務的玩家直接踢下線,然後重新登陸。
  • 服務降級:服務降級常見的排隊系統、關閉指定功能等。

如何實作上述邏輯其實挺複雜的,這裡不詳細介紹了。

服務隔離和灰階釋出

開發過程中,我們應該将大功能盡量的拆成一個個小的服務,每個服務隻負責一小塊功能。Skynet也提供了比較好的Service模式,不同的Service可以放在一個程序中,也可以放在不同的程序中。

前文已經介紹服務隔離和灰階釋出,也是為了将高風險的服務進行隔離,讓它即使出現了問題也不要影響到系統的整體可用。

3.2 主從複制

對于有狀态服務,支援水準擴充的程序可以做到一個程序出現異常不影響其他程序提供服務,但這個程序crash了會導緻這個程序提供的服務不可用,并且造成記憶體中的資料丢失等問題。

為了解決這個,常見的方案是主從複制。主從複制在資料庫中非常常見,是保證資料庫高可用性的常見方案。

主從複制就是在主節點(master)後面挂一個或者多個從節點(slave),主節點實時地将狀态/資料複制到從節點。平時是主節點提供服務,當主節點出現問題時,從節點變成主節點,繼續提供服務。因為主節點近乎事實的将資料複制到從節點,可以近似保證資料不丢失。

是以,如果想進一步地提升有狀态服務或者單點服務的可用性,可以使用主從複制的方案。

遊戲伺服器使用此方案寫業務邏輯的較少,有些叢集管理節點(非業務邏輯)會使用此方案。

此外,因為常見的db(mysql/mongo/redis)都自帶主從複制,是以無狀态服務其實也是将狀态讓db幫我們管理,進而獲得db主從複制帶來的資料不會丢失的能力。

3.3 雲服務的異常處理

除了ECS機器,我們大量使用了阿裡雲的各類SAAS服務,比如redis/Mysql/Mongo等DB,也有類似于ELK的日志服務等。

這些服務大部分都支援主從切換等高可用方案,但我們需要考慮當他們進行主從切換時對我們系統産生的影響。

在Mysql和Redis中,當發生主從切換或gateproxy當機,會導緻網絡連接配接斷線,是以,我們必須在邏輯中處理網絡中斷并重連。在網絡斷線重連階段,必然導緻某些db請求失敗,我們也需要處理這種異常問題。

在資料落地場景中,需要判斷每次db請求是否成功,若不成功進行重試并且要保證請求的幂等性以防止請求多次執行。

四、高并發和高可用的目标

為了實作高并發和高可用本身具有較大的開發成本,在大部分項目中人力資源也不是無限的。是以大家在做相關工作的時候,也要過度設計,綜合考慮業務需求、承載預期和開發成本。

其實我說的開發成本不僅是說程式開發量更大,更多的是越複雜的系統越容易出現問題,如果沒有足夠的人力去測試、維護和疊代,還不如用更簡單的方式實作,反而出問題的機率更低。

當然,如果你的項目組是王者榮耀和和平精英,高并發和高可用要求非常高,人力近乎無限,請忽略此段。

百萬同時線上

在遊戲行業中,一般将百萬同時線上作為遊戲服務端架構的并發目标,百萬的數量級也是絕大部分遊戲(除了王者和吃雞)的上限。

是以,在遊戲前期架構設計、規劃和壓測中,我們可以按照百萬同時線上作為基準去預估不同的系統所需要的承載量,達到這個承載量就可以了。

比如我們遊戲,雖然也有不少單點和性能瓶頸,但根據我們的預估,即使這些單點存在,我們也能通過加機器支援到100w的同時線上。那麼,這些單點和瓶頸就在我們的預期範圍内,我們就不會進一步去優化了。

如果哪天我們遊戲大火需要支援千萬的同時線上了,理論上也可以繼續消除單點和優化性能瓶頸,但成本會大幅度增加。

高可用 != 完全可用

我們追求伺服器叢集的高可用,并不是要求對于所有的異常都能容災,那是不可能的,也是不現實的。

按照skynet的思想,若某節點出現異常沒有及時響應,所有通路它的請求都會堵塞而不會timeout,若節點挂了,會直接報trace。相當于skynet把叢集看為一個整體,沒有做容錯機制,若某個核心節點挂了,就應該叢集整體不可用。

我上文說過,整體上我認可skynet的思想,可以有效地降低業務開發時的思想負擔。在這個基礎上,對于某些常見的異常,應該盡量降低影響,避免雪崩效應。

技術以外的高并發和高可用

高并發和高可用,和非技術也有較大的關系,比如運維能力、硬體情況,人員素質、管理水準等。