IT_Server/etc.(related Server)

[펌] 잘 죽지 않는 게임 서버를 설계 해보자 - 서버 HA (High Availability)

JJun ™ 2013. 6. 24. 04:45

 


 출처: smileeagle  님의 글

          http://lab.gamecodi.com/


 

[1부]

http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Lecture_series&page=1&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=41

 

[2부]

http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Lecture_series&page=1&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=42

 


 

 

 

 

안녕하세요? 게임 서버 개발을 업으로 하고 있는 이글입니다.  

그냥 아는건 별로 없지만 강좌하나 적어보고 싶었어요
전 게임 서버개발은 다른거 다 필요없고, 3가지만 만족하면 충분히 괜찮다고 봅니다..

 

1. 안 죽게 개발해야 함

2. 랙 없게 개발해야 함
3. 빠른 위기 대응능력을 갖추어야 함

 

여기에 유지보수성 이라던지 확장성 사용성 막 어려운 말들 많지만 위 1,2,3 이 제일 중요한것 같네요.

그런데 상기에 언급된 1번 안죽는 서버를 만족시킬 수 있는 방법이 있습니다.
(당연히 절대로 죽지 않는건 아닙니다만 용량 폭주나 데드락, 답없는 개발자의 실수 등.. )
그게 바로 지금 이야기 할 HA (High Availability) 라고 할 수 있겠네요.

서버 이중화 기능이라고 흔히 표현하는 HA 시스템은 
대형이동통신사 혹은 은행같은 곳의 서버들에서 먼저 많이 사용하는 기술입니다.
순수하게 소프트웨어 만으로 구성하는 방안도 있지만 특정 네트워크 장비를 같이 이용하여

구축하는 방안도 있지요. (장비값이 비쌈)

 

자 그럼.

 

1. HA란 무었이냐?

 

우리가 자주 참조하는 위키에서는 HA를 다음과 같이 정의하고 있네요~

High availability is a system design approach and associated service implementation that ensures a prearranged level of operational performance will be met during a contractual measurement period.

Users want their systems, for example wrist watches, hospitals, airplanes or computers, to be ready to serve them at all times. Availability refers to the ability of the user community to access the system, whether to submit new work, update or alter existing work, or collect the results of previous work. If a user cannot access the system, it is said to be unavailable.[1] Generally, the term downtime is used to refer to periods when a system is unavailable.

 

즉, 사람들은 멈추지 않는 서비스를 바라는데, 언제든지 사용자에게 멈추지 않는 서비스를 제공하는 것에

의의가 있습니다. (한줄 요약) 간혹 그런 서비스들 있지요? 몇몇 게임사들의 라이브 업데이트!?

그게 HA시스템 디자인으로 인해 가능한 것이라고 할 수 있겠지요.

 

우리가 학교에서 배운 Fault Tolerance 시스템 (RFC992) 을 돌이켜 보면 될것 같습니다.

즉, HA란 특별한 시스템 명을 지칭하는 것이 아닌 일종의 디자인 패턴같은 개발에 대한 접근방식이다.

 

HA를 구성하는 방식은 여러가지 방법이 있습니다.

 

1. Application Layer에서 구성하는 방식 (여기서 다룰 방식입니다)

2. DB ( 여기서 다룰 주제는 아니지요! )

3. RAID (이건 그냥 디스크 다중화)

 

레이드나 DB 이중화 같은 거 설명하면 글이 너무 길어지니 다음 기회로 미루도록 하겠습니다. =_=;


 

2. HA를 구현하게 되면 뭐가 좋을까요?

 

 - 라이브 업데이트가 가능해짐

 

서버를 내리지 않고도 서버 로직 업데이트를 할 수 있습니다! 모 게임들이 라이브 업데이트로 유명하지요!

 하지만(중요) 게임 서버 같은 게임클라이언트와의 종속관계가 High Coupling인 어플리케이션 서버의 경우 Client-Server간 통신 프로토콜이 변경되지 않아야 한다는 전제조건이 따릅니다. (클라이언트가 많이 바뀌면 답 없어요 점검해야 해요)


 - 서버가 죽어도 유저가 모른다 (중요)

 

아무리 훌륭한 개발자라도 사람입니다. 어제 과음한 후 피곤한 상태로 개발할 수 도 있고,

실수로 널포인터 하나 빼먹을 수도 있고 버퍼 Static으로 잡은 다음 예외처리 못 할수도 있고 그런것이죠..

그런데 그걸 QA에서 잡아내지 못한다면? 바로 사고로 직행입니다.

상용 서비스 중에 서버 다운되면 가슴아프죠..

예전에 그런적이 있습니다.

평소에는 전혀 문제가 없었는데(QA도 당연 통과) 어느 새벽 갑자기 클라이언트 해킹유저가 우편에 내용을

몇 메가바이트 어치를 담아서 초당 수십개씩 전송하고 받고 그러기 시작한겁니다. 버퍼오버플로우가 걸려서

다운. 더 심각했던건 코어 라이브러리 단에서 서버간 통신이 끊어짐.. 그 당시에는 원인은 불명!


위와 같은 긴급상황이 발생하게 되면 미리 대비해놓지 않은이상 서버가 시도 때도 없이 죽어버립니다.

라이브 서비스중인 서버에 시그널 핸들러를 넣어서 Stack Trace를 걸어놓고, 그것도 부족해서 크래쉬덤프까지 준비 해놔도 빠른 원인파악이 어려운 경우가 발생합니다.

 

결국 서버 개발자가 문제의 원인을 찾아서 해결하지 전까지는 게시판이 초토화 되는 상황이 발생.

거기에 그때 리드프로그래머가 전체 소스 파악하지 못한 상태고 해당 루틴의 오너쉽이 신입 개발자에게

있었다면?? 끔직한 사태가 발생합니다..


이때 서버가 죽을 때마다 해당 서버와 데이터를 동기화 하는 일종의 Clone(Stand-By)이 존재하고

Connection Manager 서버(릴레이) 가 로직서버가 죽을 때마다 다른 5분대기중인 로직서버로 자동으로

접속지를 변경한다면? 

 

아마 로직서버가 죽는 순간의 데이터와 동기화 된 데이터를 제공하기 약간의 랙은 발생하겠지만

유저들은 서버가 죽는것을 인지 하지 못하게 될 것입니다.

이게 서버 이중화의 기본 개념이구요.

 


 

3. 그럼 필요한게 무었이냐?

 

 

 - HeartBeat

    심장 뛰는 소리죠? 나 살아 있다고 서버들끼리 알려줍니다.

    주기가 짧을 수록 Fault에 대한 인지가 빠르겠지요. 단 너무 빠르면 부하를 주겠지요.

    어느순간 갑자기 심장이 뛰지 않으면? 심장이 뛰지 않는 서버에 너 정말 죽었냐고 물어봅니다. 

    (물론 서버 끼리 P2P로 묶여 있다면 죽으면 바로 알겠지요? TCP의 CLOSE CONNECTION이

     날아 올테니까요. 하지만 물어보는 동작도 필요합니다.)

    그런데도 응답이 없으면 해당 서버는 죽었다고 판단하는 것이죠~

 

 

 - Active Server, Stand-by Server

    현재 살아서 서비스를 제공하는 서버를 Active Sever, 언제든지 Active가 되기 위해 대기하고 있는

    서버를 Stand-by 서버라고 합니다.

 

 

 - 데이터 동기화

   Active 서버가 죽어서 Stand-by 상태로 대기중인 서버가 Active가 되었습니다.

   그런데 Active-Server가 가지고 있던 유저들 데이터를 가지고 있지 못한다면?

   이중화 해봐야 아무 쓸모 없겠지요. 

   데이터를 동기화는 서버 이중화에서 가장 중요한 요소라고 할 수 있습니다.

   어느 시점에 어떻게 동기화를 할 것인가? 여러가지 방법중 현재 시스템에 가장 적합한 방식으로

   개발합니다.

 

 

 - 릴레이서버 (게이트웨이)

   유저 컨넥션을 관리하는 Connection Pool 의 역할및 데이터를 By-pass 시켜주는 또 다른 서버.

   물론 Client에서 Primary 서버 다운이 인지되면 Secondary 서버로 재 연결 할 수도 있겠지만,

   해당 방법은 순간적인 접속 집중현상을 고려해야 합니다.개발 복잡도도 높아지구요. 

   사실 Relay 서버의 역할은.. L7 스위치로 보강이 가능합니다. 하지만 비쌈 -_- 우리는 헝그리 하니까요.

 

 

 

4. 어떻게 만들면 되나?

 

 

 

- 일반적인 게임서버의 구성

 

일반적으로 게임서버는 아래 그림과 같은 구조를 가지게 됩니다.

1개의 월드를 관리하는 서버 1개당 N개의 서버가 역할별로 분산처리 되도록 구성하는 것이죠.

 

 

 

그리고 위의 구조를 간단하게 확장하면 아래와 같은 그림이 나오게 됩니다.

( 실제 퍼블리셔나 사내 SE팀으로 보내게 되는 구조도는 장비와 1:1 맵핑을 해주어야 하고,

  SNMP등을 통한 NMS 연동, 통계, EMS 구축 등 추가 관리를 위한 프로세스들이 많기 때문에 

  훨씬 복잡합니다)

 

 

 

국내에 출시되는 대부분의 게임들의 서버 구조는 존 기반 서버 모델(Zone-based Server Model)을

사용하고 있습니다.

 

존 기반 서버 모델은 게임 공간을 미리 존 단위로 분할하는 방식으로

사용자의 캐릭터는 하나의 존에만 속할 수 있도록 구성합니다.

그렇기 때문에 존간의 상호작용은 거의 없는 특성을 가집니다.

 

상기의 서버 구조의 예는 존간의 아이템 거래가 가능한 구조를 가지고 있지만

이는 약간 특별한 경우고 일반적으로는 존간 아이템 거래또한 제공되지 않습니다.

 

서버 어플리케이션은 기본적으로 기능에 따라 멀티프로세스의 형식으로 개발됩니다.

상기 Application#1 부분은 1식의 서버군집이 되겠지요.

 

이제 위의 서버구조를 조금 더 간단하게 표현한다면... 아래와 같이 표현됩니다.

 

 

일반적으로는 로그인 인증 서버와 게임서버는 물리적으로 분리되며, 클라이언트는 로그인서버에서 인증 후 게임서버에 접속 가능한 인증키를 로그인 서버에서 획득 후 게임서버로 접속하게 됩니다.

  

 

 

- 이중화를 고려한 프로세스 구조

 

이중화 작업을 위해서는 게임서버를 클라이언트 Connection을 관리하는 릴레이서버와 실제 게임 로직을

관리하는 로직서버를 분리하고, 로직서버 2 프로세스를 같은 설정을 가지도록 Active Stand-by로 나누어 

실행합니다.


이 때 예산 및 이중화 목적에 맞게 서버를 1개의 머신에 2개의 Active Stand-by 모두 실행하는 방식으로

개발할지 (Software Fault만을 고려한 구성) Active 서버와 Stand-by 서버를 다른 머신내에서 실행할지 (Hardware Fault 까지 고려) 선택합니다.

 

제 경우 최근의 서버 Hardware를 신뢰하며, 비용적인 측면을 고려해야 하기 때문에 Software Fault 만을

고려하여 이중화를 구성합니다.

 

아래 그림은 이제 이중화를 고려한 프로세스 구조입니다.

 

A, B, C의 링크를 통해 설명하면

초기 서버 구동 시 릴레이서버는 Active 상태인 로직서버1 과 Stand-by 상태인 로직서버2에 대한 정보를

Config를 통해 가지고 있는 상태입니다.

 

릴레이서버를 구동 한 이후 로직서버들을 구동시키기 때문에 각 로직서버들의 역할을 정하는 것은

릴레이 서버 입니다.

 

A 링크를 통해 게임서비스를 제공합니다.

이때 릴레이서버내에는 Socket Descroptor와 맵핑되는 고유 유저ID와 캐릭터ID에 대한 데이터 MAP을

가지고 있어야 하고 이를 가지고 로직서버와 어느 유저가 보낸 프로토콜인지 식별할 수 있는 방법을

공유합니다.

 

A, C 링크를 모두 이용하여 로직서버들은 릴레이서버로 HeartBeat을 전송합니다.

이 때 B링크를 통해 실시간으로 서버내의 동적데이터들을 동기화 시킵니다.

 

B 링크에서는 Active 및 Stand-by 간 동적 데이터들을 동기화 시키며,

 

동기화 방식은

- 실시간 동기화 (평상시)

- Full Burst 동기화 (서버가 재시작 되었을 시) 두가지 방식으로 진행합니다.

 

제 경우 서버간 동기화 하는 데이터는 다음과 같습니다.

- 유저데이터

- 캐릭터데이터(아이템, 우편, 업적, 등)

- Local 데이터(존내에 몬스터 분포, 캐릭터 분포, 오브젝트 분포)

- 인스턴스 던전 데이터

- 몬스터 Spwan 데이터

- 파티 데이터

- PVP 방 데이터

- 채널 Status 데이터

 

Heartbeat 전송주기는 Default 1초 그때그때 상황에 따라 조정할 수 있도록 합니다.

그리고 HeartBeat이 전송되는 루틴은 독립 쓰레드로 동작시킵니다.

 

경험상 동시 접속 5000명 정도의 서버에서 Active와 Stand-By간 메모리 내의 동적데이터 전체를

Full Burst모드(서버 다운 후 재 실행 후 Stand-By 모드로 돌아가기 위한 동기화) 동기화 하는데

걸리는 시간은 1Gbps 이더넷 환경에서 십수 초 밖에 소요되지 않았습니다.

 

같은 머신내에서 루프백으로 동기화할 경우에는 수 초정도 밖에 소요되지 않는데

그 이유는 동시 접속 5000명의 경우에 1개 서버에 저장되는 동적 데이터가 100메가 바이트 이내였기

때문입니다.

 

물론 서버 내부 로직설계에 따라 용량에는 더 적던 많던 차이가 있을 수 있습니다

하지만 Burst 동기화는 서버 재 시작 이후 한번만 진행되기 때문에, 충분히 서비스를 제공하면서

동기화가 가능합니다.

 

이 때 중요한 부분은 서버 다운 시 동기화가 완벽히 되어야 합니다.

이 부분은 경험적으로 꼼꼼하게 예외처리하며 코딩해야 한다.. 라고 밖에는... -_-

 

팁이라면 실시간 동기화 시 유저의 Request에 대한 처리결과를 ACK로

보내주기전에 변경된 데이터에 대한 동기화를 해주어야 한다는 것 입니다.


보통 IOCP로 들어온 데이터를 처리하는 Worker의 수는 CPU 코어수 x 2 정도로 구성하는데

Active 서버가 다운되게 되면 Loss 되는 데이터는 서버다운 시점에 Worker에서 처리 중인

유저 Request 의 숫자입니다. 예외처리만 잘한다면 최대 Worker의 숫자 만큼의 유저 Request에 대한

무응답 처리되는 정도의 장애가 발생할 겁니다.

그러니까.. Worker의 숫자 만큼의 유저 요청들..
인터페이스의 특정버튼을 눌렀는데 응답이 없다 라던지..
몬스터를 잡았는데 애석하게도 아이템이 하나도 떨어지지 않았다 라던지
어쩌면 강화에 실패하여 아이템이 날아가야 하는데 날아가지 않았다 라던지 같은 문제들이겠지요.

 

하지만 유저가 재 요청을 하면 다시 요청이 부드럽게 Stand-by에서 처리됩니다.

그리고 로직과 DB간의 처리 구성 방식이 비동기 방식이냐 동기방식이냐에 따라 동기화에 대한 중요성이

다르기 때문에 그에 따라 처리 방식이 다르게 설계되어야 할 것입니다.

 

실시간 동기화를 하는 시점에 대해 간단히 표현하면 아래 시퀀스 다이어그램에 붉은 색으로 표기 된 것처럼

서버 내 동적 데이터 변경 내역이 발생 하고, 유저에게 데이터를 돌려주기 직전의 시점이 가장 좋습니다.

 

 

마지막으로.. HA는 리눅스의 경우 데몬형식, 윈도우의 경우 상속 받을 수 있는 라이브러리 형식으로

개발하는 것이 좋습니다.

클래스를 어떻게 상속받고 어떤 부분을 가상함수로 만들고 어떤 데이터 구조를 만드는 것이 좋을까..

사실 이게 가장 관심 많은 부분이겠지만.. 소스부분은 다루지 않겠습니다.

개발자 각자의 환경에 맞도록 구성하는 것이 맞는 것 같아요.

 



 

5, 단점은 없느냐?

 

사실 단점이 많습니다. -_-

1) 개발복잡도가 상승합니다.

- 일반적으로는 구성할 필요가 없는 릴레이 서버를 구성해야 함.


- 릴레이 서버에서 컨넥션을 관리하기 때문에 클라이언트 접속 끊어짐 등에 대한 인지 후
   해당 부분에 대한 예외처리를 분산된 전체 서버에서 인지할 수 있도록 전파해 주어야 합니다.

- 서버 간 통신 프로토콜이 너무 복잡해 지지 않도록 하기 위해 초반 설계가 매우 중요해집니다.

2) 트래픽 및 자원 사용량의 증가

- 릴레이 서버와 로직서버간의 서버간 추가 트래픽의 발생

- Active 및 Stand-by 간 데이터 동기화를 하기 위한 추가 트래픽 발생

- 설계 시 상기 두가지 서버간 통신에 필요한 추가 트래픽을 고려해야 함

 


3) 릴레이 서버가 다운되는 현상에 대한 대책이 없음

- 릴레이 서버는 최대한 가볍게 만들고 업데이트 이슈가 없어야 하는 이유입니다.

- 릴레이 서버가 다운되어버리면 마찬가지로 유저입장에서 서버가 다운된 것입니다.