포스트

TCP는 얼마나 빨리 보낼까 — 흐름 제어 vs 혼잡 제어, 그리고 톱니파의 비밀

흐름 제어(rwnd)와 혼잡 제어(cwnd)의 차이, 슬로스타트·혼잡 회피의 AIMD 톱니파, 손실 감지(타임아웃·3 dup ACK), Reno·CUBIC·BBR, 버퍼블로트와 BDP를 그림으로 설명합니다.

TCP는 얼마나 빨리 보낼까 — 흐름 제어 vs 혼잡 제어, 그리고 톱니파의 비밀

두 개의 “멈춰”는 서로 다른 사람이 외친다

TCP가 데이터를 보낼 때, “지금 얼마나 보내도 되나”를 정하는 두 개의 브레이크가 있습니다. 자주 한 덩어리로 뭉뚱그리지만 완전히 다른 문제입니다.

  • 흐름 제어(flow control): 수신자가 감당 못 할까 봐 — “내 버퍼 넘치니 천천히.” 수신 버퍼가 결정.
  • 혼잡 제어(congestion control): 네트워크가 막힐까 봐 — “중간 라우터 큐가 터지니 천천히.” 패킷 손실/지연이 결정.

실제 송신량은 둘 중 작은 쪽입니다: 전송 가능 = min(rwnd, cwnd). 이 글은 이 두 윈도우가 어떻게 움직이는지, 왜 혼잡 제어 그래프가 톱니파를 그리는지, 그리고 BBR이 왜 그 톱니를 버렸는지까지 봅니다. 전제는 TCP 핸드셰이크/신뢰성입니다.

흐름 제어: 슬라이딩 윈도우와 rwnd

수신자는 매 ACK의 윈도우 크기 필드(rwnd) 로 “내 수신 버퍼에 이만큼 여유 있다”를 알립니다. 송신자는 ACK 안 된 데이터를 그 한도 내로만 유지합니다. 윈도우가 ACK를 따라 오른쪽으로 미끄러지는 모습:

바이트 스트림 · 윈도우가 ACK 따라 전진 전송+ACK 전송 가능 윈도우

rwnd = 0이면 송신자는 멈추고, 주기적으로 작은 윈도우 프로브를 보내 “이제 여유 생겼나” 확인합니다. 수신 앱이 데이터를 안 읽으면 윈도우가 0으로 닫혀 송신이 막히는 게 정상 동작입니다(누수 아님).

혼잡 제어: 네트워크는 자기 상태를 안 알려준다

흐름 제어와 달리, 라우터는 “나 막혔어”를 친절히 알려주지 않습니다(best-effort). TCP는 간접 신호로 혼잡을 추측합니다 — 주로 패킷 손실, 또는 지연 증가(BBR). 핵심 변수가 송신자만 아는 혼잡 윈도우(cwnd) 입니다.

슬로 스타트 → 혼잡 회피 → 손실 → 반복

고전 알고리즘(Reno 계열)은 이렇게 움직입니다.

  1. 슬로 스타트: cwnd를 1 MSS에서 시작, ACK마다 지수적(매 RTT 2배) 증가. “이름과 달리 빠르게” 가용 대역을 탐색.
  2. cwnd ≥ ssthresh가 되면 혼잡 회피: 매 RTT 선형(+1 MSS) 증가. 조심스럽게.
  3. 손실 감지:
    • 3 dup ACK(같은 ack 3번) → 가벼운 혼잡 → cwnd 절반, fast retransmit/recovery(슬로 스타트로 안 돌아감).
    • 타임아웃(RTO) → 심각 → cwnd를 1로, ssthresh를 절반으로, 슬로 스타트부터 다시.

이 “선형 증가 + 곱셈 감소”가 AIMD(Additive Increase, Multiplicative Decrease)이고, 그래서 cwnd는 톱니파를 그립니다. 직접 보세요 — 초록 곡선이 올라가다 손실에서 반토막 납니다.

cwnd 시간(RTT) 슬로스타트(지수) 혼잡회피(선형)

왜 곱셈 감소인가? 혼잡은 빠르게 악화됩니다(모두가 큐를 채우면 붕괴). 그래서 줄일 땐 과감하게 절반으로 물러나 빠르게 안정시키고, 늘릴 땐 +1씩 조심합니다. 여러 흐름이 같은 병목을 공유할 때 AIMD가 공정성(각자 비슷한 몫)으로 수렴한다는 게 수학적으로 증명돼 있습니다.

Reno → CUBIC → BBR: 무엇이 바뀌었나

알고리즘혼잡 신호특징어디서
Reno/NewReno손실고전 AIMD 톱니교과서
CUBIC손실시간의 3차 함수로 증가 → 고대역·고지연(BDP 큰) 링크에서 빠른 회복리눅스 기본
BBR대역폭·RTT 모델손실을 기다리지 않고 병목 대역폭·최소 RTT를 추정해 그 지점에서 운전구글, YouTube·여러 CDN

BBR이 중요한 이유: 손실 기반 알고리즘은 큐가 가득 찰 때까지 보내다 손실을 봐야 물러납니다. 그래서 라우터 버퍼가 과도하게 크면(요즘 흔함) 큐에 패킷이 잔뜩 쌓여 지연이 치솟습니다 — 이게 버퍼블로트(bufferbloat) 입니다. BBR은 손실이 아니라 “대역폭은 안 느는데 RTT만 늘기 시작하는 지점”을 병목으로 보고 거기서 멈춰, 큐를 거의 안 채우고도 대역을 다 씁니다.

BDP: 윈도우가 얼마나 커야 하나

링크를 가득 채우려면 “날아가는 중인 데이터”가 파이프 부피만큼 있어야 합니다.

1
2
BDP(대역폭-지연 곱) = 대역폭 × RTT
예) 1 Gbps × 80 ms = 10⁹ × 0.08 / 8 ≈ 10 MB

윈도우(min(rwnd, cwnd))가 BDP보다 작으면, 데이터를 다 보내고 ACK를 기다리느라 링크가 놀아 처리량이 떨어집니다. 그래서 고대역·고지연(LFN, “롱 팻 네트워크”)에선 윈도우 스케일링 옵션(16비트 윈도우 한계 64 KB를 1 GB까지 확장)이 필수입니다. 이 관계는 성능 글에서 더 깊게 다룹니다.

디버깅: cwnd와 RTT를 직접 본다

1
2
3
4
5
6
7
8
9
10
11
# 연결별 혼잡 상태 (cwnd, rtt, retrans, 알고리즘)
ss -ti dst 10.0.0.5
#  cubic wscale:7,7 rtt:42.3/5.1 mss:1448 cwnd:120 ssthresh:80
#  bytes_retrans:... retrans:0/3 ...

# 현재 기본 알고리즘과 사용 가능 목록
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control

# BBR 켜기 (커널 4.9+)
sysctl -w net.ipv4.tcp_congestion_control=bbr

ss -ti의 cwnd가 작고 retrans가 계속 오르면 경로 어딘가에서 손실이 나고 있다는 뜻입니다. rtt가 평소의 몇 배로 부풀면 버퍼블로트를 의심합니다.

프로덕션 함정

  • 큰 버퍼 ≠ 빠름: 송수신 버퍼나 라우터 큐를 무작정 키우면 버퍼블로트로 지연이 악화됩니다. 손실 기반 알고리즘은 큐를 채워야 멈추기 때문.
  • rwnd만 보고 cwnd를 잊음: “수신 버퍼 키웠는데 안 빨라요” — 병목이 네트워크면 cwnd가 한계라 소용없습니다. ss -ti로 어느 쪽이 작은지 확인하세요.
  • 짧은 연결은 슬로 스타트에 갇힘: 작은 요청은 cwnd가 커지기도 전에 끝나, 대역폭이 아니라 RTT가 지배합니다 → keep-alive/연결 재사용, HTTP/2·3의 멀티플렉싱이 중요한 이유.

면접/리뷰 단골 질문

  • Q. 흐름 제어와 혼잡 제어의 차이는? → 흐름=수신자 보호(rwnd), 혼잡=네트워크 보호(cwnd). 실제 송신은 min(rwnd, cwnd).
  • Q. cwnd가 왜 톱니파인가? → AIMD: 선형 증가(탐색) + 손실 시 곱셈 감소(빠른 후퇴). 공정성·안정성에 유리.
  • Q. 3 dup ACK와 RTO 타임아웃의 처리가 왜 다른가? → dup ACK는 후속 패킷이 도착 중이란 신호(가벼운 혼잡)라 절반만 줄이고 fast recovery. 타임아웃은 흐름이 끊긴 심각 상황이라 cwnd를 1로.
  • Q. BBR이 CUBIC보다 나은 점은? → 손실을 기다리지 않고 대역폭·RTT를 모델링해, 버퍼블로트 환경에서 낮은 지연으로 높은 처리량을 낸다.

정리

  • 송신량 = min(rwnd, cwnd) — 흐름 제어(수신자)와 혼잡 제어(네트워크)는 다른 브레이크다.
  • 고전 혼잡 제어는 AIMD → cwnd 톱니파. 슬로 스타트(지수)→혼잡 회피(선형)→손실 시 반감.
  • CUBIC(리눅스 기본)은 BDP 큰 링크에 최적화, BBR은 손실 대신 대역폭·RTT를 모델링해 버퍼블로트를 피한다.
  • 처리량은 윈도우 ≥ BDP여야 보장된다 → 윈도우 스케일링.
  • 진단은 ss -ti로 cwnd·rtt·retrans를 직접 본다.

다음: TCP의 한계(연결당 핸드셰이크, HOL blocking)를 정면으로 다루는 UDP와 QUIC.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 네트워크 A-Z (20편)

  1. 네트워크란 무엇인가 — 회선 교환 vs 패킷 교환, 왜 인터넷은 패킷을 선택했나
  2. OSI 7계층과 TCP/IP — 왜 네트워크를 굳이 층으로 쪼갰나
  3. 이더넷·MAC·스위치 — 스위치는 어떻게 "어느 포트로 보낼지" 스스로 배우나
  4. IP 주소와 서브넷팅 — /24를 /26으로 쪼갠다는 게 비트 단위로 무슨 뜻인가
  5. 라우팅은 어떻게 길을 찾나 — 라우팅 테이블, Longest Prefix Match, 그리고 BGP
  6. ARP와 L2/L3 경계 — IP는 아는데 왜 MAC을 또 묻나
  7. TCP는 어떻게 신뢰를 만드나 — 3-way handshake, 상태 머신, 그리고 TIME_WAIT
  8. TCP는 얼마나 빨리 보낼까 — 흐름 제어 vs 혼잡 제어, 그리고 톱니파의 비밀 지금 보는 글
  9. UDP와 QUIC — TCP를 버려야 더 빨라지는 순간
  10. DNS는 어떻게 이름을 주소로 바꾸나 — 루트부터 권한 서버까지, 그리고 캐시의 마법
  11. HTTP의 진화 — 1.1의 줄 서기에서 HTTP/2 멀티플렉싱, HTTP/3까지
  12. TLS/HTTPS는 어떻게 도청 불가능한 채널을 만드나 — 핸드셰이크부터 인증서 체인까지
  13. NAT와 사설망 — 주소 하나로 수백 대가 인터넷을 쓰는 법, 그리고 P2P가 막히는 이유
  14. 로드 밸런싱 — L4와 L7은 무엇이 다르며, 왜 헬스체크가 서비스를 살리고 죽이나
  15. 프록시·리버스 프록시·CDN — 콘텐츠를 사용자 코앞으로 옮기는 기술
  16. 방화벽·보안그룹·네트워크 ACL — stateful과 stateless가 가르는 모든 것
  17. VPC & 클라우드 네트워킹 — 라우팅 테이블 한 줄이 퍼블릭과 프라이빗을 가른다
  18. 소켓과 커널 네트워크 스택 — accept 한 줄 뒤에서 벌어지는 일
  19. 네트워크 성능 — 대역폭은 왜 빠름이 아닌가 (RTT·BDP·처리량의 진실)
  20. 현대 네트워킹 — 사이드카, eBPF, 그리고 경계가 사라진 신뢰