혼잡제어, 큰 그림
지난 글에서 “패킷이 유실되면 재전송한다”까지 다뤘습니다. 그런데 애초에 왜 유실이 일어날까요? 대부분은 네트워크 중간의 라우터가 처리 용량을 넘는 트래픽을 받아서 패킷을 그냥 버리기 때문입니다. 이번 글은 이 문제, 혼잡제어(congestion control)를 다룹니다.
흐름제어와 혼잡제어, 뭐가 다른가
이름이 비슷해서 자주 헷갈리는데, 둘은 보호하는 대상이 다릅니다.
흐름제어는 “상대방(수신자)이 감당할 수 있는가”만 신경 씁니다. 혼잡제어는 “이 연결과 무관한 다른 연결들이 같은 회선을 쓰고 있는데, 그 회선 자체가 감당할 수 있는가”를 신경 씁니다. 그래서 혼잡제어는 나와 상관없는 다른 트래픽의 상태까지 간접적으로 고려해야 하고, 상대방이 명시적으로 알려주는 rwnd와 달리 네트워크가 알려주지 않는 값을 스스로 추정해야 합니다.
혼잡 윈도우(cwnd) — 또 하나의 제한
흐름제어에 rwnd가 있었다면, 혼잡제어에는 cwnd(congestion window)가 있습니다. 송신자는 이제 두 가지 제한을 동시에 지켜야 합니다.
실제로 보낼 수 있는 양 = min(cwnd, rwnd)
rwnd는 상대방이 ACK에 실어서 알려주는 값이지만, cwnd는 네트워크가 알려주는 값이 없으므로 송신자가 스스로 추정해서 조절합니다. “네트워크가 감당할 수 있는 만큼”이라는 게 정확히 얼마인지는 아무도 알려주지 않으니, TCP는 조금씩 늘려보다가 문제(유실)가 생기면 줄이는 식으로 값을 찾아갑니다.
AIMD — 느리게 늘리고 빠르게 줄인다
이 “조금씩 늘려보다가, 문제가 생기면 줄인다”는 전략을 AIMD(Additive Increase, Multiplicative Decrease)라고 부릅니다. cwnd가 시간에 따라 어떻게 움직이는지 그래프로 보면 이렇습니다.
- Slow start: 연결 초반에는 네트워크 상태를 전혀 모르므로, 작게 시작해서 ACK를 받을 때마다 cwnd를 빠르게(지수적으로) 늘립니다. 이름은 “느리게 시작”이지만 증가 속도 자체는 가장 빠른 구간입니다 — 이전에 아무것도 안 보내던 것에 비하면 “조심스럽게 시작한다”는 뜻입니다.
- ssthresh(slow start threshold): 이쯤부터는 슬슬 조심하자는 기준선입니다. cwnd가 여기 도달하면 증가 방식을 바꿉니다.
- Congestion avoidance: ssthresh를 넘으면 증가 속도를 늦춰서, RTT마다 조금씩(선형으로)만 늘립니다. 한계에 가까워졌으니 천천히 탐색하는 겁니다.
- 유실 발생: 어딘가에서 패킷이 유실되면 “한계를 넘었다”는 신호로 해석하고, cwnd를 큰 폭으로(전형적으로 절반) 줄입니다. 그리고 다시 늘리기 시작합니다.
이 전체 사이클이 계속 반복되면서 톱니 모양 그래프가 그려집니다. “느리게 늘리고 빠르게 줄인다(AIMD)”는 이름 그대로, 늘릴 때는 조심스럽게 선형으로, 줄일 때는 과감하게 한 방에 줄입니다.
그런데 왜 알고리즘이 여러 개인가
방금 설명한 건 가장 고전적인 방식(Reno 계열)의 큰 그림입니다. 실제로는 Reno, Cubic, BBR처럼 여러 혼잡제어 알고리즘이 있고, 리눅스는 기본값으로 Cubic을 씁니다. “유실이 발생하면 줄인다”는 방식 자체에 대한 다른 접근(예: 유실이 아니라 지연 시간 변화로 혼잡을 미리 감지하는 방식)도 있습니다. 각 알고리즘이 정확히 뭐가 다른지는 심화편에서 실제 커널 코드를 보면서 다룹니다.
다음 글
지금까지 기본 단계에서 연결을 맺고(3편), 데이터를 흐름제어하며 보내고(4편), 유실을 복구하고(5편), 혼잡을 피하는(6편) 법을 봤습니다. 다음 글은 기본 단계의 마지막입니다 — 연결을 어떻게 끝내는지, 4-way handshake와 TIME_WAIT을 다룹니다.