ロードバランシングアプリケーション
本稿では、サーバ-ーサイドのLoadBalancingアプリケーションの実装について説明します。
コンセプト
ロードバランシングアプリケーションは、Hiveフレームワークと実装機能(ルーム、イベント、プロパティなど)を拡張するもので、複数サーバーでのアプリケーションの実行を可能にする拡張レイヤーが加えられました。
基本設定は非常に簡単です:
マスターサーバーは常に1台で、ゲームサーバーは1台からN台まで存在します。
ロードバランシングにはロビーサポートとマッチメイキング機能も追加されました。
マスターサーバーは以下のタスクを処理します:
- ゲームサーバー上で現在オープンになっているゲームの履歴を保持します。
- 接続したゲームサーバーの負荷の履歴を保持し、適切なゲームサーバーにピアを割り当てます。
- クライアントが利用できるルームのリストを「ロビー」に保持し、アップデートします。
- クライアント向けにルームを検索し(ランダムまたは名前で)、クライアントにゲームサーバーのアドレスを転送します。
ゲームサーバーは以下のタスクを処理します:
- ゲームルームのホスティング。
- ゲームサーバーの最新の負荷と、ゲームサーバーのゲームのリストをマスターサーバーに定期的に報告します。
Photon Cloudとの違い
ロードバランシングアプリケーションは、Photon Realtime Cloudサービスとほぼ同じロジックを提供しています。
Cloudサービスとして作動するために必要でカスタムロジックの特殊なサーバーに適用できない要件は取り除かれました。
これにより、コードが大幅に簡素化されました。
- 仮想アプリケーションはありません。1つのLBインスタンスに対してゲームロジックが1つのみ実行されます。操作Authenticate内のAppIDパラメータは無視されます。
- 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操作は、AppLobbyにMasterClientPeerを追加するために使用されます。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
JoinGame操作は、AppLobbyのGameListにある、一意のGameIdで特定される既存の対戦に、クライアントが参加したい時に呼び出されます。ゲームが存在し、ピアがそれに参加することを許可されている場合、マスターサーバーはゲームが実際に実行されているゲームサーバーのIPをクライアントに返します。
マスターサーバーはGameSateもアップデートし、ピアをサーバー内の「joining peers」リストに加えます。
ゲームサーバー上のゲームに参加すると(または一定のタイムアウト後)、ピアはそこから削除されます。
このようにマスターサーバーはマスターとゲームサーバーの間を移行するピアを記録します。
ゲームがマスターサーバーによってランダムに選択され、クライアントにGameIdが返された場合を除き、JoinRandomGame は同様に動作します。
- CreateGame
CreateGame操作は、クライアントが新しいゲームを作成したい場合に呼び出されます。
マスターサーバーは、新たなゲームを作成するゲームサーバーを判定し、そのゲームサーバーのIPをクライアントに返します。
詳細は「ロードバランシングアルゴリズム」セクションを参照してください。
また、GameStateオブジェクトが作成されてGameListに追加され、ピアは「参加ピア」として保存されます。
このGameStateはゲームの記録にのみ使用されますーゲーム自体はゲームサーバー上にのみ存在します。
ゲームサーバーピアの処理
マスターサーバーは、どのゲームサーバーが利用可能か、ゲームサーバーがゲームをいくつホスティングしているか、また最新の負荷について常に把握しています。
これを実現するには、各ゲームサーバーは起動時にマスターサーバーに接続する必要があります。
MasterApplication
はGameServerCollection
を管理し、このGameServerCollection
にはIncomingGameServerPeers
が保存されています。
ゲームサーバーが呼び出せる操作は1つのみです:
- 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使用率
- トラフィック
- 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