Skip to content

✍️ 필사 모드: TCP 네트워크 스택 완전 정복 — 상태 머신, 혼잡 제어 (Cubic vs BBR), Nagle, Delayed ACK, 그리고 QUIC로의 진화 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

0. 의외로 우리가 모르는 TCP

매일 쓰는 HTTP 요청 뒤에는 항상 TCP 가 있다. 그런데 다음 질문들을 던져보자:

  • ss -tan 에서 본 TIME_WAIT 상태는 왜 30초나 있어야 하는가?
  • "connection reset by peer" 와 "broken pipe" 는 뭐가 다른가?
  • 초당 1KB 메시지 100개 보내기가 왜 4초나 걸릴 수 있는가?
  • 10Gbps 네트워크인데 iperf 가 1Gbps 밖에 안 나오는 이유가 뭔가?
  • 구글이 왜 TCP 대신 UDP 위에 QUIC 를 새로 만들었는가?

이 질문들의 답이 이 글에 있다. TCP 의 상태 머신, 혼잡 제어 알고리즘의 진화, 악명 높은 상호작용 버그들, 그리고 QUIC 시대의 등장까지.

1. TCP의 상태 머신 — 11개의 상태를 오가는 여정

1.1 연결 수립: 3-Way Handshake

Client              Server
  |  SYN (seq=x)      |
  |────────────────→ |  [LISTENSYN_RCVD]
  |                  |
  | SYN+ACK(seq=y,   |
  |  ack=x+1)        |
  | ←────────────────|
  |                  |
  |  ACK (ack=y+1)   |
  |────────────────→ |  [SYN_RCVDESTABLISHED]
  |                  |
  [SYN_SENT   ESTABLISHED]

질문: 왜 3번 인가? 2번이면 안 되나?

이유: 양방향 시퀀스 번호 동기화. 각 방향마다 독립적인 ISN (Initial Sequence Number) 을 상대에게 알리고 확인받아야 함. 클라이언트의 SYN/ACK는 자기 ISN+상대 ISN 확인, 클라이언트의 최종 ACK 는 서버 ISN 확인.

1.2 연결 종료: 4-Way Handshake

A                B
 |   FIN        |
 |─────────────→|  [ESTABLISHEDCLOSE_WAIT]
 |   ACK        |
 |←─────────────|
 |              |
 [FIN_WAIT_1
FIN_WAIT_2]
 |              |
 |   FIN        |
 |←─────────────|  [CLOSE_WAITLAST_ACK]
 |   ACK        |
 |─────────────→|
 |              |
 [FIN_WAIT_2
TIME_WAIT]    [LAST_ACKCLOSED]

4번 인가? TCP 는 전이중 (full-duplex). 각 방향을 독립적으로 닫는다. A 가 "나는 더 안 보낼게" 라고 해도 B 는 계속 보낼 수 있다.

1.3 TIME_WAIT — 오해 많은 상태

2MSL (Maximum Segment Lifetime) 동안 대기 — 보통 30초~2분.

왜?

  1. 지연 패킷이 새 연결에 섞이는 것 방지: 같은 포트 쌍으로 빠르게 새 연결을 만들면, 이전 연결의 지연 패킷이 새 연결에 도달할 수 있음. 2MSL 은 그 패킷들이 모두 소멸될 시간.
  2. 마지막 ACK 손실 대비: 상대가 FIN 을 재전송하면 ACK 를 다시 보내야 함. CLOSED 상태였다면 RST 를 보내서 상대 입장에서는 에러로 보임.

1.4 TIME_WAIT 폭발의 함정

클라이언트가 많은 연결을 빠르게 만들고 닫으면 TIME_WAIT 가 수만 개 쌓임. ephemeral port 고갈 → 새 연결 실패.

잘못된 해결: TIME_WAIT 를 0 으로 (위험, 데이터 오염 가능).

올바른 해결:

  • net.ipv4.tcp_tw_reuse=1: ephemeral port 를 TIME_WAIT 중이라도 재사용 (안전한 조건 하에).
  • Keep-Alive 연결 사용: HTTP/1.1 의 persistent, HTTP/2 의 multiplexing.
  • Connection pool: DB 드라이버, HTTP client.

1.5 CLOSE_WAIT — 앱 버그의 징후

TIME_WAIT 는 정상이지만, CLOSE_WAIT 가 쌓이면 앱 버그. 상대가 FIN 을 보냈는데 내 앱이 close() 를 안 호출한 상태.

$ ss -tan | grep CLOSE_WAIT | wc -l
50000   # 앱이 소켓 누수 중

원인: 예외 처리 없는 파일 디스크립터 관리, try-finally 누락.

1.6 TCP 상태 다이어그램 전체

         CLOSED (passive open)LISTEN
         │                          │
         (active open)         ▼                          │
      SYN_SENT         │                          │
              (SYN received)      SYN_RCVD ←─────────────── SYN_RCVD
         │                          │
         (ACK received)     (ACK)         ▼                          ▼
    ESTABLISHED ────────────→ ESTABLISHED
         │                          │
         (close)            (FIN)         ▼                          ▼
      FIN_WAIT_1                CLOSE_WAIT
         │                          │
         ▼                          ▼
      FIN_WAIT_2                LAST_ACK
         │                          │
         ▼                          ▼
      TIME_WAIT                   CLOSED
          (2MSL 후)
      CLOSED

2. TCP 의 신뢰성 메커니즘

2.1 시퀀스 번호와 ACK

모든 바이트에 시퀀스 번호. 수신자는 "다음에 받을 바이트 번호" 를 ACK 로 보냄.

송신: [1000][1001][1002][1003] ...
수신: ACK=1004  (1000~1003 까지 받음, 다음은 1004 기대)

중간에 손실되면 수신자는 같은 ACK 반복 (duplicate ACK). 송신자는 3개의 dup ACK 을 보면 Fast Retransmit 실행.

2.2 재전송 — RTO 와 Fast Retransmit

RTO (Retransmission TimeOut): 특정 시간 동안 ACK 없으면 재전송. RTT 측정으로 동적 조정 (Jacobson 알고리즘, 1988).

Fast Retransmit: 3 Dup ACK 받으면 타임아웃 안 기다리고 바로 재전송. 대부분의 손실을 수 ms 내 복구.

SACK (Selective ACK): 일반 ACK 는 연속 바이트만 표현. SACK 로 "10002000 받았고, 30004000 도 받음, 2000~3000 은 손실" 을 명시 → 정확한 재전송.

2.3 흐름 제어 — Receive Window

수신자 버퍼가 가득 차면 곤란. ACK 에 "내 수신 버퍼 여유 공간 (rwnd)" 을 같이 보냄.

송신자는 unacked_bytes < rwnd 인 동안만 전송. 수신자가 처리 못 따라가면 rwnd 가 줄어듦 → 송신 멈춤.

2.4 Window Scaling — 고대역폭 대응

TCP 헤더의 window 필드는 16비트 = 최대 65,535 바이트. 10Gbps × 100ms RTT = 125MB 필요 → 65KB 로는 턱없이 부족.

TCP Window Scaling (RFC 1323, 1992): SYN 시 "shift count" 를 교환. rwnd 를 2^shift 배 확대.

sysctl net.ipv4.tcp_window_scaling   # 1 (켜져 있음)
sysctl net.core.rmem_max              # 최대 수신 버퍼
sysctl net.ipv4.tcp_rmem              # 자동 조정 범위

3. 혼잡 제어 — 네트워크 공유의 예의범절

3.1 문제: 1986년의 Congestion Collapse

1986년 인터넷 사용량 폭증으로 UC Berkeley ↔ LBL 링크 (정상 32Kbps) 가 40bps 로 떨어짐. 1,000배 저하. 원인: 재전송 폭주 → 더 많은 손실 → 더 많은 재전송.

Van Jacobson 의 1988년 논문 "Congestion Avoidance and Control" 이 TCP 에 혼잡 제어를 도입.

3.2 Congestion Window (cwnd)

송신자는 네트워크 상태를 추정 하며 보낼 양을 제한:

전송 가능 = min(rwnd, cwnd)
  • rwnd: 수신자가 알려줌.
  • cwnd: 송신자가 추정 (네트워크 혼잡 피드백 기반).

3.3 Slow Start — 느리게 시작해서 기하급수로

초기 cwnd = 작은 값 (보통 10 MSS).

ACK 받을 때마다 cwnd += 1 MSS → RTT 한 번에 cwnd 가 2배.

cwnd: 10204080160...

이름이 "slow" 인 이유는 당시의 초기 cwnd=1 과 비교했을 때. 실제로는 지수 성장.

3.4 Congestion Avoidance — 선형 증가

ssthresh 도달 후 cwnd += 1 MSS per RTT → 선형 증가.

cwnd: 160161162...

3.5 손실 감지 시

  • 3 Dup ACK (Fast Retransmit): cwnd 절반으로. 온건한 감소.
  • Timeout: 심각한 문제로 판단. cwnd = 1 MSS 로 리셋, Slow Start 다시.

이 AIMD (Additive Increase, Multiplicative Decrease) 가 Reno 의 핵심.

3.6 TCP Reno → Cubic — 고대역폭 시대

고속 장거리 링크에서 Reno 는 너무 보수적. 1% 손실률에서 10Gbps 를 채우려면 수 분 걸림.

TCP BIC → CUBIC (Linux 2.6+ 기본): cwnd 를 시간의 3차 함수로 증가. 최근 손실 시점 기억하고 그 지점까지 빠르게 복귀 → 이후 천천히 탐색.

3.7 BBR — 구글의 혁명 (2016)

기존 알고리즘들의 공통 가정: "손실 = 혼잡". 하지만 현대 인터넷은 WiFi 노이즈, 무선 재전송, 버퍼블로트 등 비혼잡 손실이 많음.

BBR (Bottleneck Bandwidth and RTT) 의 아이디어:

"손실이 아니라 실제 대역폭과 RTT 를 직접 측정해서 cwnd 를 정하자."

  • 주기적으로 송신률을 늘려 현재 대역폭 탐색.
  • 최소 RTT 기억 (버퍼블로트 감지).
  • cwnd = BW × RTT 에 가깝게 유지 → 큐잉 최소화.

3.8 BBR 의 결과

구글 google.com, YouTube 가 BBR 전환 후:

  • YouTube 재버퍼링 4% 감소.
  • Google.com 응답 시간 감소.
  • 개발도상국 사용자에게 특히 효과.

Linux 4.9+ 에 기본 탑재. sysctl net.ipv4.tcp_congestion_control=bbr 로 활성화.

3.9 BBR 의 논란

BBR v1 이 Cubic 과 공존할 때 BBR 이 대역폭을 더 가져감 → "공정성 문제". BBRv2 (2019), v3 (2023) 에서 개선.

알고리즘특징용도
RenoAIMD, 고전레거시
Cubic3차 함수 증가Linux 기본, 장거리
BBRBW/RTT 직접 측정고속 + 비혼잡 손실
DCTCP데이터센터 내부, ECN 마킹DC 전용
CoPA저지연 우선화상 회의

4. Nagle 과 Delayed ACK — 최악의 만남

4.1 Nagle 알고리즘

작은 패킷이 많으면 오버헤드 (40바이트 헤더 / 1바이트 데이터 → 효율 2.5%).

Nagle: "전송 중인 작은 패킷이 있으면, 새로운 작은 데이터는 ACK 올 때까지 기다린 후 합쳐서 보내라."

setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));  // Nagle 끄기

효율에 좋지만 지연 추가.

4.2 Delayed ACK

수신자도 효율을 위해 ACK 을 즉시 안 보내고 모아 보냄:

  • 다음 데이터 전송 시 같이 (piggyback).
  • 또는 최대 200ms 대기 (Linux 기본은 40ms).

4.3 Nagle + Delayed ACK = 40ms 지연

A: 첫 작은 패킷 보냄
B: ACK 보류 (더 보내겠지?)
A: 두 번째 작은 패킷 있음, 하지만 NagleACK 기다림
Nagle: "ACK 안 오네? 기다리자"
Delayed ACK: "데이터 안 오네? 200ms 후 ACK 보내자"
   → 40ms 후 ACK 도착
A: 두 번째 패킷 전송

실시간 앱 (Telnet, SSH, 원격 게임) 에서 명시적으로 TCP_NODELAY 설정 필수.

4.4 TCP_CORK — 반대 방향 튜닝

"버퍼에 모아서 한 번에 보내라". sendfile + TCP_CORK 조합이 nginx 의 정적 파일 최적화.

int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));
writev(fd, iov, 10);
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));  // flush

5. TCP Fast Open — 3-Way Handshake 건너뛰기

HTTP 요청마다 SYN → SYN+ACK → ACK → 요청 → 응답 = 최소 1.5 RTT 지연.

5.1 TFO (RFC 7413, 2014)

첫 연결 시 서버가 쿠키 를 보냄. 이후 연결에서 클라이언트가 SYN 에 쿠키 + 요청 데이터를 실어서 보냄.

1st connection:  SYNSYN+ACK(cookie)ACKGET
2nd connection:  SYN(cookie, GET)SYN+ACK(data)

한 RTT 절약. CDN, 모바일 앱에서 유효.

5.2 왜 잘 안 쓰이는가

  • 미들박스 (방화벽, NAT) 가 비표준 SYN 을 버림.
  • 서버 쿠키 상태 관리 부담.
  • HTTP/2 의 connection reuse 가 어느 정도 상쇄.

QUIC 의 0-RTT handshake 가 이 문제를 우회.

6. 흔한 에러들의 진짜 의미

6.1 Connection refused

  • 의미: SYN 에 RST 응답. 상대 포트에 listen 안 함.
  • 원인: 서버 미실행, 방화벽, 잘못된 포트.

6.2 Connection reset by peer

  • 의미: 양측이 ESTABLISHED 였는데 RST 받음.
  • 원인: 상대 프로세스 크래시, SO_LINGER 0 으로 강제 종료, 방화벽의 active reset.

6.3 Broken pipe

  • 의미: 상대가 이미 닫은 소켓에 write 시도.
  • 원인: 한쪽이 close 한 줄 모르고 계속 씀. (TCP 는 양방향이라 한쪽이 닫아도 읽기는 가능할 수 있음.)

6.4 Connection timed out

  • 의미: SYN 을 여러 번 보냈는데 응답 없음 (보통 5~7회 재전송 후, 60초+).
  • 원인: 네트워크 불통, 블랙홀 방화벽, 상대 서버 다운.

6.5 No route to host

  • 의미: 라우팅 테이블에 대상 IP 경로 없음.
  • 원인: VPN 끊김, 라우팅 설정 오류.

7. 실무 튜닝 — 주요 sysctl

7.1 백로그와 큐

net.core.somaxconn          # listen 의 backlog 상한 (기본 4096, 권장 65535)
net.ipv4.tcp_max_syn_backlog # SYN_RCVD 큐 크기
net.core.netdev_max_backlog # NIC → 커널 큐 크기

nginx listen 80 backlog=65535 는 앱 쪽 백로그. 커널 상한도 같이 올려야.

7.2 TIME_WAIT 관련

net.ipv4.tcp_tw_reuse=1        # ephemeral port 재사용 (클라이언트 쪽에 유용)
net.ipv4.tcp_fin_timeout=30    # FIN_WAIT_2 대기 시간
net.ipv4.ip_local_port_range   # 1024 65535 로 확대

7.3 Keep-alive

net.ipv4.tcp_keepalive_time=600    # idle 감지까지 (기본 2시간!)
net.ipv4.tcp_keepalive_intvl=60    # probe 간격
net.ipv4.tcp_keepalive_probes=3    # 최대 probe

기본 2시간은 너무 길다. LB 뒤 서버는 보통 10분 정도.

7.4 버퍼 크기

net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem="4096 131072 16777216"   # min default max
net.ipv4.tcp_wmem="4096 131072 16777216"

BDP 만큼의 버퍼 필요. 10Gbps × 50ms = 62MB.

7.5 혼잡 제어

net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq   # BBR 권장 qdisc

8. QUIC — TCP 를 버리다

8.1 TCP 의 근본적 한계

TCP 가 수십 년 진화했지만 바꿀 수 없는 한계:

  1. HoL (Head-of-Line) Blocking: 한 패킷 손실 시 뒤 패킷들 모두 대기. HTTP/2 도 같은 TCP 연결 위에서 이 문제.
  2. Handshake 비용: TCP 3-way + TLS 1.2 2-RTT = 3 RTT 지연.
  3. 미들박스가 굳어짐: 새 TCP 옵션을 도입해도 ISP 방화벽이 차단. TFO, MPTCP, TCP Fast Open 이 실패한 이유.
  4. 커널 의존성: TCP 구현 개선은 커널 업그레이드 필요. 느림.

8.2 QUIC 의 해법 — UDP 위에 유저 공간 전송 프로토콜

2012년 구글 내부 실험 시작 → IETF 표준 (RFC 9000, 2021) → HTTP/3 기반.

  • UDP 위에 구현: 미들박스는 일반 UDP 로만 봄.
  • 유저 공간 라이브러리: 커널 업그레이드 없이 앱과 함께 배포.
  • TLS 1.3 필수 통합: 암호화 + 전송을 하나로.
  • 스트림 다중화 (진짜): 각 스트림 독립 순서 → HoL 없음.

8.3 0-RTT Handshake

재접속 시 이전 session ticket 으로 첫 패킷에 데이터 실어서 전송. TLS 1.3 의 0-RTT 와 결합.

처음:     QUIC Handshake (1 RTT)
재접속:   데이터부터 바로 (0 RTT)

8.4 Connection Migration

IP 가 바뀌어도 연결 유지. 모바일에서 WiFi ↔ 4G 전환 시 끊김 없음. Connection ID 로 인식 (소스 IP+포트 의존하지 않음).

8.5 HTTP/3 = HTTP over QUIC

  • HTTP 의미는 HTTP/2 와 동일.
  • 전송만 TCP+TLS → QUIC.
  • 주요 브라우저 + CDN (Cloudflare, Akamai, Fastly) 모두 지원.

8.6 QUIC 의 단점

  • CPU 사용량 증가: 암호화가 강제이고 유저 공간에서 실행.
  • 미들박스 호환성: 일부 방화벽이 UDP 스루풋 제한.
  • 구현 복잡도: TCP 보다 훨씬 크고 움직이는 부품 많음.
  • 관찰 어려움: 암호화된 페이로드, 디버깅 도구 적음.

구글, 메타 내부 측정: HTTP/3 가 HTTP/2 보다 모바일에서 10% 지연 감소. 데스크톱에서는 차이 작음.

9. 실전 디버깅 툴킷

9.1 연결 상태 보기

ss -tan        # 모든 TCP 연결
ss -tan state established     # ESTABLISHED 만
ss -tnp | grep :443           # HTTPS 연결 + PID
ss -s                          # 요약 통계

9.2 패킷 캡처

tcpdump -i any -w capture.pcap 'port 443'
wireshark capture.pcap   # GUI 로 상세 분석

팁: tcpdump -A 로 ASCII 출력, HTTP 헤더 빠르게 확인.

9.3 혼잡 제어 관찰

ss -ti    # 각 연결의 CC 알고리즘, cwnd, rtt, retrans 카운트

출력 예:

ESTAB  ... cubic cwnd:10 ssthresh:7 bytes_acked:1234 bytes_received:5678 rtt:25.3/3.1 rcv_rtt:25.1 delivered:10 ...

cwnd 가 작고 retrans 가 많으면 혼잡 발생 중.

9.4 bpftrace 로 내부 들여다보기

bpftrace -e 'kprobe:tcp_retransmit_skb { printf("retrans pid=%d\n", pid); }'

커널 내 재전송, 상태 전이, 혼잡 이벤트를 실시간으로 관찰.

9.5 Mtr — traceroute + ping

mtr -r -c 100 example.com

경로의 각 홉별 손실률 + RTT. ISP 품질 문제 진단.

10. 마치며 — 50년 된 TCP, 그리고 그 너머

1974년 Vint Cerf 와 Bob Kahn 의 원 논문에서 시작된 TCP 는 인터넷의 근간이 됐다. 50년간:

  • 1988 Jacobson: 혼잡 제어.
  • 1992 Window Scaling.
  • 1996 SACK.
  • 2006 Cubic.
  • 2016 BBR.
  • 2021 QUIC (RFC 표준).

그러나 QUIC 로 전송 계층이 유저 공간으로 이동하면서 새 시대가 열렸다. 각 앱이 자기만의 전송 전략을 가질 수 있고, 커널 업그레이드 없이 진화할 수 있다. Facebook 의 mvfst, Cloudflare 의 quiche, 구글의 gQUIC — 이들이 앞으로 인터넷의 모양을 정할 것이다.

동시에 TCP 는 사라지지 않는다. 수십 년 된 시스템, 레거시 프로토콜, 커널 필수 기능으로 여전히 필수. "TCP 는 죽었다" 는 말은 매년 나오지만 트래픽의 80%+ 가 여전히 TCP 다.

다음 글에서는 TLS/SSL 과 PKI 의 내부 — 인증서 체인, 암호 스위트, 0-RTT 의 replay 위험, QUIC 의 TLS 통합 방식, 그리고 Post-Quantum 암호의 도래 — 를 파볼 예정이다.

참고 자료

  • RFC 9293 — Transmission Control Protocol (2022 개정판).
  • Van Jacobson — "Congestion Avoidance and Control" (SIGCOMM, 1988).
  • Cardwell et al — "BBR: Congestion-Based Congestion Control" (ACM Queue, 2016).
  • RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport.
  • RFC 9114 — HTTP/3.
  • "Computer Networks: A Systems Approach" — Peterson & Davie.
  • Brendan Gregg — Linux Performance 블로그의 TCP 관련 글.
  • Marek Majkowski (Cloudflare) — "The story of one latency spike" 등 TCP 내부 글.
  • "High Performance Browser Networking" — Ilya Grigorik (O'Reilly, 2013).

현재 단락 (1/241)

매일 쓰는 HTTP 요청 뒤에는 항상 TCP 가 있다. 그런데 다음 질문들을 던져보자:

작성 글자: 0원문 글자: 9,136작성 단락: 0/241