TCP 연결이 맺어진 두 호스트 사이에는 많은 라우터(router)들이 존재한다. 라우터의 저장공간(버퍼, buffer)은 한정되어 있다. 라우터가 다음 라우터로 패킷을 내보내는 속도보다, 라우터에 패킷이 들어오는 속도가 더 빠르면 버퍼는 점점 차오른다. 버퍼가 가득차있는데 들어온 새로운 버퍼는 모두 드랍(drop)된다. 이런 혼잡한 상황(congestion)에서도 TCP는 데이터를 유실없이 전송하면서도, 통신 채널을 최대한 효율적으로 활용해야 하는 목표가 있다.
TCP는 전송 계층(transport layer)의 프로토콜인 한편, 라우터의 기능은 그 하위인 네트워크 계층(network layer)에 그친다. TCP가 혼잡 상황을 알 수 있는 방법은 두 가지가 있다. 네트워크 계층에서 직접 혼잡 상황을 알려주는 경우와 그렇지 않아서 직접 추론해야하는 경우다. 전자를 네트워크 지원 혼잡 제어(Network-Assisted Congestion Control)라고 하고, 후자를 종단 간 혼잡 제어(End-to-End Congestion Control)라고 한다.
네트워크 지원 혼잡 제어(Network-Assisted CC)
네트워크 계층이 전송 계층에 혼잡 사실을 알리는 방법은 두 가지가 있다. 첫째는 라우터가 직접 TCP 송신자에게 초크 패킷(choke packet)을 보내 혼잡을 알리는 것이다. 둘째는 송신자에게 전송되는 패킷의 헤더에 혼잡을 의미하는 비트를 담는 방법이다. 이것을 ECN(Explicit Congestion Notification)이라고 부른다. ECN은 IP 헤더의 2비트를 차지한다. 이 두가지 방법은 TCP에게 혼잡 정보를 정확히 알리지만, 그만큼 네트워크 계층에게 의존적이게 된다.
종단 간 혼잡 제어 (End-to-End CC)
호스트와 호스트 사이 네트워크 계층이 혼잡한지 아닌지 전송 계층은 추론할 수 있다. 그 근거는 패킷의 손실이다. OS 커널(kernel)의 TCP 혼잡 제어 구현체는 점점 더 정교하게 발전해왔다. TCP는 파이프라이닝(pipelining)된 프로토콜임을 기억하자. 수신자의 ACK를 기다리지 않고 한번에 전송할수 있는 윈도우 크기를 cwnd라는 변수값으로 부른다. 당연하게도 cwnd가 커질수록 통신 채널의 활성화(utilization) 정도가 높아진다. 쉽게 말하면 효율적으로 더 빠르게 전송한다. 하지만 cwnd가 지나치게 커지면, 호스트 사이의 라우터들의 버퍼가 차오르고 혼잡해져서 패킷 드랍이 발생할 수 있다. 따라서, 커널은 패킷이 드랍당하지 않는 선에서 최대의 cwnd를 확보하기 위해 노력한다.
TCP의 cwnd가 조절되는 방식은 AIMD(Additive Increase, Multiplicative Decrease)라는 철학으로 설명된다. TCP는 cwnd 값을 혼잡이 감지될 때까지 선형적으로(합연산으로) 증가시키지만, 혼잡이 감지되면 곱절로 깎아버린다.
좀 더 구체적으로 설명하면, TCP는 3가지 상태를 오가며 cwnd를 조절한다. 첫번째 상태는 slow start다. 처음에는 아주 작은 cwnd값으로 시작하되, cwnd를 과감하게 두배씩 늘려가며 지수적으로 cwnd를 높여간다. 그러다가 혼잡이 감지되거나, 내부적인 연산에 의해 설정된 ssthresh(slow start threshold) 변수 값에 cwnd가 도달하면 slow start 상태를 벗어난다. 두번째 상태는 congestion avoidance다. 지수적으로 cwnd를 증가시켰던 slow start 상태와 달리, 지금은 cwnd를 선형적으로 조금씩 증가시킨다. AIMD의 ‘AI’라고 볼 수 있다. 세번째 상태는 fast recovery다. 패킷 드랍이 감지되었거나, 3번의 중복된 ACK(3-duplicated ACK)를 받으면 TCP 혼잡 제어 상태는 fast recovery 상태로 돌입하여, ssthresh와 cwnd를 즉시 절반가량 낮춘다. 이 3가지 상태를 오가며 최적의 cwnd를 찾는다.
TCP 혼잡 제어의 발전
최초의 TCP 혼잡 제어 알고리즘은 TCP Tahoe다. 이때의 모델은 패킷 드랍이나 3-dup ACK가 발생하면 항상 cwnd를 1로 초기화하고 slow start 상태에 들어갔다. 매번 slow start 상태로 다시 들어가니 비효율이 발생했다. 그래서, 개선된 다음 모델인 TCP Reno는 cwnd를 1로 만드는 대신 cwnd를 절반으로 감소시키는 AIMD 전략을 사용했다. 현대의 표준은 TCP Cubic이다. 이 모델은 cwnd를 선형적으로 증가시키는 대신 3차 함수(cubic function) 곡선을 그리게 증가시킨다.
최근 구글에서 개발한 BBR(Bottleneck Bandwidth and Round-trip propagation time)은 더욱 정교하게 cwnd를 조절한다. 앞선 전략들은 패킷의 손실만을 cwnd 조절 근거로 사용했지만, BBR은 네트워크의 대역폭과 지연시간(RTT)등을 종합적으로 고려하여 최적의 cwnd를 찾는다. 일단 라우터의 버퍼가 터져야 cwnd를 조절할 수 있는 앞선 방법들보다 뛰어나다.
TCP 혼잡제어의 구현 위치도 발전되어 왔다. TCP의 혼잡제어는 OS 커널에서 구현되었다. 앱이 socket()을 만들고 send() 등을 호출할 때 커널 모드에서 혼잡 제어 알고리즘이 동작했다. 그러나, 최근에는 커널이 아니라 앱에서 이것을 제어하여 커널 메모리에서 앱 메모리로 패킷을 옮기는 시간을 절약하려는 시도가 있다(user-level TCP). HTTP/3 기반의 QUIC 또한, 응용 계층(application layer)에서 혼잡 제어를 시도한 사례다. QUIC는 TCP가 아니라 UDP 위에서 동작하기 떄문이다. UDP는 혼잡 제어를 전혀 신경쓰지 않는다.
TCP는 공평한가?
여러개의 애플리케이션이 하나의 네트워크 채널을 공유하고 있다고 하자. 이 채널은 애플리케이션들에게 공평하게 할당될까? AIMD 철학에 의하면, 그래야한다. 여러 개의 연결들이 처음에는 서로 다른 대역폭을 갖더라도, 시간이 지나면 공평한 대역폭을 갖도록 수렴한다.
그러나, 현실은 그렇지 않다. 그 이유는 첫째로, UDP 연결은 혼잡 제어를 신경쓰지 않기 때문이다. UDP 연결들은 TCP 연결들을 불공정하게 압도할 수 있다. 둘째로, 하나의 애플리케이션이 여러 개의 TCP 연결을 맺을 경우, 그렇지 않은 앱들보다 더 많은 대역폭을 확보할 수 있다. 셋째로, RTT가 짧은 쪽이 cwnd를 더 빠르게 증가시켜서 RTT가 긴 쪽을 압도할 수 있다.
'개발이야기' 카테고리의 다른 글
| 네이버지도에서 매장 정보 수집을 실패하기까지의 고군분투 이야기 (0) | 2025.12.31 |
|---|---|
| 개발 생산성을 높이는 MCP의 개념과 Cursor AI 설정법 (0) | 2025.07.16 |
| OCaml에 대해 실전 속성 압축으로 익혀보기 (0) | 2025.06.22 |
| VSCode 환경에서 make로 빌드되는 c파일 gdb로 디버깅하기 (0) | 2025.06.16 |
| TailwindCss로 도막도막 끊기는 스크롤 화면 구현하기: Full Page Scroll과 Scroll snap (1) | 2025.05.17 |