9. 질문 — 대규모 동시접속 서버 설계 (C10K / C10M)
난이도 최상내 답안
모범답안
모범답안 — 대규모 동시접속 설계: C10K / C10M, 부하분산
난이도: 최상
핵심 답변
C10K는 "한 서버가 1만 개의 동시 연결을 처리"하는 문제다. thread-per-connection은 연결마다 스레드 스택(~1MB)과 컨텍스트 스위칭 비용이 들어 1만 연결이면 메모리 수 GB·스위칭 폭증으로 붕괴한다. 해법은 **적은 수의 스레드 + 이벤트 기반 IO 멀티플렉싱(epoll/IOCP)**이다. C10M은 여기서 한 단계 더 나아가 커널 자체가 병목이 되므로, 자료구조·메모리 레이아웃·NIC 큐(RSS)·때로는 커널 우회(DPDK)까지 동원한다. 단일 서버 한계를 넘으면 **수평 확장(존/샤딩 + 로드밸런싱)**으로 간다.
깊이 있는 설명
C10K의 자원 한계 3가지
- 메모리: 스레드 1개당 기본 스택 ~1MB → 1만이면 ~10GB. 비현실적.
- CPU: 스레드가 많으면 스케줄러가 일보다 컨텍스트 스위칭(레지스터 저장/복원, TLB·캐시 오염)에 시간을 쓴다.
- O(n) 시스템콜:
select/poll은 매 호출마다 전체 fd 집합을 복사·순회 → 연결 수에 비례해 느려짐. →epoll/IOCP로 준비된 fd만 O(1)로 받는다.
C10M에서 추가로 터지는 것
- 연결당 커널 메모리(소켓 버퍼, file descriptor 구조체)가 누적 → 송수신 버퍼 크기 튜닝 필수.
- 단일 accept 스레드·단일 인터럽트 큐 병목 → SO_REUSEPORT로 다중 acceptor, RSS(NIC가 패킷을 여러 큐로 분산)로 멀티코어 활용.
- 락 경합·false sharing이 코어 수에 비례해 악화 → 연결을 코어에 고정(affinity), 코어별 독립 자료구조(shared-nothing).
- 극단적으로는 DPDK/유저스페이스 네트워킹으로 커널을 우회.
수평 확장의 핵심 문제 게임은 **상태성(stateful)**이 강하다. 같은 룸/플레이어의 패킷은 같은 서버로 가야 한다(웹의 무상태 요청과 다름). 그래서:
- 존/샤딩: 월드를 지역(zone)이나 채널로 쪼개 서버에 분배.
- 세션 고정(sticky): 게이트웨이가 플레이어→서버 매핑을 유지.
- 서버 간 통신: 존 경계를 넘는 상호작용은 메시지 패싱/내부 RPC로.
응용/실무 연결
- 로비/매치메이킹은 무상태에 가까워 수평 확장 쉬움 → 일반 LB로 분산.
- 실제 게임플레이(룸/존)는 상태가 무거워 게이트웨이가 라우팅하고 룸 단위로 서버에 핀.
- CCU가 커지면 "한 룸은 한 스레드/한 코어가 처리"하는 shared-nothing 액터 모델이 락 경합을 없애 확장에 유리.
흔한 오답·함정
- "epoll만 쓰면 C10M도 된다" — 아니다. epoll은 C10K급 해법이고, C10M은 메모리/커널/NIC까지 손봐야 한다.
- 게임 트래픽에 평범한 L7 HTTP 로드밸런서를 그대로 적용 — UDP·상태성·롱커넥션 때문에 부적합. L4(TCP/UDP) + 세션 인지 라우팅이 필요.
- 수평 확장하면 공짜로 빨라진다는 착각 — 존 경계 통신·데이터 일관성 비용이 새로 생긴다.
꼬리질문 대비
- Q. epoll ET와 LT 차이? ET는 상태 변화 시 1회만 통지 →
EAGAIN까지 다 읽어야 함(성능↑, 실수↑). LT는 데이터 남으면 계속 통지. - Q. 스티키 세션을 어떻게 구현하나? 게이트웨이가 (플레이어ID→서버) 매핑을 외부 저장소(Redis)나 일관 해싱으로 유지. 서버 추가/제거 시 재배치 최소화하려면 consistent hashing.
- Q. 한 서버가 죽으면? 그 서버가 들고 있던 룸/세션 상태가 날아간다 → 주기적 스냅샷 + 재접속 복구 또는 핫스탠바이로 대비.