Pomelo는 처음에 게임 서버를 위해 설계되었지만, 설계와 개발이 완료된 후 Pomelo가 범용 분산 실시간 애플리케이션 개발 프레임워크가 될 수 있다는 것을 발견했습니다. 여기서는 게임 서버의 요구사항을 분석하고 개발 시 직면하는 문제들을 통해 Pomelo의 설계 동기를 설명하겠습니다.
게임 서버란 무엇인가
게임 서버에서 일해본 경험이 없는 사람들은 게임 서버가 매우 신비로운 것이라고 생각할 수 있습니다. 하지만 실제로는 웹 서버보다 더 복잡하지 않으며, 클라이언트의 네트워크 요청에 대한 서비스를 제공할 뿐입니다. 본질적으로는 장시간 연결을 기반으로 한 socket 서버일 뿐입니다. 물론 논리적 복잡성, 메시지 양, 실시간 성능 등의 측면에서 웹 서버보다 더 많은 것을 요구합니다. 이제 웹 서버와 게임 서버를 비교하여 게임 서버의 몇 가지 특징을 소개하겠습니다.
복잡한 Socket 서버
실제로 웹 서버를 HTTP 서버로 취급할 수 있는 반면, 게임 서버는 원시 socket 서버로 취급할 수 있습니다. Socket 통신을 사용하여 서버 측과 클라이언트 측 간의 상호작용을 처리하도록 구현됩니다. 따라서 많은 게임 서버들이 네이티브 socket(TCP)을 기반으로 직접 구현됩니다. 단순한 socket 서버와 비교하면, 게임 서버는 더 많은 부담스러운 작업을 수행합니다:
백엔드 서버가 처리해야 하는 매우 복잡한 게임 로직
엄청난 양의 네트워크 트래픽과 실시간 요구사항
일반적으로 단일 socket 서버로는 감당할 수 없으므로 서비스를 제공하기 위해 서버 클러스터를 사용하는 경향이 있음
장시간 연결과 실시간성
웹 애플리케이션은 단시간 연결의 요청/응답 패턴을 기반으로 하므로, 게임 서버가 장시간 연결을 사용하기 때문에 웹 서버가 보유한 리소스는 게임 서버보다 훨씬 적습니다. 따라서 HTTP 기반 애플리케이션을 사용하여 단시간 연결을 처리함으로써 웹 서버의 최대 확장성을 달성할 수 있습니다. 웹 서버가 단시간 연결을 사용할 수 있는 이유는 다음과 같습니다:
단방향 통신, 일반적인 웹 애플리케이션은 pull 모드만 지원
실시간에 대한 낮은 요구사항, 일반적으로 웹 서버가 3초 이내에 응답할 수 있으면 적시라고 간주
하지만 게임 애플리케이션은 다음과 같은 이유로 장시간 연결만 사용할 수 있습니다:
양방향 통신, 게임 애플리케이션은 pull 모드와 push 모드를 모두 지원해야 하며, 더욱이 서버가 push하는 데이터의 양이 클라이언트가 처음에 pull하는 데이터보다 훨씬 많음
실시간에 대한 극도로 높은 요구사항, 서버는 클라이언트에게 메시지를 실시간으로 push해야 하며, 최대 응답 시간은 100ms 이하여야 함
분할 전략과 로드 밸런싱
일반적으로 웹 애플리케이션 간의 상호작용에는 인접한 개념이 없으며, 모든 사용자 간의 상호작용은 동등합니다. 웹 애플리케이션에서 상호작용 빈도는 사용자의 지리적 위치와 무관하지만, 게임 애플리케이션에서는 반대입니다. 게임에서 다른 플레이어와의 상호작용 빈도는 플레이어의 위치(지역)와 밀접한 관련이 있습니다. 예를 들어, 서로 인접한 두 플레이어는 서로 공격하거나 팀을 이루어 몬스터를 공격할 수 있습니다. 따라서 그들 간의 상호작용은 매우 빈번하며, 실시간 요구사항도 매우 높습니다. 이는 이 두 플레이어가 교차 프로세스 비용을 줄이기 위해 동일한 지역 서버 프로세스에 위치해야 함을 의미합니다. 따라서 게임 애플리케이션에는 지역에 따른 분할 전략이 있으며, 이는 아래와 같이 웹 애플리케이션과 다릅니다:
area
서버 프로세스는 하나의 지역 또는 여러 지역을 담당할 수 있으므로, 게임 서버의 확장성은 지역 프로세스에 의해 제한됩니다. 지역이 너무 바빠서 용량을 초과하면 전체 게임 서버가 차단되고 다운됩니다. 지역 서버는 상태를 유지하며, 특정 플레이어의 요청은 동일한 지역 서버로 전송되어야 합니다. 알려진 바와 같이, 상태를 유지하는 서버는 많은 문제를 야기하며, 이는 지역 서버가 높은 확장성과 높은 가용성 측면에서 웹 서버만큼 좋지 않다는 것으로 이어집니다. 일반적으로 게임 서버들을 서로 격리하여 이러한 문제를 완화해야 했습니다.
웹 애플리케이션의 분할은 일반적인 로드 밸런싱을 기반으로 결정될 수 있는 반면, 게임 애플리케이션의 분할은 지역 분할 전략을 기반으로 하며, 이는 동일한 지역 내의 플레이어들이 동일한 지역 서버 프로세스에서 실행되도록 하여 교차 프로세스 비용을 줄입니다.
확장성과 분산
웹 애플리케이션이든 게임 애플리케이션이든, 확장성은 항상 평가의 가장 중요한 지표 중 하나입니다. 또한 가장 어려운 문제이며, 실행 아키텍처와 다양한 최적화 전략을 포함합니다. 동시 온라인 플레이어 수와 응답 시간을 보장하는 것은 확장 가능한 설계를 통해서만 가능합니다. 전통적인 게임 서버의 실행 아키텍처는 모든 로직을 처리하는 단일 프로세스 모델을 사용하며, 동시 온라인 플레이어가 많지 않을 때 사용 가능합니다. 동시 온라인 플레이어 수의 증가는 단일 프로세스 서버의 확장성에 큰 도전을 가져옵니다. 따라서 분산 및 다중 프로세스 게임 서버는 필연적인 선택이 됩니다.
다음은 웹 서버와 게임 서버의 아키텍처 차이를 보여주는 그림입니다:
game_server_arch
웹 서버는 로드 밸런서에 따라 요청을 임의의 프로세스로 리다이렉트할 수 있으므로, 실행 아키텍처가 상대적으로 단순하며 분산이 거의 필요하지 않습니다.
하지만 게임 서버는 거미줄 같은 아키텍처를 사용하며, 각 프로세스는 자신의 책임을 가지고 있고, 이러한 프로세스들은 작업을 완료하기 위해 서로 얽혀 있습니다. 따라서 게임 서버는 전형적인 분산 아키텍처입니다.
어려움
위의 분석에서 게임 서버는 거미줄 같은 아키텍처를 사용하며, 이는 게임 서버 개발에 몇 가지 어려움을 가져옵니다. 이러한 어려움은 다음을 포함합니다:
실시간성 보장
게임 서버의 경우, 실시간 작업은 다음을 포함합니다:
실시간 Tick
일반적으로 게임 서버는 정시 작업을 수행하기 위해 정시 tick이 필요합니다. 실시간 동작을 달성하기 위해 이 tick 타이머는 100ms 이내일 수 있습니다. 이러한 작업은 일반적으로 다음 로직을 포함합니다:
지역 내의 플레이어, 몬스터 등을 포함한 엔티티를 순회하고 이동, 부활, 소멸 등의 작업을 수행
지역 내의 몬스터를 정시에 생성하며, 일부가 죽으면 다시 생성
몬스터의 공격, 도망 등의 로직과 같은 정시 AI 로직
시간 제한이 100ms이므로, 이러한 작업은 100ms보다 훨씬 적은 시간에 실행되어야 합니다.
Broadcast
플레이어의 게임 내 행동이 동일한 지역의 다른 플레이어들에게 실시간으로 알려져야 하므로, broadcast가 필요하며, 이는 게임 애플리케이션의 네트워크 트래픽을 웹 애플리케이션보다 훨씬 높게 만듭니다.
게임의 broadcast는 비용이 많이 듭니다. 플레이어의 입력과 출력은 비대칭적입니다. 예를 들어, 플레이어가 약간의 이동을 하면, 서버는 이 행동을 동일한 지역에서 이 플레이어를 볼 수 있는 다른 모든 플레이어에게 전달해야 합니다. 지역 내의 플레이어가 적으면 broadcast 메시지의 수가 너무 많지 않지만, 플레이어 수가 높은 수준에 도달하면 broadcast 메시지의 수는 기하급수적으로 증가합니다. 아래와 같이:
broadcast
지역 내에 1000명의 플레이어가 있고 각각이 행동을 하면, 서버가 이러한 행동을 지역 내의 모든 플레이어에게 알려서 모든 플레이어가 동일한 지역에서 서로를 볼 수 있도록 해야 한다면, broadcast 메시지의 수는 1,000,000에 달할 것이며, 이는 모든 것을 다운시킬 수 있을 정도로 충분히 높습니다.
분산
거의 많은 책, 강의 및 기사에서 이 점을 볼 수 있습니다: 분산 개발은 어렵습니다. 그 어려움은 다음을 포함합니다:
다중 프로세스(서버) 관리
게임 서버는 일반적으로 다중 프로세스 모델을 사용합니다. 이러한 프로세스들이 서로 얽혀 있으므로, 이러한 프로세스들을 관리하기가 어려워집니다.
서버(프로세스)에 대한 통일된 추상화와 관리가 없으면, 개발 환경에서 이러한 서버들을 시작하는 것은 매우 복잡한 작업입니다. 서버를 시작하고 재시작하는 작업은 개발 효율성에 심각한 영향을 미칩니다. 더욱이, 무거운 프로세스는 많은 머신 리소스를 소비합니다. 개발용으로 사용되는 일반적인 머신은 많은 프로세스를 견디지 못할 수 있습니다. 개발자가 여러 머신을 필요로 할 수 있으며, 이는 우리가 알고 있는 어려운 프로세스 간 디버깅을 가져올 것입니다.
RPC 호출
RPC 호출에 대한 솔루션은 많은 해 동안 존재해왔지만, 개발 효율성은 여전히 크게 개선되지 않았습니다.
여기서 우리는 가장 인기 있는 RPC 프레임워크인 thrift를 예로 들어 RPC 프레임워크를 전통적으로 어떻게 사용하는지 보여줍니다. 호출 코드를 작성하기 전에 다음 단계를 거쳐야 합니다:
.thrift 파일 작성
컴파일러를 사용하여 .thrift 파일에서 소스 코드 생성:
thrift - gen <language> <thrift_filename>
애플리케이션 개발에서 생성된 소스 코드 사용
그리고 정의된 인터페이스가 변경되면, .thrift 파일을 수정하고 위의 모든 단계를 다시 수행해야 합니다. 인터페이스가 불안정한 개발 환경에서는 이러한 방식으로 RPC를 수행하면 개발 효율성에 큰 영향을 미칩니다. RPC 개발을 더 쉽게 하려면, 인터페이스 설명 파일을 작성하고 stub 인터페이스를 생성하지 않는 유연한 방법이 필요합니다.
분산 트랜잭션 & 비동기 작업
관련 로직을 하나의 프로세스에 넣으려고 노력하더라도, 분산 트랜잭션은 여전히 불가피합니다. 2단계 커밋, 일반적인 프로그래밍 언어를 사용하는 개발에서의 비동기 작업은 쉬운 작업이 아닙니다.
로드 밸런싱, 높은 가용성
게임 서버는 상태를 유지하므로, 특정 플레이어의 요청은 특정 라우팅 규칙을 통해 동일한 서버로 라우팅되어야 하는 반면, 상태를 유지하지 않는 웹 서버의 경우 요청을 가장 부하가 적은 서버로 라우팅할 수 있습니다. 일반적으로 상태를 유지하지 않는 서버의 높은 가용성을 달성하는 것이 상태를 유지하는 서버보다 더 쉽습니다. 상태를 유지하는 서버의 경우, 높은 가용성을 달성하는 것은 매우 어렵지만, 그렇게 할 수 있는 방법도 있습니다. 다음은 두 가지 접근 방식입니다:
외부 저장소에 상태 저장
예를 들어, redis 또는 유사한 것을 사용하여 모든 플레이어의 상태를 저장할 수 있으므로, 서버는 어떤 상태도 보유할 필요가 없으며 상태를 유지하지 않게 됩니다. 하지만 모든 작업이 redis를 통과할 수 있으므로, 이는 성능 손실로 이어집니다. 그리고 어떤 경우에는 성능 손실이 견딜 수 없을 수 있습니다.
서버 중복
서버의 상태는 로그를 통해 백업을 위해 다른 중복 서버로 동기화될 수 있지만, 서버 전환 중에 순간적인 데이터 손실 문제가 있을 수 있으며, 이러한 데이터 손실은 일부 애플리케이션 경우에는 문제가 되지 않지만, 다른 애플리케이션 경우에는 심각한 데이터 불일치를 야기할 수 있습니다.
상태를 유지하는 높은 가용성은 구현하기 매우 어렵습니다. Pomelo v0.5는 높은 가용성 메커니즘을 제공하며, zookeeper와 redis를 도입하여 일부 서버(예: master 서버)의 높은 가용성 문제를 해결할 수 있지만, 실제 복잡한 애플리케이션 경우에는 로직을 애플리케이션 자체에서만 처리할 수 있습니다.
네이티브 Socket 개발의 문제점
위의 것 외에도, 네이티브 socket 개발에는 많은 문제점이 있습니다:
낮은 수준의 추상화
네이티브 socket의 추상화 수준은 사용하기에 너무 낮으며, 많은 메커니즘을 개발자가 구현해야 합니다. 예를 들어 session, filter, request, broadcast 등이 있습니다. 이는 무거운 작업이며 오류가 발생하기 쉬우며, 실제로 모든 서버가 수행할 많은 중복 작업을 야기합니다.
확장성
확장성은 메시지 밀도, 저장 정책, 서버 아키텍처 및 기타 요소를 포함한 많은 측면에 따라 달라집니다. 네이티브 socket으로 높은 확장성을 달성하려면, 개발자는 아키텍처 설계에 많은 노력을 기울여야 합니다.
서버 모니터링 및 관리
메시지 밀도, 온라인 플레이어 수, 머신 상태, 네트워크 압력 등과 같은 서버의 상태를 모니터링하는 것이 필요합니다. 네이티브 socket을 사용하여 이를 수행하면 큰 작업입니다.
프레임워크 기반 솔루션
네, 게임 서버 개발을 단순화하기 위해 프레임워크가 필요합니다. 게임 로직 자체를 제외하고, 대부분의 작업은 프레임워크에서 수행할 수 있습니다. 서버 추상화, 확장성, 확장 가능성, 이러한 문제들은 중복 작업을 피하기 위해 프레임워크에서 해결할 수 있습니다. 또한 게임 서버 프레임워크는 애플리케이션 서버 프레임워크로서 일부 작업을 수행할 수 있으며, 프레임워크를 컨테이너로 취급할 수도 있습니다. 그 사양을 충족하는 코드를 컨테이너에 넣으면 실행됩니다. 한편, 코드는 프레임워크가 제공하는 추상화, 확장성, 모니터링 & 관리 기능을 자연스럽게 갖추게 됩니다.
기존 게임 서버 프레임워크
오픈 소스 커뮤니티에는 수많은 웹 서버 프레임워크가 있으며, 게임 클라이언트 프레임워크와 라이브러리도 마찬가지이지만, 게임 서버 프레임워크는 거의 없습니다. 산발적인 라이브러리가 있을 수 있지만, 거의 완전한 솔루션이 없습니다. 따라서 일부 상용 솔루션과 비교해야 했습니다:
Sun RedDwarf
RedDwarf는 우리가 찾은 유일한 것으로, Sun Inc.에서 생산한 오픈 소스 게임 서버 프레임워크입니다. 불행하게도 Sun이 Oracle에 합병된 이후 중단되었습니다. RedDwarf는 분산 아키텍처를 사용하며, 분산 데이터 저장소와 작업 관리에 너무 많은 노력을 투자했으며, 매우 이상적이어서 매우 복잡합니다. 예를 들어, 동적 작업 마이그레이션의 구현은 매우 복잡하지만, 실제 애플리케이션에서는 거의 역할을 하지 않습니다. 확장성과 성능도 좋지 않았으므로, RedDwarf는 사라졌습니다.
SmartfoxServer
SmartfoxServer는 이탈리아의 gotoAndPlay()에서 생산했으며, 상용 게임 서버입니다. Java로 작성되었으며 Tomcat과 같은 웹 애플리케이션 서버처럼 보입니다. Smartfox는 다양한 클라이언트 플랫폼을 지원하며, 성공적인 애플리케이션 사례가 있습니다. 서버 추상화와 서버의 모니터링 & 관리 구현이 완벽합니다. 하지만 확장성 측면에서 좋지 않은 성능을 보입니다. Smartfox도 클러스터 모드를 지원하지만, JVM 메모리 복제를 기반으로 합니다. 전통적인 MMORPG 분할 솔루션을 지원하지 않습니다. Smartfox는 무료 버전이 있지만, 오픈 소스가 아닙니다. 그리고 무료 버전(온라인 사용자 수 제한)은 개발자들을 유료 버전 구매로 유도하는 것을 목표로 합니다. 유료 버전(온라인 사용자 수 제한 없음)의 가격은 $3500에 달합니다.
BigWorld
Bigworld는 호주의 Bigworld Technology에서 생산했습니다. 클라이언트 측과 서버 측을 포함한 완전한 3D MMORPG 게임 개발 솔루션입니다. Bigworld는 매우 강력하며, 동적 로드 밸런싱과 장애 허용에 많은 작업을 수행합니다. 확장성도 매우 강력합니다. 단점은 너무 무겁고 가격이 매우 비싸다는 것입니다. Bigworld는 대규모 3D MMORPG 게임을 위해 설계되었지만, 소규모 및 중규모 게임 개발에는 적합하지 않습니다.
Pomelo 솔루션
소규모 및 중규모 게임 개발에 적합한 프레임워크가 없는 현재 게임 서버 프레임워크 시장의 상황으로 인해, 우리는 Node.js를 기반으로 MIT 오픈 소스 라이선스를 사용하는 오픈 소스 프레임워크인 Pomelo 프레임워크를 출시했습니다. 고성능, 확장 가능, 경량 게임 서버 프레임워크를 제공하는 것을 목표로 합니다. 실제로 게임 서버 개발의 어려움을 해결하고 게임 서버 개발을 더 쉽게 만듭니다. 다른 유사한 프레임워크와 비교하면, 주요 장점은 다음과 같습니다:
Pomelo 개발은 설정보다 관례의 원칙을 기반으로 빠르고 접근하기 쉬우며, 이는 코드를 단순화합니다.
프레임워크 아키텍처가 제공하는 높은 확장성과 확장 가능성은 애플리케이션을 확장하고 확장하기 매우 편리하게 만듭니다.
경량, Pomelo는 분산 아키텍처를 가지고 있지만, 적은 리소스 요구로 매우 빠르게 시작할 수 있습니다.
포괄적인 참고 자료, Pomelo는 완전한 문서뿐만 아니라 완전한 MMO 데모(클라이언트는 HTML5 사용)를 제공하며, 이는 개발자에게 강력한 참고 자료로 사용될 수 있습니다.
Node.js를 선택한 이유
분산 개발의 많은 어려움에 대해 이야기한 후, Node.js를 소개하는 것은 타당합니다. 네이티브 비동기 프로그래밍 모델 때문에 분산 개발의 많은 어려움을 해결할 수 있습니다:
네이티브 분산
Node.js가 node라고 불리는 이유는 다중 프로세스 개발 모델을 기본적으로 지원하기 때문이며, 여러 노드(프로세스)가 통신하고 서로 얽혀 분산 시스템을 형성할 수 있습니다. 프로그래밍 모드는 비동기이며, 트랜잭션을 위한 2단계 커밋, 비동기 작업 등 복잡해 보이는 작업들이 Node.js에서 기본적으로 지원됩니다.
단일 스레드 애플리케이션 모델
Node.js가 제공하는 단일 스레드 애플리케이션 모델은 다른 언어보다 강력하며, 데드락과 lock race를 피하면서 단일 스레드 모델을 사용하여 게임 로직을 처리하는 것이 가장 간단하며, 오류가 발생하기 쉽지 않습니다.
네트워크 IO & 확장성
게임 서버는 IO 집약적 애플리케이션이므로, Node.js는 이벤트 기반, 논블로킹 IO 모델을 사용하기 때문에 가장 적합합니다. Node.js는 네트워크 프로그래밍을 위한 높은 수준의 추상화를 도입하여 네트워크 프로그래밍을 쉽게 합니다. 한편, Node.js를 사용하여 확장 가능한 애플리케이션을 구축하기가 쉽습니다.
언어 장점
JavaScript는 현재 중요한 언어가 되고 있으며, JavaScript로 개발하면 빠른 반복을 얻을 수 있습니다. 단순성과 경량성은 개발 효율성을 높일 수 있습니다. 또한 HTML5 또는 JavaScript를 지원하는 Unity3D와 같은 일부 클라이언트 플랫폼의 경우 서버 측과 클라이언트 측 간의 소스 코드 공유가 가능해집니다. 또한 JavaScript가 제공하는 동적 타이핑은 DSL 설계 및 설정보다 관례 구현과 같은 프레임워크 설계에 많은 편의를 가져올 수 있습니다. Ruby보다는 다소 좋지 않지만, Pomelo 프레임워크에서 사용하기에는 충분히 좋습니다.
게임에서 실시간 애플리케이션으로
Pomelo의 설계 목표 분석을 완료한 후, 핵심 프레임워크가 게임 서버에만 국한되지 않으며 범용 실시간 애플리케이션 프레임워크라는 것을 발견했습니다. 제공된 데모 채팅은 실시간 애플리케이션입니다.
실제로 Pomelo는 많은 비게임 분야에서 사용되었습니다. Netease에서 생산한 메시지 push 서버는 Pomelo를 기반으로 개발되었으며, Netease에서 생산한 일부 앱의 모바일 클라이언트와 웹 클라이언트에 메시지 push를 지원하며, 현재 온라인 상태입니다.
요약
이 섹션에서는 먼저 게임 서버의 특징을 분석했으며, 복잡성 때문에 게임 서버를 개발하는 것이 웹 서버보다 더 어렵다는 결론을 내렸습니다. 기존 게임 서버 프레임워크의 결함을 제시하고 Pomelo가 이를 어떻게 해결하는지 설명했습니다. 또한 Pomelo의 설계 동기와 목표를 간단히 소개했습니다. 다음은 Pomelo 프레임워크 개요입니다.