
송신 측은 congestion window(혼잡 윈도우, cwnd)라는 변수를 하나 둡니다. 확인받지 못한 채 네트워크에 떠 있어도 되는 바이트 수입니다. 전송 속도는 대략 다음과 같습니다.
rate ≈ cwnd / RTT (bytes/sec)
cwnd만큼 보내고 한 바퀴 돌아오기를 기다리므로, RTT당 cwnd만큼 나가는 셈입니다. RTT는 마음대로 바꿀 수 없으니 조절할 수 있는 것은 cwnd 하나입니다. 실제 송신 가능량은 flow control까지 고려해 min(cwnd, rwnd)가 됩니다. 상대 버퍼와 네트워크 사정 중 더 빡빡한 쪽을 따르는 것입니다.
문제는 이렇게 바뀝니다. 손실이라는 신호만 보고 cwnd를 어떻게 올리고 내릴 것인가.
연결이 막 시작됐을 때는 적정 속도를 전혀 모릅니다. 그래서 1 MSS에서 출발합니다. 그리고 ACK를 하나 받을 때마다 cwnd를 1 MSS씩 늘립니다.
결과적으로 RTT마다 cwnd가 두 배가 됩니다. 1, 2, 4, 8로 늘어나는 지수 증가입니다. 이름이 slow start인 것은 증가가 느려서가 아니라 시작점이 1로 낮아서입니다. 아무것도 모르는 상태에서 조심스럽게 시작하되, 적정선을 빨리 찾기 위해 빠르게 올리는 전략입니다.
무한정 두 배씩 늘릴 수는 없습니다. 그래서 ssthresh(slow start threshold)라는 경계를 둡니다.
여기서부터가 AIMD(Additive Increase Multiplicative Decrease)입니다.
올릴 때는 조심스럽게, 내릴 때는 과감하게가 핵심입니다. 혼잡은 빨리 해소해야 하지만 여유는 천천히 확인해도 되기 때문입니다. 이 때문에 cwnd 그래프는 톱니 모양이 됩니다.
손실을 알아채는 경로는 두 가지입니다. timeout과 duplicate ACK 세 개. 두 버전은 이 둘을 같게 보느냐 다르게 보느냐에서 갈립니다.
| TCP Tahoe | TCP Reno | |
|---|---|---|
| 역할 | 최초의 혼잡 제어 내장 버전 | Tahoe에 fast recovery 추가 |
| timeout 시 | ssthresh = cwnd/2, cwnd = 1, slow start부터 다시 | 동일 |
| 중복 ACK 3개 시 | 위와 동일하게 cwnd = 1 | ssthresh = cwnd/2, cwnd도 그 근처로만 낮추고 congestion avoidance로 복귀 |
| 판단 | 두 신호를 같은 심각도로 봄 | 중복 ACK는 가벼운 혼잡으로 봄 |
| 오버헤드 | 회복이 느림 | 상태 관리가 복잡 |
Reno의 근거는 단순합니다. 중복 ACK가 온다는 것은 뒤의 세그먼트들이 도착하고 있다는 뜻이므로 경로가 완전히 막힌 것은 아닙니다. 반면 timeout은 아무 응답도 없는 상태이니 훨씬 심각합니다. 그래서 Reno는 중복 ACK에 대해서는 cwnd를 1까지 떨어뜨리지 않고 절반 수준에서 이어갑니다. 이것이 fast recovery입니다.
ssthresh = 8에서 시작해 12에서 손실이 난 경우를 라운드별로 비교하면 차이가 분명합니다.
| round | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8(손실) | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Tahoe | 1 | 2 | 4 | 8 | 9 | 10 | 11 | 12 | 1 | 2 |
| Reno | 1 | 2 | 4 | 8 | 9 | 10 | 11 | 12 | 6 | 7 |

같은 병목 링크를 두 연결이 나눠 쓴다고 해봅시다. 둘 다 AIMD를 따르면 결국 대역폭을 반씩 나눠 갖는 쪽으로 수렴합니다.
이유는 증가와 감소의 성질이 다르기 때문입니다. additive increase는 두 연결에 같은 양을 더합니다. 많이 쓰던 쪽이나 적게 쓰던 쪽이나 똑같이 1씩 올라가므로 둘의 격차는 그대로입니다. 반면 multiplicative decrease는 각자의 현재 값에 비례해 깎습니다. 많이 쓰던 쪽이 더 많이 깎이므로 격차가 줄어듭니다.
이 과정이 반복되면 격차는 계속 줄어들고 결국 균등한 지점 근처를 오가게 됩니다. 손실이라는 벌칙을 현재 사용량에 비례해 부과하는 것이 공평성의 원천입니다.
다만 조건이 붙습니다. RTT가 다르면 짧은 쪽이 더 자주 cwnd를 올리므로 유리합니다. UDP는 애초에 이 규칙을 따르지 않습니다. 그리고 한 애플리케이션이 TCP 연결을 여러 개 열면 그만큼 더 가져갑니다. 브라우저가 한 서버에 여러 연결을 맺는 것이 이와 무관하지 않습니다.