This document is about: SERVER 5
SWITCH TO

로드밸런싱 어플리케이션

이 글에서는 로드밸런싱 어플리케이션의 서버측 구현에 대해서 설명 합니다.

Content

개념

Photon 3 에서 로드밸런싱 어플리케이션은 Lite 어플리케이션을 확장한 것이었습니다.
로드밸런싱 어플리케이션은 룸, 이벤트, 프로퍼티들과 같은 잘 알려진 모든 Lite 함수들을 제공 했고 어플리케이션을 다수 서버에서 수행할 수 있도록 해주는 확장성 레이어를 추가했습니다.
로드밸런싱은 또한 로비와 매치메이킹 기능을 추가 했습니다.

Photon 4 에서는 Lite 는 Hive로 대체되어 사용되지 않게 되었습니다. 따라서 로드밸런싱은 이제 Hive로 확장되어 더 복잡하긴 하나 더 많은 기능이 추가되었습니다.

기본 설정에 대한 것은 변경되지 않았으며 간단 합니다:항상 1개의 마스터 서버와 1..N개의 게임서버가 존재 합니다.

Photon Server Concept: LoadBalancing Setup
Photon Server Concept: LoadBalancing Setup

마스터 서버가 처리하는 작업들:

  • 현재 게임 서버에서 오픈되어있는 게임들을 추적.
  • 게임서버에 연결되어 있는 부하량과 해당 게임서버에 할당된 피어들을 추적
  • "로비"내의 클라이언트를 위한 사용할 수 있는 룸의 리스트를 유지하고 갱신.
  • 클라이언트를 위한 룸 검색(무작위 또는 이름으로)과 클라이언트를 게임서버로 포워딩.

게임 서버가 처리하는 작업들:

  • 게임룸을 호스트. 이 목적의 Lite 어플리케이션의 약간 변형된 버전으로 수행됩니다.
  • 주기적으로 마스터 서버에게 게임의 목록과 현재 작업 부하량을 리포트 합니다.

Photon 클라우드와의 차이점

로드밸런싱 어플리케이션은 Photon Cloud 서비스와 거의 동일한 로직을 제공 합니다.
Cloud 서비스로 작동하기 위해 필요한 커스텀 로직을 가진 특별한 서버가 필요 없으므로 제거되었습니다. 따라서 코드는 매우 간단합니다.

  • 가상 어플리케이션이 필요 없습니다. 로드밸런싱 인스턴스당 하나의 게임로직만 실행 됩니다. 권한 인증 오퍼레이션의 AppId는 무시됩니다.
  • "게임버전" 파라미터들로 플레이어들이 분리되지 않습니다. 이것은 가상 앱의 일부분 입니다.

기본 작업 흐름

클라이언트 측 관점에서의 작업 흐름은 또한 매우 간단 합니다:

클라이언트는 로비에 조인할 수 있는 마스터 서버로 접속하고 오픈되어 있는 게임의 목록을 받습니다.

클라이언트가 CreateGame 오퍼레이션을 마스터에 호출 할 때 게임은 실제 생성되지는 않습니다 - 마스터 서버는 가장 부하가 적은 게임서버를 결정하여 클라이언트에게 게임서버의 IP를 리턴 해 줍니다.

클라이언트들이 JoinGame 또는 JoinRandomGame 오퍼레이션을 마스터에 호출 할 때 마스터는 게임서버에서 어떤 게임이 실행되고 있는지 찾아 보고 그 IP를 클라이언트에게 리턴 합니다.

클라이언트는 마스터서버에서 접속을 해제 합니다. 수신된 IP로 게임서버에 접속하고 CreateGame 또는 JoinGame 오퍼레이션을 다시한번 호출 하게 됩니다.

이 때 부터 모든 것이 Lite 어플리케이션과 동일한 방식으로 동작하게 됩니다.

Photon Server: LoadBalancing Sequence Diagram
Photon 서버: 로드밸런싱 시퀀스 다이어그램

마스터 서버

이 섹션에서는 마스터 서버 구현에 대해서 설명 합니다 - \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 오퍼레이션은 MasterClientPeer를 게임 리스트(모든 게임 서버에 오픈된 모든 게임의 목록)를 포함하고 있는 AppLobby에 추가시키는 데 사용 됩니다.

    피어는 GameList(JoinLobby 오퍼레이션의 선택적인 프로퍼티에 의해서 필터링된)내의 현재 게임의 목록을 포함하고 있는 초기 GameListEvent를 수신 합니다:

    이후에 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 오퍼레이션은 클라이언트가 유일한 GameID 를 지정하여 AppLobby의 게임리스트에 있는 존재하고 있는 게임에 참여하기를 원할 때 호출 됩니다. 만약 게임이 존재하고 피어가 참여 할 수 있으면 마스터 서버는 실제로 그 게임이 동작하고 있는 게임서버의 IP 를 클라이언트에게 리턴해 줍니다.

    마스터 서버는 GameState 를 업데이트 하고 "참여하고 있는 피어들"의 목록에 해당 피어를 추가 시켜 줍니다. 피어는 게임서버의 게임에 참여했으면 목록에서 제거 됩니다(또는 특정한 타임아웃이 지난 후에).

    이 방식은 마스터 서버가 마스터와 게임 서버간 전환하고 있는 피어들의 상태를 추적할 수 있습니다.

    JoinRandomGame 은 마스터 서버가 무작위로 게임을 골라 클라이언트에 리턴하는 것 외에는 동일한 방식으로 동작합니다.

  • CreateGame 오퍼레이션은 클라이언트가 새로운 게임을 생성하려고 할 때 호출 됩니다.
    마스터 서버는 어떤 게임서버에서 룸이 생성 될지를 결정 하고 게임 서버의 IP를 클라이언트에게 리턴 해 줍니다. 상세한 내용은 "로드밸런싱 알고리즘" 섹션을 참고 하시기 바랍니다.

    추가적으로, GameState 객체는 생성되고 GameList 에 추가 되며 피어는 "joining peer"로서 저장됩니다.
    이 GameState 는 게임을 추적하기 위해서만 사용된다는 것에 주의 하세요 - 게임 자체는 게임 서버에서만 존재합니다.

마스터 : 게임 서버 피어들의 처리

마스터 서버는 어떤 게임 서버를 사용할 수 있고, 게임 서버가 몇개의 게임을 운영중이며 현재 부하량이 어느 정도 인가를 항상 알 수 있습니다.

이렇게 하려면 각 게임 서버는 시작 할 때 마스터 서버에 연결 해야 합니다. MasterApplication 은 IncomingGameServerPeers 가 저장되어 있는 GameServerCollection 을 관리 합니다.

게임 서버는 하나의 오퍼레이션만 호출 할 수 있습니다:

  • RegisterGameServer
    게임 서버는 RegisterGameServer 오퍼레이션을 마스터 서버에 접속 후 한번 호출 합니다.
    게임서버는 마스터 서버의 GameServerCollection과 마스터 서버의 LoadBalancer(아래의 "로드밸런싱 알고리즘" 섹션을 보세요) 에 추가 됩니다. 접속 해제 되었을 때 게임서버는 GameServerCollection 에서 제거 됩니다.

게임 서버

이 섹션에서는 게임 서버 구현을 설명 합니다.\src-server\Loadbalancing\Loadbalancing.sln 솔루션에서 LoadBalancing.GameServer 네임스페이스를 보세요.

게임 서버: 클라이언트 피어 처리

게임서버는 Lite 어플리케이션에서 파생되었습니다.
클라이언트가 마스터에서 게임 서버 주소를 받자마자, 클라이언트는 Lite 에서 사용할 수 있는 게임서버에 대한 모든 오퍼레이션을 호출 할 수 있습니다.
유일한 차이점은 JoinGameCreateGame 에 대한 코드를 분리 해 놓았는데 Lite 에서는 JoinGame 에서 두 메소드 모두를 처리한다는 점 입니다.

게임 서버: 마스터에게 게임 상태 리포팅

게임 서버내에서 마스터 서버와의 연결은 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 부하량에 대해서 중점을 둘 것 입니다.

이러한 모든 팩터들은 하나의 값으로 요약 될 수 있습니다 - 이 값은 게임 서버의 "Load Level"로 마스터에게 리포트 되는 값 입니다.

로드 레벨이 낮을 수록 게임 서버가 새로운 게임을 호스트 하는데 적합 합니다.

구현 상세 내용

게임 서버는 위에서 언급된 팩터에 대한 "피드백" 을 수집 합니다. 각 팩터에 대하여 FeedbackController 객체가 있습니다 - 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% 까지는 "lowest" 값을 가지며 CPU 사용률이 90% 가 되면 FeedbackLevel 이 "highest"에 도달하는 것과 같은 것들 입니다.

C#

// 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 에서 설정될 수 있습니다.
LoadBalancing.LoadShedding.Configuration 네임스페이스는 config 파일로 부터 값을 읽어오거나 설정이 존재하지 않으면 DefaultConfiguration 을 적용하도록 해줍니다.

주기적으로 게임 서버는 일부 Windows 성능 카운터를 체크하여 FeedbackControllers의 현재 값을 설정하며 새로운 "overall feedback" 을 계산 합니다.

이 사항은 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);
}

마스터 서버: 로드밸런싱 알고리즘

구현에 대한 상세 내용은 \src-server\Loadbalancing\Loadbalancing.sln 솔루션의 LoadBalancing.LoadBalancer 클래스를 보시기 바랍니다.

마스터 서버는 LoadBalancer 클래스에 각각의 게임서버 LoadLevel 을 저장 합니다.
LoadBalancer 클래스에는 현재 가장 낮은 로드레벨을 가진 모든 서버들의 추가적인 목록을 가지고 있습니다.

클라이언트가 CreateGame 오퍼레이션을 호출 할 때 마다 마스터 서버는 LoadBalancer에서 가장 낮은 로드 서버의 주소를 가져와서 클라이언트에게 리턴 해 줍니다.

환경 설정과 디플로이

설명을 위해서 SDK 에는 delploy 디렉토리안에 1개의 마스터와 2개의 게임 서버에 대한 설정이 포함되어 있습니다:

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

이 셋업은 로컬 개발만을 위한 목적 입니다.

게임 서버 디플로이 하기

LoadBalancing 프로젝트를 프로덕션 서버에 디플로이 할 때 서버에 2개의 게임서버 어플리케이션을 호스트 하지 않아야 합니다.
PhotonServer.config 에서 "GameServer2" 에 대한 모든 설정을 제거 하시고 /deploy/LoadBalancing/GameServer2 디렉토리를 삭제 하세요.

게임 서버들이 마스터 서버에 등록될 수 있다는 것을 확인하실 필요가 있습니다.
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&gt; -->
      </setting>
      
      <!-- [...] -->
&lt;/Photon.LoadBalancing.GameServer.GameServerSettings&gt;

Photon Control을 사용하여 퍼블릭 IP를 설정 할 수 도 있습니다.

마스터 서버 디플로이 하기

단 한개의 마스터 서버만 가지고 있다는 것을 확인 해 주셔야 합니다:게임서버의 PhotonServer.config 에서 "Master" 어플리케이션에 대한 모든 설정을 제거 하거나 게임 서버들과 클라이언트들이 동일한 단 하나의 마스터 서버 IP 를 사용하는지 확인 해 주시기 바랍니다.

그 외에는 마스터 서버에 특별한 환경설정이 필요 없습니다.

Take a Game Server out of rotation

"[로드밸런싱] (lbilb)" 섹션에서 설명한 대로 마스터 서버는 "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