게임 서버의 품질을 위해 개발할 때 목표로 두어야 하는 것은 다음과 같습니다.
- 안전성
- 확장성
- 성능
- 관리 편의성
"게임 서버가 얼마나 죽지 않는가"
게임 서버는 항상 켜져 있는 컴퓨터이기 때문에 고장나면 안 됩니다. 하지만 게임 서버 프로그램의 버그 때문에 서버 프로세스가 중간에 비정상 종료되는 경우가 있을 수 있습니다. 혹은 게임 서버에 교착 상태나 어떤상황이 발생해서 프로세스가 일시 정지하는 경우도 있을 수 있죠. 심지어 실수로 네트워크 케이블을 물리적으로 고장내는 경우도 있습니다. 안정성이 위협당하는 상황들이죠.
소프트웨어 측면에서 안정성에 악영향을 주는 주된 원인은 버그입니다. 구조적으로 설계가 잘못되었거나 작은 코딩 실수가 서버의 안정성을 위협할 수 있습니다.
안정성에는 "게임 서버가 얼마나 오작동을 하지 않는가"도 포함됩니다. 죽지 않는다고 해서 다가 아니라는 이야기입니다. 가끔 연산 결과가 이상하게 나오는 것도 불안정한 서버로 볼 수 있습니다. 이는 심각한 문제를 초래하기도 합니다. free-to-play 게임에서 현금으로 구매한 아이템이 갑자기 사라지거나 반대로 사지도 않은 아이템이 생겨나거나 하는 일이 그 예시입니다.
서버의 안정성을 위해서는 소프트웨어 품질 관리가 엄격해야 합니다.
치밀한 개발과 유닛 테스트
작성한 코드를 꼼꼼히 검수하고, 엄격한 코딩 가이드라인을 따릅니다. 그리고 개발된 프로그램의 각 부분은 반드시 자동화된 자가 검증, 즉 유닛 테스트를 만들어야 한다는 규정을 정합니다. 당장의 귀찮음이 미래에 다가올 후폭풍을 예방해줄 것입니다.
80:20 법칙
모든 프로그램 성능의 80%는 20%의 소스 코드에서 나타난다는 파레토 법칙입니다. 성능에 지대한 영향을 주는 일부분의 소스 코드에서만 프로그램 구조보다 성능의 최적화를 우선시하고, 나머지는 유지 보수하기 쉬운 구조로로 개발하는 것입니다.
코드 리뷰
모든 개발의 결과물을 동료의 검토를 받습니다. 다른 사람이 작성한 코드를 읽는 것은 분명 쉬운 일은 아닙니다. 하지만 다른 사람의 관점에서 내게는 보이지 않던 버그를 발견하는 경우가 꽤 있습니다. 또한, 지식 공유의 효과도 있고 팀에서 누군가의 빈 자리를 채울 수도 있을 것입니다.
가정보다는 검증
유닛 테스트만으로는 부족합니다. 실제로는 많은 플레이어가 서버에 접속하기 때문에 서버가 과부하를 감당할 수 있는지 서버에 접속한 플레이어들의 복잡한 요청을 처리하다 오작동을 일으키지 않도록 검증해야 합니다. 테스트 기간에 많은 사람을 모아 테스트하기는 쉽지 않습니다. 그래서 사람없이 컴퓨터만으로 작동하는 게임 클라이언트를 대량으로 실행시켜 테스트하기도 합니다. 이를 봇 테스트 혹은 더미 클라이언트 테스트, 스트레스 테스트라고 합니다.
서버를 띄우고 대량의 더미 클라이언트를 실행시킵니다. 입력 처리와 렌더링을 생략한 채로 미리 프로그래밍된 행동을 반복합니다. 이를 대량으로 서버에 접속시킵니다. 그리고 서버에서는 성능 지표를 켜서 서버에 걸리는 과부하나 이상 행동 현상을 관찰합니다. 문제점이 발견되면 테스트를 멈추고 문제를 해결합니다. 이를 문제가 없어질 때까지 반복합니다.
다만, 더미 클라이언트도 한계는 분명합니다. 미리 정해진 행동만을 반복하기 때문에 모든 상황을 테스트하는 것은 쉽지 않습니다. 그래도 이 테스트를 꼼꼼히 하는 것만으로도 정말 많은 문제를 해결할 수 있기 때문에 가치는 있습니다.
스트레스 테스트가 확인하지 못하는 문제를 해결하기 위해 클로즈 베타 테스트와 오픈 베타 테스트가 진행되기도 합니다. 실제 유저들을 제한을 두고 모집해서 여러 번의 클로즈 베타 테스트를 거친 끝에 문제점이 없다고 판단되면 제한을 두지 않고 사람을 모아 오픈 베타 테스트를 진행합니다. 아무래도 직접 사람들이 테스트하는 것이기 때문에 많은 문제점들이 쏟아져 나오게 됩니다.
하지만 이러한 노력에도 서버 안정성을 100% 확보하는 것은 어렵습니다. 실제로 100% 확보한 서버는 저는 없다고 생각합니다. 프로그램의 양이 많고 구조가 복잡할수록 문제점이 없을 가능성이 낮습니다. 특히, 플레이어들의 요구 사항을 충족시켜주는 업데이트를 진행하게 되는데, 이것이 누적될수록 프로그램은 더 복잡해집니다. 초기에 잘 정리가 되어 있던 구조가 많이 어지러워지게 되죠.
결국 안정성을 미리 챙기는 것도 중요하지만 불안정한 상황에 대한 대처도 굉장히 중요합니다. 불안정한 서버에 대해서는 다음과 같은 방법을 취할 수 있습니다.
- 서버가 죽더라도 최대한 빨리 다시 살아나게 합니다.
- 서버는 죽더라도 최대한 적은 서버스만 죽게 합니다.
- 서버 오작동에 대해 기록을 남길 수 있도록 합니다.
서버 프로그램이 죽더라도 다시 자동으로 켜지도록 프로그램을 만듭니다. 서버 프로그램과 그를 감시하는 프로그램을 동시에 실행시킵니다. 서버 프로그램이 죽을 경우 감시하던 프로그램이 서버 프로그램을 다시 실행시키는 것입니다.
피해를 최소하해야 합니다. 서버 프로세스를 하나만 띄우지 않고 여러 개 띄웁니다. 서버 프로세스가 여러 개일 때, 어떤 서버 프로세스가 1개 죽더라도 전체 사용자 중 그 프로세스의 사용자들만 이용이 중단될 뿐입니다. 같은 역할을 하는 서버를 두 대 이상 두는 것도 방법입니다. 한 대는 실제 서버 역할, 나머지 한 대는 예비입니다. 실제 서버가 죽으면 예비가 이어받아 서버 작동을 유지하는 것입니다.
오작동에 대해 기록을 남깁니다. 서버가 죽을 경우 이 때까지 서버에서 벌어진 상황을 자동으로 기록하도록 합니다. 이는 당장의 문제를 해결하는 것보다는 미래에 반복될 수 있는 문제를 예방하기 위한 정보를 확보할 수 있습니다. 서버가 비정상 종료를 하는 마지막 순간의 프로세스 상태, 즉 크래시 덤프를 파일로 남기거나 서버가 최근에 받았던 메시지 종류를 파일로 남기는 방법이 그 예시입니다.
확장성이란 서버를 얼마나 많이 설치할 수 있느냐를 의미합니다. 게임 사용자 입장에서 보면 '사용자 수가 늘어나더라도 서비스 품질이 떨어지지 않고 유지되느냐'가 확장성이라고 할 수 있습니다. 이를 확보한 게임 서버는 동시 접속자 수가 10억 명 정도될 때에도 서비스를 이용할 때 서버가 죽죽거나 느려지지 않고 쾌적한 게임 환경을 제공합니다.
서버 확장성을 올리는 방법에는 크게 두 가지가 있습니다. 서버의 하드웨어를 더 좋은 것으로 바꾸는 수직적 확장, 서버의 컴퓨터 개수를 늘리는 수평적 확장이 있습니다.
수직적 확장 | 수평적 확장 | |
---|---|---|
확장 종류 | 서버 머신의 부품을 업그레이드 or 서버 머신 안의 CPU, RAM을 증설 | 서버 머신 개수 증설 |
서버 소프트웨어 설계 비용 | 낮다 | 높다 |
확장 비용 | 기하급수적으로 높아진다 | 선형적으로 높아진다 |
과부하 지점 | 서버 컴퓨터 자체 | 네트워크 장치 |
오류 가능성 | 낮다(로컬 머신 안에서 동기 프로그래밍 방식으로 작동) | 높다(여러 머신에 걸쳐 비동기 프로그래밍 방식으로 작동) |
단위 처리 속도 | 높다(로컬 컴퓨터의 CPU와 RAM만 사용) | 낮다(여러 서버 컴퓨터 간의 메시징이 오가면서 처리) |
처리 기능 총량 | 낮다(서버 컴퓨터 한 대의 성능만 사용) | 높다(여러 서버 컴퓨터로 부하가 분산) |
수직적 확장은 하드웨어를 더 좋은 것으로 바꾸는 것입니다. 소프트웨어의 구조에 변화를 줄 필요는 없고, 소프트웨어 설계에 시간이 덜 듭니다. 반면 수평적 확장은 서버 프로그램의 구조가 복잡해집니다. 소프트웨어 설계에 시간이 더 들게 되겠죠. 개발의 용이함에 있어서는 수직적 확장이 유리할 것입니다.
하지만 수직적 확장은 비용이 많이 들 것입니다. 고사양 컴퓨터의 가격은 기하급수적으로 오릅니다. 그리고 현재 CPU 속도는 코어 하나당 4GHz인데, 한동안은 이 수치를 넘기 어려울 것으로 예상된다고 합니다. 컴퓨터 성능을 200배 높이는 것은 너무 어려울 것이라는 이야기입니다. 이 부분에 있어서는 수평적 확장이 더 용이합니다. 컴퓨터 200대를 가져다 놓으면 총 처리량은 200배가 되겠죠.
수직적 상황의 과부하 지점은 서버 컴퓨터입니다. 서버 컴퓨터 한 대에 몰리게 된다는 것이죠. 수평적 확장을 할 수 있는 서버는 다릅니다. 서버 대수를 늘림으로써 해결할 수 있죠. 과부하 지점이 서버들을 묶고 있는 네트워크 장비들입니다. 네트워크 장비는 쉽게 과부화에 부딪히지 않습니다. 매우 많은 양의 처리를 쉽게 감당하기 때문입니다.
수평적 확장은 소프트웨어 구조가 복잡해지는 것만이 아닙니다. 특정 데이터 처리를 여러 서버에 걸쳐서 작동해야 하는데, 여러가지 문제가 발생합니다. 일반적으로 컴퓨터 한 대의 처리 속도보다 여러 컴퓨터에 걸쳐서 처리하는 속도가 훨씬 느립니다. 그래서 서버 간 상호 작용 처리에서 성능 하락이나 예전 데이터를 다루는 에러 현상이 발생하기도 합니다.
수평적 확장은 단위 처리 성능에서도 좋지 못합니다. 클라이언트가 어떤 메시지를 처리하기 위해 서로 다른 서버 1 -> 2 -> 3을 거쳐야 한다고 가정해봅시다. 메시지를 처리하는 데 소요되는 시간은 서버 1 -> 2, 서버 2 -> 3을 거치는 데 걸리는 시간입니다. 이 메시지를 처리하는 데 다음 과정이 반복되는 것입니다.
유저 프로세스 -> 커널 -> 디바이스 -> 회선 -> 라우터 -> 회선 -> 디바이스 -> 커널 -> 유저 프로세스
이 과정에서 버퍼링으로 시간 지연이 발생합니다. 심지어 TCP 네트워크라면 Nagle 알고리즘이 작용하거나 서버 간 통신 자체가 과부하를 일으키면 원치 않는 지연 시간이 추가로 발생합니다. 결과적으로 수평적 확장의 단위 처리 성능은 수직적 확장보다 느립니다. 하지만 이를 모두 상회하는 장점이 총 처리량입니다. 수직적 확장은 앞서 언급했던 하드웨어 성능의 한계가 있습니다. 수평적 확장의 경우 서버 대수를 제약없이 늘릴 수가 있죠.
정리하자면 수직적 확장은 단위 성능과 구조 측면에서 유리하지만 확장성에 한계가 있습니다. 수평적 확장은 그 반대입니다. 많은 대용량 서버에서는 수평적 확장이 고려됩니다. 하지만 개발의 경제성과 성능을 위해 혼합해서 설계된다고 합니다.