TCP가 손실을 언제 판정하느냐고 물으면 나는 오랫동안 "왕복 시간(RTT) 평균에 안전 마진 조금 더한 값"이라고 답했다. 재전송 타이머(RTO, Retransmission Timeout)를 실제로 따라가 보니 이 답은 틀렸다. RTO를 지배하는 건 평균이 아니라 RTT의 편차(variation) 다. 이 글은 RTO = SRTT + 4·RTTVAR이라는 한 줄이 왜 그렇게 생겼는지, 그리고 왜 편차가 마진을 지배해야 혼잡 붕괴를 막는지를 정리한 것이다.
RTO는 "이 시간까지 ACK가 안 오면 세그먼트가 사라졌다고 보고 재전송한다"는 타이머다. 너무 짧으면 멀쩡한 지연을 손실로 오판해 불필요한 재전송을 쏟아내고, 너무 길면 진짜 손실에 굼뜨게 반응한다. 이 둘 사이를 맞추는 게 RTO 계산의 전부다.
RFC 6298 §2에 따르면 TCP는 RTT 추정 대상마다 두 값을 든다.
SRTT — smoothed round-trip time. RTT의 지수가중이동평균(EWMA).RTTVAR — round-trip time variation. RTT 편차의 이동평균.RTT 측정치 R'이 도착하면 이렇게 갱신한다. 순서가 핵심이다.
RTTVAR = (1 - beta)·RTTVAR + beta·|SRTT - R'| # beta = 1/4, 이때 SRTT는 아직 옛값
SRTT = (1 - alpha)·SRTT + alpha·R' # alpha = 1/8
RTO = SRTT + max(G, 4·RTTVAR) # G = 클럭 granularity
RTTVAR을 먼저 계산해야 한다. |SRTT − R'|의 SRTT는 이번 라운드에 아직 갱신되지 않은 직전 값이어야 하기 때문이다. 순서를 바꿔 SRTT를 먼저 올리면 방금 R'을 반영한 SRTT가 R'에 더 가까워져 |SRTT−R'|가 작아지고, 편차가 과소평가된다. 나는 이 부분을 반대로 알고 있었다.
1988년 이전 BSD는 RTO = beta · SRTT (beta≈2) 같은 고정 배수를 썼다. 문제는 부하가 높아 RTT 분산이 커질 때다. 평균의 상수배는 커진 분산을 못 따라가서, 정상적인 지연을 손실로 오판한다. 오판은 불필요한 재전송을 낳고, 재전송은 혼잡을 더 키운다 — 이것이 1980년대 인터넷을 멈춰 세운 혼잡 붕괴(congestion collapse)의 한 원인이었다.
Jacobson의 처방은 마진을 RTT 편차에 비례시키는 것이다. 정규 근사에서 평균 + 4·평균편차는 대략 상위 극단을 덮으므로, RTT가 안정적이면 RTO가 SRTT 바로 위에 붙고 출렁이면 자동으로 크게 벌어진다.
RTT 안정: RTTVAR≈0 → RTO ≈ SRTT (타이트, 손실 빨리 감지)
RTT 요동: RTTVAR 큼 → RTO = SRTT + 4·RTTVAR (여유, 오판 방지)
마진을 정하는 건 "얼마나 느린가(평균)"가 아니라 "얼마나 들쭉날쭉한가(편차)"다.
alpha=1/8, beta=1/4가 예뻐 보이는 건 우연이 아니다. 분수 곱을 매번 부동소수점으로 하면 느리니, Jacobson은 SRTT를 8배(<<3), RTTVAR을 4배(<<2) 스케일로 저장해 gain을 비트 시프트로 바꿨다.
err = R_prime - (srtt >> 3); // 스케일 해제한 SRTT와의 오차
srtt += err; // srtt는 8·SRTT 저장 → 실질 SRTT += err/8
if (err < 0) err = -err; // |err|
err -= (rttvar >> 2);
rttvar += err; // rttvar는 4·RTTVAR 저장 → RTTVAR += (|err|-RTTVAR)/4
// rto = (srtt >> 3) + (rttvar >> 2 << 2) == SRTT + 4·RTTVAR
beta=1/4, alpha=1/8이 각각 >>2, >>3으로 떨어지는 게 상수 선택의 이유다. Linux는 여기에 마이크로초 해상도(rtt_us)와, RTT가 급감할 때 RTTVAR이 너무 빨리 줄지 않게 하는 mdev_max 보정을 얹는 것으로 알려져 있다.
산술을 직접 밟아보면 편차의 지배력이 드러난다. R=100ms로 시작해 계속 안정적이라 가정하자.
초기: SRTT=100, RTTVAR=50, RTO=100+4·50 = 300ms
R'=100: RTTVAR=(3/4)·50 + 0 = 37.5, RTO=100+4·37.5 = 250ms
R'=100: RTTVAR=(3/4)·37.5 = 28.125, RTO=100+4·28.125 = 212.5ms
RTT가 일정하면 RTTVAR이 매 라운드 3/4로 감쇠해 RTO가 SRTT(100ms)로 수렴한다. 여기서 한 번 R'=300ms로 튀면:
RTTVAR=(3/4)·28.125 + (1/4)·|100-300| = 71.09
SRTT =(7/8)·100 + (1/8)·300 = 125
RTO =125 + 4·71.09 = 409ms
SRTT는 125로 살짝 움직였는데 RTO는 250→409로 크게 벌어진다. 마진을 지배하는 건 평균이 아니라 편차라는 걸 숫자가 보여준다.
RFC 6298은 최종 RTO를 최소 1초로 클램프하길 권한다(SHOULD). LAN에서 RTT가 수백 µs여도 RTO를 sub-ms로 두면 지터·delayed ACK에 성급히 재전송하기 때문이다. (Linux는 TCP_RTO_MIN을 200ms로 두는 등 실무 튜닝이 있어, 표준의 1초와는 다르다.) 상한은 최소 60초 이상을 허용해야 한다.
타이머가 만료되면 가장 오래된 미확인 세그먼트를 재전송하고 RTO = RTO * 2로 지수 백오프한다. 이렇게 두 배가 된 RTO는 다음 유효 측정이 나올 때까지 유지된다 — 성공 ACK 한 번에 즉시 원복하지 않는다. 반복 손실 구간에서 타이머가 다시 조급해지는 걸 막기 위해서다.
마지막 함정은 Karn 알고리즘이다. 세그먼트를 재전송한 뒤 온 ACK는 원본에 대한 것인지 재전송본에 대한 것인지 구분할 수 없다(retransmission ambiguity). 이 ACK로 RTT를 재면 SRTT가 오염된다. 그래서 Karn의 규칙은 재전송이 낀 왕복은 RTT 표본에서 제외한다. 단, RFC 7323 timestamp 옵션을 쓰면 각 ACK가 어느 전송을 반영하는지 echo되므로 이 경우엔 예외적으로 측정할 수 있다.
send S(원본) ── t0
재전송 send S(retx) ── t1
ACK ── t2 ← t0의 응답? t1의 응답? 모름
⇒ Karn: 이 왕복은 SRTT에 넣지 않음
SRTT + 4·RTTVAR이고, 지배 항은 편차다. 고정 배수는 1988년 이전 방식이고 혼잡 붕괴의 원인이었다.|SRTT−R'|의 SRTT가 옛값이어야 편차가 제대로 잡힌다.더 파고들 만한 것: RTO 오발동을 회복하는 F-RTO(RFC 5682), 시간 기반 손실 감지로 dupACK 카운팅을 대체하는 현대 Linux 기본 감지기 RACK-TLP(RFC 8985).