LoadBalancingアプリケーション

本稿では、サーバー側のロードバランサーアプリケーションの実行について説明いたします。

目次

コンセプト

Photon3では、ロードバランサーアプリケーションは(文字通り) Lite アプリケーションを拡張するものです。 標準の Lite 機能- Rooms 、イベント、プロパティなど-に、多数のサーバーでアプリケーションの実行を可能にするスケーラビリティレイヤーが加えられました。ロードバランシングはロビーサポートとマッチメイキングの機能も与えます。Photon4ではLiteは非推奨となりHiveに変わりました。そのため現在ロードバランシングは複雑だけど拡張性の高いHiveの拡張となっています。

基本設定はとても簡単です: 常に1つのマスターサーバーに数台のゲームサーバーが繋がっているシンプルな構成です。

Photon Server Concept: LoadBalancing Setup

マスターサーバーは次のタスクを実行いたします:

  • ゲームサーバーで開戦中の対戦を記録。
  • 接続されているゲームサーバーの負荷を記録、および peer に適したゲームサーバーをアサイン。
  • 「Lobby」(ロビー)内のクライアントに対して、使用可能な room のリストを維持、アップデート。
  • クライアントに対して、 room をアサイン(ランダムまたはネーム指定)およびゲームサーバーアドレスを転送。

ゲームサーバーは次のタスクを実行いたします:

  • 対戦 room のホスティング。そのためには少し変更を加えたバージョンの Lite Applicationが実行されています。
  • マスターサーバーにそれぞれの最新の負荷や対戦リストを定期的にレポート。

Photon Cloudとの違い

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

  • 仮想アプリケーションはありません。1つの LB インスタンスに対して1つだけの対戦ロジックが実行されます。 Authorize 作動中のアプリケーション ID は無視されます。
  • 「game version」(対戦バージョン)パラメータでのプレイヤー識別はありません。これは仮想アプリの一部です。

Back to Content

基本ワークフロー

クライアント側の観点からのワークフローも大変シンプルです:

クライアントはマスターサーバーに接続し、ロビーに入り、開戦中の対戦のリストを読み出します。

マスターの CreateGame 操作を呼び出す時、対戦は実際には作成されません-マスターサーバーはあくまで負荷の一番少ないゲームサーバーを特定し、その IP をクライアントに返すだけです。

クライアントがマスターで JoinGame 或いは JoinRandomGame の操作を呼び出す時、マスターは対戦が実行されているゲームサーバーを調べ、その IP をクライアントに返します。

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

ここからは全て Lite Applicationと同じように行われます。

Photon Server: LoadBalancing Flowchart

Back to Content

マスターサーバー

ここではマスターサーバーの実行について説明します- \src-server\Loadbalancing\Loadbalancing.sln ファイルの LoadBalancing.MasterServer namespace をご覧ください。

MasterApplicationは、受信接続の発信元がゲームクライアント(「client port」(クライアントポート))なのか、ゲームサーバー(「game server port」(ゲームサーバーポート))なのかを判断します。

マスター: クライアント Peer の処理

MasterClientPeerはマスターサーバーへのクライアント接続を現わしています。 MasterClientPeer での操作は以下の通りです:

  • 認証
    認証 操作にはダミー実行しかありません。これは開発者が独自の認証メカニズムを実行する際、出発点として利用するためのものです。
    // 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 を、 GameList が含まれている AppLobby に追加するために使われます- GameList には全てのゲームサーバーで開戦されている対戦が含まれています。 Peer は GameList にある最新の対戦リストが含まれている初期の GameListEvent を受信します。( GameList は JoinLobby 操作のオプショナルプロパティでフィルタリングした状態です): その後、変更された対戦のリストを含む(これもJoinLobby操作のオプショナルプロパティでフィルタリングした状態です) GameListUpdateEvent が一定の間隔でクライアントに送信されます。クライアントは接続されている間、アップデートイベントを受信し続けます。

    その後、変更された対戦のリストを含む(これも JoinLobby 操作のオプショナルプロパティでフィルタリングした状態です) GameListUpdateEvent が一定の間隔でクライアントに送信されます。 クライアントは接続されている間、アップデートイベントを受信し続けます。

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

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

    JoinRandomGame は同じように作動しますが、この場合マスターサーバーは、対戦をランダムに選んで GameId をクライアントに返します。

  • CreateGame
    CreateGame 操作は、クライアントが新しい対戦を作りたい時に呼び出されます。 マスターサーバーは、新しい対戦が作成されるゲームサーバーを特定し、ゲームサーバーの IP をクライアントに返します。詳細は以下の「ロードバランサーアルゴリズム」をご覧ください。

    さらに、 GameState オブジェクトが作成されて、 GameList に加えられ、 peer は「joining peer」として記録されます。
    GameStateはあくまで対戦を記録するためだけにあります-対戦そのものはゲームサーバー上でのみ存在するものです。

Back to Content

マスター: ゲームサーバー Peer の処理

マスターサーバーは、どのゲームサーバーが利用可能なのか、これらがいくつの対戦をホスティングしているのか、そして最新の負荷などの情報を常に持っています。

そのためには、各ゲームサーバーが起動時にマスターサーバーに接続します。 MasterApplication が、 IncomingGameServerPeers が記憶されている GameServerCollection を維持しています。

ゲームサーバーは1つの操作のみ呼び出すことができます:

  • RegisterGameServer
    ゲームサーバーはマスターサーバーに接続した後、 RegisterGameServer 操作を呼び出します。 ゲームサーバーはマスターの GameServerCollection と LoadBalancer (以下の「ロードバランサーアルゴリズム」を参照) に追加されます。 Disconnect 操作で GameServerCollection から削除されます。

    ゲームサーバーがどのように自分の対戦や負荷をマスターに送信しているかについては以下「ゲームサーバー」の項目をご覧ください。

Back to Content

ゲームサーバー

ここではゲームサーバーの実行について説明いたします。\src-server\Loadbalancing\Loadbalancing.sln ファイルの LoadBalancing.GameServer namespace をご覧ください。

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

ゲームサーバーは Lite Application から派生します。 クライアントがマスターからゲームサーバーアドレスを受信した時点で、クライアントは Lite で利用可能なゲームサーバーで、どの操作でも呼び出すことができます。 唯一違うのは、ゲームサーバーでは JoinGameCreateGame 操作に別々の操作コードが使われているのに対し、 Lite は両方ともに JoinGame 操作で処理しているということです。

Back to Content

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

ゲームサーバーでは、マスターサーバーとの接続は OutgoingMasterServerPeer として現れます。 接続が完了した時点で、ゲームサーバーはマスターサーバーで Register 操作を呼び出します。 その後、ゲームサーバーは既存の対戦ステートを全て、マスターサーバーに公開します。

これは、各 room に、対戦ステートをマスターに送るようメッセージを送信して、実行されます。

Room はこれを ProcessMessage メソッドで処理し、 UpdateGameStateOnMaster メソッドを呼び出して、マスターに UpdateGameEvent を送信します。:

 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());
        }
}

新しい対戦が作成された時、クライアントがroomに入室/退室した時、そしてroomのプロパティが変更された時にも、対戦ステートはマスター側でアップデートされます。

Back to Content

ロードバランサーの実行

次の項目では、ゲームサーバーがどのように自分の負荷に関する情報をサーバーにレポートしているのか、マスターサーバーがどのように新しい CreateGame リクエストに相応しいゲームサーバーを特定しているのか-実際のロードバランサーアルゴリズムについて説明します。

Back to Content

ゲームサーバー: 負荷の特定

実行の詳細については \src-server\Loadbalancing\Loadbalancing.sln ファイルの LoadBalancing.LoadShedding namespace をご覧ください。

ゲームサーバーは、自身の負荷について最新の情報を定期的にマスターサーバーにレポートしています。負荷の情報には例えば、次のような情報が含まれています:- CPU 使用率-バンド幅使用率-ENet + Business Queue Length、サーバーが各リクエストに費やす平均時間などの Photon 独特の値-(自分にリクエストを送信した時の)レイテンシー。:

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

これら全てのファクターは一つの値-ゲームサーバーの「Load Level」(ロードレベル)に集約され、マスターサーバーにレポートされます。

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

Back to Content

実行に関する詳細

ゲームサーバーは上記ファクターについて「Feedback」(フィードバック) を収集します。各ファクターには1つの FeedbackController オブジェクトがあります- FeedbackName と FeedbackLevel で構成されています:

internal enum FeedbackName
{
    CpuUsage,     
    Bandwidth,
    TimeSpentInServer
}

public enum FeedbackLevel
{
    Highest = 4,
    High = 3,
    Normal = 2,
    Low = 1,
    Lowest = 0
}

DefaultConfiguration クラスはそれぞれの値の閾値を定義しています- 例えば20%までの CPU 使用率ならサーバーの FeedbackLevel は「lowest」と判定され、 90%までなら FeedbackLevel は「highest」と判定される、などなど。

// DefaultConfiguration.cs:

internal static List<FeedbackController> GetDefaultControllers()
{
    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 fileファイル でも設定することができます。 LoadBalancing.LoadShedding.Configuration namespaces が config file から値を読み込む役割を果たし、または configファイル が存在しない場合は、 DefaultConfiguration を適用します。

一定の間隔でゲームサーバーは Windows Performance Counters を一部チェックし、自分の全ての FeedbackControllers の最新値を設定して、新しい「overall feedback」(全体フィードバック)を計算します。

これは WorkloadController クラスで実行されます:

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 がマスターに新しいサーバーのステートをレポートします。

 public void UpdateServerState()
 {
    // [...]
    this.UpdateServerState(
        GameApplication.Instance.WorkloadController.FeedbackLevel,
        GameApplication.Instance.PeerCount,
        GameApplication.Instance.WorkloadController.ServerState);
}

Back to Content

マスターサーバー: ロードバランサーアルゴリズム

実行の詳細については \src-server\Loadbalancing\Loadbalancing.sln ファイルの LoadBalancing.LoadBalancer クラスをご覧ください。

マスターサーバーは各ゲームサーバーの LoadLevel を、 LoadBalancer クラスに記憶しています。 また、最新のロードレベルが lowest であるサーバーのリストも、別にもっています。

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

Back to Content

コンフィギュレーションとデプロイメント

デモとして SDK には、デプロイディレクトリに1つのマスターサーバーと2つのゲームサーバーのセットアップが含まれています:

  • /deploy/LoadBalancing/Master
  • /deploy/LoadBalancing/GameServer1
  • /deploy/LoadBalancing/GameServer2

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

ゲームサーバーのデプロイメント

LoadBalancing プロジェクトをプロダクションサーバーにデプロイする時は、1つのサーバーに2つのゲームサーバーアプリケーションをホスティングしてはなりません。 PhotonServer.config から「GameServer2」の全ての設定を取り消し、 /deploy/LoadBalancing/GameServer2 ディレクトリを削除してください。

ゲームサーバーがマスターサーバーに確実に登録できることを確認する必要があります。 Photon.LoadBalancing.dll.config にある MasterIPAddress をマスターのパブリック IP に設定してください。

また、対戦クライアントが確実にゲームサーバーに辿りつけることを確認してください。それぞれのゲームサーバのパブリック IP アドレスを設定してください。 値を empty (空)のままにしますと、パブリック IP アドレスは自動的に検知されてしまいます。

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

マスターサーバーのデプロイメント

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

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

目次に戻る

Game Serverのローテーションを止める

目次に戻る

"[Loadbalancing] (lbilb)"項目で説明したとおり、マスタサーバはゲームサーバの状態を"ServerState" の値から認識します。

  • "online" (デフォルトです)
  • "out of rotation(ローテーションを止める)" (オープンなゲームはロビーに表示されプレイヤーはサーバ上の既存ゲームに参加できますが、新規ゲームは作成されません)
  • "offline" (サーバ上の既存ゲームに参加できず、ゲームサーバ上に新規ゲームの作成も出来ません)

ゲームサーバは定期的に状態をマスタに送信します。OutgoingMasterServerPeerクラスを参照:

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)からサーバ状態を読むようにします。

クライアントやデータベースから呼ぶオペレーションを構築することもできます。

 ドキュメントのトップへ戻る