Outdated/Game

[Summary] 게임 서버 프로그래밍 교과서 - Ch4

해달 2019. 8. 28. 08:00

시작하기 전에

  • 본 글은 배현직 저자님의 게임 서버 프로그래밍 교과서를 읽고 썼습니다.

  • 본 글은 저자님의 요청으로 언제든지 지워질 수 있습니다.



목차

  1. 게임 서버의 역사

  2. 게임 서버가 하는 일

  3. 논블로킹 소켓

  4. Overlapped I/O 혹은 비동기 I/O

  5. epoll

  6. IOCP

  7. 실습


1. 게임 서버의 역사

 게임 서버의 기원은 패키지 게임에서부터 살펴봐야 한다. 인터넷이 보급되기 전에는, 인터넷 연결이 안되는 컴퓨터가 많았고, 연결되더라도 속도가 매우 느렸으며, 요금이 비싸기 때문에 대부분의 패키지 게임은 네트워크를 사용하지 않았다. 설령, 사용하더라도 전화선을 통해 상대방 컴퓨터와 1:1로 플레이하는 방식이었다.


 인터넷이 보급되기 시작하면서, 세 명 이상 인터넷을 통해 플레이하는 게임이 등장하였다.  이 때, 플레이어 한 명의 컴퓨터가 모든 플레이어의 게임 플레이 상태를 취합해서 유지하였고, 이를 서버(server)라고 불렀다. 패키지 게임에서의 서버 역할은 세션(session) 처리를 담당했다. 일부 패키지 게임에서는 순전히 클라이언트의 연결을 받는 세션을 처리만 하는 프로그램이 따로 들어있기도 했는데 이를 데디케이티드 서버(dedicated server)라 하였다.


 그 후, 온라인 게임이라는 것이 등장했고, 온라인 게임에서의 게임 서버는 컴퓨터 네트워크 어딘가에서 게임 로직을 처리하였고, 플레이어들이 그 서버에 접속해 게임을 즐기는 형태가 되었다.

2. 게임 서버가 하는 일

 대부분 온라인 게임에서의 게임 서버는 클라이언트로부터 데이터를 받아서 여러 플레이어 사이의 행동을 중재하여 결과를 판정한다. 이는 클라이언트의 해킹을 방지하고, 여러 사용자와 상호작용을 하기 위함이다. 그래서 기본적으로는 모든 게임 플레이 판정을 할 수 있어야 하지만, 쾌적한 품질을 위해서 일부 처리를 게임 클라이언트 쪽에 맡기게 된다. 이외에도 플레이어의 정보 혹은 상태를 보관한다.


 먼저 상호작용(interaction)이라는 것은 클라이언트와 서버 간에 메시지가 오가면서 특정 행동을 하는 것을 말한다. 여기에는 연결, 요청-응답, 능동적 통보, 연결 해제가 있다. 연결은 최초로 클라이언트가 서버와 데이터를 주고받을 준비를 하는 것을 말한다. 요청-응답은 연결을 마친 후, 클라이언트가 서버에 메시지를 보내고, 서버는 이를 처리한 후 결과를 응답하는 것을 말한다. 이때, 서버는 클라이언트의 신원을 확인해 연결을 계속 유지할지 아니면 추방할지 판단할 수 있으며, 서버는 클라이언트의 모든 행동에 대해 반응하지 않을 수 있다. 능동적 통보는 서버에서 능동적으로 클라이언트에게 통보하는 것을 말하며, 세션의 상태를 주로 전달하게 된다.


 다음으로 플레이어 정보를 저장하는 것에 대해서 알아보자. 요즘 게임들은 플레이어의 데이터 관리를 중요하게 여긴다. 게임을 구동하는 컴퓨터 자체의 디스크에 데이터를 저장하는 싱글플레이 게임과는 달리(물론, 요즘은 일부의 데이터를 클라우드 저장소에 저장한다. 예로 스팀 클라우드가 있다.) 온라인 게임은 서버에 저장한다. 왜냐하면 클라이언트는 해킹에 취약하고, 같은 사용자가 다른 기기를 사용할 때 문제가 되기 때문이다. 온라인 게임에서 서버에 저장된 데이터를 불러오는 과정은 다음과 같다.


  1. 사용자는 클라이언트를 실행한 후 아이디와 비밀번호를 입력한다.

  2. 서버는 사용자가 누군지 확인한 후, 해당하는 사용자의 플레이어 데이터를 디스크에서 로드한다.

  3. 그 중 필요한 것만 골라 클라이언트에게 전송한다.


 게임 서비스의 규모가 큰 경우 서버의 디스크가 아니라 별도의 DB 서버를 사용한다. 가령, 동시접속자가 수천 명 이상이거나, 플레이어 데이터를 더 안전하게 보관하고 관리해야 하는 경우 등이 있다.


 또한, 데이터를 저장하는 데는 파일 시스템을 이용하거나 DB 시스템을 이용할 수 있다. 비교하자면 다음과 같다.


구분

파일 시스템

DB 시스템

비고

SW 비용

없다.

없거나 높다.

오픈 소스 제품은 제한적으로 무료다.

저장 및 로딩 속도

빠르다.

느리다.

DB 시스템도 결국은 파일 시스템을 사용한다.

데이터 관리 및

분석 속도

느리다.

빠르다.

DB는 빠른 검색을 위한 인덱스 기능이 존재한다.

데이터 백업 및

복원 기능

없다.

있다.

-

원자성

불가능하다.

가능하다.

DB에는 트랜잭션(transaction)이 있다.

일관성

없다.

있다.

DB에는 제약조건(constraints)이 있다.

고립성

없다.

있다.

경쟁 상태에서 자유로울 수 있게 한다.

DB에는 락(lock)이 있다.

지속성

없다.

있다.

장애 직전 상태로 복구할 수 있다.

DB에는 로그 버퍼 기능이 있다.


3. 게임 서버의 품질

 게임 서버의 품질을 높이려면 4가지 기준을 고려해야 한다. 안정성(stability), 확장성(scalability), 성능(performance), 관리 편의성이 그것이다.


 안정성은 게임 서버가 얼마나 죽지 않는지 혹은 게임 서버가 얼마나 오작동을 하지 않는지를 말한다. 발생하는 상황은 다음과 같다.


  1. 서버 프로그램의 버그 때문에 서버 프로세스가 비정상으로 종료되는 경우

  2. 서버에 교착 상태나 어이없는 상황이 발생해 프로세스가 일시 정지하는 경우

  3. 물리적으로 타격을 입었을 경우


 첫번째 같은 경우 주된 요인이기 때문에 설계의 구조적인 문제나 사소한 코딩 실수가 발생하지 않도록 주의를 기울여야 한다. 안정성을 높이려면 아래 4가지를 시행해보자.


  1. 치밀한 개발과 유닛 테스트
    결과물을 꼼꼼히 검토하고, 엄격하게 규정된 코딩 가이드라인을 따르자.

  2. 80:20 법칙
    모든 프로그램 성능의 80%는 20%의 소스 코드에서 나타난다는 파레토 법칙이다. 성능에 지대한 영향을 주는 일부분의 소스 코드에서만 성능을 최적화하고 나머지는 유지 보수하기 쉬운 단순한 구조로 개발하자.

  3. 1인 이상의 코드 리뷰
    다른 사람의 관점에서, 몰랐던 버그를 발견하는 경우가 종종 있다. 코드 리뷰를 하게 되면, 상대방 코드를 이해하면서 서로 지식을 공유하는 효과도 있으며, 누군가 퇴사하더라도 그 자리를 완충해준다.

  4. 가정하지 말고 검증하라
    유닛 테스트만으로는 충분하지 않으며, 스트레스 테스트(stress test)와 클로즈 베타 테스트(CBT, close beta test), 오픈 베타 테스트(OBT, open beta test) 등을 해야한다. 스트레스 테스트 방법은 다음과 같다.

    1. 서버를 띄우고, 대량의 더미 클라이언트(dummy client)를 실행한다. 더미 클라이언트는 입력 처리와 렌더링 과정이 생략되어 있으며, 미리 프로그래밍된 행동을 반복한다.

    2. 더미 클라이언트를 서버에 접속시키고, 서버는 성능 지표를 켜서 서버에 걸리는 과부하나 이상 행동 현상을 관찰한다.

    3. 문제점이 발견되면 테스트를 중단하고 문제를 해결한다.

    4. 더 이상 문제가 발견되지 않을 때까지 반복한다.


 그럼에도 안정성을 100% 확보하기는 매우 어렵다. 그래서, 서버가 불안정해지더라도 극복할 수 있는 방안들을 구해야 한다.

  1. 서버가 죽더라도 최대한 빨리 다시 살아나게 한다.
    서버 프로그램과 감시 프로그램을 동시에 실행시키고, 서버 프로그램이 중지할 경우 감시하던 프로그램이 서버 프로그램을 다시 실행시킨다.

  2. 서버는 죽더라도 최대한 적은 서비스만 죽게 한다.
    서버 프로세스를 여러개 띄워 처리를 각자 나누게 한다.

  3. 서버 오작동에 대해서 기록을 남긴다.
    크래시 덤프(crash dump)를 파일로 남기거나 서버가 최근에 받았던 메시지 종류를 파일로 남긴다.


 확장성은 서버를 얼마나 많이 설치할 수 있는지를 얘기한다. 사용자 측면에서 본다면 사용자 수가 늘어나더라도 서비스 품질이 떨어지지 않고 유지되는지로 얘기할 수 있다. 확장에는 수직적 확장(scale-up)과 수평적 확장(scale-out)이 있다. 다음 표로 비교해보자.


구분

수직적 확장

수평적 확장

확장 종류

서버 머신의 부품을 업그레이드 혹은 서버 머신 안의 CPU, RAM을 증설한다.

서버 머신의 개수를 증설한다.

서버 소프트웨어
설계 비용

낮다

높다

확장 비용

기하급수적으로 높아진다.

선형적으로 높아진다.

과부하 지점

서버 컴퓨터 자체

네트워크 장치

오류 가능성

로컬 머신 안에서 동기 프로그래밍 방식으로 작동하므로 낮다.

여러 머신에 걸쳐 비동기 프로그래밍 방식으로 작동하므로 높다.

단위 처리 속도

로컬 컴퓨터의 CPU, RAM만 사용하기 때문에 높다

여러 서버 컴퓨터 간의 메시징이 오가면서 처리하므로 낮다.

처리 가능 총량

서버 컴퓨터 한 대의 성능만 사용하므로 낮다.

여러 서버 컴퓨터로 부하가 분산되므로 높다.


 성능은 얼마나 빠르게 처리하는가를 말한다. 성능과 확장성은 서로 연관되는 경우가 많으나, 항상 그러한 것은 아니다. 성능만 좋으면 게임 사용자 수가 늘어날수록 플레이어의 행동 처리가 완료되는 데 걸리는 시간이 크게 증가하고, 확장성만 좋으면 게임 사용자 수에 상관없이 처리하는 데 걸리는 시간은 일정하게 크다. 온라인 게임의 처리 성능은 서버 뿐만 아니라 네트워크 환경에 따라서도 결정되기 때문에 다양하게 살펴봐야 하는데, 아래를 고려해볼 수 있다.

  1. 서버의 단위 처리 속도를 높인다.
    게임 서버의 성능을 높이는 기본적인 원칙이다. 코드 및 알고리즘을 최적화하거나, 더 빠른 프로그래밍 언어를 사용하거나 더 좋은 서버 하드웨어를 사용함으로써 높일 수 있다.

  2. 서버의 과부하 영역을 분산한다.
    분산은 코드 프로파일링(code profiling)을 통해 지연에 영향을 주는 로직을 발견하고 최대한 개선했음에도 문제가 될 때 하는 것이 좋다.

  3. 네트워크 프로토콜 최적화한다.
    네트워크 프로토콜을 최적화하려면 메시지를 압축해 메시지 크기를 줄이거나 묶을 수 있는 메시지는 같이 묶어 보내는 식으로 메시지 교환 횟수를 줄여 목적을 달성할 수 있다. 메시지 압축에는 수 킬로바이트 이상의 텍스트 데이터의 경우 ZLib을 이용한 압축과 같은 무손실 알고리즘을 사용하고, 이동 처리와 같이 데이터의 단위 크기가 수십 바이트 정도로 작고, 압축 여지가 있는 데이터 패턴을 이루지 않는 경우에는 압축 대신 양자화(quantization)를 한다.

  4. 네트워크 전송 시간을 줄인다.
    네트워크 전송 시간을 줄이기 위해 고품질 네트워크 회선을 가진 데이터센터에 서버를 지리적으로 가까운 곳에 분산하여 설치한다. 실제로 많은 게임 서비스가 채택하는 방식이다.

  5. 클라이언트끼리 직접 통신하게 한다.
    이를 P2P(Peer to Peer) 네트워킹이라고 한다. 클라이언트 간 레이턴시가 클라이언트와 서버 간 레이턴시보다 짧은 경우 사용할 수 있다. 클라이언트가 직접 통신하는 것이라 서버에 걸리는 부하도 줄어들게 된다. 단점은 서버가 메시지 검증을 할 수 없다. 다시 말해 클라이언트 해킹에 취약하다. 또한, NAT 때문에 중계 서버가 필요할 때도 있다.


 마지막으로 관리 편의성을 살펴보자. 일반적으로 게임 서버는 콘솔 프로그램 형태로 작동하며, 개중에는 사용자의 키 입력을 받는 기능조차 없이 작동되는 것들도 존재한다. 보통 백그라운드 프로세스로 프로그램을 실행하는데, 사용자 로그인을 하지 않아도 자동으로 실행하기 위해 운영체제에 서버 프로그램을 등록하기도 한다. 백그라운드 프로세스를 윈도우즈에서는 서비스(service)라고 하며, 리눅스 등 유닉스 기반 운영체제에서는 데몬(daemon)이라고 한다.


 서버 프로그램은 보통 입력도 하지 않고, 출력도 하지 않아 관리 도구 혹은 운영 도구가 필요하다. 관리 도구는 관리자가 서버 프로그램을 관리하기 위해 원격으로 관리하는 프로그램으로 GUI 앱, 콘솔 프로그램, 웹앱 등일 수 있다. 필수 기능은 (1) 서버 켜기/끄기, (2) 동시접속자 수 보기, (3) CPU, RAM 사용량 보기이다. 게임을 운영하는 조직이 클수록 역할이 분담되는데, 실수나 의도적인 사고를 막으려고 사용자에 따라 일부 기능이 제한되게 만들기도 한다.

4. 서버 구동 환경

 서버는 게임 회사가 물리적 서버를 둘 수도 있고, 클라우드 서버를 이용할 수도 있다. 물리적 서버를 이용하는 경우 하드웨어, 운영체제, 서버를 둘 장소를 고려해야 한다.


 운영체제는 1990년대 중반에는 Solaris를 사용되다가 후반부터는 윈도우즈 서버(windows server)가 대체하게 되었다. 그 이유는 IOCP 덕분에 최소 수백 개 이상의 네트워크 클라이언트와의 매우 빠른 처리 속도를 만족했고, 일반 윈도우즈 운영체제를 그대로 사용해서 서버와 클라이언트를 한 자리에 구동할 수 있었기 때문에, 서버와 클라이언트가 같은 소스 코드를 직접 공유해서 빌드할 수 있다는 장점 때문이었다. 하지만, 2000년 이후부터는 유닉스 계열 운영체제에 epoll, kqueue와 같은 것이 제공됨으로써, 골고루 사용되고 있다.


 하드웨어는 서버 전용 하드웨어가 있다. 이것은 일반 컴퓨터보다 성능이 크게 뛰어난 건 아니지만, 쉽게 고장이 나지 않거나 고장이 나더라도 켜진 상태에서 노후 부품을 교체할 수 있는 등의 특별한 기능이 있어서 고가인 부품이다. 여기에는 Intel사의 Xeon CPU, 내구도가 높은 ECC RAM, RAID 디스크 드라이브 등이 있다.


 장소는 (1) 인터넷 품질이 신뢰성이 있어야 하고, (2) 서버가 쉽게 고장나지 않아야 하고, (3) 도난이나 침입 사고에서 안전해야 한다. 그래서 서버는 데이터 센터라는 특별한 건물에 설치된다.


 물리적 서버는 처리 속도가 빠르지만, 초기 비용이 많이 들고 확장성이 떨어지는 단점이 있다.


 클라우드 서버에서 클라우드란, 구름이 많은 물방울이 모여서 만들어진 것처럼 매우 많은 수의 서버가 모여 있는 집합체를 말하며, 실상은 가상 머신의 집합체이다. 클라우드 서버는 서버 컴퓨터만 가상 머신으로 구동하는 것이 아니라, 네트워크 장치나 외장 디스크 드라이브도 모두 가상화 되어 있다. 클라우드 서버가 제공하는 기능은 계층별로 다양하다. 다음은 그 예이다.


  • IaaS(Infrastructure as a Service)

    • 가상 머신 자체를 제공하는 서비스이다.

    • 원하는만큼 가상 머신을 대여할 수 있다.

    • AWS EC2, Azure Virtual Machine

  • PaaS(Platform as a Service)

    • 서버 프레임워크까지 구동되는 머신을 제공하는 서비스이다.

    • 더 쉽고 빠르게 서버를 개발할 수 있다.

    • 자유도가 낮다. 운영체제에는 접근할 수 없고, PaaS에서 제공하는 프레임워크의 기능을 뛰어넘는 작업은 하기 어렵다.

    • AWS Lambda, Azure Functions, 여러 웹 호스팅 서비스

  • SaaS(Software as a Service)

    • 코딩이 불필요하다.

    • 과금이나 데이터 분석, 페이스북 로그인 연동 같은 특화된 기능을 제공한다.

    • 가장 빠르고 편리하게 이용할 수 있다.

    • 자유도는 가장 낮다.

    • AWS Cognito, Azure Maketplace, Google Analysis 등


 클라우드 서버는 물리적 서버보다 처리 속도가 느리지만, 확장성이 높고, 초기 비용이 적게 든다는 장점이 있다. 그래서, 동시접속자 기복이 심하지 않거나 처리 속도가 중요한 경우에는 물리적 서버를 주로 사용하고, 이외에는 클라우드 서버를 사용한다. 레이턴시가 민감한 게임에서는 클라우드 서버와 온프림 서버를 혼용하기도 한다. 아래는 물리적 서버와 클라우드 서버를 비교한 것이다.


구분

물리적 서버

클라우드 서버

특징

서버 컴퓨터 한 대에 운영체제 하나를 구동한다.

서버 컴퓨터 한 대에 여러 운영체제를 가상 머신으로 구동한다.

서버 한 대당 유지 비용

낮다.

높다.

서버 증설 속도

낮다.

높다.

서버 철거 속도

낮다.(경우에 따라서는 불가능)

높다.

자동 스케일 아웃

불가능하다.

가능하다.

처리 속도의 균일성

높다.

낮다.

장애 처리

느리다.

빠르다.