✍️ 필사 모드: TCP 네트워크 스택 완전 정복 — 상태 머신, 혼잡 제어 (Cubic vs BBR), Nagle, Delayed ACK, 그리고 QUIC로의 진화 (2025)
한국어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) |
|────────────────→ | [LISTEN → SYN_RCVD]
| |
| SYN+ACK(seq=y, |
| ack=x+1) |
| ←────────────────|
| |
| ACK (ack=y+1) |
|────────────────→ | [SYN_RCVD → ESTABLISHED]
| |
[SYN_SENT →
ESTABLISHED]
질문: 왜 3번 인가? 2번이면 안 되나?
이유: 양방향 시퀀스 번호 동기화. 각 방향마다 독립적인 ISN (Initial Sequence Number) 을 상대에게 알리고 확인받아야 함. 클라이언트의 SYN/ACK는 자기 ISN+상대 ISN 확인, 클라이언트의 최종 ACK 는 서버 ISN 확인.
1.2 연결 종료: 4-Way Handshake
A B
| FIN |
|─────────────→| [ESTABLISHED → CLOSE_WAIT]
| ACK |
|←─────────────|
| |
[FIN_WAIT_1
→ FIN_WAIT_2]
| |
| FIN |
|←─────────────| [CLOSE_WAIT → LAST_ACK]
| ACK |
|─────────────→|
| |
[FIN_WAIT_2
→ TIME_WAIT] [LAST_ACK → CLOSED]
왜 4번 인가? TCP 는 전이중 (full-duplex). 각 방향을 독립적으로 닫는다. A 가 "나는 더 안 보낼게" 라고 해도 B 는 계속 보낼 수 있다.
1.3 TIME_WAIT — 오해 많은 상태
2MSL (Maximum Segment Lifetime) 동안 대기 — 보통 30초~2분.
왜?
- 지연 패킷이 새 연결에 섞이는 것 방지: 같은 포트 쌍으로 빠르게 새 연결을 만들면, 이전 연결의 지연 패킷이 새 연결에 도달할 수 있음. 2MSL 은 그 패킷들이 모두 소멸될 시간.
- 마지막 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: 10 → 20 → 40 → 80 → 160 → ...
이름이 "slow" 인 이유는 당시의 초기 cwnd=1 과 비교했을 때. 실제로는 지수 성장.
3.4 Congestion Avoidance — 선형 증가
ssthresh 도달 후 cwnd += 1 MSS per RTT → 선형 증가.
cwnd: 160 → 161 → 162 → ...
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) 에서 개선.
| 알고리즘 | 특징 | 용도 |
|---|---|---|
| Reno | AIMD, 고전 | 레거시 |
| Cubic | 3차 함수 증가 | Linux 기본, 장거리 |
| BBR | BW/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: 두 번째 작은 패킷 있음, 하지만 Nagle 이 ACK 기다림
→ 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: SYN → SYN+ACK(cookie) → ACK → GET
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 가 수십 년 진화했지만 바꿀 수 없는 한계:
- HoL (Head-of-Line) Blocking: 한 패킷 손실 시 뒤 패킷들 모두 대기. HTTP/2 도 같은 TCP 연결 위에서 이 문제.
- Handshake 비용: TCP 3-way + TLS 1.2 2-RTT = 3 RTT 지연.
- 미들박스가 굳어짐: 새 TCP 옵션을 도입해도 ISP 방화벽이 차단. TFO, MPTCP, TCP Fast Open 이 실패한 이유.
- 커널 의존성: 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 가 있다. 그런데 다음 질문들을 던져보자: