테마
TCP의 신뢰성과 혼잡 제어 2
TCP의 신뢰성과 혼잡 제어에서 본 흐름 제어(flow control)가 받는 쪽을 지킨다면, 여기서는 중간 네트워크를 지키는 혼잡 제어(congestion control)를 이어서 다룬다. TCP(Transmission Control Protocol, 전송 제어 프로토콜)의 보내는 쪽은 손실 신호로 네트워크 상태를 짐작해, 확인 없이 내보내는 양을 스스로 늘리고 줄인다. 끝으로 작은 데이터를 모아 보내는 Nagle 알고리즘(Nagle's algorithm)이 받는 쪽의 확인 응답 미루기와 맞물릴 때의 지연을 본다.
혼잡 제어
흐름 제어는 받는 쪽이 헤더(header)에 적어 알려 주는 남은 버퍼 공간만 본다. 하지만 받는 쪽이 넉넉해도 중간 라우터(router)의 대기 공간이 넘치면 패킷(packet)이 버려진다. 모든 송신자가 손실이 날 때마다 더 세게 다시 보내면, 네트워크가 재전송(retransmission)으로 가득 차 쓸모 있는 데이터를 거의 전달하지 못하는 상태가 될 수 있다.
보내는 쪽은 혼잡 윈도우(congestion window, cwnd)라는 자기만의 한도를 둔다. 실제로 확인 없이 내보낼 수 있는 양은 수신 윈도우(receive window, rwnd)와 혼잡 윈도우 중 작은 값이다. 윈도우 크기는 TCP 세그먼트(segment, TCP가 데이터를 나눠 헤더를 붙인 한 덩어리) 하나에 실을 수 있는 최대 데이터 크기인 MSS(Maximum Segment Size, 최대 세그먼트 크기) 단위로 센다. RFC(Request for Comments, 인터넷 표준 문서) 5681의 기본 규칙은 다음과 같다.
- 슬로 스타트(slow start): 혼잡 윈도우를 작게 시작해, 새 데이터를 확인하는 ACK(Acknowledgement, 확인 응답)를 받을 때마다 최대 1 MSS씩 늘린다. 보낸 만큼 확인이 오면 윈도우가 두 배가 되므로 RTT(Round-Trip Time, 왕복 시간)마다 대략 두 배로 커진다. 이름과 달리 늘어나는 속도는 빠르고, "느린" 것은 시작 크기다.
- 혼잡 회피(congestion avoidance): 혼잡 윈도우가 임계값(slow start threshold, ssthresh)을 넘으면 한계가 가깝다고 보고 RTT마다 1 MSS 정도만 늘린다.
- 타임아웃(timeout): 보낸 데이터의 ACK가 RTO(Retransmission Timeout, 재전송 타임아웃)가 지나도록 오지 않은 경우다. ACK가 하나도 돌아오지 않았다는 것은 뒤에 보낸 세그먼트들도 도착하지 못했다는 뜻이므로 심한 혼잡으로 본다. 임계값을 확인받지 못한 채 나가 있던 양의 절반으로 낮추고, 혼잡 윈도우를 1 MSS로 줄여 슬로 스타트부터 다시 한다.
- 중복 ACK(duplicate ACK) 세 개: 빠른 재전송(fast retransmit) 뒤 빠른 회복(fast recovery)에 들어간다. 중간 세그먼트가 빠지면 받는 쪽은 뒤 세그먼트가 올 때마다 빠진 번호를 가리키는 같은 ACK를 되풀이하므로, 중복 ACK는 뒤 세그먼트들이 도착하고 있다는 뜻이라 덜 심한 혼잡으로 본다. 임계값은 똑같이 절반으로 낮추지만, 혼잡 윈도우를 1 MSS까지 떨어뜨리지 않고 새 임계값에서 혼잡 회피를 이어 간다.
아래는 임계값 16에서 이 규칙대로 혼잡 윈도우가 움직이는 예다. 중간에 중복 ACK 세 개(절반 근처로)와 타임아웃(1로)이 한 번씩 난다.
다이어그램 로딩…
평소에는 조금씩 더하고 손실이 나면 반으로 줄이는 이 방식을 AIMD(Additive Increase Multiplicative Decrease, 가산 증가 곱셈 감소)라고 한다. 여러 연결이 한 링크를 나눠 쓸 때 AIMD를 따르면 각자의 몫이 비슷해지는 쪽으로 움직인다. 실제 운영체제는 이 기본 틀을 개선한 CUBIC·BBR 같은 혼잡 제어 알고리즘을 쓰기도 한다. CUBIC은 손실 뒤 윈도우를 3차 함수 곡선으로 다시 키우고, BBR은 손실 대신 측정한 대역폭(bandwidth)과 RTT로 보낼 양을 정한다.
흐름 제어와 혼잡 제어
| 기준 | 흐름 제어 | 혼잡 제어 |
|---|---|---|
| 보호 대상 | 받는 쪽 버퍼 | 중간 네트워크 |
| 쓰는 값 | 수신 윈도우 rwnd | 혼잡 윈도우 cwnd |
| 정하는 쪽 | 받는 쪽이 헤더로 알려 줌 | 보내는 쪽이 스스로 추정 |
| 판단 근거 | 남은 버퍼 공간 | 손실 신호(타임아웃·중복 ACK) |
둘은 따로 동작하지만, 보내는 쪽은 매 순간 두 윈도우 중 작은 값까지만 확인 없이 보낸다.
Nagle 알고리즘과 지연 ACK
몇 바이트짜리 데이터를 자주 보내면, 데이터마다 IP(Internet Protocol, 인터넷 프로토콜) 헤더와 TCP 헤더가 최소 40바이트씩 붙어 낭비가 크다. Nagle 알고리즘은 확인받지 못한 데이터가 남아 있는 동안 작은 데이터를 바로 보내지 않고 모아 두었다가 한꺼번에 보낸다. 받는 쪽의 지연 ACK(delayed ACK)는 ACK를 바로 보내지 않고 잠깐 기다려, 여러 세그먼트를 ACK 하나로 확인하거나 응답 데이터에 함께 싣는다. RFC 5681은 이 지연을 500밀리초 안으로 제한하고, 꽉 찬 세그먼트 두 개마다 적어도 한 번은 ACK를 보내도록 한다.
둘이 함께 켜져 있으면 서로를 기다리게 된다. 보내는 쪽은 앞 데이터의 ACK가 올 때까지 작은 데이터를 붙잡고, 받는 쪽은 함께 실어 보낼 응답이 없으니 ACK를 지연 타이머가 끝날 때까지 미룬다. 그래서 요청마다 지연 ACK 타이머만큼 응답이 늦어질 수 있어, 응답이 급한 프로그램은 소켓(socket) 옵션 TCP_NODELAY로 Nagle 알고리즘을 끈다.
면접 포인트
- "흐름 제어와 혼잡 제어의 차이는?" → 받는 쪽 보호(rwnd, 받는 쪽이 알려 줌) vs 네트워크 보호(cwnd, 보내는 쪽이 추정). 실제 한도는 min(rwnd, cwnd).
- "슬로 스타트는 왜 '슬로'인가요?" → 작은 윈도우에서 시작해서. 증가는 RTT마다 약 두 배로 빠르다. 임계값을 넘으면 혼잡 회피(선형 증가).
- "타임아웃과 중복 ACK 세 개에 대한 반응 차이는?" → 둘 다 임계값을 절반으로. 타임아웃은 cwnd를 1 MSS로 줄이고 슬로 스타트, 중복 ACK는 빠른 회복으로 절반 근처에서 이어 간다.
- "
TCP_NODELAY는 언제 켜나요?" → 작은 요청에 바로 응답해야 하는 프로그램. Nagle 알고리즘과 지연 ACK가 맞물려 생기는 지연을 없앤다.
