TCP가 잃어버린 세그먼트를 다시 보내는 두 가지 방식(Fast Retransmit, RTO)과, 재전송 하나가 같은 연결의 뒤따르는 데이터까지 멈추게 하는 Head-of-Line 블로킹을 보여준다. 작은 패킷이 띄엄띄엄 오가는 게임 트래픽은 RTO로 빠지기 쉬워 수백 ms 정지가 생긴다. 같은 사건이 클라, 서버, 인프라에서 각각 어떤 지표로 보이는지와 확인 명령, 완화 방법까지 정리한다.
위쪽 선이 송신측(서버), 아래쪽 선이 수신측(클라)이다. Seq 1, Seq 2는 정상 도착하고 각각 ACK 2, ACK 3이 돌아간다. Seq 3은 중간에서 사라진다.
이후 Seq 4, 5, 6이 도착해도 수신측은 3번을 아직 못 받았으므로 계속 "3번이 필요하다"는 ACK를 반복해서 보낸다. 이것이 Dup ACK다. 송신측은 같은 ACK가 3번 중복되면 타이머를 기다리지 않고 Seq 3을 바로 다시 보낸다(Fast Retransmit). 재전송이 도착하면 수신측은 4~6번까지 한 번에 확인하고 ACK 7을 보낸다.
TCP는 모든 바이트가 순서대로, 반드시 도착해야 다음으로 넘어간다. 재전송은 이 보장을 지키기 위한 동작이다.
손실 원인은 달라도 결과는 같다. ACK가 오지 않으면 송신측은 다시 보낸다. 화면에는 네 가지 원인이 나온다.
- 혼잡, 라우터 큐 넘침: 트래픽이 몰려 장비 큐가 가득 차면 뒤에 온 패킷을 버린다(
tail drop).
- 무선, 모바일 구간: 와이파이 간섭이나 기지국 핸드오버 순간에 패킷이 사라진다.
- 버퍼, NIC 오버플로: 커널이 NIC 수신 링 버퍼를 제때 비우지 못하면 넘친 패킷이 버려지고
ethtool -S의 rx_missed, rx_fifo 계열 카운터가 증가한다.
- ACK 유실, MTU 블랙홀: 데이터는 도착했는데 마지막 ACK가 중간에 사라지거나(ACK는 누적이라 뒤 ACK가 있으면 덮어 준다), 경로 MTU 문제로 큰 패킷만 버려진다.
위 줄은 패킷이 20ms 간격으로 촘촘하게 나가는 경우다. 100ms 지점의 패킷이 손실되면 바로 뒤 패킷들이 Dup ACK 3개를 만들고, 송신측이 즉시 재전송한다. 정지는 약 100ms, 대략 1 RTT 수준이다.
아래 줄은 패킷이 띄엄띄엄 나가는 경우다. 손실 뒤 다음 패킷이 RTO보다 늦게 나가므로 손실을 알려 줄 ACK가 없다. 송신측은 RTO 타이머를 기다린다. 첫 재전송은 RTO 200ms 뒤에 나가지만 이것도 손실되고, 타이머가 2배로 늘어 RTO 400ms 뒤에야 재전송이 성공한다. 이 예에서 정지는 620ms다.
게임 트래픽은 작고 간격이 넓어서, 손실 뒤 다음 패킷이 RTO 전에 나가지 않으면 RTO로 빠진다. 리눅스에서 RTO 최솟값은 200ms이고 실패할 때마다 200, 400, 800, 1600ms로 두 배씩 늘어난다.
재전송 뒤에는 혼잡 윈도우(cwnd)도 줄어든다. 영상은 Fast Retransmit이면 줄이고(Reno는 절반, 리눅스 기본 CUBIC은 0.7배), RTO면 1부터 Slow Start로 다시 시작한다고 설명하며, 그만큼 처리량이 급감한다.
클라 커널 수신 버퍼에 Seq 4~Seq 8은 이미 도착해 있지만 Seq 3 칸은 비어 있다. 앱은 3번부터 순서대로만 읽을 수 있으므로, 뒤 데이터는 도착하고도 버퍼에서 기다린다. 재전송된 3번이 들어오는 순간 쌓인 데이터가 한꺼번에 게임 앱의 recv()로 넘어간다.
아래 타임라인은 앱 입장에서 본 메시지 도착이다. 일정 간격으로 오던 메시지가 한동안 하나도 오지 않다가, 재전송 시점에 여러 개가 몰려 들어온다.
- 멈췄다가 한꺼번에 처리된다.
- 러버밴딩, 순간이동처럼 보인다.
- 이동, 스킬, 채팅이 같은 연결에 있으면 모두 같이 멈춘다.
- 몰아서 도착한 이동 패킷 때문에 속도핵 탐지가 오탐할 수 있다.
TCP 연결 하나는 한 줄로 선 대기열과 같아서, 앞의 하나가 빠지면 같은 줄의 메시지는 전부 기다린다.
같은 손실 사건이 세 곳에서 다른 모습으로 보인다. 각 열 위의 그래프는 사건 동안 대표 지표가 튀는 모양을 그린 것이다.
- 클라: 표시 핑이 40ms에서 600ms 가까이 급등한다. 화면이 멈칫한 뒤 몰아치고, 러버밴딩과 스킬, 채팅 지연이 동시에 나타난다. RTO가 반복되면 타임아웃으로 연결이 끊긴다.
- 서버: 세션 송신 큐가 수 KB에서 380KB대까지 쌓인다. 소켓 송신 버퍼(
SO_SNDBUF)가 가득 차고, 앱 송신 큐와 세션당 메모리가 늘어난다. 복구 순간에는 수신이 몰리고, 느린 세션 정리(킥)가 늘어난다.
- 인프라: 재전송률이 0.2%에서 6% 가까이 오른다.
TcpRetransSegs, TCPTimeouts가 증가하고, SRTT와 RTTVAR는 오르며 cwnd는 줄어 처리량이 떨어진다. 인터페이스 drop, discard가 늘고 LB 헬스체크 플랩이나 연결 리셋이 생긴다.
왼쪽은 확인 방법이다.
ss -ti '( sport = :7777 )': 서버 기준으로 소켓별 rtt, rto, cwnd, retrans를 본다. 화면의 예시 출력은 rtt:182/90 rto:1200 cwnd:4 retrans:0/37이다.
nstat -az TcpRetransSegs TcpExtTCPTimeouts: 호스트 전체의 재전송 세그먼트 수와 RTO 타임아웃 횟수를 본다.
- Prometheus(node_exporter):
rate(node_netstat_Tcp_RetransSegs[1m]) / rate(node_netstat_Tcp_OutSegs[1m])로 재전송률을 구한다.
- Wireshark:
tcp.analysis.retransmission 필터로 재전송 패킷만 골라 본다.
재전송률 기준은 예시로 0.5% 미만이면 정상, 2%를 넘으면 조사 대상으로 제시된다.
오른쪽은 줄이는 방법이다.
- 이동, 위치는 UDP로 보낸다. 최신 상태만 의미 있는 데이터는 다시 보낼 필요가 없다.
TCP_NODELAY로 Nagle을 끄고, tcp_thin_linear_timeouts로 thin-stream의 RTO 백오프를 완화한다.
- 세션 송신 큐에 상한을 두고, 넘으면 오래된 상태 패킷을 버리거나 느린 세션을 정리한다.
TCP_USER_TIMEOUT, keepalive 설정으로 죽은 연결을 빨리 정리한다.
- 장비 큐, MTU, 무선 구간 drop을 구간별로 점검한다.
작은 패킷을 보내는 소켓에 Nagle이 켜져 있으면 ACK를 못 받은 세그먼트가 하나뿐이라 dup ACK가 생기지 않고, 손실은 빠른 재전송 대신 RTO로만 복구됩니다. 이 관계는 Nagle과 TCP_NODELAY 영상에서 다룹니다.
용어 정리
- Seq / ACK
- Seq는 세그먼트의 순번, ACK는 수신측이 "여기까지 받았고 다음은 이 번호가 필요하다"고 알려 주는 응답이다.
- Dup ACK
- 같은 번호의 ACK가 반복되는 것이다. 중간 세그먼트가 빠졌는데 뒤 세그먼트가 계속 도착할 때 생긴다.
- Fast Retransmit
- Dup ACK 3개를 받으면 타이머를 기다리지 않고 바로 재전송하는 방식이다. 지연은 대략 1 RTT 수준이다.
- RTO (Retransmission Timeout)
- ACK를 기다리는 타이머가 만료되어 재전송하는 방식이다. 리눅스 최솟값은 200ms이고 실패할 때마다 두 배로 늘어난다.
- RTT / SRTT / RTTVAR
- RTT는 패킷이 갔다가 응답이 돌아오는 왕복 시간이다. SRTT는 커널이 평활화한 RTT 평균, RTTVAR는 그 변동폭이며 RTO 계산에 쓰인다.
- cwnd (혼잡 윈도우)
- ACK 없이 한 번에 보낼 수 있는 데이터 양의 상한이다. 손실이 감지되면 줄어들고 처리량도 같이 떨어진다.
- Slow Start
- cwnd를 작은 값에서 시작해 ACK가 올 때마다 빠르게 키우는 단계다. RTO 이후에는 cwnd 1부터 이 단계를 다시 밟는다.
- Head-of-Line 블로킹
- 앞의 데이터 하나가 빠져서 뒤에 이미 도착한 데이터까지 앱에 전달되지 못하는 현상이다. TCP는 연결 하나가 순서를 보장하는 한 줄이라 생긴다.
- tail drop
- 장비 큐가 가득 찼을 때 새로 들어온 패킷을 버리는 동작이다. 혼잡 구간 손실의 대표 원인이다.
- NIC 링 버퍼
- NIC가 받은 패킷을 커널이 가져가기 전까지 담아 두는 고정 크기 버퍼다. 커널이 제때 비우지 못하면 넘쳐서
rx_missed, rx_fifo 계열 카운터(ethtool -S)가 오른다.
- MTU 블랙홀
- 경로 중간 장비가 큰 패킷을 버리면서 그 사실을 알리지 않는 상황이다. 작은 패킷은 통과하고 큰 패킷만 계속 사라진다.
- 재전송률
- 전체 송신 세그먼트 중 재전송된 비율이다.
TcpRetransSegs / OutSegs로 계산한다.
- thin-stream
- 작은 패킷을 드문드문 보내는 연결이다. 게임, 채팅 트래픽이 여기에 해당하며 손실 뒤 다음 패킷이 늦게 나가 RTO에 의존하기 쉽다.
- TCP_USER_TIMEOUT
- 보낸 데이터가 ACK 없이 이 시간 이상 남아 있으면 연결을 끊는 소켓 옵션이다. 죽은 연결을 오래 붙잡고 있지 않게 해 준다.