월 $6 VPS 한 대로 MMORPG 동접 1,000명에 도전해봤습니다
배경
예전부터 MMORPG를 직접 만들어보고 싶었습니다.
여러 사이드 프로젝트를 만들어보며 뼈저리게 느낀 점은 프로젝트의 생명은 결국 인프라 비용에 달려 있다는 것이었습니다. 인프라 비용이 거의 들지 않아야 수익이 없어도 몇 년이고 부담 없이 서비스를 지속할 수 있기 때문입니다.
그래서 처음부터 "작은 VPS 한 대로 최대한 많은 유저를 커버해보자"라는 목표로 웹 액션 MMORPG를 만들기 시작했습니다. 클라이언트는 Phaser 기반이고, 운영 서버는 월 $6, 1 vCPU / 1GB RAM VPS 단 한 대입니다.

설계
작은 서버에서 버티기 위해 단순화할 수 있는 부분은 최대한 단순화했습니다.
1. Node.js에서 Go로 전환
초기에는 Node.js로 서버를 구성했으나 1 vCPU, 1GB RAM 환경에서 복수의 Zone을 효율적으로 운영하기 위해 Go로 이관했습니다.
동일 부하 조건(10개 Zone, 20Hz 틱 시뮬레이션)에서 측정한 결과 Node.js 프로토타입 RSS는 약 51.6MB였으나, Go 전환 후 틱당 객체 재사용(sync.Pool)을 적용하면서 백엔드 RSS는 약 26.0MB로 측정되었습니다. 단, 이 수치는 구조 변경도 함께 포함된 결과라 순수 언어 벤치마크는 아닙니다.
2. 아키텍처 단순화
- Zone 분할과 100명 상한: 월드를 작은 Zone 단위로 쪼개고 Zone당 최대 100명 상한을 두어 틱 루프 연산 부하를 제어했습니다.
- 델타 스냅샷 포기 및 최신 전체 스냅샷 유지: 초기에는 이전 시점부터 변경된 델타만 클라이언트에 전송하는 방식을 시도했으나, 유저와 몬스터 수가 늘어날수록 서버에서 수행하는 델타 계산 부하가 오히려 급증했습니다. 이에 스냅샷 전송 주기마다 최신 전체 스냅샷 1건만 브로드캐스트하고, 패킷 수신이 늦어진 클라이언트는 이전 내역 대신 최신 상태로 덮어쓰도록 구조를 단순화했습니다.
- 틱당 1회 직렬화 전송: Zone 월드 상태는 100ms 틱(초당 10회)마다 1회만 MsgPack으로 직렬화한 뒤, 같은 Zone에 있는 플레이어들의 수신 큐에 바이트 슬라이스 그대로 브로드캐스트합니다.
- 인메모리 처리 및 비동기 저장: 실시간 캐릭터 위치와 전투 상태는 순수 메모리에서만 처리하고, DB 저장은 로컬 SQLite(WAL 모드)에 주기적으로 비동기 배치 기록합니다.
- 매니지드 DB 없이 SQLite + 주기적 백업: 별도 DB 서버 없이 로컬 SQLite 파일 하나로 운영하고, 주기적인 파일 백업으로 장애에 대비합니다. 월정액 DB 비용이 발생하지 않아 인프라 비용을 최소로 유지할 수 있습니다.

검증
이 구조가 실제로 어디까지 버티는지 보기 위해, 실제 플레이어처럼 몬스터와 싸우고 레벨업하거나 죽고 아이템을 획득하는 루프를 수행하는 봇 1,000마리로 부하테스트를 진행했습니다. 봇은 별도 머신에서 실행했으며, 10개 Zone에 100개씩 분산했습니다.

1차 테스트: 연결 폭주와 TCP OOM
봇 1,000마리가 차례로 연결하기 시작하고 약 300명에 도달하는 시점에 커널 로그에 TCP: out of memory가 터지며 프로세스가 마비되었습니다.
2차 테스트: 연결 속도 조정
접속 속도를 초당 50명에서 10명 수준으로 낮춰 부드럽게 올렸으나, 마찬가지로 동접자가 약 300명을 넘어서면서 동일한 TCP OOM이 발생했습니다. 접속 속도의 문제가 아니라 지속 소켓 버퍼 메모리의 한계였습니다.
3차 테스트: 커널 튜닝 후 발견된 송신 큐 적체
tcp_mem 커널 파라미터를 늘려 OOM을 넘겼으나, 동접자가 300명을 넘어가자 전체 연결의 Send-Q 합계가 약 75MiB까지 증가하면서 메모리가 다시 고갈되기 시작했습니다.
100명 규모 Zone의 snapshot 패킷 크기는 약 15–20KB 수준입니다. 계산상 1,000명 전송에는 초당 약 150–200MB가 필요합니다. 실제 테스트에서도 약 300개 봇부터 Send-Q가 계속 증가했기 때문에, 서버가 생성하는 snapshot을 네트워크가 제때 처리하지 못한 것으로 판단했습니다.
테스트 후 패킷을 확인해보니 스프라이트·표시 정보처럼 매 snapshot마다 반복해서 보낼 필요 없는 정적 데이터가 상당 부분 포함되어 있었습니다. 정적 데이터는 Zone 진입 시 한 번만 전달하고, 이후 snapshot에는 위치·체력·행동 상태 등 실시간 정보만 담는 컴팩트 패킷으로 개선할 계획입니다.
결과 및 정리
결국 1,000명 유지 목표는 실패했습니다. 현재 $6 VPS (1 vCPU / 1GB RAM) 구조에서는 약 300명 접속 시점부터 TCP 메모리 및 소켓 버퍼 병목이 발생하는 것을 확인했습니다.
대역폭 한도도 넘을 수 없는 벽
TCP 문제를 전부 해결하더라도 $6 요금제에는 월 3TB outbound 한도라는 또 다른 벽이 있습니다.
아래 수치는 컴팩트 패킷 적용 후를 가정한 이론값으로, 아직 실제 구현 전입니다. 전송 주기를 5Hz로 낮추고 정적 데이터를 분리하면 Zone 기본 스냅샷 크기가 약 1.4KB, 플레이어 1명 추가마다 약 36B씩 증가할 것으로 예상됩니다. 이를 기반으로 월 3TB 한도 안에서 24시간 상시 유지 가능한 평균 동접은 약 127명으로 추산됩니다.
| 평균 동접 | 3TB 소진 예상 |
|---|---|
| 100명 | 약 40일 |
| 127명 | 약 30일 |
| 200명 | 약 17일 |
| 500명 | 약 4일 |
| 1,000명 | 약 34시간 |
현재 테스트에서 쓴 15–20KB 패킷을 5Hz로 그대로 전송하면 약 8–11시간 만에 3TB를 소진합니다. 컴팩트 패킷을 적용하더라도 1,000명이 상시 접속하면 하루 반이면 한도가 찹니다.
우선 컴팩트 패킷을 적용하고, 실제 동접이 크게 늘어나는 시점에 추가 최적화와 상위 요금제 업그레이드를 비교해 결정할 생각입니다. 상위 요금제는 대역폭 한도도 함께 늘어나는 경우가 많아, 스펙업 자체가 대역폭 문제에 대한 근본적인 해결책이기도 합니다.
MODAK Online 소개
이 테스트에 사용한 실제 게임이 MODAK Online입니다.
울티마 온라인과 디아블로 같은 고전 탑다운 게임의 감성을 담은 웹 액션 MMORPG입니다. 현재 5개의 클래스와 5개의 필드가 구현되어 있습니다.
현재는 동시 접속자가 많지 않아 월 $6 VPS로도 리소스가 남아돕니다.
서버 업그레이드를 진지하게 고민할 만큼 많은 유저분들이 놀러와 주시면 좋겠습니다.
완전 무료이며, 회원가입이나 설치 없이 브라우저에서 즉시 플레이할 수 있습니다.