MMO 게임서버가 패킷을 받아 처리하고 다시 내보내는 스레드 구조와, 로직 스레드가 50ms마다 반복하는 틱 루프를 보여준다. 큐 적체, 인원 밀집, DB·락·GC 대기가 각각 어떻게 틱을 늦추는지 설명하고, 구간마다 어떤 지표를 봐야 하는지 정리한다. 틱 하나의 길이는 단계마다 걸린 시간의 합이고, 전체 처리량은 가장 느린 구간이 정한다.
화면 왼쪽의 클라이언트에서 들어온 패킷은 I/O 스레드(IO #1~IO #4)가 받아 수신, 복호화, 역직렬화를 한다. 해석된 메시지는 존마다 하나씩 있는 메시지 큐에 쌓이고, 존당 1개인 로직 스레드(Zone A, Zone B, Zone C)가 틱 루프 안에서 이를 꺼내 처리한다. 처리 결과는 송신 큐에서 직렬화되고 묶인 뒤, I/O 송신 단계에서 암호화되어 오른쪽 클라이언트로 나간다.
DB 작업은 로직 스레드가 직접 기다리지 않는다. 별도의 DB · 비동기 워커에 요청을 넘기고, 결과는 다시 메시지 형태로 존 큐에 들어온다.
한 존의 상태는 한 스레드만 순서대로 만지므로 존 내부에서는 락이 필요 없다. 그 대신 그 스레드가 어떤 이유로든 막히면 존 전체가 같이 멈춘다. 뒤 챕터에서 다루는 지연은 대부분 이 특성과 연결된다.
틱레이트가 20Hz이면 틱 하나에 쓸 수 있는 예산은 50ms다. 화면에는 Tick N, N+1, N+2가 같은 패턴으로 반복된다.
- 메시지 처리
10ms: 큐에 쌓인 클라 요청을 순서대로 꺼내 실행한다.
- 월드 업데이트
14ms: AI, 이동, 전투, 버프, 타이머를 진행한다.
- AOI 계산
8ms: 시야 그리드로 누가 누구를 보는지 정한다.
- 브로드캐스트
6ms: 바뀐 내용을 주변 플레이어에게 보낼 패킷을 만든다.
- 대기
12ms: 남은 예산만큼 쉬고 다음 틱을 시작한다.
마지막 대기 시간이 서버의 여유분이다. 이 값이 0이 되면 다음 틱이 제시간에 시작하지 못하고 밀리기 시작한다.
왼쪽 패널은 Zone A의 메시지 큐다. 처음에는 유입 800/s, 처리 1,000/s로 큐 깊이가 40개 안팎에서 유지된다. 유입이 1,420/s로 늘면 처리량을 넘어서고, 큐 깊이가 수천 개로 계속 늘어난다. 화면의 대기 시간은 큐 깊이 ÷ 처리량으로 계산되며 약 3초까지 올라간다.
오른쪽 그래프는 틱 처리시간이다. 처음에는 50ms 예산선 아래인 30ms대에 있다가, 적체가 시작되면 예산을 넘어 최대 175ms까지 올라가고 틱레이트 표시는 20Hz에서 6Hz 수준으로 떨어진다.
틱이 늦게 시작하면 그 사이 메시지가 더 쌓이고, 다음 틱은 더 많은 일을 해야 한다. 유입이 처리량을 넘는 동안 이 악순환이 이어지면 존 전체 서버렉, 메모리 증가, 결국 서버 다운으로 간다. 이 상황은 큐 깊이, 큐 대기시간, 틱 p99, 틱레이트 지표로 드러난다.
왼쪽은 시야 안 인원 N이 6명에서 60명까지 늘어나는 모습이고, 오른쪽은 틱당 브로드캐스트 갱신(메시지) 수를 N × (N − 1) 곡선으로 그린다. 각 플레이어의 변화를 나머지 모두에게 보내야 하므로, 60명이 모이면 틱당 3,540개, 20Hz 기준 초당 70,800개가 된다. 갱신은 세션마다 틱당 하나로 묶어 보내므로 패킷 수는 N × 20 수준이고, 인원의 제곱으로 느는 것은 바이트다. N이 40명을 넘으면 송신 큐 상태 표시가 송신 큐 적체로 바뀐다.
비용이 인원의 제곱으로 늘기 때문에 공성전이나 월드보스처럼 한곳에 사람이 몰릴 때 문제가 된다. 송신 버퍼가 차면 서버 메모리가 늘고, 수신이 느린 클라이언트부터 강제로 끊긴다.
화면에 나오는 대응은 시야 반경 축소, 거리별 갱신 빈도 차등, 델타 압축이다.
로직 스레드 바깥에서 생기는 대기 네 가지를 카드로 보여준다. 각 카드 아래 막대는 해당 상황에서 스레드가 어떻게 막히는지 그린 것이다.
- I/O 스레드 포화: 암호화, 압축, 패킷 폭주로 I/O CPU가 100%에 가까워지고 수신이 밀린다. 이때 로직 스레드 사용률은 30%로 한가하고 틱 길이도 정상이다. 늘어나는 것은 입력이 로직에 닿기까지의 지연이다. 지표는
IO CPU 100% · 수신 대기다.
- 동기 DB 호출: 로직 스레드가 DB 응답을 기다리는 180ms 동안 틱 전체가 멈춘다. 틱 p99나 max가 DB 응답시간만큼 튄다.
- 락 경합, 존 간 호출:
Zone B가 경매장 락을 잡고 있는 동안 Zone A가 140ms 동안 락을 기다린다. 길드, 경매장 같은 공용 자원에서 생기며 지표는 락 대기 시간이다.
- GC, 메모리: C#·Java 같은 GC 언어에서는 Stop-the-world 동안 같은 프로세스의 IO, Zone A, Zone B, DB 레인이 모두 120ms 같이 멈춘다. 지표는 GC 일시정지 시간(ms)이다.
구간별로 볼 지표와 이상 신호를 표로 정리한다.
- I/O 스레드: CPU 사용률, 수신 대기 바이트. CPU 100%와 수신 지연이 이상 신호다.
- 메시지 큐: 큐 깊이, 메시지 대기시간. 계속 증가하면 처리량 부족이다.
- 틱 루프: 틱 처리시간 p99, 틱레이트. 예산 초과와 Hz 하락을 본다.
- AOI·브로드캐스트: 시야 내 엔티티 수, 틱당 송신 수. 인원의 제곱으로 증가한다.
- 송신: 세션별 송신 큐 바이트, 대역폭. 적체와 끊김 증가를 본다.
- DB·락·GC: DB 응답시간, 락 대기, GC 일시정지. 틱 p99·max와 같이 튀면 원인 후보다.
틱은 단계 시간의 합이고 처리량은 가장 느린 구간이 정하므로, 전체 평균 하나로는 병목을 찾기 어렵다. 구간마다 지표를 따로 두어야 어디서 막혔는지 구분할 수 있다.
용어 정리
- 틱 (Tick)
- 서버가 게임 월드를 한 번 갱신하는 주기다. 이 영상에서는 50ms마다 메시지 처리부터 브로드캐스트까지 한 바퀴를 돈다.
- 틱레이트
- 초당 틱 횟수로 Hz 단위로 쓴다.
20Hz면 틱당 50ms 예산이고, 틱이 길어지면 실제 Hz가 떨어진다.
- 틱 예산
- 틱 하나에 허용된 시간이다. 처리 시간이 예산을 넘으면 다음 틱이 늦게 시작한다.
- I/O 스레드
- 소켓 수신·송신, 암호화와 복호화, 직렬화를 맡는 스레드다. 게임 로직은 돌리지 않는다.
- 로직 스레드
- 존 하나의 게임 상태를 처리하는 스레드로 존당 1개다. 한 스레드만 상태를 바꾸므로 존 내부에는 락이 없다.
- 존 (Zone)
- 서버가 월드를 나눠 관리하는 단위다. 존마다 메시지 큐와 로직 스레드가 하나씩 붙는다.
- 메시지 큐
- I/O 스레드가 해석한 요청을 로직 스레드가 꺼내 가기 전까지 쌓아 두는 대기열이다. 깊이를 처리량으로 나누면 대기 시간이 나온다.
- AOI (Area of Interest)
- 각 플레이어가 보는 범위, 즉 시야다. 서버는 이 범위 안의 플레이어에게만 변경 사항을 보낸다.
- 브로드캐스트
- 한 플레이어의 상태 변화를 AOI 안의 다른 플레이어들에게 전송하는 일이다. 인원이 N명이면 갱신(메시지)이 틱당
N × (N − 1)개까지 늘어난다.
- 송신 큐
- 클라이언트로 보낼 패킷이 실제 전송 전에 대기하는 버퍼다. 느린 클라이언트의 송신 큐가 차면 메모리가 늘고 연결이 끊긴다.
- p99
- 측정값을 정렬했을 때 상위 1% 경계 값이다. 틱 p99는 가끔 튀는 느린 틱을 잡아낸다.
- 락 경합
- 여러 스레드가 같은 공용 자원의 락을 잡으려고 기다리는 상황이다. 경매장, 길드처럼 존 사이에 공유되는 데이터에서 생긴다.
- GC Stop-the-world
- 가비지 컬렉터가 메모리를 정리하는 동안 프로세스의 스레드를 멈추는 구간이다. C#·Java 같은 GC 언어에서는 같은 프로세스의 모든 존 틱이 같이 멈춘다.
- 죽음의 나선
- 처리가 늦어져 일이 더 쌓이고, 쌓인 일 때문에 처리가 더 늦어지는 악순환이다. 유입이 처리량을 넘는 상태가 이어질 때 생기고, 방치하면 서버 다운으로 이어진다.