TCP 흐름/혼잡 제어 심화
TCP 흐름/혼잡 제어 심화
섹션 제목: “TCP 흐름/혼잡 제어 심화”L2 선수 지식: 3-way handshake, rwnd 기반 Sliding Window 기초, MSS/MTU 개념은 L2(tcp-udp-internals.md)에서 다뤘다. 이 문서는 그 위에서 “왜 네트워크가 느려지는가”와 “어떻게 튜닝하는가”를 다룬다.
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”흐름 제어(Flow Control) 는 수신자 버퍼 overflow를 막는 메커니즘이고, 혼잡 제어(Congestion Control) 는 네트워크 중간 경로(라우터·스위치)의 overflow를 막는 메커니즘이다. 둘 다 “얼마나 빨리 보낼 수 있는가”를 결정하지만, 바라보는 대상이 다르다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”- TTFB 직결: SSR 서버가 클라이언트에게 첫 바이트를 보내는 속도는 TCP의 초기 cwnd(혼잡 윈도우)에 따라 좌우된다. 서버 코드가 빠르더라도 TCP가 느리게 시작하면 사용자는 빈 화면을 본다.
- 클라우드 환경 필수: AWS EC2 간 통신, EKS Pod 간 통신, RDS 연결 모두 TCP 위에서 동작한다. 혼잡 제어 알고리즘과 소켓 버퍼 설정은 throughput과 latency에 직접적 영향을 미친다.
- 장애 원인 분석:
ss -ti출력에서retrans,rtt,cwnd값을 읽지 못하면 네트워크 병목을 구분할 수 없다.
3. Sliding Window 심화: rwnd와 cwnd의 관계
섹션 제목: “3. Sliding Window 심화: rwnd와 cwnd의 관계”3-1. 두 개의 윈도우가 동시에 작동한다
섹션 제목: “3-1. 두 개의 윈도우가 동시에 작동한다”L2에서 rwnd(receive window)만 배웠지만, 실제 송신 속도는 두 윈도우 중 작은 값으로 결정된다.
실제 전송 가능량 = min(rwnd, cwnd)
rwnd: 수신자가 "내 버퍼 여유 공간이 이만큼이야"라고 알려주는 값 (수신자 제어)cwnd: 송신자가 "네트워크가 이 정도는 견딜 수 있어"라고 스스로 추정하는 값 (송신자 제어)- rwnd 는 TCP 헤더의 Window Size 필드(16bit, 최대 65535 bytes, Window Scaling 옵션으로 최대 1GB까지 확장)로 전달된다.
- cwnd 는 운영체제 커널이 내부적으로 관리하는 값이며, 패킷 손실과 ACK 수신 패턴에 따라 동적으로 변한다.
3-2. 윈도우 상태 관찰
섹션 제목: “3-2. 윈도우 상태 관찰”# 현재 연결의 cwnd, rwnd, RTT 확인ss -ti dst 10.0.0.1
# 출력 예시:# State Recv-Q Send-Q Local Peer# ESTAB 0 0 10.0.0.2:8080 10.0.0.1:54321# cubic wscale:7,7 rto:200 rtt:1.2/0.6 ato:40# mss:1448 pmtu:1500 rcvmss:1448 advmss:1448# cwnd:10 ssthresh:2147483647 bytes_sent:140800# bytes_acked:140800 bytes_received:2048 segs_out:98 segs_in:3# send 96.5Mbps lastsnd:48 lastrcv:48 lastack:48# pacing_rate 115.8Mbps delivery_rate 96.5Mbps# rcv_rtt:1.167 rcv_space:87380 rcv_ssthresh:87380핵심 필드:
cwnd:10→ 현재 혼잡 윈도우가 10 MSS (약 14.4KB)ssthresh→ Slow Start Threshold. 이 값 이상이면 Congestion Avoidance 단계rtt:1.2/0.6→ 평균 RTT 1.2ms, 분산 0.6msretrans:0/0→ 재전송 없음 (첫 번째 숫자: 현재 미확인, 두 번째: 전체 재전송 횟수)
4. 혼잡 제어 알고리즘: 상태 머신
섹션 제목: “4. 혼잡 제어 알고리즘: 상태 머신”TCP 혼잡 제어는 다음 네 가지 상태로 구성된다.
연결 시작 ↓[Slow Start] ↓ (cwnd >= ssthresh)[Congestion Avoidance] ↓ (3-duplicate ACK 발생)[Fast Retransmit → Fast Recovery] ↓ (복구 완료)[Congestion Avoidance] ↓ (타임아웃 발생)[Slow Start] (ssthresh = cwnd/2, cwnd = 1 MSS로 리셋)4-1. Slow Start: 지수 증가
섹션 제목: “4-1. Slow Start: 지수 증가”이름은 “느린 시작”이지만 실제로는 지수 증가로 가장 빠른 구간이다.
초기: cwnd = 1 MSS (초기 cwnd는 RFC 6928 이후 10 MSS가 기본값)매 RTT마다: cwnd += (새로 ACK된 세그먼트 수) → 사실상 2배씩 증가
RTT 0: cwnd = 10 MSS (현대 기본값)RTT 1: cwnd = 20 MSS (ACK 10개 → 10 MSS 추가)RTT 2: cwnd = 40 MSS...ssthresh에 도달하거나 손실 발생 시까지 계속cwnd ^40│ *20│ *10│ * 1│ * └──────────────→ RTT 0 1 2 3실무 의미: 새 TCP 연결은 처음 몇 RTT 동안 대역폭을 다 쓰지 못한다. HTTP/1.1에서 이미지 수백 개를 각각 새 연결로 받으면 매번 Slow Start를 거쳐야 한다. HTTP/2의 멀티플렉싱과 연결 재사용이 중요한 이유다.
4-2. Congestion Avoidance: 선형 증가
섹션 제목: “4-2. Congestion Avoidance: 선형 증가”ssthresh에 도달하면 더 조심스럽게 증가한다.
매 RTT마다: cwnd += 1 MSS (선형 증가)
이유: 이미 한 번 혼잡이 발생했던 지점 근처에서 조심스럽게 탐색cwnd ^ │ ......40│ ........ │ ....20│ **** ← Congestion Avoidance (선형) │ **** ← Slow Start (지수) 1│* └──────────────────────────→ RTT4-3. Fast Retransmit: 타임아웃 없이 즉시 재전송
섹션 제목: “4-3. Fast Retransmit: 타임아웃 없이 즉시 재전송”패킷 손실을 감지하는 방법은 두 가지다:
- RTO(Retransmission Timeout): 타이머가 만료될 때까지 ACK 없음 → cwnd = 1 MSS로 완전 리셋 (가장 가혹한 처벌)
- 3 Duplicate ACK: 같은 ACK 번호가 3번 연속 수신 → 해당 패킷만 즉시 재전송, cwnd는 절반만 줄임
클라이언트가 보낸 패킷: [1][2][3][4][5]3번 패킷이 손실:서버 응답: ACK(2), ACK(2), ACK(2), ACK(2) ← 3번째 중복 ACK ↑ 1번 2번이 도착함을 알림 ↑ 3개 중복 → Fast Retransmit 발동송신자: 3번 패킷 즉시 재전송 (타임아웃 기다리지 않음)3 Duplicate ACK는 손실이지만 “이후 패킷은 도달하고 있다”는 신호다. 완전한 네트워크 단절이 아니므로 가혹한 처벌(cwnd=1)을 피한다.
4-4. Fast Recovery
섹션 제목: “4-4. Fast Recovery”Fast Retransmit 이후 Slow Start로 돌아가지 않고 Congestion Avoidance로 바로 진입한다.
TCP Reno 기준:1. 3-dup ACK 감지2. ssthresh = cwnd / 23. cwnd = ssthresh + 3 MSS (3개의 중복 ACK가 "버퍼에 있음"을 의미)4. 누락 패킷 재전송5. 이후 ACK마다 cwnd += 1 (임시로 빠르게 증가)6. 새 ACK(손실 패킷에 대한 ACK) 수신 시: cwnd = ssthresh → Congestion Avoidance 진입5. TCP Reno vs Cubic vs BBR
섹션 제목: “5. TCP Reno vs Cubic vs BBR”5-1. TCP Reno (전통적 알고리즘)
섹션 제목: “5-1. TCP Reno (전통적 알고리즘)”문제: 손실을 혼잡의 유일한 신호로 간주한다. 고대역폭-고지연(High BDP: Bandwidth-Delay Product) 경로에서 비효율적이다.
BDP = 대역폭 × RTT예: 1Gbps × 100ms RTT = 100Mb = 12.5MB
Reno로 이 파이프를 채우려면 cwnd가 12.5MB에 도달해야 한다.손실 후 cwnd/2로 줄었다가 다시 선형 증가로 회복하는 데 수백 RTT가 걸린다.→ 위성 통신, 대륙 간 링크에서 처참한 성능5-2. TCP Cubic (Linux 기본값, 2.6.19 이후)
섹션 제목: “5-2. TCP Cubic (Linux 기본값, 2.6.19 이후)”핵심 아이디어: 시간의 3차 함수(cubic function)로 cwnd를 증가시킨다. 손실 후 회복 속도가 훨씬 빠르다.
W(t) = C × (t - K)³ + W_max
W_max: 손실 직전의 cwnd 값K: W_max * β / C의 세제곱근 (β = 0.7, C = 0.4)t: 마지막 손실 이후 경과 시간cwnd ^ │ * ← W_max │ * ← 급격히 복구 │ * │ * ← 손실 직전에 천천히 탐색 (평탄한 구간) │ * │ * ← 손실 후 빠르게 회복 └──────────────────────→ 시간장점: 고BDP 경로에서 Reno보다 훨씬 효율적. 동일 경로의 여러 Cubic 흐름 간 공정성(fairness) 좋음. 단점: 여전히 손실 기반. 버퍼가 가득 찬 후에야 반응한다(bufferbloat 문제).
5-3. TCP BBR (Bottleneck Bandwidth and Round-trip propagation time)
섹션 제목: “5-3. TCP BBR (Bottleneck Bandwidth and Round-trip propagation time)”Google이 2016년 개발, Linux 4.9에 도입. 패러다임 전환: 손실이 아니라 실제 병목 대역폭과 RTT를 직접 측정해서 제어한다.
BBR의 핵심 통찰
섹션 제목: “BBR의 핵심 통찰”전통 알고리즘의 문제:버퍼가 꽉 참 → 손실 발생 → 뒤늦게 속도 줄임 (버퍼링 레이턴시 높음)
BBR의 접근:병목 링크의 실제 BW와 RTT를 주기적으로 측정→ 버퍼를 채우지 않고도 최적 전송 속도 유지→ 낮은 레이턴시 + 높은 throughput 동시 달성BBR의 4단계 상태 머신
섹션 제목: “BBR의 4단계 상태 머신”1. STARTUP (Slow Start와 유사) - 측정된 BW가 더 이상 증가하지 않을 때까지 지수 증가 - 병목 BW 추정치 확보
2. DRAIN - STARTUP 중 쌓인 큐(버퍼)를 비우는 단계 - pacing_gain < 1 (의도적으로 느리게 전송)
3. PROBE_BW (정상 운전) - 8 RTT 주기로 반복: * 1 RTT: gain=1.25 (대역폭 탐색, 약간 빠르게) * 1 RTT: gain=0.75 (큐 비우기) * 6 RTT: gain=1.0 (안정 유지)
4. PROBE_RTT - 10초마다 cwnd를 4 MSS로 줄여 실제 min RTT 재측정 - 버퍼 점유로 인한 RTT 부풀림 제거왜 클라우드 환경에서 중요한가
섹션 제목: “왜 클라우드 환경에서 중요한가”# Google Cloud, AWS의 많은 인스턴스는 BBR을 기본으로 사용# 직접 확인:sysctl net.ipv4.tcp_congestion_control# 출력: net.ipv4.tcp_congestion_control = bbr
# 사용 가능한 알고리즘 목록:sysctl net.ipv4.tcp_available_congestion_control# 출력: net.ipv4.tcp_available_congestion_control = reno cubic bbr클라우드 환경 특성과 BBR의 궁합:
| 상황 | Cubic | BBR |
|---|---|---|
| 긴 RTT (리전 간 통신) | BDP가 크면 회복 느림 | RTT 직접 측정으로 최적화 |
| 얕은 버퍼 (NIC 큐 작음) | 손실 잦아짐 | 버퍼 최소 사용으로 손실 감소 |
| 무선/셀룰러 노이즈 손실 | 혼잡으로 오해해 속도 줄임 | BW 측정으로 노이즈 손실 구분 |
| 다중 테넌트 공유 링크 | 공격적인 흐름에 bandwidth 뺏김 | 안정적 BW 확보 |
실무 적용: BBR 활성화
# /etc/sysctl.conf 또는 /etc/sysctl.d/99-bbr.confnet.ipv4.tcp_congestion_control = bbrnet.core.default_qdisc = fq # BBR은 fq(Fair Queuing) qdisc와 함께 써야 효과적
# 즉시 적용sysctl -p
# 검증sysctl net.ipv4.tcp_congestion_controltc qdisc show dev eth0 # fq 확인주의: BBR v1은 multiple flows 간 공정성 문제(Cubic 흐름을 starve)가 보고된다. 프로덕션 도입 전 벤치마크 필수. BBR v2(실험적)는 이를 개선했다.
BBR이 1% 패킷 손실에서도 고속 전송을 유지하는 이유:
기존 알고리즘(Reno, Cubic)은 패킷 손실을 “네트워크 혼잡”으로 간주한다. 그러나 무선 네트워크나 장거리 링크에서는 혼잡이 없어도 패킷이 손실된다(noise loss). BBR은 이를 구분한다.
Cubic의 패킷 손실 반응: 패킷 손실 감지 → ssthresh = cwnd/2 → 속도 절반 감소 → 선형 증가로 복구 → 손실률 1%: 지속적으로 cwnd가 줄어 throughput 저하
BBR의 패킷 손실 반응: 패킷 손실 감지 → 그러나 최근 측정 BW가 충분히 높으면 → 단순 노이즈 손실로 판단, cwnd를 줄이지 않음 → BW 측정값이 실제로 감소할 때만 속도 조정
Google 실측: 1% 손실률에서: Cubic: ~3 Mbps BBR: ~9,100 Mbps (2700배 차이)📖 더 보기: BBR: Congestion-Based Congestion Control — ACM Queue — Google 엔지니어가 직접 작성한 BBR 설계 원리와 실측 데이터. 이 섹션의 BBR 4단계 상태머신의 공식 레퍼런스
6. Nagle 알고리즘과 Delayed ACK의 상호작용
섹션 제목: “6. Nagle 알고리즘과 Delayed ACK의 상호작용”6-1. Nagle 알고리즘
섹션 제목: “6-1. Nagle 알고리즘”소규모 패킷을 버퍼링해서 더 큰 세그먼트로 합쳐 보내는 알고리즘. 1980년대 초 네트워크 정체(Silly Window Syndrome) 해결을 위해 도입됐다.
Nagle 규칙:"미확인(in-flight) 데이터가 있으면, 새 데이터는 MSS 크기가 될 때까지 버퍼링"
즉:- 보낼 데이터가 MSS 이상: 즉시 전송- in-flight 데이터 없음: 즉시 전송- in-flight 데이터 있음 + 데이터 < MSS: ACK 올 때까지 대기6-2. Delayed ACK
섹션 제목: “6-2. Delayed ACK”TCP 수신자는 ACK를 즉시 보내지 않고, 최대 40ms(Linux 기본값)까지 기다렸다가 데이터와 함께 piggyback하거나 여러 ACK를 합쳐 보낸다.
목적: ACK 패킷 수 감소 → 네트워크 부하 감소6-3. 두 알고리즘의 치명적 상호작용
섹션 제목: “6-3. 두 알고리즘의 치명적 상호작용”클라이언트 (Nagle ON) 서버 (Delayed ACK) | | |── Data(1) ───────────────→| (작은 첫 패킷) |── Data(2) [버퍼링됨] | ← Nagle: in-flight 있음, MSS 미만이므로 대기 | | ← Delayed ACK: ACK를 40ms 기다리는 중 | | | (40ms 대기) | | | |←─────────── ACK(1) ───────| ← 40ms 후 ACK 전송 |── Data(2) ───────────────→| ← 이제 Nagle이 전송 허용 | |총 지연: 40ms 추가실제 사례: HTTP/1.1 request-response 패턴에서 클라이언트가 작은 POST body를 두 패킷으로 나눠 보낼 때 발생. MySQL 프로토콜에서도 자주 발생.
6-4. TCP_NODELAY로 해결
섹션 제목: “6-4. TCP_NODELAY로 해결”// C 소켓 코드int flag = 1;setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));const net = require("net");const socket = new net.Socket();socket.setNoDelay(true); // TCP_NODELAY 활성화// Goconn, _ := net.Dial("tcp", "10.0.0.1:8080")conn.(*net.TCPConn).SetNoDelay(true)TCP_NODELAY 설정 권장 상황:
| 상황 | 권장 |
|---|---|
| 인터랙티브 프로토콜 (SSH, telnet, MySQL) | TCP_NODELAY ON |
| 게임 서버, 실시간 채팅 | TCP_NODELAY ON |
| 대용량 파일 전송 | TCP_NODELAY OFF (Nagle이 효율적) |
| HTTP/2 (이미 자체 멀티플렉싱) | TCP_NODELAY ON 권장 |
| gRPC | TCP_NODELAY ON (기본값) |
# 운영 중인 프로세스의 소켓 옵션 확인ss -tnop | grep :3306 # MySQL 포트
# /proc/net/tcp 에서 확인 (더 낮은 레벨)cat /proc/net/tcp7. TIME_WAIT 상태 심화
섹션 제목: “7. TIME_WAIT 상태 심화”7-1. TIME_WAIT가 필요한 두 가지 이유
섹션 제목: “7-1. TIME_WAIT가 필요한 두 가지 이유”L2에서 4-way handshake와 TIME_WAIT 존재는 배웠다. 여기서는 왜 2MSL(Maximum Segment Lifetime, 보통 60초)이나 기다려야 하는지 이해한다.
이유 1: 지연된 패킷이 새 연결에 섞이지 않게 ──────────────────────────────────── 이전 연결의 패킷이 라우터에서 지연됐다가 같은 4-tuple(src IP:port, dst IP:port)로 새 연결이 생기면 오래된 패킷이 새 연결에 전달될 수 있음 → 2MSL 대기로 모든 지연 패킷이 소멸되길 보장
이유 2: 마지막 ACK가 안전하게 전달되게 ──────────────────────────────────── FIN을 받고 ACK를 보냈는데 그 ACK가 손실되면 서버는 FIN을 재전송함 → TIME_WAIT 상태에서 재전송된 FIN에 ACK를 다시 보낼 수 있음 → CLOSED 상태라면 RST를 보내 서버 쪽에서 에러 발생7-2. TIME_WAIT 폭증 문제
섹션 제목: “7-2. TIME_WAIT 폭증 문제”단기 연결이 많은 서버(API 게이트웨이, 로드밸런서)에서 TIME_WAIT가 수만 개 쌓일 수 있다.
# TIME_WAIT 개수 확인ss -tan state time-wait | wc -l
# 상태별 연결 수 확인ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn문제: 로컬 포트는 65535개(보통 1024~65535, 약 64511개)가 한계다. 같은 대상 IP:port로의 연결에서 TIME_WAIT가 포트를 점유하면 새 연결을 못 만들 수 있다.
7-3. tw_reuse와 tw_recycle
섹션 제목: “7-3. tw_reuse와 tw_recycle”# tw_reuse: TIME_WAIT 소켓을 새 연결에 재사용 (안전함)net.ipv4.tcp_tw_reuse = 1# 조건: TCP timestamp가 활성화돼 있어야 함 (net.ipv4.tcp_timestamps = 1)# 동작: 새 연결의 timestamp가 이전 연결보다 크면 재사용 허용
# tw_recycle: 절대 사용 금지 (Linux 4.12에서 제거됨)# net.ipv4.tcp_tw_recycle = 1 ← NAT 환경에서 패킷 드롭 유발# 이유: NAT 뒤의 여러 클라이언트가 같은 IP를 공유할 때# timestamp 단조 증가 검사를 클라이언트별로 못 함 → 패킷 드롭실무 권장 설정:
# 안전한 TIME_WAIT 관리net.ipv4.tcp_tw_reuse = 1 # 클라이언트 측(아웃바운드 연결)에서만 동작net.ipv4.tcp_timestamps = 1 # tw_reuse의 전제 조건net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT2 → CLOSED 타임아웃 (기본 60초 → 30초)
# 로컬 포트 범위 확장net.ipv4.ip_local_port_range = 1024 65535중요:
tw_reuse는 서버(LISTEN 소켓)가 아닌 클라이언트(아웃바운드) 측에서 동작한다. API 서버가 DB나 외부 서비스에 연결할 때의 TIME_WAIT를 줄여준다.
8. Keep-Alive와 커넥션 풀링
섹션 제목: “8. Keep-Alive와 커넥션 풀링”8-1. TCP Keep-Alive
섹션 제목: “8-1. TCP Keep-Alive”유휴 연결이 살아있는지 확인하는 메커니즘이다. 미들박스(NAT, 방화벽)가 유휴 연결을 조용히 끊어버리는 것을 방지한다.
# 커널 수준 TCP Keep-Alive 설정 확인sysctl net.ipv4.tcp_keepalive_time # 마지막 데이터 후 probe 시작까지 대기 (기본 7200초)sysctl net.ipv4.tcp_keepalive_intvl # probe 간격 (기본 75초)sysctl net.ipv4.tcp_keepalive_probes # 최대 probe 횟수 (기본 9회)
# 총 감지 시간 = tcp_keepalive_time + tcp_keepalive_intvl × tcp_keepalive_probes# 기본: 7200 + 75×9 = 7875초 ≈ 2.2시간 (너무 길다!)AWS/클라우드 환경 권장 설정:
net.ipv4.tcp_keepalive_time = 60 # 60초 후 probe 시작net.ipv4.tcp_keepalive_intvl = 10 # 10초 간격net.ipv4.tcp_keepalive_probes = 3 # 3번 실패 시 연결 종료# 총 감지 시간: 60 + 10×3 = 90초 (AWS ELB 유휴 타임아웃 60초에 맞춤)AWS ELB/ALB 주의사항:
- ALB 기본 유휴 타임아웃: 60초
- 백엔드 서버의 Keep-Alive 타임아웃이 60초보다 짧으면 ALB가 아직 살아있다고 생각하는 연결을 서버가 먼저 끊음 → 502 에러
// Node.js HTTP 서버 설정const server = http.createServer(app);server.keepAliveTimeout = 65000; // 65초 (ALB 60초보다 길게)server.headersTimeout = 70000; // keepAliveTimeout보다 반드시 길게8-2. 애플리케이션 수준 Keep-Alive
섹션 제목: “8-2. 애플리케이션 수준 Keep-Alive”HTTP/1.1은 기본으로 Keep-Alive 커넥션을 사용한다. 같은 TCP 연결로 여러 HTTP 요청을 처리해 Slow Start 반복을 피한다.
Keep-Alive 없음 (HTTP/1.0 기본):TCP 연결 → HTTP 요청 → HTTP 응답 → TCP 종료 (매번 3-way handshake + Slow Start)
Keep-Alive 있음 (HTTP/1.1 기본):TCP 연결 → HTTP 요청1 → HTTP 응답1 → HTTP 요청2 → HTTP 응답2 → ... → TCP 종료 (한 번의 3-way handshake + 한 번의 Slow Start)8-3. 커넥션 풀링
섹션 제목: “8-3. 커넥션 풀링”DB, Redis, 외부 API 등 연결 수립 비용이 높은 서비스에 필수적이다.
// PostgreSQL 커넥션 풀 (pg 라이브러리)const { Pool } = require("pg");const pool = new Pool({ host: "db.internal", port: 5432, max: 20, // 최대 연결 수 (DB의 max_connections 고려) min: 5, // 최소 유지 연결 수 idleTimeoutMillis: 30000, // 유휴 연결 제거 시간 connectionTimeoutMillis: 2000, // 연결 획득 타임아웃});
// Keep-Alive를 통해 연결 유지 확인pool.on("connect", (client) => { client.query("SET statement_timeout = 5000");});# SQLAlchemy 커넥션 풀engine = create_engine( DATABASE_URL, pool_size=20, max_overflow=10, pool_pre_ping=True, # 연결 사용 전 alive 확인 (keep-alive 대안) pool_recycle=3600, # 1시간마다 연결 재생성 (NAT 타임아웃 대비))9. 소켓 버퍼 튜닝
섹션 제목: “9. 소켓 버퍼 튜닝”9-1. 버퍼 구조 이해
섹션 제목: “9-1. 버퍼 구조 이해”애플리케이션 커널 네트워크 │ │ │ write() │ │──────────────────────→│ send buffer (SO_SNDBUF) │ │──────────────────────→ 네트워크 │ │ │ read() │ │←──────────────────────│ recv buffer (SO_RCVBUF) │ │←────────────────────── 네트워크
rwnd = recv buffer의 남은 공간(수신 버퍼가 크면 rwnd가 크고, 더 많은 in-flight 데이터를 허용)9-2. 주요 커널 파라미터
섹션 제목: “9-2. 주요 커널 파라미터”# 현재 설정 확인sysctl net.core.rmem_max # recv buffer 최대값 (bytes)sysctl net.core.wmem_max # send buffer 최대값sysctl net.core.rmem_default # recv buffer 기본값sysctl net.core.wmem_default # send buffer 기본값sysctl net.ipv4.tcp_rmem # TCP recv buffer: min default maxsysctl net.ipv4.tcp_wmem # TCP send buffer: min default maxsysctl net.ipv4.tcp_mem # TCP 전체 메모리 사용량 제한: low pressure high
# 예시 출력:# net.ipv4.tcp_rmem = 4096 87380 6291456# ↑min ↑def ↑max (6MB)9-3. 고성능 서버 튜닝 설정
섹션 제목: “9-3. 고성능 서버 튜닝 설정”# 소켓 버퍼 최대값 확대net.core.rmem_max = 134217728 # 128MBnet.core.wmem_max = 134217728 # 128MB
# TCP 버퍼 자동 조정 범위 (min, default, max)net.ipv4.tcp_rmem = 4096 87380 134217728net.ipv4.tcp_wmem = 4096 65536 134217728
# 자동 버퍼 튜닝 활성화 (커널이 BDP에 맞게 자동 조정)net.ipv4.tcp_moderate_rcvbuf = 1
# 백로그 큐 크기 (LISTEN 소켓의 대기열)net.core.somaxconn = 65535 # accept() 대기 최대 연결 수net.ipv4.tcp_max_syn_backlog = 65535
# TIME_WAIT 버킷 (많은 TIME_WAIT를 처리할 메모리)net.ipv4.tcp_max_tw_buckets = 2621449-4. BDP에 맞는 버퍼 계산
섹션 제목: “9-4. BDP에 맞는 버퍼 계산”BDP(Bandwidth-Delay Product) = 대역폭 × RTT
예: 서울 ↔ 도쿄 = 10Gbps × 30ms RTTBDP = 10,000,000,000 bps × 0.030 s = 300,000,000 bits = 37.5MB
→ 파이프를 꽉 채우려면 tcp_rmem max가 최소 37.5MB 이상이어야 함→ 기본값 6MB로는 10Gbps의 16%만 사용 가능# RTT 측정ping -c 100 tokyo-server.example.com | tail -1# rtt min/avg/max/mdev = 28.5/31.2/35.1/1.8 ms
# 현재 연결의 실제 throughput 한계 계산# ss -ti 출력의 send Xbps 값과 이론값 비교9-5. 소켓 버퍼 애플리케이션 수준 설정
섹션 제목: “9-5. 소켓 버퍼 애플리케이션 수준 설정”// C: 특정 소켓의 버퍼 크기 설정int rcvbuf_size = 4 * 1024 * 1024; // 4MBsetsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));// 주의: 커널은 요청값의 2배를 실제로 할당 (메타데이터 포함)// 실제 할당값: getsockopt()로 확인// Go: net.Dialer로 소켓 옵션 설정dialer := &net.Dialer{ Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, 4*1024*1024) }) },}conn, err := dialer.Dial("tcp", "10.0.0.1:8080")10. 프론트엔드 → 플랫폼 브릿지: React SSR과 TCP 튜닝
섹션 제목: “10. 프론트엔드 → 플랫폼 브릿지: React SSR과 TCP 튜닝”10-1. TTFB(Time to First Byte)와 TCP의 관계
섹션 제목: “10-1. TTFB(Time to First Byte)와 TCP의 관계”프론트엔드 개발자 시절에는 TTFB를 “서버가 느린 것”으로만 이해했다. 하지만 TCP 관점에서 TTFB에는 여러 레이어가 있다.
브라우저 CDN/엣지 SSR 서버 │ │ │ │── TCP SYN ───────────────→│ │ │←─ SYN-ACK ────────────────│ │ │── ACK ───────────────────→│ │ │ ↑ 3-way handshake (RTT1)│ │ │ │── TCP SYN ───────→│ │ │←─ SYN-ACK ────────│ │ │── ACK ────────────→│ │ │ ↑ RTT2 (CDN→서버)│ │ │── HTTP Request ───→│ │ │ [React rendering] │ │←─ HTTP 200 ───────│ │←─ HTTP 200 ───────────────│ ↑ Slow Start 영향 ↑ 사용자가 느끼는 TTFBTTFB = RTT(클라이언트→CDN) + RTT(CDN→서버) + 서버 처리 시간 + TCP Slow Start 지연
10-2. 초기 cwnd가 TTFB에 미치는 영향
섹션 제목: “10-2. 초기 cwnd가 TTFB에 미치는 영향”HTML 응답이 15KB라고 가정:
초기 cwnd = 10 MSS = 10 × 1460 bytes = 14.6KB
→ 첫 번째 RTT에서 14.6KB 전송 → HTML의 97%가 첫 RTT에 도착→ 두 번째 RTT에서 나머지 0.4KB 전송
만약 HTML이 30KB라면:→ 첫 RTT: 14.6KB (cwnd=10)→ ACK 수신 후 cwnd=20: 두 번째 RTT에서 나머지 15.4KB 전송→ 총 2 RTT 소요
→ 미들웨어를 최대한 줄이고, 초기 HTML을 14.6KB(10 MSS) 이하로 만들면 첫 RTT에서 전체 페이지 구조를 전달 가능10-3. SSR 서버 TCP 튜닝 실무
섹션 제목: “10-3. SSR 서버 TCP 튜닝 실무”# SSR 서버 (Next.js, Remix 등) 최적화 설정# 초기 cwnd 확인 (Linux 3.0+ 기본값 10, 구형은 4)# ip route show | grep initcwnd# 만약 낮다면:ip route change default via 10.0.0.1 initcwnd 10
# 빠른 연결 수립net.ipv4.tcp_fastopen = 3 # TFO 활성화 (클라이언트+서버 모두)# TFO: 3-way handshake 중 SYN에 데이터 포함 → 1 RTT 절약
# Keep-Alive (ALB와 연동)net.ipv4.tcp_keepalive_time = 60net.ipv4.tcp_keepalive_intvl = 10net.ipv4.tcp_keepalive_probes = 3Next.js 서버 설정 예시:
const nextConfig = { // HTTP 압축으로 응답 크기 줄여 Slow Start 영향 최소화 compress: true,
// 헤더 최적화 async headers() { return [ { source: "/(.*)", headers: [ // 연결 재사용 명시 { key: "Connection", value: "keep-alive" }, { key: "Keep-Alive", value: "timeout=65" }, ], }, ]; },};
// server.js (Custom Server)const http = require("http");const next = require("next");const app = next({ dev: false });
app.prepare().then(() => { const server = http.createServer(app.getRequestHandler());
// ALB 유휴 타임아웃(60초)보다 길게 설정 server.keepAliveTimeout = 65000; server.headersTimeout = 70000;
server.listen(3000, () => { console.log("Ready on port 3000"); });});10-4. Streaming SSR과 TCP의 상호작용
섹션 제목: “10-4. Streaming SSR과 TCP의 상호작용”React 18의 Streaming SSR(renderToPipeableStream)은 HTML을 청크(chunk)로 나눠 보낸다. 여기서 TCP Nagle 알고리즘이 문제가 될 수 있다.
Streaming SSR 흐름:서버: [<html><head>...</head><body>] → flush서버: [<div id="root">Loading...</div>] → flush서버: [<script>] ... React hydration ... [</script>] → flush
문제: 작은 청크들이 Nagle에 의해 버퍼링될 수 있음 → 첫 청크를 브라우저가 늦게 받아 LCP(Largest Contentful Paint) 지연// Node.js HTTP 서버에서 TCP_NODELAY 설정const server = http.createServer((req, res) => { // 소켓에 TCP_NODELAY 설정 req.socket.setNoDelay(true);
// React Streaming const { pipe } = renderToPipeableStream(<App />, { onShellReady() { res.statusCode = 200; res.setHeader("Content-Type", "text/html"); // Transfer-Encoding: chunked가 자동 설정됨 pipe(res); }, });});10-5. 실무 진단 워크플로우
섹션 제목: “10-5. 실무 진단 워크플로우”# 1단계: TTFB 측정curl -o /dev/null -s -w " DNS: %{time_namelookup}s TCP: %{time_connect}s TLS: %{time_appconnect}s TTFB: %{time_starttransfer}s Total: %{time_total}s" https://myapp.example.com/
# 2단계: 연결 상태 확인ss -ti 'sport = :3000' # SSR 서버의 연결 상태
# 3단계: 패킷 수준 분석sudo tcpdump -i eth0 -w /tmp/capture.pcap port 3000# Wireshark로 열어 TCP Stream Graph → Time/Sequence 그래프로 Slow Start 확인
# 4단계: 재전송 통계netstat -s | grep -i retransmit# 또는cat /proc/net/snmp | grep Tcp11. 핵심 정리 및 점검 질문
섹션 제목: “11. 핵심 정리 및 점검 질문”알고리즘 선택 가이드
섹션 제목: “알고리즘 선택 가이드”| 환경 | 권장 알고리즘 | 이유 |
|---|---|---|
| 데이터센터 내부 (RTT < 1ms) | Cubic (기본값) | BBR의 PROBE_RTT가 오히려 비효율 |
| 리전 간 (RTT 10~100ms) | BBR | 고BDP에서 throughput 향상 |
| 모바일/무선 환경 | BBR | 노이즈 손실을 혼잡으로 오해 방지 |
| 위성 통신 (RTT > 500ms) | BBR | Cubic의 선형 회복이 너무 느림 |
점검 질문
섹션 제목: “점검 질문”rwnd와cwnd중 어느 것이 더 작으면 네트워크 병목이고, 어느 것이 더 작으면 수신 측 병목인가?- Fast Retransmit가 일반 타임아웃 재전송보다 빠른 이유는 무엇인가?
- AWS ALB 뒤에 Node.js SSR 서버를 둘 때, keepAliveTimeout을 ALB 타임아웃보다 길게 설정해야 하는 이유를 TCP 관점에서 설명하라.
- Nagle 알고리즘과 Delayed ACK가 동시에 활성화됐을 때 40ms 지연이 발생하는 구체적인 시나리오를 설명하라.
tw_recycle이 위험한 이유를 NAT와 TCP timestamp의 관점에서 설명하라.
답안 힌트
섹션 제목: “답안 힌트”- cwnd < rwnd이면 네트워크(중간 경로) 병목, rwnd < cwnd이면 수신 측 애플리케이션/버퍼 병목
- 3-dup ACK는 “뒤의 패킷이 도달하고 있다”는 신호 → 타임아웃 만료 없이 즉시 재전송 가능
- 서버가 먼저 연결을 끊으면 ALB는 살아있다고 보고 새 요청을 보냄 → 502 에러
- 서버가 Delayed ACK로 40ms 대기, 클라이언트가 Nagle로 ACK 오기를 기다림 → 교착
- NAT 뒤 다른 클라이언트들의 timestamp가 단조 증가하지 않을 수 있어 패킷 드롭 발생
트러블슈팅 (실무 사례)
섹션 제목: “트러블슈팅 (실무 사례)”🔧 Node.js SSR 서버에서 ALB 502 — keepAliveTimeout 미설정
섹션 제목: “🔧 Node.js SSR 서버에서 ALB 502 — keepAliveTimeout 미설정”증상:
ALB 액세스 로그: "elb_status_code": 502, "target_status_code": "-"# 특이한 점: 일부 요청만 산발적으로 502 발생, 재시도하면 성공# 서버 로그에는 아무것도 없음 (서버가 연결을 먼저 끊었기 때문)원인: Node.js HTTP 서버의 기본 keepAliveTimeout이 5000ms(5초)이다. ALB의 기본 유휴 타임아웃(60초) 동안 ALB는 연결이 살아있다고 판단하고 새 요청을 보내는데, 서버는 이미 해당 TCP 연결을 종료한 상태다. 서버가 RST를 보내면 ALB는 502를 반환한다.
해결:
// server.js — ALB 타임아웃(60초)보다 길게 설정const server = http.createServer(app);server.keepAliveTimeout = 65000; // 65초 (ALB 60초 + 여유 5초)server.headersTimeout = 70000; // keepAliveTimeout보다 반드시 크게
server.listen(3000);console.log("Server keepAliveTimeout:", server.keepAliveTimeout);// Server keepAliveTimeout: 65000# ALB 유휴 타임아웃 확인aws elbv2 describe-load-balancer-attributes \ --load-balancer-arn arn:aws:elasticloadbalancing:... \ --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'# [{"Key": "idle_timeout.timeout_seconds", "Value": "60"}]
# 연결 상태 확인 (CLOSE_WAIT 폭증이 증거)ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn# CLOSE_WAIT가 수백~수천이면 서버가 먼저 연결을 끊고 있는 것🔧 TIME_WAIT 포트 고갈 — “connection refused: no ephemeral port available”
섹션 제목: “🔧 TIME_WAIT 포트 고갈 — “connection refused: no ephemeral port available””증상:
# API 서버 → DB 연결 시 에러:dial tcp: connect: cannot assign requested address# 또는:FATAL: role "myapp" is already connected to the database
# 확인:ss -tan state time-wait | wc -l# 62000 ← 포트 고갈 임박 (최대 약 64511개)
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn# 62000 TIME-WAIT# 150 ESTABLISHED# 5 LISTEN원인: 단기 연결을 많이 생성하는 서비스(커넥션 풀 없이 DB 직접 연결, 외부 API 호출)에서 TIME_WAIT가 쌓여 로컬 포트가 고갈된다. TIME_WAIT는 2MSL(약 60초)간 포트를 점유한다.
해결:
# 1. 즉시 완화 (런타임 적용)sysctl -w net.ipv4.tcp_tw_reuse=1sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 2. 영구 설정 (/etc/sysctl.d/99-tcp-tune.conf)net.ipv4.tcp_tw_reuse = 1 # TIME_WAIT 소켓 재사용 (클라이언트 아웃바운드에만 동작)net.ipv4.tcp_timestamps = 1 # tw_reuse의 전제 조건net.ipv4.ip_local_port_range = 1024 65535 # 로컬 포트 범위 확장net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT2 타임아웃 단축 (기본 60초)
# 3. 근본 해결: 커넥션 풀 적용# pg Pool({ max: 20 }) → 고정 연결로 TIME_WAIT 자체를 줄임
# 적용 확인sysctl -p && ss -tan state time-wait | wc -l# 적용 후 수 분 내에 TIME_WAIT 수 감소🔧 리전 간 파일 전송 throughput 저조 — cwnd 병목
섹션 제목: “🔧 리전 간 파일 전송 throughput 저조 — cwnd 병목”증상:
# 서울 → 도쿄 파일 전송 시 속도 저조:scp large-file.tar user@tokyo-server:/data/# 속도: 5MB/s (10Gbps 링크인데도)
# 진단:ss -ti dst tokyo-server.example.com# ESTAB ...# cubic wscale:7,7 rto:250 rtt:32.0/1.0# cwnd:45 ssthresh:80 bytes_sent:160000000# send 16.5Mbps delivery_rate 16.5Mbps원인: BDP = 10Gbps × 32ms = 40MB인데, cwnd가 45 MSS ≈ 65KB에 불과하다. 소켓 버퍼 크기가 BDP보다 훨씬 작아 파이프를 채우지 못하고 있다.
해결:
# 1. 커널 소켓 버퍼 확대 (/etc/sysctl.d/99-network-tuning.conf)net.core.rmem_max = 134217728 # 128MBnet.core.wmem_max = 134217728net.ipv4.tcp_rmem = 4096 87380 134217728net.ipv4.tcp_wmem = 4096 65536 134217728net.ipv4.tcp_moderate_rcvbuf = 1 # 자동 버퍼 조정
sysctl -p
# 2. BBR 전환 (고지연 링크 최적화)net.ipv4.tcp_congestion_control = bbrnet.core.default_qdisc = fq
sysctl -p
# 3. 재전송 확인ss -ti dst tokyo-server.example.com# 적용 후: cwnd:2000+ send 800Mbps+ 가 되어야 정상
# 이론 최대 throughput 검증:# BDP / RTT = 40MB / 0.032s = 1.25GB/s (링크 한계)# cwnd 제한 없을 때: 10Gbps 링크의 80%+ 활용 가능🔧 Nagle + Delayed ACK 40ms 레이턴시 — MySQL/Redis 지연
섹션 제목: “🔧 Nagle + Delayed ACK 40ms 레이턴시 — MySQL/Redis 지연”증상:
# MySQL 쿼리 레이턴시가 40ms 단위로 증가:# 예상: 1-2ms, 실제: 41ms, 42ms (40ms 가산)
# 진단: tcpdump로 패킷 타이밍 확인sudo tcpdump -i eth0 -nn port 3306 -w /tmp/mysql.pcap# Wireshark에서 Time/Sequence 그래프 확인 → 40ms 공백 구간 발견
# ss로 TCP_NODELAY 확인 (nodelay가 없으면 Nagle 활성화)ss -tione dst db.internal | grep -i nodelay# (출력 없음) ← TCP_NODELAY 미설정원인: 클라이언트의 Nagle 알고리즘과 서버의 Delayed ACK(40ms)가 결합되어 교착 상태 발생. MySQL 프로토콜처럼 요청이 여러 TCP 세그먼트로 분할될 때 항상 발생 가능하다.
해결:
// Node.js MySQL2 드라이버const mysql = require("mysql2");const conn = mysql.createConnection({ host: "db.internal", // ...});// 연결 후 TCP_NODELAY 설정conn.on("connect", () => { conn.stream.setNoDelay(true); // TCP_NODELAY 활성화});
// 또는 커넥션 풀 생성 시const pool = mysql.createPool({ // ... enableKeepAlive: true, keepAliveInitialDelay: 10000,});# MySQL 서버 측에서도 확인mysql -h db.internal -e "SHOW STATUS LIKE 'Bytes_received'"# 적용 전후 레이턴시 비교 (mysqlslap 또는 sysbench)mysqlslap --concurrency=50 --iterations=100 --auto-generate-sql \ --host=db.internal --auto-generate-sql-load-type=mixed참고: 자주 쓰는 진단 명령어 모음
섹션 제목: “참고: 자주 쓰는 진단 명령어 모음”# TCP 상태 및 성능 통계ss -ti # 모든 TCP 연결 상세 정보ss -tan state time-wait | wc -l # TIME_WAIT 개수ss -tan state established | wc -l # ESTABLISHED 개수netstat -s | grep -E 'retransmit|segment' # 재전송 통계
# 커널 파라미터 확인sysctl -a | grep tcp_ # 모든 TCP 파라미터sysctl net.ipv4.tcp_congestion_control # 현재 혼잡 제어 알고리즘
# 패킷 캡처sudo tcpdump -i eth0 -nn port 80 -c 1000 -w /tmp/http.pcapsudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-rst) != 0' # RST 패킷만
# 실시간 소켓 모니터링watch -n 1 'ss -s' # 소켓 요약 통계
# 라우팅 및 MTUip route show # 라우팅 테이블 + initcwnd 확인ip link show eth0 # MTU 확인tracepath google.com # 경로상 MTU 탐색 (PMTUD)주요 명령 예상 출력:
# 현재 혼잡 제어 알고리즘 확인sysctl net.ipv4.tcp_congestion_control예상 출력:
net.ipv4.tcp_congestion_control = bbr# TCP 연결 상태 요약ss -s예상 출력:
Total: 1245TCP: 385 (estab 152, closed 198, orphaned 0, timewait 194)
Transport Total IP IPv6* 1245 - -RAW 0 0 0UDP 8 5 3TCP 187 143 44# 특정 연결의 상세 TCP 정보ss -ti dst 10.0.1.100예상 출력:
State Recv-Q Send-Q Local PeerESTAB 0 0 10.0.0.10:8080 10.0.1.100:54321 cubic wscale:7,7 rto:208 rtt:8.2/2.1 ato:40 mss:1448 pmtu:1500 cwnd:30 ssthresh:120 bytes_sent:8294400 bytes_acked:8294400 bytes_received:2048 segs_out:5730 segs_in:32 send 42.6Mbps pacing_rate 51.1Mbps delivery_rate 42.6Mbps# 재전송 통계netstat -s | grep -E "retransmit|Retransmit"예상 출력:
1234 segments retransmitted 0 fast retransmits 2 retransmits in slow start 0 SACK retransmits failed📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 Coping with TCP TIME-WAIT — Vincent Bernat — TIME_WAIT 상태, tw_reuse vs tw_recycle 차이, NAT 환경 주의사항을 가장 명확하게 설명한 글 (중급)
- 📖 TCP BBR: Exploring TCP Congestion Control — Andree Toonk — BBR의 4단계 상태 머신과 실제 벤치마크 결과. 이 문서의 5-3절 BBR과 직접 연결됨 (중급)
- 📖 BBR: Congestion-Based Congestion Control — Google/ACM Queue — Google이 BBR을 개발한 배경과 YouTube에서의 성능 개선 사례. 공식 논문 수준 (고급)
- 📖 TCP Congestion Control — Wikipedia — Reno, Cubic, BBR 알고리즘 간 비교와 상태 전이 다이어그램 (입문)
- 📖 Linux TCP Tuning — linux-admins.net — sysctl 파라미터 전체 목록과 역할 설명. 소켓 버퍼, TIME_WAIT, Keep-Alive 튜닝 레퍼런스 (중급)