icon

안동민 개발노트

6장 : TCP

흐름 제어와 혼잡 제어


TCP가 신뢰성을 보장한다는 것은, 단순히 패킷이 빠지면 다시 보낸다는 것만을 의미하지 않습니다. 송신 측이 수신 측의 처리 능력을 초과하여 데이터를 보내면 안 되고, 네트워크 전체가 혼잡해지는 것도 방지해야 합니다.

이 두 가지 제어가 흐름 제어(Flow Control)혼잡 제어(Congestion Control)입니다. 이름이 비슷해 혼동하기 쉽지만, 목적과 동작 방식이 완전히 다릅니다.

흐름 제어 vs 혼잡 제어
┌─────────────────────────────────────────────────────────┐
│                                                         │
│  흐름 제어 (Flow Control)                               │
│  ─────────────────────                                  │
│  누구를 보호?  → 수신 측 (Receiver)                     │
│  무엇을 방지?  → 수신 버퍼 오버플로우                   │
│  어떻게?       → rwnd (Receive Window) 크기 조절        │
│  피드백 방향   → 수신자 → 송신자                        │
│                                                         │
│  혼잡 제어 (Congestion Control)                         │
│  ─────────────────────────                              │
│  누구를 보호?  → 네트워크 (Network)                     │
│  무엇을 방지?  → 라우터 큐 오버플로우, 패킷 손실        │
│  어떻게?       → cwnd (Congestion Window) 크기 조절     │
│  피드백 방향   → 네트워크 상태 추론 → 자가 조절         │
│                                                         │
│  실제 전송량 = min(rwnd, cwnd)                          │
│                                                         │
└─────────────────────────────────────────────────────────┘

흐름 제어: 수신 측 보호

흐름 제어는 송신 측이 수신 측의 처리 속도에 맞추어 데이터를 보내도록 조절하는 메커니즘입니다. 수신 측의 버퍼가 넘치지 않도록 보호하는 것이 목적입니다.

수신 버퍼 오버플로우 문제
흐름 제어가 없다면

송신 측 (고성능 서버)           수신 측 (저성능 IoT 장치)
  100MB/s 전송 ──────→          버퍼: [████████████] 64KB
                                      [████████████] FULL!
                                      [새 데이터 도착] → 폐기!
                                      → 재전송 요청
                                      → 또 폐기!
                                      → 무한 재전송 루프

흐름 제어가 있으면
  rwnd=32KB ←────────────────── "나 32KB만 받을 수 있어"
  32KB 전송 ──────────────→     버퍼: [████        ] 처리 중
                        
  rwnd=48KB ←────────────────── "처리했으니 48KB까지 가능"
  48KB 전송 ──────────────→     버퍼: [████████    ]

수신 측에는 도착한 데이터를 일시적으로 저장하는 수신 버퍼(Receive Buffer)가 있습니다. 애플리케이션이 이 버퍼에서 데이터를 읽어가는 속도보다 데이터가 도착하는 속도가 빠르면, 버퍼가 가득 차서 새로 도착하는 데이터를 폐기해야 합니다. 이것을 방지하기 위해 흐름 제어가 필요합니다.


슬라이딩 윈도우와 rwnd

TCP의 흐름 제어는 슬라이딩 윈도우(Sliding Window) 방식으로 동작합니다.

슬라이딩 윈도우 동작
바이트 스트림:  1  2  3  4  5  6  7  8  9  10  11  12  13 ...
              ├──────────┼───────────────┼──────────────────
              │ ACK 완료 │  전송 가능    │  전송 불가
              │(확인됨)  │  (윈도우 내)  │  (rwnd 초과)
                         ├───────────────┤
                         │   rwnd = 5    │
                         └───────────────┘

Step 1: rwnd=5, 바이트 4~8 전송 가능
  [1 2 3 | 4 5 6 7 8 | 9 10 11 ...]
        ↑ 전송 가능 ↑

Step 2: 4, 5에 대한 ACK 수신 → 윈도우 슬라이드
  [1 2 3 4 5 | 6 7 8 9 10 | 11 12 ...]
             ↑ 전송 가능  ↑

Step 3: 수신 측 버퍼 부족으로 rwnd=3
  [1 2 3 4 5 | 6 7 8 | 9 10 11 ...]
              ↑  3  ↑
              윈도우 축소!

수신 측은 자신의 수신 버퍼에서 현재 여유가 있는 크기를 rwnd(Receive Window)라는 값으로 송신 측에 알려줍니다. 이 값은 TCP 헤더에 포함되어 모든 ACK 패킷과 함께 전달됩니다.

송신 측은 ACK를 받지 않은 상태로 보낼 수 있는 데이터의 양을 rwnd 이하로 제한합니다. rwnd가 10,000바이트이면, 최대 10,000바이트의 미확인 데이터만 네트워크에 존재할 수 있습니다.

수신 측이 데이터를 처리하여 버퍼 여유가 생기면 rwnd 값이 증가하고, 송신 측은 더 많은 데이터를 보낼 수 있습니다. 반대로 수신 측이 바빠서 처리가 밀리면 rwnd가 줄어들고, 결국 0이 되면 송신 측은 데이터 전송을 멈춥니다.

rwnd가 0이 된 후에도 송신 측은 주기적으로 윈도우 프로브(Window Probe)라는 1바이트짜리 패킷을 보내서, 수신 측의 윈도우가 다시 열렸는지 확인합니다. 이것을 Zero Window Probe라고 합니다.


혼잡 제어: 네트워크 보호

혼잡 제어는 네트워크 전체의 혼잡 상태를 감지하고, 송신량을 적절히 조절하는 메커니즘입니다. 흐름 제어가 수신 측을 보호한다면, 혼잡 제어는 네트워크를 보호합니다.

네트워크 혼잡의 악순환
                     정상 상태

              트래픽 증가 ▼

            ┌───────────┴────────────┐
            │  라우터 큐 포화        │
            │  → 패킷 드롭           │
            └───────────┬────────────┘

            ┌───────────┴────────────┐
            │  송신 측: 패킷 손실    │
            │  → 타임아웃 후 재전송  │
            └───────────┬────────────┘

            ┌───────────┴─────────────┐
            │  재전송 = 추가 트래픽   │
            │  → 혼잡 더 심화!        │ ← 악순환!
            └───────────┬─────────────┘

              혼잡 제어 없으면 → 네트워크 붕괴 (Congestion Collapse)

네트워크가 혼잡해지면 라우터의 큐가 가득 차서 패킷이 손실되고, 재전송이 증가하면서 혼잡이 더 심해지는 악순환이 발생합니다. TCP는 이 악순환을 방지하기 위해 혼잡 윈도우(cwnd: Congestion Window)를 관리합니다.

실제 송신할 수 있는 데이터량은 min(rwnd, cwnd)로 결정됩니다. 흐름 제어와 혼잡 제어 중 더 제한적인 쪽을 따르는 것입니다.


Slow Start

TCP 연결이 처음 수립되면, 네트워크의 상태를 알 수 없으므로 조심스럽게 시작합니다. 이것이 Slow Start입니다.

Slow Start → Congestion Avoidance 전환
cwnd
(MSS)

16├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ssthresh 도달
  │                              ╱ (선형 증가로 전환)
  │                            ╱
  │                          ╱  Congestion Avoidance
  │                        ╱    (RTT당 +1 MSS)
  │                      ╱
  │                    ╱
  │                  ╱── ssthresh = 16
  │                ╱
8 ├─ ─ ─ ─ ─ ─ ─ ╱─ ─ ─ ─ ─ ─ ─ ─
  │            ╱ 
  │          ╱   Slow Start
4 ├─ ─ ─ ─ ╱     (RTT당 ×2, 지수 증가)
  │      ╱
2 ├─ ─ ╱
  │  ╱
1 ├
  └───┬───┬───┬───┬───┬───┬───┬───┬── RTT
      1   2   3   4   5   6   7   8

RTT 1: cwnd=1 → 1개 전송                     (합계: 1)
RTT 2: cwnd=2 → 2개 전송 (ACK 2개 → +2)      (합계: 3)
RTT 3: cwnd=4 → 4개 전송 (ACK 4개 → +4)      (합계: 7)
RTT 4: cwnd=8 → 8개 전송 (ACK 8개 → +8)      (합계: 15)
RTT 5: cwnd=16 = ssthresh → 선형 전환
RTT 6: cwnd=17 (RTT당 +1만 증가)
RTT 7: cwnd=18

cwnd를 1 MSS(Maximum Segment Size, 보통 1460바이트)로 시작합니다. ACK를 하나 받을 때마다 cwnd를 1 MSS씩 증가시킵니다. cwnd가 1일 때 1개의 세그먼트를 보내고, ACK를 받으면 cwnd가 2가 됩니다. 2개를 보내고 각각 ACK를 받으면 cwnd가 4가 됩니다.

결과적으로 cwnd는 매 RTT(Round Trip Time)마다 두 배로 증가합니다. Slow Start라는 이름이지만 지수적으로 증가하므로 실제로는 매우 빠르게 올라갑니다.

cwnd가 ssthresh(Slow Start Threshold)에 도달하면 Slow Start를 멈추고 Congestion Avoidance 단계로 전환합니다.


Congestion Avoidance

ssthresh를 넘어서면 cwnd의 증가 속도가 선형으로 바뀝니다. 매 RTT마다 cwnd를 1 MSS씩만 증가시킵니다. 네트워크의 한계 용량에 가까워졌을 수 있으므로, 조심스럽게 전송량을 늘리는 것입니다.

이 상태에서 패킷 손실이 감지되면(타임아웃 또는 중복 ACK 3개), TCP는 혼잡이 발생했다고 판단합니다.

패킷 손실 감지 후 동작
cwnd
  │              패킷 손실!
  │              (타임아웃)
  │                ╲
  │                 ╲  ssthresh = cwnd/2
  │                  ╲ cwnd = 1
  │                   ╲
  │                    ╲→ Slow Start 재시작
1 ├─────────────────────╲──────────
  │                      Slow Start부터 다시
  └───────────────────────────────── RTT

  vs.

cwnd
  │              패킷 손실!
  │              (3 Dup ACK)
  │                ╲
  │                 ╲  ssthresh = cwnd/2
  │                  ╲  cwnd = cwnd/2 ← 1이 아님!
  │                   ╲→ Fast Recovery
  │                    선형 증가 계속
  └───────────────────────────────── RTT

타임아웃으로 손실을 감지한 경우, ssthresh를 현재 cwnd의 절반으로 설정하고, cwnd를 다시 1 MSS로 떨어뜨려 Slow Start부터 다시 시작합니다.


Fast Retransmit과 Fast Recovery

중복 ACK 3개로 손실을 감지한 경우에는, 타임아웃보다 덜 심각한 혼잡으로 판단합니다. 상대방이 ACK를 보내고 있다는 것은, 그 밖의 패킷은 잘 도착하고 있다는 의미이기 때문입니다.

Fast Retransmit 동작
송신측                              수신측
  │ seq=100 ──────────────→ 수신 OK  │
  │ seq=200 ──── X (손실)            │
  │ seq=300 ────────────→ 순서 어긋남│
  │          ←── ACK 200 (Dup 1) ────│ "200번을 기다리고 있어"
  │ seq=400 ───────────────→         │
  │          ←── ACK 200 (Dup 2) ────│ "아직도 200번!"
  │ seq=500 ───────────────→         │
  │          ←── ACK 200 (Dup 3) ────│ "200번 3번째!"
  │                                  │
  │ ★ 3 Dup ACK → seq=200 즉시 재전송│
  │ seq=200 ──────────────→ 수신 OK  │
  │          ←── ACK 600 ────────────│ "200~500 모두 받았어"
  │                                  │
  타임아웃을 기다리지 않고 즉시 재전송 = Fast Retransmit

이 경우 TCP는 Fast Retransmit로 손실된 세그먼트를 즉시 재전송하고, Fast Recovery로 cwnd를 절반으로 줄인 후 선형 증가를 계속합니다. cwnd를 1까지 떨어뜨리지 않으므로, 완전한 Slow Start보다 빠르게 회복합니다.


현대 혼잡 제어 알고리즘

전통적인 Reno/NewReno 이후 다양한 혼잡 제어 알고리즘이 개발되었습니다.

알고리즘방식특징사용처
RenoLoss-based기본 Slow Start + CA + Fast Recovery전통적 구현
CUBICLoss-basedcwnd를 3차 함수로 증가, Linux 기본Linux 서버 기본값
BBRModel-based대역폭×RTT 추정, 손실에 덜 민감Google, YouTube
VegasDelay-basedRTT 변화로 혼잡 추정실험적
DCTCPECN-based데이터센터 환경 최적화데이터센터 내부
CUBIC vs BBR 비교
CUBIC (Loss-based)
  - 패킷 손실 = 혼잡 신호
  - 손실 후 cwnd를 크게 줄이고 3차 함수로 복구
  - 문제: 랜덤 손실(무선 등)도 혼잡으로 오인
          → 불필요한 cwnd 감소

BBR (Bottleneck Bandwidth and RTT)
  - 패킷 손실이 아닌 대역폭과 RTT를 직접 측정
  - 측정된 BtlBw × RTprop = 최적 전송량 계산
  - 장점: 무선 네트워크에서도 높은 처리량 유지
  - 채택: Google(2016~), YouTube, Google Cloud
congestion_simulation.py
def simulate_slow_start(ssthresh, max_rounds=15):
    """TCP Slow Start → Congestion Avoidance 시뮬레이션"""
    cwnd = 1
    phase = "Slow Start"
    total_sent = 0

    print(f"{'RTT':>4} {'Phase':>22} {'cwnd':>6} {'전송량':>8} {'누적':>8}")
    print("-" * 55)

    for rtt in range(1, max_rounds + 1):
        total_sent += cwnd

        print(f"{rtt:>4} {phase:>22} {cwnd:>6} {cwnd:>8} {total_sent:>8}")

        if phase == "Slow Start":
            cwnd *= 2  # 지수 증가
            if cwnd >= ssthresh:
                cwnd = ssthresh
                phase = "Congestion Avoidance"
        else:
            cwnd += 1  # 선형 증가

    return total_sent

print("=== ssthresh=16 시뮬레이션 ===")
simulate_slow_start(ssthresh=16)

# 출력:
# RTT                  Phase   cwnd     전송량     누적
# -------------------------------------------------------
#    1             Slow Start      1        1        1
#    2             Slow Start      2        2        3
#    3             Slow Start      4        4        7
#    4             Slow Start      8        8       15
#    5             Slow Start     16       16       31
#    6  Congestion Avoidance     17       17       48
#    7  Congestion Avoidance     18       18       66

이 Slow Start → Congestion Avoidance → 손실 감지 → Fast Recovery의 순환이 TCP 혼잡 제어의 기본 패턴입니다.


실무에서의 혼잡 제어 확인

congestion_check.sh
#!/bin/bash
# === TCP 혼잡 제어 관련 확인 ===

echo "=== 현재 사용 중인 혼잡 제어 알고리즘 ==="
cat /proc/sys/net/ipv4/tcp_congestion_control
# 출력: cubic (Linux 기본)

echo ""
echo "=== 사용 가능한 알고리즘 ==="
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# 출력: reno cubic bbr

echo ""
echo "=== BBR로 변경 ==="
# sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

echo ""
echo "=== TCP 연결별 윈도우 크기 확인 ==="
ss -ti | head -20
# 출력 예시:
# cwnd:10 ssthresh:65535 rtt:3.5/0.5
# → cwnd=10 세그먼트, ssthresh=65535, RTT=3.5ms (편차 0.5ms)

echo ""
echo "=== 전송 버퍼/수신 버퍼 크기 확인 ==="
echo "수신 버퍼: $(cat /proc/sys/net/ipv4/tcp_rmem)"
echo "송신 버퍼: $(cat /proc/sys/net/ipv4/tcp_wmem)"
# 형식: min default max (바이트)
# 수신: 4096 131072 6291456
# 송신: 4096 16384  4194304
파라미터설명권장값 (고성능)
tcp_congestion_control혼잡 제어 알고리즘bbr (장거리, 손실 환경)
tcp_rmem수신 버퍼 (min default max)4096 131072 16777216
tcp_wmem송신 버퍼 (min default max)4096 65536 16777216
tcp_window_scaling64KB 이상 윈도우 허용1 (활성화)
tcp_timestampsRTT 정밀 측정1 (활성화)

다음 절에서는 실무에서 자주 만나는 TCP 심화 주제와 실무 이슈를 살펴보겠습니다.

목차