Nagle과 TCP_NODELAY
TCP 소켓은 기본으로 Nagle 알고리즘이 켜져 있어서, 앞 패킷의 ACK를 받기 전에 쓴 작은 데이터는 바로 나가지 않습니다. 받는 쪽이 ACK를 늦추는 Delayed ACK와 겹치면 스킬 입력 같은 작은 패킷이 수십 ms 늦게 도착합니다. 손실이 나면 Nagle 때문에 빠른 재전송도 못 쓰고 RTO(최소 200 ms)까지 기다릴 수 있습니다. 게임 서버와 클라는 TCP_NODELAY를 켜고, 패킷 수는 서버가 틱마다 모아서 보내는 방식으로 관리합니다.
01작은 패킷
이동 · 스킬 패킷은 수십 바이트, 헤더보다 작다 · ▶ 이 장면 보기
MMO 클라이언트가 보내는 패킷은 대부분 수십 바이트입니다. 화면의 예시에서 MoveReq는 데이터 24B, SkillUseReq는 16B이고, 여기에 IPv4 20B + TCP 20B + 타임스탬프 옵션 12B, 합계 52B 헤더가 붙습니다.
세그먼트 하나에는 MSS(이 예시에서 1448B)까지 데이터를 담을 수 있으니, 게임 패킷은 그 수십 분의 일만 채운 채 나갑니다. MoveReq 기준으로 패킷의 68%(52 ÷ 76)가 헤더입니다.
이동 패킷은 캐릭터가 움직이는 동안 초당 10~20회, 스킬 패킷은 입력 즉시 이동 패킷 사이에 섞여 나갑니다. 크기와 빈도는 예시이며 게임마다 다릅니다.
02Nagle 규칙
앞 패킷의 ACK가 오기 전엔 작은 데이터를 모아 둔다 · ▶ 이 장면 보기
Nagle 알고리즘(RFC 896)은 1바이트 입력마다 41바이트 패킷이 나가던 원격 터미널 트래픽을 줄이려고 만든 규칙이고, Linux와 Windows 모두 TCP 소켓에서 기본으로 켜져 있습니다.
- 앞 패킷의 ACK를 이미 받았으면 바로 보냅니다.
- 모인 데이터가 MSS를 채우면 바로 보냅니다.
- ACK를 받지 못한 작은 세그먼트가 아직 있으면, 새 작은 데이터는 커널 송신 버퍼에 보류합니다.
화면에서는 MoveReq가 먼저 나가 ACK를 기다리는 동안 SkillUseReq가 버퍼에 머물고, ACK가 돌아온 뒤에야 나갑니다. 이동 · 스킬 패킷은 작고 자주 나가기 때문에 세 번째 경우에 자주 걸립니다.
03Delayed ACK
받는 쪽이 ACK를 늦추면 보류 시간이 그만큼 길어진다 · ▶ 이 장면 보기
받는 쪽은 ACK를 바로 보내지 않고 잠시 기다렸다가, 보낼 데이터가 생기면 거기에 실어 보냅니다(Delayed ACK). Linux는 이 타이머가 40 ms부터 시작해 최대 200 ms, Windows는 기본 200 ms입니다.
세로축이 시간인 시퀀스 다이어그램의 예시 조건은 RTT 30 ms, 서버 delayed ACK 40 ms입니다. 0 ms에 MoveReq를 보내고 10 ms에 스킬을 입력하면, 스킬은 Nagle 규칙에 걸려 보류됩니다. 서버는 15 ms에 MoveReq를 받지만 보낼 응답이 없어 ACK를 55 ms까지 미루고, 그 ACK가 70 ms에 클라에 도착해야 스킬이 나가 85 ms에 서버에 닿습니다. Nagle이 없었다면 25 ms에 도착했을 패킷이 60 ms 늦어집니다.
서버가 그사이 클라에 보낼 데이터(주변 상태 등)가 있으면 ACK가 거기에 실려 오므로 보류는 짧아집니다. 실제 지연은 RTT, 상대편 ACK 타이머, 서버가 응답을 보내는 주기에 따라 0부터 수백 ms까지 달라집니다.
04손실 나면: 재전송
Nagle이면 빠른 재전송 대신 RTO를 기다린다 · ▶ 이 장면 보기
패킷 하나가 손실됐을 때 Nagle이 어떻게 복구를 늦추는지 양쪽을 나란히 비교합니다. 둘 다 첫 패킷 A가 손실되고, 이어서 B, C, D를 보내려는 상황입니다. 수치는 예시로 RTT 30 ms, Linux의 최소 RTO 200 ms를 썼습니다.
Nagle이 켜져 있으면 ACK를 받지 못한 작은 세그먼트가 하나 있는 동안 B, C, D는 송신 버퍼에 머뭅니다. 서버는 아무것도 받지 못했으니 중복 ACK(dup ACK)를 보낼 일이 없고, 클라는 손실을 알아챌 단서가 없습니다. 결국 RTO 타이머가 끝나는 200 ms 뒤에야 A를 재전송하고, A는 215 ms에 도착합니다. 재전송한 A도 아직 ACK를 받지 못한 작은 세그먼트라 B, C, D는 계속 보류됩니다. RTO 직후에는 cwnd도 1이라 A 하나만 나갈 수 있습니다. B, C, D는 A의 ACK가 돌아온 뒤에야 나가서, 서버가 ACK를 바로 보내면 245 ms, delayed ACK에 걸리면 285 ms쯤 도착합니다.
TCP_NODELAY면 B, C, D가 바로 나가 서버에 먼저 도착합니다. 서버는 A가 빠졌다는 뜻으로 패킷마다 dup ACK를 돌려보내고, 클라는 dup ACK 3개를 받는 순간 RTO를 기다리지 않고 A를 다시 보냅니다(빠른 재전송). 이 예시에서는 75 ms에 A부터 D까지 모두 전달됩니다.
다만 빠른 재전송은 뒤따르는 패킷이 있어야 동작합니다. 메시지가 드문 연결이면 NODELAY여도 dup ACK가 모자라 RTO까지 가는 경우가 있습니다. 재전송 자체는 TCP 재전송 영상에서 자세히 다룹니다.
05TCP_NODELAY
소켓 옵션 하나로 send() 즉시 전송 · ▶ 이 장면 보기
TCP_NODELAY는 Nagle을 끄는 소켓 옵션입니다. 켜면 send()한 데이터가 앞 패킷의 ACK와 상관없이 바로 나갑니다. 같은 예시 조건에서 스킬 패킷은 85 ms가 아니라 25 ms에 서버에 도착합니다.
옵션은 보내는 쪽 소켓에 적용됩니다. 클라 소켓에 켜면 클라 → 서버 방향(이동 · 스킬 입력)이, 서버 소켓에 켜면 서버 → 클라 방향(주변 상태 · 판정 결과)이 바로 나갑니다. 서버 → 클라 방향의 ACK는 클라(Windows)가 보내므로 이 방향에서 Nagle이 남아 있으면 보류가 Windows 기본 타이머(200 ms) 가까이 길어질 수 있습니다. 양쪽 모두 설정합니다.
코드 예: C/C++ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one), .NET Socket.NoDelay = true, Unreal FSocket::SetNoDelay(true).
06패킷 수 비용
NODELAY만 켜면 메시지마다 패킷이 하나씩 나갈 수 있다 · ▶ 이 장면 보기
TCP_NODELAY만 켜고 서버가 메시지마다 send()를 부르면, 최악의 경우 메시지 수만큼 패킷이 나갑니다. 예시 조건(5,000 CCU, 20 Hz 틱, 클라당 틱마다 30B 메시지 6개, 헤더 52B)에서 서버 송신은 최악의 경우 초당 600,000 패킷, IP+TCP 헤더 포함 394 Mbps이고 그중 63%가 헤더입니다. Linux는 기본으로 tcp_autocorking이 켜져 있어, 앞 패킷이 아직 NIC 큐에 남아 있으면 이어진 작은 write를 합쳐 보냅니다. 그래서 부하가 걸린 서버에서는 실제 pps가 이보다 낮게 측정될 수 있습니다. send() 호출 수는 그대로 6배입니다.
서버가 한 틱 동안 클라별로 메시지를 모아 send()를 한 번만 부르면 초당 100,000 패킷, 186 Mbps, 헤더 비율 22%가 됩니다. 메시지는 원래 틱 단위로 만들어지므로 모아서 보내도 추가 대기는 거의 없습니다.
Nagle을 켜 둬도 패킷은 모이지만, 언제 보낼지가 ACK 도착에 달려 있어 03의 보류가 생깁니다. 두 방식 모두 대기 없이 보내는 조건에서 비교하면, 차이는 패킷 수와 헤더 비율입니다. pps 비용은 NIC · 커널 처리량이 병목일 때 커집니다.
07확인할 지표
지연 ACK · 재전송 · pps를 서버에서 직접 확인 · ▶ 이 장면 보기
서버에서 바로 볼 수 있는 지표입니다. 화면의 출력 값은 예시입니다.
ss -ti: 연결별rto(재전송 타임아웃, ms),rtt,ato(현재 delayed ACK 타이머, ms),mss를 보여 줍니다.nstat -az TcpExtDelayedACKs TcpRetransSegs: 타이머로 늦춰 보낸 ACK와 재전송한 세그먼트의 누적 수입니다. 절대값보다 증가 속도를 봅니다.sar -n DEV 1의txpck/s: NIC 송신 pps입니다. NODELAY 적용과 틱 배칭 전후로 비교합니다.- 입력 → 서버 수신 p99: 클라 입력 시각과 서버 수신 시각을 게임 서버 로그에 남겨 계산합니다. 커널 지표가 바뀌어도 이 값이 줄지 않으면 다른 병목을 찾아야 합니다.
이 영상에 나온 용어는 용어집에 정리했다.