22. RTT 추정과 재전송 타임아웃(RTO): SRTT·RTTVAR·Karn 알고리즘과 게임 RUDP 적용
난이도 상난이도: 상
모범답안 — RTT 추정과 재전송 타임아웃(RTO)
1. 고정 타임아웃의 문제
RTO 는 "이 정도 지났으면 패킷이 유실됐다고 봐도 좋다"는 판단 기준이다. 그런데 실제 RTT 는 경로·시간대·단말 환경에 따라 수 ms 에서 수백 ms 까지 변한다. 고정값은 이 분포를 따라갈 수 없다.
- 너무 작을 때 — 조기 타임아웃(spurious retransmission) 아직 도착 중인 패킷을 유실로 오판해 재전송한다. 대역폭을 낭비할 뿐 아니라, 혼잡이 원인이라면 재전송이 혼잡을 더 키우는 양의 되먹임을 만든다. 네트워크가 느려질수록 재전송이 늘고, 재전송이 늘수록 더 느려지는 혼잡 붕괴(congestion collapse)로 이어진다.
- 너무 클 때 — 복구 지연 실제로 유실된 패킷을 오래 기다린다. 신뢰 채널의 헤드 오브 라인 블로킹으로 뒤 패킷 처리까지 멈춰 체감 렉이 커진다. 모바일 게임에서 "가끔 1초씩 멈춘다"는 리포트의 흔한 원인이다.
즉 RTO 는 경로의 RTT 분포를 추정해 동적으로 정해야 한다.
2. SRTT 와 RTTVAR (RFC 6298)
RTT 샘플 R 을 측정할 때마다 지수 이동 평균으로 갱신한다.
첫 측정:
SRTT = R
RTTVAR = R / 2
RTO = SRTT + max(G, 4 * RTTVAR)
이후 측정:
RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R| (beta = 1/4)
SRTT = (1 - alpha) * SRTT + alpha * R (alpha = 1/8)
RTO = SRTT + max(G, 4 * RTTVAR)
G 는 시계 알갱이(clock granularity). RTTVAR 를 먼저 갱신한 뒤 SRTT 를 갱신하는 순서가 중요하다
(RTTVAR 계산에 갱신 전 SRTT 를 써야 편차가 제대로 반영된다).
alpha = 1/8, beta = 1/4 는 시프트 연산으로 구현할 수 있어 커널에서 부동소수점 없이 계산된다.
왜 평균만 쓰지 않는가
RTT 는 분포이지 값 하나가 아니다. SRTT 는 EWMA 평균이라 중앙값과는 다르지만, SRTT 만 쓰면 상당수의 패킷이 이를 초과하고,
그때마다 조기 타임아웃이 난다. RTTVAR 는 평균 절대 편차(mean deviation)의 추정치이고,
4 * RTTVAR 를 더하는 것은 분포의 꼬리까지 덮는 안전 마진이다.
원래 Jacobson 알고리즘은 표준편차 대신 계산이 싼 평균 절대 편차를 썼고, 정규분포 가정에서
평균 절대 편차 ≈ 0.8σ 이므로 4 * RTTVAR ≈ 3.2σ 정도가 된다. 대략 99% 이상의 샘플을 덮는
보수적인 값이다. RTT 변동이 클수록(불안정한 무선망) 마진이 자동으로 커지고, 안정적인 유선망
에서는 SRTT 에 바짝 붙는 것이 이 식의 핵심이다.
하한과 상한
- 하한 1초 (RFC 6298). 다만 실제 리눅스 구현은
TCP_RTO_MIN = 200ms를 쓴다. 데이터센터 내부처럼 RTT 가 마이크로초 단위인 환경에서는 이 하한 자체가 성능 문제(incast TCP)가 된다. - 상한 최소 60초.
- 계산 결과가 하한 미만이면 하한으로 올린다.
3. Karn 알고리즘과 재전송 모호성
모호성
패킷 P 를 보내고 타임아웃 후 P 를 재전송했는데 ACK 가 왔다고 하자. 이 ACK 가
- 원래 전송에 대한 것인가(→ 실제 RTT 는 매우 길다), 아니면
- 재전송에 대한 것인가(→ RTT 는 짧다)
를 구분할 수 없다. 잘못 측정하면 SRTT 가 크게 왜곡된다. 특히 재전송 기준으로 측정하면 RTT 가 실제보다 짧게 추정되어 RTO 가 더 작아지고, 조기 타임아웃이 더 늘어나는 악순환에 빠진다.
Karn 알고리즘
- 재전송된 세그먼트의 ACK 로는 RTT 샘플을 취하지 않는다.
- 대신 타임아웃마다 RTO 를 지수 백오프하고, 백오프한 값은 새 RTT 샘플을 얻을 때까지 유지한다.
(2)가 없으면 심각한 문제가 생긴다. 경로 RTT 가 갑자기 크게 늘어난 상황(경로 변경, 혼잡)에서는 모든 전송이 타임아웃 → 재전송되므로 (1) 때문에 새 샘플을 영영 얻지 못하고, RTO 는 낡은 작은 값에 머문 채 재전송만 반복하게 된다. 백오프가 이 구멍을 메운다.
TCP timestamp 옵션 (RFC 7323)
송신 측이 TSval 에 전송 시각을 넣어 보내면 수신 측이 TSecr 로 그대로 되돌려준다. 그러면
어느 전송에 대한 ACK 인지 명확해지므로 모호성 자체가 사라진다. 그 결과
- 재전송된 세그먼트에서도 RTT 를 측정할 수 있고(Karn 의 제약 해제),
- 세그먼트마다 샘플을 얻어 RTT 추정 정밀도가 크게 올라가며,
- PAWS(순환된 시퀀스 번호 방어)와 조기 타임아웃 탐지(Eifel/F-RTO)의 기반이 된다.
RUDP 를 직접 만든다면 패킷 헤더에 전송 시각 또는 전송 시퀀스 ID 를 넣고 ACK 에 그대로 에코하는 설계를 처음부터 넣는 것이 좋다. 비용은 4~8바이트이고 얻는 것이 크다.
4. 지수 백오프
타임아웃이 날 때마다 RTO = min(RTO * 2, RTO_MAX).
- 이유: 타임아웃이 났다는 것은 (a) 추정이 틀렸거나 (b) 네트워크가 실제로 나빠졌다는 신호다. 둘 다 "더 기다려야 한다"를 뜻한다. 동시에, 혼잡이 원인일 때 송신율을 지수적으로 낮춰 네트워크에 회복 여지를 준다. 백오프는 사실상 분산 시스템의 부하 차단 장치다.
- 없으면: 손실률이 오르는 순간 모든 송신자가 같은 짧은 간격으로 재전송을 퍼부어 혼잡이 더 심해진다. 원인이 사라져도 재전송 부하가 스스로를 유지하는 메타스테이블 장애가 된다. 1986년 인터넷 혼잡 붕괴가 정확히 이 현상이었다.
- 복원 시점: 재전송되지 않은 세그먼트에서 새 RTT 샘플을 얻었을 때 SRTT/RTTVAR 로부터 RTO 를 다시 계산한다. ACK 를 받았다는 이유만으로 즉시 되돌리면 안 된다(그 ACK 가 재전송에 대한 것일 수 있다). timestamp 옵션이 있으면 판단이 명확해진다.
5. 게임 RUDP 에의 적용
TCP 의 가정과 다른 점
TCP 는 대용량 스트림을 최대 처리량으로 옮기는 것을 목표로 설계됐다. 게임은 반대다.
| TCP 가정 | 게임 트래픽 | |
|---|---|---|
| 패킷 크기 | MSS 가득 | 수십~수백 바이트 |
| 전송 패턴 | 연속 스트림 | 틱 주기(20~60Hz) 고정 간격 |
| 목표 | 처리량 최대화 | 지연 최소화·예측 가능성 |
| 손실 대응 | 반드시 재전송 | 종종 버리는 편이 낫다 |
특히 RTO 하한 200ms 는 60Hz 틱(16.7ms) 게임에서 12틱 이상이다. 그 사이 상태는 이미 낡았다. 게임 RUDP 는 하한을 훨씬 낮게(예: 30~50ms) 잡되, 대신 조기 타임아웃 비용을 감수한다. 또 패킷이 드문드문 나가면 RTT 샘플 자체가 적어 추정이 낡는다. 하트비트나 keepalive 로 최소 샘플링 주기를 확보해야 한다.
재전송보다 "최신 상태 재전송"
위치·회전 같은 덮어쓰기 가능한 상태(idempotent state) 는 옛 패킷을 재전송할 이유가 없다. 150ms 전 위치를 다시 보내느니 지금 위치를 보내는 편이 항상 낫다. 이것이 스냅샷 기반 동기화의 핵심 아이디어다.
반면 이벤트(스킬 시전, 아이템 획득, 채팅) 는 누락되면 안 되므로 신뢰 전송이 필요하다. (채널 분류 체계 자체는 기본기 network/problem4 에서 다룬다. 여기서는 그 분류가 RTT 추정과 어떻게 맞물리는지에 집중한다.)
RTT 추정은 어느 채널에서?
두 채널을 합쳐서 하나의 경로 RTT 를 추정한다. RTT 는 채널의 속성이 아니라 경로의 속성 이기 때문이다. 다만 측정은 신뢰 채널의 ACK 만으로 하면 샘플이 부족하므로, 모든 패킷에 시퀀스와 전송 시각을 넣고 수신 측이 주기적으로 ACK/피드백 패킷으로 에코하게 해서 비신뢰 채널에서도 샘플을 얻는 설계가 일반적이다(QUIC 의 ACK 프레임과 같은 접근).
주의할 점은 지연 ACK 시간을 빼야 한다는 것이다. 수신 측이 ACK 를 20ms 지연시켰다면
그 20ms 는 경로 RTT 가 아니다. ACK 패킷에 "얼마나 지연시켰는지"를 담아 보정한다
(QUIC 의 ACK Delay 필드가 정확히 이 역할).
버퍼블로트와 스파이크
Wi-Fi 와 모바일 망은 큰 버퍼를 갖고 있어, 혼잡 시 패킷을 버리는 대신 큐에 쌓아 지연을 수백 ms 로 늘린다(버퍼블로트). 그러면
- RTT 샘플이 크게 튀고 RTTVAR 가 폭증한다.
SRTT + 4*RTTVAR가 수백 ms~수 초로 커져 RTO 가 사실상 무력해진다.- 손실이 없으니 손실 기반 혼잡 제어는 아무 신호도 받지 못한다.
대응:
- RTO 상한을 게임 도메인에 맞게 낮게 잡는다(예: 1초). 그 이상 걸리면 어차피 게임 진행이 불가능하므로 연결 품질 경고나 세션 재협상이 더 나은 선택이다.
- 하한은 조기 타임아웃 비용과 저울질해 정한다. 30~50ms 가 흔한 선택이며, 반드시 실측 데이터로 튜닝해야 한다.
- 최근 최소 RTT(min RTT) 를 별도 추적해 큐 지연을 분리한다.
현재 RTT - min RTT가 큐잉 지연 추정치이며, BBR·QUIC 계열이 이 값을 혼잡 신호로 쓴다. - RTTVAR 를 그대로 쓰지 않고 백분위수(p95/p99) 기반 RTO 를 쓰는 구현도 있다. 스파이크에 덜 민감하다.
- 스파이크 구간의 샘플을 이상치로 제외하는 필터를 넣되, 경로가 진짜로 나빠진 경우와 구분해야 한다(min RTT 가 함께 올라갔는가로 판단).
6. 실무 판단 — "재전송이 너무 많다"
측정 없이 파라미터부터 만지면 안 된다. 순서는 원인 계층을 좁히는 방향으로 간다.
1단계: 실제 손실률 측정 (원인인가, 증상인가)
- 시퀀스 번호 기반으로 수신 측이 관측한 갭 비율을 집계한다. 송신 측 재전송률이 아니라 수신 측 손실률을 봐야 조기 타임아웃과 진짜 손실을 구분할 수 있다.
- 핵심 판별: 손실률은 낮은데(예: 0.1%) 재전송률이 높다면(예: 5%) → 조기 타임아웃이 주범이다. 손실률 자체가 높다면 → 네트워크·혼잡 문제.
- 지표:
retransmit_count / sent_count,receiver_gap_count / expected_count, 그리고 중복 수신률(재전송된 패킷이 실은 도착했다는 증거).
2단계: RTT 분포 확인
- SRTT, RTTVAR, RTO 를 히스토그램/백분위수로 기록한다. 평균만 보면 안 된다.
- RTO 가 실측 RTT p99 보다 작다면 조기 타임아웃이 구조적으로 발생한다. → RTO 하한과 RTTVAR 계수를 조정한다.
- min RTT 와 현재 RTT 의 차이로 버퍼블로트 여부를 판단한다.
3단계: ACK 정책 점검
- 지연 ACK: 수신 측이 ACK 를 40ms 지연시키는데 RTO 하한이 50ms 라면, 정상 통신에서도 타임아웃이 난다. 이 조합이 재전송 폭증의 흔한 원인이다. → 지연 ACK 시간을 RTT 추정에서 보정하거나, 게임 트래픽에서는 지연 ACK 를 끄고 즉시 ACK 를 쓴다.
- 누적 ACK vs 선택적 ACK(SACK): 누적 ACK 만 있으면 중간 하나가 유실됐을 때 이후 패킷을 모두 재전송하게 되어 재전송량이 폭증한다. → SACK/ACK 비트맵을 도입한다.
- ACK 자체의 유실: ACK 가 비신뢰 채널로 가면 ACK 유실이 곧 재전송이다. ACK 를 누적적으로 설계해 하나 유실돼도 다음 ACK 가 커버하게 한다.
4단계: 빠른 재전송 도입 검토
RTO 만으로 손실을 감지하면 항상 느리다. 중복 ACK 3개(TCP fast retransmit) 나 후속 패킷의 수신 확인(RACK 방식) 으로 타임아웃 전에 손실을 감지하면 재전송 지연이 크게 줄고, RTO 는 최후의 안전망 역할만 하게 된다. 이것이 현대 구현(QUIC 의 loss detection)의 표준 접근이다.
정리
확인 순서는 손실률 → RTT 분포 → ACK 정책 → 감지 알고리즘. 파라미터 튜닝은 맨 마지막이며, 그 전에 "재전송이 많은 것이 조기 타임아웃 때문인가 실제 손실 때문인가"를 반드시 먼저 가려야 한다.