해당 블로그의 내용은 게임 서버 프로그래밍 교과서의 내용을 요약, 정리한 것입니다.
게임 서버 품질을 논할 때, 확장성이 있음.
사용자 수가 늘어나더라도 쉽게 대응할 수 있어야 한다.
사용자 수가 늘어나도 서버 성능을 유지하려면 수직확장, 수평 확장 중 하나를 수행해야 한다.
서버의 하드웨어를 더 좋은 것으로 교체, 처리량을 늘리는 방법
서버 대수를 늘려서 더 많은 처리를 하는 것
일반적으로는 수평 확장이 더 현실적이다. 서버의 하드웨어 사양은 결국 한계가 있기 때문.
만약 모든 게임 서버 로직은 서버 프로세스 하나에서 수행한다고 가정해 보자.
이러한 게임 서버에 동시접속자 수가 증가한다면,
32비트 서버에서는 메모리를 수 기가바이트까지 사용하며, 메모리 할당 함수에서 null을 리턴한다.
결국 비정상 종료로 이어진다.
64비트 서버에서는 대량의 메모리 스와핑이 발생한다. 프로그램 실행 속도가 급락하면서 메모리 할당량이 더욱 증가하며, 악순환이 발생한다.
서버 클러스터는 인증 서버 한대와 채널 서버 한대로 구성된다.
인증서버는 사용자가 ID와 비밀번호를 입력했을때 그것을 인증 처리하는 역할을 한다.
사용자 인증 외에는 아무 일도 하지 않다보니 굳이 수평적 확장을 하지 않더라도 전 세계 모든 사람이 동시 접속해도 처리할 수 있다.
각 채널서버는 게임 서버 한대와 데이터베이스로 구성된다.
게임은 보통 국가적, 문화적 차이가 있어 서로 다른 서버로 분리된다.
하지만 다음과 같은 단점이 있다.
일단 단일 서버로 만든 후 성능 과부하 지점을 알아냈다고 가정해보자.
이를 분산하는 방법은 크게 데이터 단위 분산과 기능 단위 분산이 있다.
또한 게임 로직의 분산 처리 방식은 동기 분산 처리, 비동기 분산 처리, 데이터 복제 및 로컬 처리가 있다.
데이터 분산이란 한 머신이 처리해야 하는 데이터를 같은 역할을 하는 여러 머신이 나누어서 처리하는 것이다.
기능적 분산이란 한 머신이 처리해야 하는 데이터의 처리 단계를 세분화해서 여러 머신이 나누어 처리한다.
어떤 연산을 다른 서버에 던져 놓고 그 결과가 올 때까지 대기한다. 또한 그 연산과 관계된 데이터가 도중에 변경되지 않게 잠금 해야 한다.
예를 들어, 서버 1에서는 플레이어 캐릭터 정보를 가지고 서버 2에서는 몬스터 캐릭터 정보를 갖고 있다고 가정하자.
플레이어가 몬스터를 공격하면, 서버 1에서는 몬스터 사냥에 사용한 무기를 1개 없애는 것부터 시작한다.그리고 서버 2에 몬스터에게 대미지를 주어라 라는 처리를 하라고 전송한다.
이 명령이 전송되고 나서 서버 1은 서버 2의 응답이 올 때까지 기다린다.
한편, 서버 2는 이 명령을 받아 몬스터 정보를 업데이트 한다.업데이트 한 결과를 서버 1에 알려 준다.
이러한 모델은 게임 개발 이외에 MPI나 액터 모델 등으로 부른다.
서버 1에서 서버 2에 몬스터에 대미지를 주었다 라고 통보한다.
서버 2는 서버 1에 플레이어에게 아이템을 주었다 라고 통보한다.
이러한 방식의 가장 큰 장점은 잠금으로 인한 병목이 없다는 것이다.
하지만 모든 로직을 이러한 방식으로 구현하는 것은 어렵다. 쉽게 생각하면 반환값 없는 함수로 로직을 구현하는 것과 유사하다.
앞의 두 방식은 분산 처리에 관여하는 데이터들이 원본 서버에만 있는 경우이다.
서버 1은 플레이어 정보만 가지며, 몬스터를 담당하는 서버 2는 몬스터 정보만 가진다.
데이터 복제에 기반을 둔 로컬 처리는 서버 1이 플레이어 정보와 함께 몬스터 정보까지 같이 가지고 있다. 이는 서버 2도 마찬가지이다.서버 1과 서버 2는 자기가 가진 데이터에 변경이 일어나면 변경되었다는 사실을 상대방에게 알려 준다.
결과적으로 게임 플레이를 위한 로직 처리는 두 서버에서 분산된다. 그러나 다른 서버와 맞물려 돌아가지 않고 각 서버가 알아서 처리한다.
이런 방식으로 분산 처리를 하면 병목 현상이 없을 뿐만 아니라 여러 머신에 걸쳐 연산하지도 않으므로 응담 속도도 분산하기 전과 같이 빠르다.
하지만, 사본 데이터는 원본 데이터와 간발의 차이가 발생할 수 있는데, 이를 통해 발생하는 잘못된 연산은 하이젠버그라고 한다.
위의 방식들은 장단점이 있다. 따라서 분산 처리가 가장 적게 일어나는 방법을 찾으면 좋다.
그 중 한 방법은 응집도가 높은 데이터끼리는 가급적 분산 처리를 하지 말고, 응집도가 낮은 데이터에 대해 분산 처리를 하는 것이다.
만약 게임 안에 월드가 대륙 단위로 이루어져 있고, 서로 다른 대륙에 있는 캐릭터끼리는 상호 작용을 할 수 없다면 데이터 응집도는 분명하게 구분된다. 대륙별로 서버를 두면 된다.
매치메이킹을 담당하는 게임 로비 서버를 만든다고 가정하자. 로비 서버를 분산해야 할때, 데이터 응집도는 실력이 비슷한 플레이어끼리 매칭되도록 할 것이다.
이처럼 플레이어 실력을 기준으로 분산할 수도 있다.
동기 분산 처리, 비동기 분산 처리, 데이터 복제에 기반을 둔 분산 처리를 사용할 수 없을 때 기능적 분산 처리 혹은 수직적 분산 처리를 사용한다.
만약 경매장 시스템을 만든다고 가정한다. 게임 서버에서 경매장을 담당하는 처리를 분리하고, 경매장 처리만 담당하는 서버를 개발하면 된다. 이를 기능적 분산 처리라 한다.
기능적 분산 처리는 분산 처리를 할 수 있는 범위가 제한되어있다. 기능적 분산 처리에서 분산의 양은 기능을 쪼갠 만큼만 할 수 있다.
또한 기능적 분산 처리는 수평 분산보다 분산 효율성이 떨어진다. 해당 기능을 담당하는 서버가 과부하가 걸리면 다른 서버가 이를 대신해 주지 못한다.
기능적 분산 처리는 최후의 수단이다. 수평 분산 처리를 할 수 있는 방법을 최대한 찾아본 후 도저히 안 되면 기능적 분산 처리를 해야 한다.
지나친 분산 처리는 피하는 것이 좋다. 지나친 분산 처리는 네트워크 장비에 부하 몰림, 네트워크 장비 과부하로 서비스 장애를 일으킬 수도 있다.
네트워크 장비는 쉽게 과부하에 걸리지 않지만 일단 걸리면 해결이 어렵다.
게다가 분산 처리는 디버깅이 까다롭다.여러 원격 프로그램에 원격 디버깅을 하거나 로그 출력을 보고 원인 분석을 해야 한다.
또한 분산 처리는 운영체제가 해야 하는 일을 불필요하게 증가시킨다.
클라우드 서버 환경에서는 클라우드 서버 인스턴스 간에 통신 회선의 신뢰성도 문제가 될 수 있다.
게임 서버가 분산 처리되어 있다고 하더라도 데이터베이스를 분산 처리하지 않으면 결국 서비스 가용성은 떨어진다. 더 많은 사용자를 처리하고자 데이터베이스가 수평 확장을 할 때는 갖고 있는 레코드를 서로 다른 데이터베이스에 나누어 놓는다. 이를 파티셔닝이라 한다.
한편 여러 테이블 각각을 다른 데이터베이스 서버 기기에 분배하기도 한다. 이것을 수직 파티셔닝이라 한다.