This document is about: SERVER 4
SWITCH TO

ロードバランシングアプリケーション

本稿では、サーバ-ーサイドのLoadBalancingアプリケーションの実装について説明します。

コンセプト

ロードバランシングアプリケーションは、Hiveフレームワークと実装機能(ルーム、イベント、プロパティなど)を拡張するもので、複数サーバーでのアプリケーションの実行を可能にする拡張レイヤーが加えられました。

基本設定は非常に簡単です: マスターサーバーは常に1台で、ゲームサーバーは1台からN台まで存在します。

ロードバランシングにはロビーサポートとマッチメイキング機能も追加されました。

photon server concept: loadbalancing setup
Photon Serverのコンセプト: ロードバランシングの設定

マスターサーバーは以下のタスクを処理します:

  • ゲームサーバー上で現在オープンになっているゲームの履歴を保持します。
  • 接続したゲームサーバーの負荷の履歴を保持し、適切なゲームサーバーにピアを割り当てます。
  • クライアントが利用できるルームのリストを「ロビー」に保持し、アップデートします。
  • クライアント向けにルームを検索し(ランダムまたは名前で)、クライアントにゲームサーバーのアドレスを転送します。

ゲームサーバーは以下のタスクを処理します:

  • ゲームルームのホスティング。
  • ゲームサーバーの最新の負荷と、ゲームサーバーのゲームのリストをマスターサーバーに定期的に報告します。

Photon Cloudとの違い

ロードバランシングアプリケーションは、Photon Realtime Cloudサービスとほぼ同じロジックを提供しています。 Cloudサービスとして作動するために必要でカスタムロジックの特殊なサーバーに適用できない要件は取り除かれました。 これにより、コードが大幅に簡素化されました。

  • 仮想アプリケーションはありません。1つのLBインスタンスに対してゲームロジックが1つのみ実行されます。操作Authenticate内のAppIDパラメータは無視されます。
  • AppVersionパラメータによってプレイヤーは分離されません。これは、仮想アプリの一部です。

これらの違いについて、詳細は こちらを参照してください。

基本ワークフロー

クライアント側の観点からのワークフローも非常に簡潔です:

クライアントはマスターサーバーに接続し、ロビーに入ってオープンになっているゲームのリストを取得します。 マスターでCreateGame操作を呼ぶと、ゲームは実際には作成されませんーマスターサーバーはゲームサーバーに対して最小負荷のみを判定し、そのIPをクライアントに返します。

クライアントがマスター上でJoinGameまたはJoinRandomGame操作を呼ぶと、マスターはゲームが実行中のゲームサーバーを調査し、そのIPをクライアントに返します。

クライアントはマスターサーバーから切断し、受信したばかりのIPでゲームサーバーに接続して、再びCreateGameまたはJoinGame操作を呼びます。

photon server: loadbalancing sequence diagram
Photon Server: ロードバランシング・シーケンスダイアグラム

マスターサーバー

このセクションでは、マスターサーバーの実装について説明しますー「\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操作は、AppLobbyにMasterClientPeerを追加するために使用されます。AppLobbyにはGameList、すなわちすべてのゲームサーバーでオープンとなっているすべてのゲームのリストが含まれます。 ピアは初期のGameListEventを取得し、これにはGameList内にあるゲームの最新のリスト(JoinLobby 操作のオプションのプロパティでフィルタリングされます)が含まれます。 その後、変更されたゲームのリストを含むGameListUpdateEventJoinLobby 操作のオプションのプロパティでフィルタリングされます)が一定の間隔でクライアントに送信されます。 クライアントは、接続中はアップデートイベントを受信します。

    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
    JoinGame操作は、AppLobbyのGameListにある、一意のGameIdで特定される既存の対戦に、クライアントが参加したい時に呼び出されます。ゲームが存在し、ピアがそれに参加することを許可されている場合、マスターサーバーはゲームが実際に実行されているゲームサーバーのIPをクライアントに返します。

マスターサーバーはGameSateもアップデートし、ピアをサーバー内の「joining peers」リストに加えます。 ゲームサーバー上のゲームに参加すると(または一定のタイムアウト後)、ピアはそこから削除されます。 このようにマスターサーバーはマスターとゲームサーバーの間を移行するピアを記録します。

ゲームがマスターサーバーによってランダムに選択され、クライアントにGameIdが返された場合を除き、JoinRandomGame は同様に動作します。

  • CreateGame CreateGame操作は、クライアントが新しいゲームを作成したい場合に呼び出されます。 マスターサーバーは、新たなゲームを作成するゲームサーバーを判定し、そのゲームサーバーのIPをクライアントに返します。 詳細は「ロードバランシングアルゴリズム」セクションを参照してください。

また、GameStateオブジェクトが作成されてGameListに追加され、ピアは「参加ピア」として保存されます。 このGameStateはゲームの記録にのみ使用されますーゲーム自体はゲームサーバー上にのみ存在します。

ゲームサーバーピアの処理

マスターサーバーは、どのゲームサーバーが利用可能か、ゲームサーバーがゲームをいくつホスティングしているか、また最新の負荷について常に把握しています。 これを実現するには、各ゲームサーバーは起動時にマスターサーバーに接続する必要があります。 MasterApplicationGameServerCollectionを管理し、このGameServerCollectionにはIncomingGameServerPeersが保存されています。

ゲームサーバーが呼び出せる操作は1つのみです:

  • RegisterGameServer ゲームサーバーはマスターサーバーに接続後、RegisterGameServer 操作を呼び出します。 ゲームサーバーはマスターのGameServerCollectionとロードバランサー (「ロードバランシング・アルゴリズム」を参照してください)に追加 されます。 ゲームサーバーは、切断時にGameServerCollectionから削除されます。

ゲームと負荷のアップデートをゲームサーバーがマスターサーバーにどのように送信するかに ついては、"ゲームサーバー"セクションを確認してください

ゲームサーバー

このセクションでは、ゲームサーバーの実装について説明します。 「\src-server\Loadbalancing\Loadbalancing.sln」ソリューション内の LoadBalancing.GameServerネームスペースを参照してください。

ゲームサーバー:クライアントピアの処理

クライアントがマスターからゲームサーバーアドレスを受信した時点で、クライアントはHiveで利用可能なゲームサーバー上で、どの操作でも呼び出すことができます(ルームに参加する際に)。 唯一違うのは、ゲームサーバーではJoinGameCreateGame操作に別々の操作コードが使われる点です。

マスターにゲームステートを報告

ゲームサーバーでは、マスターサーバーへの接続は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使用率
  • トラフィック
  • ENetおよびBusiness Queue Length、各リクエストにサーバーが要する平均時間など、Photon固有の値。
  • レイテンシー(自身にリクエストを送信している場合)

もっとも重要な(そして一番分かりやすい)要因は CPU負荷なので、ここでは CPU負荷に焦点を当てて説明します。

これらの要因はすべて、1つの値-ゲームサーバーの「負荷レベル」に集約され、マスターサーバーに報告されます。

負荷レベルが低ければ低いほど、ゲームサーバーは新しいゲームをホスティングしやすくなります。

実装の詳細

ゲームサーバーは、上記の要因について「フィードバック」を収集します。 各要因には、1つの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は「lowest」と判定され、90%ならばFeedbackLevel は「highest」と判定される、などです。

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 Performance Countersを一定の間隔で確認し、全ての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%を「Highest」、70%を「High」、50%を「Normal」、35%を「Low」、20%を「Lowest」としてください。 ただし、これらの%値を使用したくない場合には、任意の値でLowest/Low/Normal/High/Highestを定義することもできます。

サーバーの帯域幅が20Mbps (20000000 B/秒)の場合、以下の値を設定可能です:

  • Lowest: 4000000
  • Low: 7000000
  • Normal: 10000000
  • High: 14000000
  • Highest: 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>

「Highest」の場合のみ結果が生じ、値が超過する場合にはこのゲームサーバーにはもうゲームが作成されません。 さらに高い上限を使用したい場合には、そのアプリケーションがそのメッセージ数を処理できるかテストする必要があります。

ロードバランシングのアルゴリズム

実装の詳細については、「\src-server\Loadbalancing\Loadbalancing.sln」ソリューション内のLoadBalancing.LoadBalancerクラスを参照してください。

マスターサーバーは各ゲームサーバーの LoadLevel を、LoadBalancerクラスに格納しています。 またマスターサーバーは、現状の負荷レベルが最低であるすべてのサーバーの追加リストも保持しています。

クライアントが CreateGame 操作を呼び出す度に、マスターサーバーはLoadBalancerから、負荷レベルが最低のサーバーのアドレスをフェッチし、それをクライアントに返します;クライアントは、そのアドレスでゲームサーバーに接続します。

設定とデプロイメント

This setup is only intended for local development. デモ用に、SDKにはデプロイディレクトリに1つのマスターサーバーと2つのゲームサーバーのセットアップが含まれています:

  • "/deploy/LoadBalancing/Master"
  • "/deploy/LoadBalancing/GameServer"

このセットアップはローカル開発のみを目的としています。

ゲームサーバーのデプロイ

LoadBalancingプロジェクトを本番サーバーにデプロイする時は、1つのサーバーに2つのゲームサーバーアプリケーションをホスティングしてはいけません。

ゲームサーバーがマスターサーバーに登録できることを確認する必要があります。

「Photon.LoadBalancing.dll.config」にあるMasterIPAddressをマスターのパブリックIPに設定してください。

また、ゲームクライアントがゲームサーバーに到達できることを確認してください。 それぞれのゲームサーバーで、ゲームサーバーにパブリック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>

パブリックIPの設定にPhoton Controlを使用することもできます。

マスターサーバーのデプロイ

マスタサーバが1つであることを確認してください:ゲームサーバーのPhotonServer.configから「Master」アプリケーションのすべての設定を取り除くか、またはゲームサーバーと全てのクライアントが同じIPで1つのマスタサーバーに接続していることを確認してください。

それ以外は、マスタサーバー上で特別に必要な設定はありません。

ゲームサーバーをローテーションから排除

ロードバランシングの実装」セクションで説明したとおり、マスターサーバーはゲームサーバーの状態を「ServerState」 の列挙から認識します:

  • "online" (これがデフォルトです)
  • "out of rotation" (オープンなゲームはまだロビーにリストされ、プレイヤーはそのサーバー上の既存のゲームに参加できます。ただし、新しいゲームは作成されません。)
  • "offline" (そのサーバー上の既存のゲームには参加できず、またそのゲームサーバーには新しいゲームを作成できません)

ゲームサーバーは、自身の状態をマスターサーバーに定期的に送信しますーOutgoingMasterServerPeerクラスを参照してください:

C#

public void UpdateServerState()
{
  if (this.Connected == false)
  {
    return;
  }

  this.UpdateServerState(
                this.application.WorkloadController.FeedbackLevel,
                this.application.PeerCount,
                this.application.WorkloadController.ServerState);
}

サーバーの状態をプログラミングで設定するには、以下をおこなう必要があります:

  • WorkloadControllerクラスを修正して、現在のサーバーの状態を判定させます。
  • たとえば、「file watcher」を追加してテキストファイルからサーバー状態を読み込みます(0 / 1 / 2)。

クライアントから呼び出されるオペレーションを構築することもできます。またはデータベースから読み込むなど、任意の設定をおこなえます。

Back to top