6.1통신량 절약하기
호스트간 통신량은 최대한 절약할 수 있는 방법을 모색하는 것이 좋습니다.
데이터 양자화은 1개의 메시지의 크기를 줄이는데 도움이 됩니다.
추측 항법(dead reckoning)은 주고 받는 메시지의 갯수를 줄이는 데 도움이 됩니다.
불필요한 메시지는 주고받지 않는 것이 좋습니다. 가령 게이머에게 보여지는 퀘스트의 경우 퀘스트 문자열을 모두 보내주는 것 보다는 게임 클라이언트에 미리 저장된 리소스로 대신 보여주는 것이 좋습니다. (멀티 언어 지원의 게임에서는 오히려 필수가 되기도 합니다.)
6.2멀티캐스트 최적화
한번에 여러 호스트에게 동일한 메시지를 보내는 것을 멀티캐스트(multicast)라고 부릅니다. ProudNet에서는 멀티캐스트를 할 때 다음과 같이 최적화 하는 것을 권합니다.
멀티캐스트 최적화의 기본
RMI로 멀티캐스트를 할 때 한 번의 RMI 함수 호출로 여러 호스트에 동시다발적으로 보내는 것이 성능상 훨씬 좋습니다. 동시다발적으로 보내는 방법은 22. 메시지 송신하기를 참고하십시오.
MMO 서버의 멀티캐스트 비용 줄이기
MMO 게임 서버 유지비용으로 큰 비중을 차지하는 것은 서버가 작동하는 곳(예: IDC)에서의 회선 속도입니다. ProudNet은 이를 최적화하기 위한 다음과 같은 장치가 있습니다.
아래 기능을 사용할 때의 효과에 대한 소개는 넷텐션 홈페이지(http://www.nettention.com)에서 확인하실 수 있습니다.
NPC의 시뮬레이션 결과를 멀티캐스트하기
통상적으로, NPC(Non player character)의 시뮬레이션은 서버에서 합니다. NPC의 시뮬레이션 한 결과를 클라이언트들에게 multicast하되, 각 클라이언트가 연계되어 있는 P2P 연결을 활용하여 전송하는 방식입니다. 자세한 것은 13. 서버에서 다수의 클라이언트에게 routed multicast를 하기를 참고하세요.
PC(Player character)의 시뮬레이션 결과를 멀티캐스트하기
통상적으로, PC의 시뮬레이션은 각 클라이언트에서 합니다. 클라이언트는 PC의 위치, 속도 정보 등을 서버에게 전송하고, 서버는 이를 받아 PC의 주변에 있는 클라이언트들에게 멀티캐스트를 합니다.
그러나 ProudNet의 P2P 생성,파괴,증원,감원 처리는 매우 빠릅니다. 이를 활용하면, 각 PC의 위치 멀티캐스트를 서버를 경유하지 않고, 각 클라이언트의 PC를 가시영역에 포함시키는 다른 클라이언트들에게 P2P 통신으로 직접 전송할 수 있습니다.
그림 6-1P2P 통신을 활용한 PC 상태 멀티캐스트
주요 방법은 다음과 같습니다.
일정 시간마다, 각 PC의 가시 영역을 두고 있는 클라이언트끼리 P2P 그룹을 맺습니다.
각 클라이언트는 시뮬레이션하는 PC의 위치,속도 등을 서버와 P2P 그룹이 맺어진 클라이언트들에게 전송합니다.
서버가 받은 정보를 근거로 서버는 P2P 그룹 생성,파괴,증원,감원을 지속적으로 합니다.
클라이언트는 받은 정보를 근거로 PC의 위치를 갱신합니다.
6.3게임 서버의 운영체제(OS)
동시접속자수가 100 이하인 경우에는 게임 서버의 운영체제로 무엇을 사용하던지 상관없습니다. 하지만 동시접속자수가 그 이상을 넘어가는 경우 게임 서버의 운영체제는 서버 전용 운영체제를 쓰는 것이 성능상 큰 이익을 봅니다.
ProudNet이 지원하는 서버 전용 운영체제는 Windows 2003 Server 혹은 이후 버전입니다. Linux도 지원하며, CentOS 6, Ubuntu 12 이상에서 테스트되었습니다.
6.4서버의 UDP 포트 사용 방식
클라이언트가 서버에 접속하면 1 개의 TCP 포트가 배정됩니다. 이는 모든 TCP 응용 프로그램의 특징이므로 별로 특이할 것이 없습니다.
ProudNet은 클라이언트와의 연결을 위해 1개의 UDP 포트를 사용합니다. (단, 클라이언트가 UDP 포트를 쓸 수 없는 환경인 경우에는 TCP 포트만을 사용합니다.)
이때 Proud.CNetServer 는 각 클라이언트와의 연결에 대한 UDP 포트 배정 정책을 다음과 같이 구분하고 있습니다.
• 클라이언트 하나 하나 마다 서로 다른 UDP 포트를 배정하기
• 모든 클라이언트를 위한 일정 갯수의 미리 준비된 UDP 포트를 배정하기
전자를 per-client assign mode, 후자를 static assign mode라고 지칭하겠습니다.
per-client assign mode에서는 서버에 접속하는 클라이언트 각각은 서로 다른 UDP 포트를 배정받습니다. 서버가 사용할 UDP 포트 번호 목록 외의 임의의 포트 번호가 사용됩니다.
static assign mode에서는 서버가 사용할 UDP 포트 번호 목록 외의 다른 UDP 포트가 사용되는 일은 없습니다. 그리고 서버에 접속하는 각 클라이언트는 서버가 사용할 UDP 포트 번호 목록의 포트 중 하나가 사용됩니다. 즉 두개 이상의 클라이언트가 같은 UDP 포트를 공유하게 됩니다. (이렇게 된다고 해서 두 개 이상의 클라이언트가 메시지 흐름에 문제가 생기는 일은 없습니다.)
일반적으로 Per-client assign mode가 static assign mode보다 더 성능상으로 좋습니다.
• static assign mode는 클라이언트의 수가 많으면 소량의 UDP socket이 엄청나게 많은 클라이언트와의 통신을 감당해야 하기 때문에 UDP socket의 내부 버퍼량이 딸릴 경우 패킷 로스로 이어질 위험이 있습니다.
• 그렇다고 해서 UDP socket을 처음부터 너무 많이 준비하면 지나치게 많은 UDP socket을 위한 서버측의 처리 부하로 메모리와 CPU 사용량이 증가할 수 있습니다.
• static assign mode는 ICMP host unreachable packet에 대한 내성이 약합니다. 따라서 3.2 ICMP 관련 방화벽 설정 을 해주어야 합니다.
부득이한 경우가 아닌 이상 per-client assign mode를 쓰는 것을 권장합니다. per-client assign mode를 쓸 경우에는 위와 같은 단점이 없습니다.
하지만 주의 사항이 있습니다. 서버가 사용할 UDP 포트 번호 목록보다 더 많은 수의 클라이언트가 접속할 경우 임의의 UDP 포트가 할당되는데, 이때 할당된 포트가 서버측 방화벽에서 허락되지 않은 번호이면 UDP 통신이 원활하지 못할 수 있습니다.
어떤 서버 방화벽은 outbound 패킷 감지가 있는 경우에 한해 포트 사용을 허용하는 기능이 있습니다. 이를 활용하면 per-client assign mode를 쓰면서도 UDP 포트 허용을 꺼두어도 안전하면서도 원활한 통신이 이루어집니다. 이에 대한 자세한 내용은 DDOS 공격을 버티기 위한 방화벽 설정 을 참고하십시오.
서버가 어떤 assign mode를 쓰게 할 것인지 설정하려면 Proud.CNetServer.Start 호출 시 Proud.CStartServerParameter.m_udpAssignMode를 설정하면 됩니다. 그리고 서버가 사용할UDP 포트 목록은 Proud.CStartServerParameter.m_udpPorts에 설정하면 됩니다.
지금까지의 내용을 정리하자면, ProudNet 사용시 assign mode, UDP 포트 목록의 길이, 방화벽 설정의 권장하는 유형은 다음과 같습니다.
권장 수준 | assign mode | UDP 포트 목록의 길이 | 방화벽 설정 | 적용 가능 조건 |
---|---|---|---|---|
최고로 권장됨 | per-client | - ( per-client이면 무시됩니다. ) | outbound 패킷 감지시 일시 허용 | 클라우드 서버가 아닌 'outbound 패킷 감지시 일시 허용' 기능을 on/off할 수 있는 일부 물리 서버. |
권장됨 | per-client | - | UDP 포트 목록에 등록된 번호들을 항상 허용 | |
권장됨 | static | 동시접속자 수의 1/10 ( 예:4000) | UDP 포트 목록에 등록된 번호들을 항상 허용, ICMP host unreachable 차단 | |
위험! 사용하지 말것! | per-client | - | UDP 포트 목록에 등록된 번호들을 항상 허용 | |
위험! 사용하지 말것! | static | 상관없음 | UDP 포트 허용 범위에 상관없고, ICMP host unreachable을 차단하지 않음. |
6.5서버의 스레드 풀의 스레드 갯수
서버 실행시(Proud.CNetServer.Start) 스레드 풀(Thread pool) 의 스레드 갯수를 지정할 수 있습니다.
서버 컴퓨터에서 몇 개의 서버 프로세스를 띄우느냐, 몇 개의 스레드를 생성하느냐 여부는 서버의 역할과 기능에 따라 다릅니다.
몇 가지 대표적인 가이드를 설명하자면 아래와 같습니다.
우선, 스레드 수를 서버의 CPU 코어 수 만큼이라고 가정합니다.
ProudNet 서버가 사용되는 서버가 순수하게 CPU burst time을 차지하지 않고 device burst time을 차지할 경우 스레드의 갯수를 늘립니다. (Burst time에 대한 이해 참고) 게임 서버에서 대표적인 device burst time을 차지하는 경우는 유저 DB를 접근하고 있을 때입니다.
게임 서버의 로직이 여러 스레드에서 동시 접근가능한 구조로 만들어져 있으면 1개의 게임 서버 프로세스만 서버 컴퓨터에서 실행시켜도 무방합니다. 하지만, 게임 서버의 로직이 언제나 1개의 스레드 에서만 실행할 수 있도록 critical section이나 mutex로 보호되고 있는 경우 게임 서버 프로세스를 여러개를 띄우는 것이 좋습니다.
6.6스피드핵 감지 기능과 서버의 성능
17. Speed Hack 탐지 기능 은 클라이언트와 서버간의 일정량의 트래픽을 유발하며 서버에 연결된 클라이언트의 수에 비례하여 트래픽이 증가합니다.
Xeon E312XX(샌디브릿지) 메모리 4G 의 서버PC에 1만개의 클라이언트를 Connection 만 하였을 경우
17. Speed Hack 탐지 기능 을 사용하지 않으면 0 ~ 3% 의 CPU 사용량이,
17. Speed Hack 탐지 기능 을 사용하면 25 ~ 35% 의 CPU 사용량이 측정되었습니다.
따라서 스피드핵 감지 기능이 불필요하면 꺼버리시는 것이 좋습니다. (Proud.CNetServer.EnableSpeedHackDetector 사용)
6.7Burst time에 대한 이해
Burst time은 크게 CPU burst time과 device burst time으로 나뉩니다.
• CPU burst time이란, 특정 루틴을 실행하는데 CPU 연산만을 하는 시간을 의미합니다. 쉽게 말해서, CPU burst time 중인 스레드의 CPU 코어 점유율은 100라고 볼 수 있습니다.
• Device burst time이란, 특정 루틴을 실행하는 동안 CPU가 다른 처리의 완료를 기다리는 시간을 의미합니다. 즉, Device burst time 중인 스레드의 CPU 코어 점유율은 0입니다. CPU가 다른 처리의 완료를 기다리는 대표적인 경우는 파일 읽기/쓰기중, DB 쿼리 실행중, 다른 호스트로의 서비스 응답 대기중이 있습니다.
게임 서버를 개발할 때 device burst time이 긴 경우 몇 가지 성능상 주의 사항이 있습니다.
• Device burst time 중에는 게임 서버의 로컬 메모리 보호하고 있는 mutex나 critical section을 lock 한 상태를 피하는 것이 좋습니다. 만약 lock를 하고 있을 경우 device burst time이 긴 상태로 정작 게임 서버는 제 성능을 발휘하지 못하게 되며, 심각한 랙 현상으로 이어지게 됩니다.
6.8수신 처리 루틴의 최적화
서버 성능 저하에 많은 영향을 끼치는 것 중 하나는 수신 처리 루틴 중 오랜 시간을 차지하는 것들입니다.
우선, 16.2 수신측(Stub) 호출 시점을 접근하기를 통해 성능이 떨어지는 수신 처리 루틴을 찾아 해결해 나가는 것이 중요합니다.
그 다음, 느린 수신 처리 루틴이 차지하는 것이 device burst time인지 CPU burst time인지 찾아야 합니다.
• 만약 device burst time이 긴 경우라면 critical section lock을 최소화하게 설계하고, device burst의 원인 (데이터베이스 등)의 성능을 올려야 합니다.
• 만약 CPU burst time이 긴 경우라면 계산 루틴을 최적화하거나 병렬 처리를 하거나 다른 서버로의 분산 처리를 해야 합니다. 이런 경우의 대표적 예는 NPC의 AI입니다.
6.9직접 P2P 통신과 relay 통신의 성능 차이
6. 클라이언트간 P2P 통신의 개요에서 설명하듯이, 호스트간 peer-to-peer 통신에서는 직접 P2P 통신을 하는 경우와 relay 통신을 하는 경우가 존재하게 됩니다.
일반적으로 P2P 통신을 하는 경우에 비해 relay 통신을 하는 경우가 레이턴시가 두배 이상 큽니다. 특히 서버가 먼 거리에 있는 경우 (예를 들어 서버가 3000 킬로미터 이상 떨어져 있는 경우) 그 격차는 더욱 커지게 됩니다.
따라서 필요한 경우 다음과 같은 고려를 해보시는 것을 권장합니다.
• relay 통신을 하는 경우에 한해서 P2P 메시징 총량(가령 RMI 호출 횟수)를 줄이기: Proud.CNetClient.GetPeerInfo를 통해 RMI를 호출받을 타 클라이언트가 자신과 직접 P2P 통신을 하는 중인지 여부를 알 수 있습니다. 예를 들어 만약 relay 통신을 하는 것이라면 자신의 캐릭터 위치를 초당 8번씩 전송하는 RMI 호출의 횟수를 초당 4번으로 줄이는 방법이 있겠습니다.
하지만 relay통신이 가진 장점도 있습니다: P2P그룹의 멤버 갯수가 굉장히 많은 경우 클라이언트가 P2P 그룹을 상대로 RMI를 던지면 매우 많은 업로드 양이 발생합니다. 이에 대한 해결법에 대해서는 Proud.RmiContext.m_maxDirectP2PMulticastCount 설명 및 11.2 최종 메시지만 송출하기를 참고하십시오.
6.10프로토콜 선택의 요령
3. ProudNet의 프로토콜 종류에서 설명하듯이 ProudNet은 Reliable 메시징
과 Unreliable 메시징
을 지원합니다. 여기서는 어떤 메시징 수단을 선택하느냐에 대한 팁을 설명합니다.
먼저, 처음 게임을 개발할 때는 모든 메시지를 reliable 메시징으로 보내게 만듭니다. (Proud.RmiContext.ReliableSend 사용)
그리고 16. 모든 RMI 호출 시점을 접근하기기 를 참고하여 RMI 메시지 송수신 내역을 수집합니다.
일반적인 게임 프로그램에서는, 수집된 RMI 메시지 송수신 내역을 분석해보면 정의된 RMI 메시지 종류 중 20% 이하들이 전체 RMI 메시지 송수신량의 80% 이상을 차지하고 있게 됩니다. 이들 중 총 송신 시도 중 20% 이하의 유실이 발생해도 문제가 별로 없는 RMI들이 존재하기 마련입니다. 예를 들어 초당 5회 이상 자기 캐릭터의 위치를 전송하는 RMI가 한 예가 됩니다.
이러한 과정을 통해 송신 빈도가 매우 높으면서 유실이 문제되지 않는 RMI들을 찾아내서 Unreliable 메시징을 쓰도록 프로그램을 수정하면 됩니다.
6.11송신 과다 감지하기
각 호스트에서의 송신 과다 감지하기
ProudNet은 내부적으로 송신 큐(send queue)를 가지고 있습니다. 송신 큐는 네트워크 회선이 감당할 수 있는 송신 속도보다 송신하려는 데이터의 양이 상회할 경우, 송신이 완료될 때까지 메모리에 대기되는 '송신할 데이터'를 말합니다.
송신량이 과다한 경우에는 11. 송신량 자동 조절 기능 (Throttling) 등을 통해 완화할 수 있습니다. 한편, 송신량을 측정해서 과다한 송신량을 예방하는 것도 좋은 방법이 됩니다. 송신량이 과다한 경우 송신큐의 양은 계속 증가할테니까요.
송신큐를 측정하는 메서드들은 다음과 같은 것들이 있습니다.
• Proud.CNetServer.GetClientInfo
• Proud.CNetClient.GetPeerInfo
그리고 Proud.CNetPeerInfo.m_sendQueuedAmountInBytes 를 사용하면 됩니다.
각 RMI 종류별 송신 과다 감지하기
RMI를 호출할 때마다 발생하는 이벤트 콜백 Proud.IRmiProxy.NotifySendByProxy은 파라메터 Proud.MessageSummary를 갖고 있습니다. 이를 통해 호출되는 각 RMI가 발생하는 통신량을 측정할 수 있습니다.