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