컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 06

TCP 혼잡 제어

네트워크TCP혼잡 제어

TCP 혼잡 제어는 네트워크 과부하를 방지하기 위해 송신 속도를 동적으로 조절하는 메커니즘입니다. 혼잡 윈도우(cwnd)를 통해 한 번에 보낼 수 있는 데이터 양을 제어하며, Slow Start·Congestion Avoidance·Fast Retransmit·Fast Recovery 4단계로 동작합니다.

핵심 개념

Slow Start

연결 초기 또는 타임아웃 후 시작합니다. cwnd를 작은 값(여기서는 1 MSS)에서 시작해 새 데이터를 확인하는 ACK를 받을 때마다 최대 1 MSS씩 증가합니다(RTT마다 대략 2배). 실제 초기 윈도우는 구현과 표준에 따라 더 큽니다. ssthresh(Slow Start Threshold)에 도달하면 Congestion Avoidance로 전환합니다. 이름은 "Slow"지만 지수적으로 증가합니다.

핵심 개념

Congestion Avoidance

ssthresh 도달 후 선형 증가로 전환합니다. RTT마다 cwnd를 1 MSS씩만 증가합니다(각 ACK마다 1/cwnd 증가). 네트워크 용량에 조심스럽게 접근하며 안정적인 전송을 유지합니다. 패킷 손실이 감지될 때까지 계속됩니다.

핵심 개념

Fast Retransmit

3개의 중복 ACK(Duplicate ACK)를 수신하면 타임아웃을 기다리지 않고 즉시 손실된 패킷을 재전송합니다. 중복 ACK는 패킷 손실을 의미하지만, 이후 패킷들은 수신되고 있다는 신호이기도 합니다. 타임아웃보다 빠른 복구가 가능합니다.

핵심 개념

Fast Recovery (TCP Reno)

3-Dup ACK 감지 시: ssthresh = cwnd/2, cwnd = ssthresh + 3 MSS로 설정하고 Congestion Avoidance로 진입합니다(교재식 단순화이고 RFC 5681은 전송 중인 데이터 양의 절반을 기준으로 합니다). Slow Start로 돌아가지 않아 타임아웃보다 빠르게 회복합니다. TCP Tahoe는 Fast Recovery 없이 항상 Slow Start로 복귀했습니다.

핵심 개념

혼잡 윈도우 (cwnd)

송신자가 ACK 없이 보낼 수 있는 최대 데이터 양입니다. 실제 송신 윈도우 = min(cwnd, rwnd)로, 수신 측 버퍼(rwnd)와 혼잡 윈도우 중 작은 값을 사용합니다. cwnd는 네트워크 상태에 따라 동적으로 변하며 혼잡을 감지하면 줄어듭니다.

핵심 개념

TCP CUBIC / BBR

Linux는 보통 CUBIC을 기본으로 쓰며(RFC 9438), 기본값은 커널 설정에 따라 다르고 설정으로 BBR 같은 다른 알고리즘을 고를 수 있습니다. CUBIC은 3차 함수로 cwnd를 조절해 고속·장거리 네트워크에서 효율적입니다. BBR(Bottleneck Bandwidth and RTT)은 손실 대신 병목 대역폭과 왕복 시간을 측정해 전송 속도를 정합니다.

ssthresh (Slow Start Threshold) — 혼잡 감지 시 대략 절반으로 줄여 설정됩니다. Slow Start 단계에서 cwnd가 ssthresh에 도달하면 Congestion Avoidance로 전환하는 임계값입니다. 초기값은 RFC 5681이 임의로 크게 잡도록 하고, 수신 윈도우 크기를 쓰는 것을 예로 듭니다.
타임아웃 vs 3-Dup ACK — 타임아웃은 더 심각한 혼잡 신호입니다. 타임아웃 발생 시 cwnd=1로 리셋하고 Slow Start부터 재시작합니다. 3-Dup ACK는 덜 심각하며 Fast Recovery를 통해 더 빠르게 복구합니다.
AIMD (Additive Increase Multiplicative Decrease) — Congestion Avoidance의 핵심 원리입니다. 혼잡이 없으면 선형 증가(+1), 혼잡 감지 시 반으로 감소(×0.5)합니다. 이 비대칭적 조절 덕분에 여러 흐름이 대역폭을 나눠 쓰는 쪽으로 수렴하도록 설계되어 있습니다.
ECN (Explicit Congestion Notification) — 패킷 손실 없이 라우터가 혼잡을 미리 알려주는 메커니즘입니다. IP 헤더의 ECN 비트로 혼잡을 신호하면, 수신자가 이를 ACK에 포함해 송신자에게 전달합니다. 손실 전에 선제적으로 전송 속도를 줄일 수 있습니다.

혼잡 제어는 왜 생겼나

송신자가 네트워크 용량을 모른 채 보내기만 하면 라우터 큐가 넘치고, 손실된 패킷의 재전송이 다시 부하를 키워 모두의 처리량이 무너지는 혼잡 붕괴(congestion collapse)가 생길 수 있습니다. 1980년대 중반 실제 인터넷에서 이 현상이 관찰되었고, 1988년 Van Jacobson의 논문 'Congestion Avoidance and Control'이 슬로 스타트와 혼잡 회피 같은 해법을 제안했습니다. 지금의 표준 설명은 RFC 5681에 있습니다.

숫자로 확인하기

슬로 스타트의 성장 — cwnd가 1 MSS에서 RTT마다 2배가 되면 1, 2, 4, 8, 16, 32, 64로 늘어 64 MSS에 닿는 데 6 RTT가 걸립니다. RTT가 100ms이면 600ms입니다.
처리량의 근사식 — 한 RTT에 cwnd만큼 보낼 수 있으므로 처리량은 대략 cwnd / RTT입니다. cwnd가 10 MSS(MSS 1,460바이트이면 14,600바이트)이고 RTT가 100ms이면 약 146KB/s, 곧 약 1.17Mbps입니다. RFC 6928은 초기 윈도우를 최대 10세그먼트까지 올리는 것을 제안합니다.
AIMD 톱니 — cwnd가 20일 때 손실로 절반인 10이 되면, RTT마다 1 MSS씩 늘어 다시 20에 닿는 데 10 RTT가 걸립니다. 줄어드는 것은 한 번에, 회복은 천천히라서 그래프가 톱니 모양이 됩니다.
파이프를 채우는 데 필요한 양 — 1Gbps(125MB/s) 링크에서 RTT가 100ms이면 한 번에 떠 있어야 할 데이터가 125MB/s × 0.1초 = 12.5MB, 약 8,562개 세그먼트(1,460바이트 기준)입니다. 윈도우가 이보다 작으면 링크를 다 쓰지 못합니다.

자주 하는 오해

슬로 스타트는 느리게 증가한다 — 시작 값이 작을 뿐 RTT마다 2배가 되는 지수 증가입니다. 이름은 한꺼번에 큰 양을 쏟던 초기 방식과 비교해 붙은 것입니다.
혼잡 제어는 라우터가 한다 — 속도 조절은 송신 쪽 TCP가 합니다. 라우터는 큐가 차면 패킷을 버리거나 ECN으로 표시하고, 송신자가 그 신호로 혼잡을 추정합니다.
흐름 제어와 같은 것이다 — 흐름 제어는 수신자 버퍼를 보호하고(rwnd), 혼잡 제어는 네트워크 경로를 보호합니다(cwnd). 실제 송신량은 둘 중 작은 값입니다.
패킷 손실은 항상 혼잡이다 — 손실 기반 알고리즘은 손실을 혼잡 신호로 읽습니다. 무선 구간의 전송 오류처럼 혼잡과 무관한 손실도 있어서 이때는 속도를 불필요하게 줄일 수 있습니다.

면접에서 이렇게 물어요

면접 질문

슬로 스타트가 필요한 이유는 무엇인가요?

연결 시작 시점에는 경로의 가용 대역폭을 모릅니다. 작은 윈도우로 시작해 ACK가 올 때마다 늘려 가며 용량을 탐색하면, 처음부터 크게 보내 큐를 넘치게 하는 일을 피할 수 있습니다.

면접 질문

타임아웃과 3 duplicate ACK는 어떻게 다르게 처리하나요?

3 duplicate ACK는 뒤 패킷이 도착하고 있다는 뜻이라 덜 심각한 신호입니다. Reno 계열은 빠른 재전송 후 윈도우를 절반 근처로 줄이고 혼잡 회피로 이어 갑니다. 타임아웃은 더 심각해서 cwnd를 1 MSS로 줄이고 슬로 스타트부터 다시 시작합니다.

면접 질문

Reno, CUBIC, BBR은 무엇이 다른가요?

Reno는 손실 때 절반으로 줄이고 RTT마다 1 MSS씩 늘리는 고전적 방식입니다. CUBIC은 마지막 손실 시점의 윈도우를 기준으로 3차 함수 모양으로 늘려 고속·장거리 망에서 빨리 회복합니다. BBR은 손실 대신 병목 대역폭과 왕복 시간의 측정으로 전송 속도를 정합니다.

면접 질문

흐름 제어와 혼잡 제어의 차이는 무엇인가요?

흐름 제어는 수신자가 알려 주는 수신 윈도우로 수신 버퍼 넘침을 막고, 혼잡 제어는 송신자가 스스로 관리하는 혼잡 윈도우로 네트워크 과부하를 막습니다. 송신량은 두 윈도우 중 작은 값으로 제한됩니다.

실제 시스템에서는

Linux에서 알고리즘 확인 — tcp(7) 매뉴얼은 tcp_congestion_control 설정으로 기본 혼잡 제어 알고리즘을 고른다고 설명하고, 소켓마다 TCP_CONGESTION 옵션으로 바꿀 수도 있습니다. ss -ti로 연결별 cwnd 같은 내부 정보를 볼 수 있습니다.
표준과 구현 — CUBIC은 RFC 9438로, ECN은 RFC 3168로 문서화되어 있습니다. BBR은 Google이 설계해 공개했고 논문 형태로 설명되어 있습니다.
고성능 서버의 전송 설정 — 높은 대역폭·긴 RTT 환경에서는 윈도우 스케일 같은 TCP 확장(RFC 7323)과 충분한 소켓 버퍼가 함께 있어야 윈도우가 실제로 커질 수 있습니다.

함께 보면 좋은 토픽

TCP/IP — 흐름 제어와 재전송 등 혼잡 제어의 바탕이 되는 TCP 기본을 봅니다.
HTTP — TCP 위에서 응답 시간에 영향을 주는 요소를 봅니다.
CDN — RTT를 줄여 윈도우가 빨리 커지게 하는 방법을 봅니다.
WebSocket — 오래 유지하는 TCP 연결에서 혼잡 제어가 영향을 주는 사례를 봅니다.

참고 자료

혼잡 제어 표준 — RFC 5681 — TCP Congestion Control
Linux tcp(7) — tcp(7) — Linux manual page
논문 — V. Jacobson, "Congestion Avoidance and Control", ACM SIGCOMM 1988 / N. Cardwell 외, "BBR: Congestion-Based Congestion Control", ACM Queue 2016

Java — TCP 혼잡 제어 시뮬레이션 (Reno 알고리즘)

public class TcpRenoSimulator {

    enum Phase { SLOW_START, CONGESTION_AVOIDANCE, FAST_RECOVERY }

    static double cwnd     = 1;   // 혼잡 윈도우 (MSS 단위)
    static double ssthresh = 16;  // Slow Start 임계값
    static Phase  phase    = Phase.SLOW_START;

    // ACK 수신 시 처리
    static void onAck() {
        if (phase == Phase.SLOW_START) {
            cwnd += 1;                      // ACK마다 +1 → RTT마다 2배
            if (cwnd >= ssthresh)
                phase = Phase.CONGESTION_AVOIDANCE;

        } else if (phase == Phase.CONGESTION_AVOIDANCE) {
            cwnd += 1.0 / cwnd;              // ACK마다 1/cwnd → RTT마다 +1

        } else if (phase == Phase.FAST_RECOVERY) {
            cwnd += 1;                      // 추가 Dup ACK마다 +1
        }
    }

    // 3개 중복 ACK 감지 → Fast Retransmit + Fast Recovery
    static void onThreeDupAck() {
        ssthresh = Math.max(cwnd / 2, 2);  // ssthresh = cwnd / 2
        cwnd     = ssthresh + 3;            // cwnd = ssthresh + 3 (Dup ACK 3개 반영)
        phase    = Phase.FAST_RECOVERY;
        // 손실 패킷 즉시 재전송 (Fast Retransmit)
    }

    // Fast Recovery 완료 (새 ACK 수신)
    static void onNewAckInFastRecovery() {
        cwnd  = ssthresh;                  // cwnd = ssthresh로 복귀
        phase = Phase.CONGESTION_AVOIDANCE;
    }

    // 타임아웃 → Slow Start 재시작
    static void onTimeout() {
        ssthresh = Math.max(cwnd / 2, 2);  // ssthresh = cwnd / 2
        cwnd     = 1;                      // cwnd = 1 MSS로 리셋
        phase    = Phase.SLOW_START;        // Slow Start 재시작
    }

    public static void main(String[] args) {
        System.out.printf("%-5s %-8s %-10s %-20s%n",
                          "RTT", "cwnd", "ssthresh", "Phase");
        for (int rtt = 1; rtt <= 20; rtt++) {
            System.out.printf("%-5d %-8.1f %-10.1f %-20s%n",
                              rtt, cwnd, ssthresh, phase);

            // 시뮬레이션: RTT 10에서 3-Dup ACK, RTT 15에서 타임아웃
            if (rtt == 10) onThreeDupAck();
            else if (rtt == 11) onNewAckInFastRecovery(); // 새 ACK 수신 → cwnd=ssthresh로 deflate 후 CA 복귀
            else if (rtt == 15) onTimeout();
            else {
                int acksPerRtt = (int) cwnd;
                for (int a = 0; a < acksPerRtt; a++) onAck();
            }
        }
    }
}

Slow Start에서 ACK마다 cwnd+1이면 RTT마다 2배가 됩니다. RTT 안에 cwnd개의 ACK를 받으므로 cwnd → cwnd + cwnd = 2×cwnd가 되는 것입니다.
Congestion Avoidance에서 ACK마다 1/cwnd를 더하면 RTT 동안 합계가 (1/cwnd)×cwnd = 1이 되어 RTT마다 정확히 1 MSS 증가합니다.
3-Dup ACK 발생 시 cwnd = ssthresh + 3은 이미 수신된 3개 Dup ACK가 3개 패킷이 네트워크를 통과했음을 의미하기 때문입니다.
타임아웃이 3-Dup ACK보다 훨씬 심각한 이유는 cwnd=1로 완전히 리셋하기 때문입니다.
3-Dup ACK는 일부 패킷이 여전히 도달하고 있다는 신호여서 덜 공격적으로 반응합니다.

Python — 실제 소켓의 TCP 혼잡 제어 파라미터 확인

import socket
import struct
import subprocess

# ── Linux TCP 혼잡 제어 알고리즘 확인 ───────────────────
def get_tcp_congestion_info():
    # 현재 사용 중인 혼잡 제어 알고리즘
    with open('/proc/sys/net/ipv4/tcp_congestion_control') as f:
        algo = f.read().strip()
    print(f"혼잡 제어 알고리즘: {algo}")  # cubic (Linux 기본)

    # 사용 가능한 알고리즘 목록
    with open('/proc/sys/net/ipv4/tcp_available_congestion_control') as f:
        available = f.read().strip()
    print(f"사용 가능: {available}")

    # 초기 cwnd 크기 (기본값: 10 MSS)
    result = subprocess.run(
        ['ip', 'route', 'show'], capture_output=True, text=True
    )
    print(result.stdout)  # initcwnd 10 표시됨

# ── TCP 소켓 혼잡 제어 정보 (TCP_INFO) ─────────────────
def get_socket_tcp_info(sock):
    # TCP_INFO 구조체 (Linux): 혼잡 윈도우 포함
    TCP_INFO = 11
    info = sock.getsockopt(socket.IPPROTO_TCP, TCP_INFO, 104)

    # 주요 필드 파싱 (오프셋은 Linux 커널 버전에 따라 다름)
    fields = struct.unpack('B' * 8 + 'I' * 24, info[:104])
    state        = fields[0]   # TCP 상태
    snd_cwnd     = fields[14]  # 송신 혼잡 윈도우 (MSS 단위)
    snd_ssthresh = fields[15]  # ssthresh
    rtt          = fields[16]  # RTT (마이크로초)
    rttvar       = fields[17]  # RTT 분산

    print(f"cwnd: {snd_cwnd} MSS")
    print(f"ssthresh: {snd_ssthresh}")
    print(f"RTT: {rtt/1000:.2f}ms")

# ── 혼잡 제어 알고리즘 변경 (root 권한 필요) ───────────
# $ sysctl -w net.ipv4.tcp_congestion_control=bbr
# $ sysctl -w net.ipv4.tcp_congestion_control=cubic
# $ sysctl -w net.ipv4.tcp_congestion_control=reno

# ── ss 명령으로 실시간 cwnd 모니터링 ───────────────────
# $ watch -n 0.5 'ss -tin | grep cwnd'
# → cwnd:10 ssthresh:7 send 14.2Mbps ...

Linux에서 TCP_INFO 소켓 옵션으로 실시간 cwnd와 ssthresh를 확인할 수 있습니다. 성능 이슈 디버깅 시 유용합니다.
현대 Linux의 기본 혼잡 제어는 CUBIC입니다. CUBIC은 패킷 손실 후 회복 속도를 3차 함수로 계산해 고대역폭·고지연 네트워크에서 효율적입니다.
BBR(Bottleneck Bandwidth and RTT)은 Google이 개발한 알고리즘으로, 손실 기반이 아닌 대역폭과 RTT 측정으로 병목을 찾습니다.
YouTube·Gmail 등에서 사용됩니다.
initcwnd 10은 연결 시작 시 cwnd를 1이 아닌 10 MSS로 시작하는 최적화입니다.
작은 HTTP 응답(10 MSS ≈ 14KB)을 Slow Start 없이 한 번에 전송할 수 있습니다.

Q1

TCP 혼잡 제어에서 Slow Start(SS) 단계의 특징은?

처음엔 조심스럽게 시작하지만 점점 빠르게 늘립니다.
Slow Start는 cwnd를 1부터 시작해 ACK을 받을 때마다 지수적으로 증가시켜 가용 대역폭을 빠르게 탐색합니다.

Q2

TCP 혼잡 제어에서 Slow Start(SS) 단계의 특징은?

처음엔 조심스럽게 시작하지만 점점 빠르게 늘립니다.
Slow Start는 cwnd를 1부터 시작해 ACK을 받을 때마다 지수적으로 증가시켜 가용 대역폭을 빠르게 탐색합니다.

Q3

혼잡 회피(Congestion Avoidance, CA) 단계로 전환되는 시점은?

일정 지점을 넘으면 증가 속도를 늦춥니다.
cwnd가 ssthresh에 도달하면 지수적 증가 대신 선형적으로(RTT마다 1씩) 증가하는 혼잡 회피 단계로 전환해 네트워크 혼잡을 조심스럽게 피합니다.

Q4

혼잡 회피(Congestion Avoidance, CA) 단계로 전환되는 시점은?

일정 지점을 넘으면 증가 속도를 늦춥니다.
cwnd가 ssthresh에 도달하면 지수적 증가 대신 선형적으로(RTT마다 1씩) 증가하는 혼잡 회피 단계로 전환해 네트워크 혼잡을 조심스럽게 피합니다.

Q5

패킷 손실이 감지되면 TCP 혼잡 제어는 일반적으로 어떻게 반응하나요?

혼잡이 감지됐으니 속도를 줄여야 합니다.
패킷 손실은 네트워크 혼잡의 신호로 간주되어, TCP는 ssthresh를 낮추고 cwnd를 줄인 뒤(또는 Slow Start부터 재시작) 다시 조심스럽게 전송 속도를 늘려갑니다.

Q6

패킷 손실이 감지되면 TCP 혼잡 제어는 일반적으로 어떻게 반응하나요?

혼잡이 감지됐으니 속도를 줄여야 합니다.
패킷 손실은 네트워크 혼잡의 신호로 간주되어, TCP는 ssthresh를 낮추고 cwnd를 줄인 뒤(또는 Slow Start부터 재시작) 다시 조심스럽게 전송 속도를 늘려갑니다.