LoadBalancing Application
這篇文章解釋了LoadBalancing應用程序的伺服器端執行。
概念
LoadBalancing應用程序擴展了Hive框架,執行了Rooms、Events、Properties等功能,並增加了一層可擴展性,使你可以在多個伺服器上運行該應用程序。
基本設置很簡單。
總是有1個主伺服器和1...N個遊戲伺服器。
LoadBalancing還增加了大廳支持和匹配能力。
 
    主伺服器處理這些任務:
- 追蹤當前在遊戲伺服器上打開的遊戲。
- 跟蹤連接的遊戲伺服器的工作量,並將對等人分配到適當的遊戲伺服器上。
- 保持並更新 "大廳 "中可供客戶使用的房間列表。
- 為客戶找到房間(隨機或按名稱),並將遊戲伺服器地址轉發給他們。
遊戲伺服器處理這些任務:
- 主持遊戲房間。
- 定期向主伺服器報告他們當前的工作負荷和遊戲列表。
與Photon Cloud的差異
負載平衡應用程序提供的邏輯與Photon實時雲端服務幾乎相同。
一些作為Cloud服務運行的要求並不適用於具有自定義邏輯的特殊伺服器,所以它們被移除。
這也大大簡化了代碼。
- 沒有虛擬應用程序。每個LB實例只有一個遊戲邏輯在運行。操作中的AppId參數Authenticate被忽略。
- 沒有通過AppVersion參數的玩家分離。這也是虛擬應用程序的一部分。
你可以閱讀更多的差異點此。
基本工作流程
從客戶端的角度來看,工作流程也很簡單。
客戶端連接到主伺服器,在那裡他們可以加入大廳並檢索開放遊戲的列表。
當他們在主伺服器上調用CreateGame操作時,遊戲實際上並沒有被創建-主伺服器只是確定工作量最小的遊戲伺服器並將其IP返回給客戶端。
當客戶端在主伺服器上調用JoinGame或JoinRandomGame操作時,主伺服器會查找正在運行遊戲的遊戲伺服器,並將其IP返回給客戶端。
客戶端斷開與主伺服器的連接,用它剛收到的IP連接到遊戲伺服器,並再次調用CreateGame或JoinGame操作。
 
    主伺服器
本節解釋了主伺服器的執行-見"\src-server\Loadbalancing\Loadbalancing.sln "解決方案中的LoadBalancing.MasterServer命名空間。
MasterApplication 決定傳入的連接是由遊戲客戶端(在 "客戶端端口")還是由遊戲伺服器(在 "遊戲伺服器端口")發起的。
主伺服器: 處理客戶端同伴
MasterClientPeer代表與主伺服器的客戶端連接。
下列操作對MasterClientPeer是可用的:
- Authenticate
 Authenticate操作只有一個假的執行。
 開發者應該把它作為一個起點來執行他自己的認証機制:
C#
    // MasterClientPeer.cs: 
    private OperationResponse HandleAuthenticate(OperationRequest operationRequest)
    {
        OperationResponse response;
        var request = new AuthenticateRequest(this.Protocol, operationRequest);
        if (!OperationHelper.ValidateOperation(request, log, out response))
        {
            return response;
        }
        
        this.UserId = request.UserId;
        // publish operation response
        var responseObject = new AuthenticateResponse { QueuePosition = 0 };
        return new OperationResponse(operationRequest.OperationCode, responseObject);
    }
- JoinLobby 
 JoinLobby操作用於將MasterClientPeer添加到AppLobby中,AppLobby包含一個GameList-任何遊戲伺服器上所有開放遊戲的列表。
 對象收到一個初始的GameListEvent,其中包含GameList中的當前遊戲列表(由JoinLobby操作的可選屬性過濾)。
 之後,定期向客戶端發送- GameListUpdateEvent,其中包含變化的遊戲列表(同樣由JoinLobby操作的可選屬性過濾)。
 只要客戶端處於連接狀態,它就會收到更新事件。- C# - // AppLobby.cs: protected virtual OperationResponse HandleJoinLobby(MasterClientPeer peer, OperationRequest operationRequest, SendParameters sendParameters) { // validate operation var operation = new JoinLobbyRequest(peer.Protocol, operationRequest); OperationResponse response; if (OperationHelper.ValidateOperation(operation, log, out response) == false) { return response; } peer.GameChannelSubscription = null; var subscription = this.GameList.AddSubscription(peer, operation.GameProperties, operation.GameListCount); peer.GameChannelSubscription = subscription; peer.SendOperationResponse(new OperationResponse(operationRequest.OperationCode), sendParameters); // publish game list to peer after the response has been sent var gameList = subscription.GetGameList(); var e = new GameListEvent { Data = gameList }; var eventData = new EventData((byte)EventCode.GameList, e); peer.SendEvent(eventData, new SendParameters()); return null; }
- JoinGame / JoinRandomGame 
 當客戶端想加入AppLobby的GameList中列出的現有遊戲時,JoinGame操作被調用,該遊戲由一個唯一的GameId指定。
 如果遊戲存在並且允許對象加入,主伺服器會將遊戲伺服器的IP返回給客戶端,該遊戲在該伺服器上實際正在運行。- 主伺服器還更新遊戲狀態,並將對象添加到其 "加入的對象 "列表中。 
 一旦它加入了遊戲伺服器上的遊戲(或經過一定的超時),它將被刪除。
 這樣,主伺服器就可以跟蹤那些在主伺服器和遊戲伺服器之間過渡的對象。- JoinRandomGame的工作方式類似,只是遊戲是由主伺服器隨機選擇的,GameId會返回給客戶端。 
- CreateGame 
 當客戶端想要創建一個新遊戲時,CreateGame操作被調用。
 主伺服器確定一個遊戲伺服器,新遊戲將在這個伺服器上創建,並將遊戲伺服器的IP返回給客戶端。
 詳情見"LoadBalancing Algorithm "部分。- 此外,一個GameState對象被創建並添加到GameList中,該對象被存儲為 "加入對象"。 
 注意,這個GameState僅用於追蹤遊戲-遊戲本身只存在於遊戲伺服器上。
處理遊戲伺服器對象
主伺服器總是知道哪些遊戲伺服器是可用的,它們承載了多少遊戲以及當前的工作量如何。
為了達到這個目的,每個遊戲伺服器在啟動時都會連接到主伺服器。
MasterApplication 維護一個 GameServerCollection,其中存儲有 IncomingGameServerPeers。
遊戲伺服器只能調用一個操作:
- RegisterGameServer
 遊戲伺服器在連接到主伺服器後調用一次RegisterGameServer操作。
 遊戲伺服器會被添加到主伺服器的GameServerCollection和它的負載平衡器中(見"負載平衡算法")。
 它將在斷開連接時從GameServerCollection中刪除。
檢查"遊戲伺服器 "部分,看看遊戲伺服器如何向主伺服器發送有關其遊戲和工作量的進一步更新。
遊戲伺服器
本節描述了遊戲伺服器的執行。
見"\src-server\Loadbalancing\Loadbalancing.sln "解決方案中的LoadBalancing.GameServer命名空間。
遊戲伺服器:處理客戶端的對象
一旦客戶端從主控端獲得了遊戲伺服器的地址,客戶端就可以調用Hive中可用的遊戲伺服器上的任何操作(同時加入房間)。
唯一不同的是,我們為遊戲伺服器上的JoinGame和CreateGame設置了單獨的操作代碼。
向主伺服器報告遊戲狀態
與主伺服器的連接在遊戲伺服器中被表示為 OutgoingMasterServerPeer。
一旦連接建立,遊戲伺服器就會在主伺服器上調用一個Register操作。
之後,遊戲伺服器將所有現有的遊戲狀態發布到主伺服器上:
C#
// OutgoingMasterServerPeer.cs: 
protected virtual void HandleRegisterGameServerResponse(OperationResponse operationResponse)
{
    // [...]
    
    switch (operationResponse.ReturnCode)
    {
    case (short)ErrorCode.Ok:
        {
            log.InfoFormat("Successfully registered at master server: serverId={0}", GameApplication.ServerId);
            this.IsRegistered = true;
            this.UpdateAllGameStates();
            this.StartUpdateLoop();
            break;
        }
    }
}
這是通過向每個遊戲發送一個消息,告訴它將其遊戲狀態發送給主伺服器來執行:
C#
// OutgoingMasterServerPeer.cs:
public virtual void UpdateAllGameStates()
{
    // [...]
    
    foreach (var gameId in GameCache.Instance.GetRoomNames())
    {
        Room room; 
        if (GameCache.Instance.TryGetRoomWithoutReference(gameId, out room))
        {
            room.EnqueueMessage(new RoomMessage((byte)GameMessageCodes.ReinitializeGameStateOnMaster));
        }                
    }
}
遊戲在 ProcessMessage方法中處理,並調用 UpdateGameStateOnMaster 方法,向主伺服器發送UpdateGameEvent:
C#
 protected virtual void UpdateGameStateOnMaster(
            byte? newMaxPlayer = null, 
            bool? newIsOpen = null,
            bool? newIsVisble = null,
            object[] lobbyPropertyFilter = null,
            Hashtable gameProperties = null, 
            string newPeerId = null, 
            string removedPeerId = null, 
            bool reinitialize = false)
        {            
            // [...]
            
            var e = this.CreateUpdateGameEvent();
            e.Reinitialize = reinitialize;
            e.MaxPlayers = newMaxPlayer;
            // [ ... more event data is set here ... ]
            
            var eventData = new EventData((byte)ServerEventCode.UpdateGameState, e);
            GameApplication.Instance.MasterPeer.SendEvent(eventData, new SendParameters());
        }
}
每當一個遊戲被客戶端創建、加入或離開,或其屬性被改變時,遊戲狀態也會在主控端上更新。
負載平衡的執行
下一節描述了遊戲伺服器如何向主伺服器報告它們當前的工作負載
以及主伺服器如何確定最適合處理新的CreateGame請求的遊戲伺服器-實際的負載平衡算法。
確定工作負載
參見"\src-server\Loadbalancing\Loadbalancing.sln "解決方案中的LoadBalancing.LoadShedding命名空間,了解執行細節。
遊戲伺服器會定期向主伺服器報告它們當前的工作負載。
工作負載包括,例如:
- CPU使用率
- 流量
- 一些Photon的特定值,如ENet+隊列長度,伺服器在每個請求上花費的平均時間,等等。
- 延遲(向自己發送請求時)
最重要的(也是最容易理解的)因素是CPU負載,所以我們在本文檔中主要討論CPU負載。
所有這些因素都歸納為一個單一的數值-遊戲伺服器的 "負載水平",它被報告給主伺服器。
負載水平越低,遊戲伺服器就越適合主持新遊戲。
執行細節
遊戲伺服器收集有關上述因素的 "反饋"。
每個因素都有一個FeedbackController對象-它由一個FeedbackName和一個FeedbackLevel組成:
C#
internal enum FeedbackName
{
    CpuUsage,     
    Bandwidth,
    TimeSpentInServer
}
    
public enum FeedbackLevel
{
    Highest = 4, 
    High = 3, 
    Normal = 2, 
    Low = 1, 
    Lowest = 0
}
DefaultConfiguration類定義了每個值的閾值。
例如,一台伺服器在CPU使用率達到20%時有 "最低 "的FeedbackLevel,在CPU達到90%時達到 "最高 "的FeedbackLevel,以此類推。
C#
// DefaultConfiguration.cs: 
internal class DefaultConfiguration
{
    internal static List<FeedbackController> GetDefaultControllers()
    {
        var cpuController = new FeedbackController(
        FeedbackName.CpuUsage,
        new Dictionary<FeedbackLevel, int>
                {
                    { FeedbackLevel.Lowest, 20 },
                    { FeedbackLevel.Low, 35 },
                    { FeedbackLevel.Normal, 50 },
                    { FeedbackLevel.High, 70 },
                    { FeedbackLevel.Highest, 90 }
                },
        0,
        FeedbackLevel.Lowest);
    // [...]
}
這些值也可以在 "workload.config "文件中進行配置。
參見"workload.config 範例"。
LoadBalancing.LoadShedding.Configuration 命名空間負責從配置文件中讀取數值,如果沒有配置文件,則應用DefaultConfiguration。
每隔一段時間,遊戲伺服器會檢查一些Windows性能計數器,為其所有FeedbackControllers設置當前值,並計算新的 "整體反饋"。
這是在WorkloadController類中完成的:
C#
private void Update()
{
    FeedbackLevel oldValue = this.feedbackControlSystem.Output;
    if (this.cpuCounter.InstanceExists)
    {
        var cpuUsage = (int)this.cpuCounter.GetNextAverage();
        Counter.CpuAvg.RawValue = cpuUsage;
        this.feedbackControlSystem.SetCpuUsage(cpuUsage);
    }
    
    // [...]
    
    if (this.timeSpentInServerInCounter.InstanceExists && this.timeSpentInServerOutCounter.InstanceExists)
    {
        var timeSpentInServer = (int)this.timeSpentInServerInCounter.GetNextAverage() + (int)this.timeSpentInServerOutCounter.GetNextAverage();
        Counter.TimeInServerInAndOutAvg.RawValue = timeSpentInServer;
        this.feedbackControlSystem.SetTimeSpentInServer(timeSpentInServer); 
    }
    
    this.FeedbackLevel = this.feedbackControlSystem.Output;
    Counter.LoadLevel.RawValue = (byte)this.FeedbackLevel; 
    if (oldValue != this.FeedbackLevel)
    {
        if (log.IsInfoEnabled)
        {
            log.InfoFormat("FeedbackLevel changed: old={0}, new={1}", oldValue, this.FeedbackLevel);
        }
        this.RaiseFeedbacklevelChanged();
    }
}
如果整體反饋水平發生變化,OutgoingMasterServerPeer將向主伺服器報告新的伺服器狀態:
C#
 public void UpdateServerState()
 {
    // [...]
    this.UpdateServerState(
        GameApplication.Instance.WorkloadController.FeedbackLevel,
        GameApplication.Instance.PeerCount,
        GameApplication.Instance.WorkloadController.ServerState);
}
workload.config 範例
ㄒ如果您想知道我們是如何配置流量的工作負載的,那麼只要把最大流量的90%作為 "最高",70%作為 "高",50%作為 "正常",35%作為 "低",20%作為 "最低"。
但是,如果您不想使用這些百分比值,您可以隨意聲明最低/低/正常/高/最高。
如果您的伺服器的帶寬是20 Mbps(20000000 b/s),那麼您可以有以下數值:
- 最低:4000000
- 低:7000000
- 正常:10000000
- 高:14000000
- 最高: 18000000
XML
<?xml version="1.0" encoding="utf-8" ?>
<FeedbackControlSystem>
  <FeedbackControllers>
    <add Name="Bandwidth" InitialInput="0" InitialLevel="Lowest">
      <FeedbackLevels>
        <add Level="Lowest" Value="4000000"/>
        <add Level="Low" Value="7000000"/>
        <add Level="Normal" Value="10000000"/>
        <add Level="High" Value="14000000"/>
        <add Level="Highest" Value="18000000"/>
      </FeedbackLevels>
    </add>
  </FeedbackControllers>
</FeedbackControlSystem>
只有 "最高" 會有後果,如果它的值被超過,那麼在這個遊戲伺服器上就不會有更多遊戲被創建。
如果您想使用更高的限制,您應該測試應用程序是否能夠處理信息的數量。
負載平衡算法
參見"\src-server\Loadbalancing\Loadbalancing.sln "解決方案中的LoadBalancing.LoadBalancer類,以了解執行細節。
主伺服器在 LoadBalancer 類中存儲每個遊戲伺服器的LoadLevel。
它還持有一個額外的列表,其中包括所有目前擁有最低負載水平的伺服器。
每當客戶端調用CreateGame 操作時,主伺服器就會從LoadBalancer中獲取負載最低的伺服器的地址,並將其返回給客戶端,然後客戶端就會連接到該伺服器。
配置和部署
出於演示的目的,SDK在其部署目錄中包含了1個主伺服器和1個遊戲伺服器的設置:
- "deploy\LoadBalancing\Master"
- "deploy\LoadBalancing\GameServer"
這個設置只用於本地開發。
部署一個遊戲伺服器
當您把您的LoadBalancing項目部署到生產伺服器上時,您不應該在一台伺服器上主持兩個遊戲伺服器應用程序。
您需要確保這些遊戲伺服器能夠在主伺服器上注冊。
您還需要確保遊戲客戶端能夠索取遊戲伺服器。
在每個遊戲伺服器上,您需要設置該遊戲伺服器的公共IP地址。
如果您把這個值留空,公共IP地址將被自動檢測。
XML
<Photon.LoadBalancing.GameServer.GameServerSettings>
      <setting name="MasterIPAddress" serializeAs="String">
        <value>127.0.0.1</value>
<setting>      
    <setting name="PublicIPAddress" serializeAs="String">
        <value>127.0.0.1</value>
        
        <!-- use this to auto-detect the PublicIPAddress: -->
        <!-- <value></value> -->
      </setting>
      
      <!-- [...] -->
</Photon.LoadBalancing.GameServer.GameServerSettings>
您也可以用Photon Control來設置一個公共IP。
部署一個主伺服器
您需要確保您只有一個主伺服器:要麼從您的遊戲伺服器上的PhotonServer.config中刪除所有 "主 "應用程序的設置,要麼至少確保您的遊戲伺服器和客戶端都使用相同的IP連接到同一個單一的主伺服器。
否則,在主伺服器上無法特殊配置。
將遊戲伺服器從輪替中取出
正如在"Loadbalancing Implementation "一節中所討論的,主伺服器知道遊戲伺服器的狀態,正如 "ServerState "舉例中所指出的。
- "在線"(這是默認的)。
- "非輪替"(=開放的遊戲仍然列在大廳中,玩家可以加入該伺服器上的現有遊戲,但不會創建新遊戲)
- "離線"(該伺服器上的現有遊戲不能被加入,並且沒有新的遊戲在該GS上創建)。
GS定期向主伺服器發送他們的伺服器狀態-見 OutgoingMasterServerPeer  類別:
C#
public void UpdateServerState()
{
  if (this.Connected == false)
  {
    return;
  }
  this.UpdateServerState(
                this.application.WorkloadController.FeedbackLevel,
                this.application.PeerCount,
                this.application.WorkloadController.ServerState);
}
如果您想以編程方式設置伺服器狀態,您需要做的是:
- 修改WorkloadController類別,使其能夠確定當前的伺服器狀態
- 例如,您可以添加一個 "文件檢視者",從一個文本文件中讀取伺服器狀態(0 / 1 / 2)。
您也可以建立一個從客戶端調用的操作,從數據庫或任何您想到的地方讀取。
Back to top