혼잡 신호
TCP: 패킷 손실, duplicate ACK, RTT 증가, ECN(Explicit Congestion Notification)
API: 429, Retry-After, queue depth, p95 latencyL2 선수 지식: 3-way handshake, 4-way close, Sliding Window,
rwnd,MSS/MTU, 기본 Slow Start는 L2tcp-udp-internals.mdx에서 다뤘다. 이 문서는 그 위에서 “TCP가 얼마나 보내도 되는지 어떻게 판단하는가”와 “느린 네트워크를 어떤 신호로 분해할 것인가”를 다룬다.
첫 회독 기준: 처음에는 모든 커널 파라미터를 외우지 않는다.
rwnd,cwnd,RTT,MSS,BDP,retrans,TIME_WAIT가 각각 어떤 병목을 가리키는지만 잡으면 된다. 튜닝 명령은 마지막 선택 부록으로 미뤄도 본문 이해가 끊기지 않는다.
흐름 제어(Flow Control) 는 수신자가 감당할 수 있는 만큼만 보내게 하는 장치다. 수신자의 버퍼가 작거나 애플리케이션이 read()를 늦게 호출하면 송신자는 네트워크가 멀쩡해도 더 보낼 수 없다.
혼잡 제어(Congestion Control) 는 중간 경로가 감당할 수 있는 만큼만 보내게 하는 장치다. 라우터, 스위치, 가상 NIC 큐, 무선 구간, ISP 경로 중 어딘가가 포화되면 송신자는 수신자 버퍼가 넉넉해도 속도를 줄여야 한다.
두 제어는 모두 “지금 ACK 없이 떠 있어도 되는 데이터 양”을 제한한다. 차이는 누가 제한하는가다.
실제 전송 가능량 = min(rwnd, cwnd)
rwnd(receive window) 수신자가 ACK에 실어 보내는 수신 가능 공간. 목적: 수신자 버퍼와 애플리케이션을 보호한다.
cwnd(congestion window) 송신자 커널이 내부적으로 계산하는 혼잡 윈도우. 목적: 중간 네트워크 경로를 보호한다.ACK(Acknowledgement)는 수신자가 “여기까지 받았다”고 알려주는 확인 신호다. MSS(Maximum Segment Size)는 TCP 세그먼트 하나에 담을 수 있는 최대 데이터 크기다. Ethernet MTU(Maximum Transmission Unit) 1500 bytes에서 IPv4 헤더 20 bytes와 TCP 헤더 20 bytes를 빼면 일반적인 MSS는 1460 bytes가 된다.
RTT(Round Trip Time)는 송신자가 데이터를 보낸 뒤 그 데이터에 대한 ACK를 받기까지 왕복 시간이다. TCP의 전송률은 대역폭만으로 결정되지 않고, RTT 동안 얼마나 많은 데이터를 in-flight로 유지할 수 있는지에 크게 좌우된다.
L2의 Sliding Window는 수신자 버퍼를 넘치지 않게 하는 데 충분하다. 하지만 송신자와 수신자 사이에는 여러 큐가 있다.
송신 애플리케이션 -> 송신 커널 버퍼 -> 송신 NIC 큐 -> 스위치/라우터 큐 -> 인터넷/클라우드 백본 -> 수신 NIC 큐 -> 수신 커널 버퍼 -> 수신 애플리케이션수신자의 rwnd가 10MB라고 해서 중간 경로가 10MB의 in-flight 데이터를 안전하게 받아줄 수 있다는 뜻은 아니다. 반대로 중간 경로가 10Gbps를 처리할 수 있어도 수신 애플리케이션이 느리게 읽으면 rwnd가 작아져 송신자는 멈춘다.
이 차이를 모르면 느린 전송을 모두 “네트워크 문제”로 부르게 된다. 실제로는 다음 셋이 서로 다르다.
| 관찰된 현상 | 더 가까운 병목 | 먼저 볼 개념 |
|---|---|---|
cwnd가 작고 손실 뒤 회복이 느리다 | 중간 경로 혼잡 또는 손실 기반 회복 한계 | 혼잡 제어 |
rcv_space나 rwnd가 작다 | 수신 커널 버퍼 또는 애플리케이션 read 지연 | 흐름 제어 |
send는 높지만 rtt가 계속 부풀어 오른다 | 큐가 길어지는 bufferbloat | 혼잡 신호 해석 |
| 새 연결마다 첫 응답이 늦다 | handshake, TLS, Slow Start 반복 | 연결 재사용 |
| 작은 요청만 40ms 단위로 튄다 | Nagle + Delayed ACK 상호작용 | 작은 패킷 지연 |
여기서 TLS handshake는 HTTPS 보안 채널을 만들기 전에 인증서 검증과 키 합의를 수행하는 왕복 절차다. 이 문서에서는 “새 연결마다 반복되는 왕복 비용”으로만 다루고, 암호화와 인증의 세부 의미는 다음 tls-https.mdx에서 다룬다.
BDP(Bandwidth-Delay Product)는 대역폭과 RTT를 곱한 값이다. 경로 위에 동시에 떠 있어야 파이프를 꽉 채울 수 있는 데이터 양을 뜻한다.
BDP = bandwidth x RTT
예: 1Gbps 링크, RTT 100ms1,000,000,000 bits/s x 0.100s = 100,000,000 bits100,000,000 bits / 8 = 12.5MB이 경로에서 송신자가 ACK 없이 12.5MB 정도를 계속 띄워 놓을 수 있어야 1Gbps를 채운다. cwnd나 송신/수신 버퍼가 1MB 수준이면 링크가 1Gbps여도 실제 처리량은 훨씬 낮다.
이 예제는 TCP 튜닝의 핵심 오해를 깨준다. 대역폭을 샀다고 처리량이 자동으로 나오지 않는다. RTT가 크면, 같은 처리량을 내기 위해 더 큰 in-flight 데이터와 더 큰 버퍼가 필요하다.
초기 혼잡 윈도우가 IW10(Initial Window 10)이면 첫 RTT에 보낼 수 있는 데이터는 대략 10 x MSS다. MSS를 1460 bytes로 보면 약 14.6KB다.
초기 cwnd = 10 MSS ~= 14.6KB
HTML 응답 12KB: 첫 RTT 안에 대부분 전달 가능
HTML 응답 30KB: 첫 RTT: 약 14.6KB ACK 이후 두 번째 RTT: 나머지 데이터TTFB(Time to First Byte)나 SSR(Server-Side Rendering) 응답을 볼 때도 이 감각이 필요하다. 서버 렌더링 자체가 빠르더라도 새 TCP 연결, TLS handshake, Slow Start가 반복되면 사용자는 첫 바이트를 늦게 받는다. 이 문서의 초점은 프론트엔드 최적화가 아니라, 그 지연을 TCP 개념으로 분해하는 것이다.
Reno 계열 TCP는 패킷 손실과 중복 ACK를 혼잡 신호로 삼았다. 이 방식은 작은 버퍼와 낮은 대역폭이 일반적이던 환경에서는 충분히 실용적이었다. 큐가 차면 패킷이 떨어지고, 손실이 보이면 송신자가 속도를 줄이면 됐기 때문이다.
문제는 현대 네트워크에서 손실이 항상 혼잡을 뜻하지 않는다는 점이다.
따라서 TCP 혼잡 제어의 진화는 “윈도우를 더 크게 만든다”가 아니다. 핵심 질문은 무엇을 혼잡 신호로 볼 것인가다.
Reno: 손실과 3 duplicate ACK를 주 신호로 본다. 손실 이후 cwnd를 줄이고 천천히 회복한다.
CUBIC: 여전히 손실 기반이지만, 시간의 3차 함수로 손실 전 윈도우 근처까지 빠르게 회복한다. 고BDP 경로에서 Reno보다 훨씬 빠르게 파이프를 다시 채운다.
BBR(Bottleneck Bandwidth and Round-trip propagation time): 손실 자체보다 병목 대역폭과 최소 RTT를 추정한다. pacing으로 송신 속도를 조절해 큐를 짧게 유지하려 한다.Google Cloud의 BBR 소개 글은 10Gbps 서버 링크, RTT 100ms, 손실률 1% 조건에서 CUBIC이 약 3.3Mbps까지 떨어진 반면 BBR은 9,100Mbps 이상을 유지한 사례를 제시한다. 마지막 마일 10Mbps, RTT 40ms, 1,000패킷 버퍼 조건의 큐잉 지연도 CUBIC 1090ms 대 BBR 43ms로 비교한다. 이 숫자는 일반적인 데이터센터 내부 경로의 보편값이 아니라, 손실 기반 제어가 잘못된 신호를 믿을 때 얼마나 크게 무너질 수 있는지 보여주는 학습용 극단 사례다.
TCP 송신자는 미래의 네트워크 상태를 알 수 없다. 그래서 작은 시도를 보내고, 돌아오는 피드백으로 다음 전송량을 조정한다.
보낸다 -> ACK를 받는다 -> RTT와 손실을 본다 -> cwnd를 조정한다 -> 다시 보낸다이 루프에서 중요한 것은 “한 번의 값”이 아니라 변화 방향이다.
| 피드백 | 의미 | 과하게 단순화하면 생기는 오해 |
|---|---|---|
| ACK가 제때 돌아온다 | 경로가 현재 전송량을 처리하고 있다 | ACK가 온다고 무한히 늘려도 되는 것은 아니다 |
| 3 duplicate ACK가 온다 | 일부 세그먼트가 빠졌지만 뒤 세그먼트는 도착했다 | 완전 단절은 아니므로 RTO만큼 가혹하게 줄일 필요는 없다 |
| RTO가 발생한다 | 일정 시간 ACK가 없어 더 심각한 손실로 본다 | 타임아웃은 가장 비싼 신호라 회복 비용이 크다 |
| RTT가 기준보다 오른다 | 큐가 쌓이고 있을 가능성이 있다 | RTT 증가는 혼잡뿐 아니라 경로 변경, CPU 지연도 포함할 수 있다 |
| delivery rate가 떨어진다 | 병목 대역폭 추정이 낮아질 수 있다 | 애플리케이션이 적게 보내서 낮아진 것과 구분해야 한다 |
RTO(Retransmission Timeout)는 재전송 타이머다. ACK가 일정 시간 안에 오지 않으면 송신자는 세그먼트가 사라졌다고 보고 재전송한다. 3 duplicate ACK는 같은 ACK 번호가 반복해서 오는 현상이다. 이는 “빠진 세그먼트 뒤의 데이터는 도착했다”는 의미라서, 타임아웃을 기다리지 않고 빠르게 복구할 수 있다.
이 철학은 TCP 밖에서도 반복된다. API Rate Limit, Circuit Breaker, 스트림 백프레셔도 모두 “신호를 보고 보내는 양을 조절한다”는 점에서 같은 구조다.
TCP: 패킷 손실, duplicate ACK, RTT 증가, ECN(Explicit Congestion Notification)
API: 429, Retry-After, queue depth, p95 latencyTCP: cwnd x MSS = ACK 없이 떠 있을 수 있는 데이터
Stream: request(N), queue capacity, worker concurrencyTCP: Slow Start, Congestion Avoidance, Fast Recovery
API: exponential backoff, jitter, circuit half-openTCP: BDP = bandwidth x RTT
서비스: Little's Law의 N = lambda x W이 비교는 비유가 아니라 읽기 프레임이다. 새 시스템을 만났을 때 “신호”, “허용량”, “회복”, “용량 추정”을 분리하면 튜닝값을 외우기 전에 구조를 먼저 볼 수 있다.
rwnd(receive window)는 수신자가 알려주는 값이다. TCP 헤더의 Window Size 필드와 Window Scaling 옵션으로 표현된다. 기본 Window Size 필드는 16bit라 최대 65,535 bytes만 직접 표현할 수 있고, 고BDP 경로에서는 이 값이 너무 작다. Window Scaling은 “창 크기 값을 몇 배로 해석할지”를 합의해 긴 RTT·큰 대역폭 경로에서도 충분히 큰 rwnd를 표현하게 해준다.
cwnd(congestion window)는 송신자가 계산하는 값이다. 수신자가 직접 알려주지 않는다. 송신 커널은 ACK, 손실, RTT, 알고리즘 상태를 보고 “중간 경로가 이 정도는 견딜 것 같다”고 추정한다.
예시 1: 수신자 병목rwnd = 64KBcwnd = 4MB실제 전송 가능량 = 64KB
해석: 네트워크가 더 받을 수 있어도 수신자 버퍼나 애플리케이션이 병목이다. 혼잡 제어 알고리즘을 바꿔도 효과가 작다.
예시 2: 네트워크 병목rwnd = 8MBcwnd = 128KB실제 전송 가능량 = 128KB
해석: 수신자는 넉넉하지만 송신자가 경로를 신뢰하지 못한다. 손실, RTT 증가, BDP, 혼잡 제어 상태를 봐야 한다.이 작은 공식 때문에 TCP 병목 분석의 첫 질문은 항상 같다.
세 번째 질문도 중요하다. 애플리케이션이 10KB만 보내고 있다면 cwnd가 작아 보이지 않을 수 있고, delivery rate도 의미 있는 병목 신호가 아니다.
ss -ti 같은 도구는 cwnd, rtt, retrans, delivery_rate, rcv_space를 보여준다. 하지만 숫자 하나만으로 결론을 내리면 위험하다.
| 필드 | 풀어 쓴 뜻 | 공부할 때의 해석 |
|---|---|---|
cwnd:10 | 혼잡 윈도우가 10 MSS | 초기 연결 또는 손실 후 회복 초입일 수 있다 |
ssthresh | Slow Start Threshold | 이 값 이상에서는 더 보수적으로 늘어난다 |
rtt:1.2/0.6 | 평균 RTT와 편차 | 편차가 커지면 큐잉 또는 경로 불안정을 의심한다 |
retrans:0/0 | 현재/누적 재전송 | 손실 기반 병목의 직접 신호 중 하나다 |
rcv_space | 수신 버퍼 여유 추정 | 작으면 수신 측 흐름 제어 병목을 의심한다 |
delivery_rate | 최근 실제 전달률 | BBR류 알고리즘의 병목 대역폭 추정과 연결된다 |
좋은 진단은 필드 하나를 외우는 것이 아니라 조합을 본다.
cwnd 작음 + retrans 증가 + rtt 편차 증가: 중간 경로 혼잡 또는 손실 기반 회복 문제 가능성
rcv_space 작음 + 애플리케이션 처리 지연: 수신자 버퍼 또는 read 루프 병목 가능성
delivery_rate 낮음 + 애플리케이션이 조금만 보냄: TCP 문제가 아니라 송신 workload가 부족한 상태일 수 있음Slow Start라는 이름은 오해를 부른다. 실제로는 RTT마다 cwnd가 거의 두 배로 증가하는 가장 빠른 증가 구간이다. 느린 것은 증가 속도가 아니라, 처음에 경로 용량을 모르는 상태에서 작은 값으로 시작한다는 점이다.
초기 cwnd = 10 MSS라고 가정
RTT 0: cwnd = 10 MSSRTT 1: ACK 10개 수신 -> cwnd ~= 20 MSSRTT 2: ACK 20개 수신 -> cwnd ~= 40 MSSRTT 3: ACK 40개 수신 -> cwnd ~= 80 MSS이 증가가 계속되면 언젠가는 경로의 실제 용량을 넘는다. 그래서 ssthresh(Slow Start Threshold)라는 경계가 있다. cwnd가 ssthresh에 도달하거나 손실이 발생하면 TCP는 Congestion Avoidance로 넘어가 더 조심스럽게 증가한다.
cwnd ^80| *40| *20| *10| * 1| * +------------------------> RTT 0 1 2 3실무에서 새 연결이 느린 이유는 이 탐색을 매번 다시 하기 때문이다. HTTP Keep-Alive, HTTP/2 multiplexing, DB connection pool은 모두 “새 연결과 Slow Start를 반복하지 말자”는 의미가 있다.
Congestion Avoidance는 이미 한 번 위험 지점에 가까워졌다고 보고, RTT마다 대략 1 MSS씩 선형으로 늘린다. Reno의 기본 모형에서는 손실이 보이면 cwnd를 줄이고 다시 천천히 올라간다.
Slow Start: 10 -> 20 -> 40 -> 80 MSS
Congestion Avoidance: 80 -> 81 -> 82 -> 83 MSS이 차이는 고BDP 경로에서 크게 보인다. 12.5MB BDP를 채워야 하는 경로에서 손실 후 cwnd가 절반으로 줄면, 1 MSS씩 회복하는 데 수천 RTT가 필요할 수 있다. CUBIC이 등장한 이유가 여기에 있다.
패킷 손실 감지는 크게 두 가지다.
RTO(Retransmission Timeout): 일정 시간 ACK가 오지 않는다. 경로가 심각하게 막혔다고 보고 cwnd를 크게 줄인다.3 duplicate ACK: 같은 ACK 번호가 반복된다. 빠진 세그먼트 뒤 데이터는 도착했으므로, 누락 세그먼트만 빠르게 재전송한다.송신자 전송: [1][2][3][4][5]3번 세그먼트 손실
수신자 응답: ACK(3) -> 다음으로 3번을 기대함 ACK(3) -> 4번이 왔지만 3번이 없어 여전히 3번을 기대 ACK(3) -> 5번이 왔지만 3번이 없어 여전히 3번을 기대 ACK(3) -> 같은 ACK가 반복되며 duplicate ACK가 쌓임
송신자: 3번을 즉시 재전송Fast Recovery는 여기서 Slow Start로 완전히 돌아가지 않게 해준다. 뒤 세그먼트가 도착했다는 사실은 경로가 완전히 죽지 않았다는 신호다. 그래서 Reno는 대략 ssthresh = cwnd / 2로 낮추고, 복구 후 Congestion Avoidance로 돌아간다.
이 메커니즘의 경계도 중요하다. 중복 ACK가 아니라 RTO가 발생했다면 송신자는 더 심각한 문제로 보고 훨씬 보수적으로 돌아간다. 같은 “재전송”이어도 Fast Retransmit와 RTO는 실패 신호의 강도가 다르다.
퀴즈
힌트: 두 윈도우 중 작은 값이 실제 상한이다.
수신자 버퍼보다 중간 경로 혼잡이나 손실 기반 회복 문제를 먼저 의심한다. 실제 전송 가능량은 min(rwnd, cwnd)이므로 작은 cwnd가 전체 전송량을 제한한다.
TCP Reno는 전통적인 손실 기반 알고리즘이다. 패킷 손실을 혼잡의 강한 신호로 보고, 손실 이후 cwnd를 줄인 뒤 선형으로 회복한다.
Reno의 직관: ACK가 잘 온다 -> 조금 더 보낸다 손실이 난다 -> 너무 많이 보냈다고 보고 줄인다 3 duplicate ACK -> 절반쯤 줄이고 빠르게 복구한다 RTO -> 더 심각하므로 크게 후퇴한다Reno는 간단하고 안정적이지만, 고BDP 경로에서 회복이 느리다.
BDP = 1Gbps x 100ms = 12.5MBMSS ~= 1460 bytes
12.5MB / 1460 ~= 8,562 MSS
손실 후 cwnd가 4,281 MSS로 줄고,Congestion Avoidance에서 RTT마다 1 MSS씩 늘어난다면,손실 전 수준으로 돌아가는 데 약 4,281 RTT가 필요하다.
RTT가 100ms이면: 4,281 x 0.1s = 428.1s이 계산은 단순화된 학습 모델이지만, 왜 “긴 RTT + 큰 대역폭”에서 Reno가 답답해지는지 보여준다.
CUBIC은 Linux에서 널리 쓰이는 기본 혼잡 제어 알고리즘이다. 이름처럼 시간의 3차 함수(cubic function)를 사용해 cwnd를 증가시킨다.
W(t) = C x (t - K)^3 + W_max
W_max: 직전 손실 직전의 cwnd
t: 마지막 손실 이후 경과 시간
C, K: 곡선 형태를 정하는 파라미터수식을 외울 필요는 없다. 학습 핵심은 곡선의 모양이다.
cwnd ^ | * W_max 근처 | * | * | * | * | * +--------------------------------> 시간 손실 직후 빠르게 회복 W_max 근처에서는 조심스럽게 탐색CUBIC은 Reno보다 고BDP 경로를 빨리 다시 채운다. 하지만 여전히 손실 기반이다. 큐가 충분히 깊으면 손실이 나기 전에 RTT가 크게 부풀 수 있고, CUBIC은 그 지연을 늦게 알아차릴 수 있다. 이 현상을 bufferbloat라고 부른다. 버퍼가 “좋은 완충재”가 아니라 “지연을 숨기는 큐”가 되는 상황이다.
BBR(Bottleneck Bandwidth and Round-trip propagation time)은 손실 자체보다 병목 대역폭과 최소 RTT를 추정한다. 손실이 나야만 속도를 줄이는 모델이 아니라, 경로가 실제로 전달할 수 있는 속도와 지연을 측정해 pacing한다.
pacing은 데이터를 한 번에 몰아 보내지 않고 일정한 속도로 펴서 보내는 것이다. BBR은 “파이프를 채우되 큐는 길게 만들지 않는다”는 목표를 가진다.
BBR의 간단한 모델:
1. 최근 delivery rate로 병목 대역폭을 추정한다.2. 최근 min RTT로 큐가 비었을 때의 전파 지연을 추정한다.3. BDP = 병목 대역폭 x min RTT를 계산한다.4. 그 정도의 in-flight 데이터만 유지하도록 pacing rate와 cwnd를 조절한다.BBR의 대표 상태는 다음과 같이 읽으면 된다.
| 상태 | 목적 | 첫 독서 기준 |
|---|---|---|
STARTUP | 병목 대역폭을 빠르게 찾는다 | Slow Start처럼 빠르게 올리지만 목표는 bandwidth 추정이다 |
DRAIN | STARTUP에서 쌓인 큐를 비운다 | 일부러 덜 보내 큐잉 지연을 줄인다 |
PROBE_BW | 평상시 대역폭을 탐색한다 | 빠르게 보내는 RTT와 느리게 보내는 RTT를 섞는다 |
PROBE_RTT | 최소 RTT를 다시 측정한다 | 잠시 in-flight를 낮춰 큐가 빈 지연을 본다 |
BBR의 장점은 노이즈 손실과 혼잡 손실을 덜 혼동한다는 점이다. 무선 손실 1%가 있어도 delivery rate가 유지된다면, BBR은 “경로 용량이 무너진 것은 아니다”라고 볼 수 있다.
하지만 BBR도 만능이 아니다.
token bucket policer는 정해진 속도까지는 토큰을 써서 통과시키고, 허용량을 넘는 burst는 drop 또는 mark하는 rate limiter다. 이런 경로에서는 BBR의 대역폭 탐색 구간이 중간 손실을 만들 수 있다.손실과 duplicate ACK를 중심 신호로 삼는다. 이해하기 쉽지만 고BDP 경로에서 손실 후 회복이 느리다.
학습 기준선, 단순한 손실 기반 제어를 이해할 때손실 기반이지만 3차 함수로 손실 전 cwnd 근처까지 빠르게 회복한다. Linux 기본값으로 흔하다.
일반 서버, 데이터센터 내부, 기본값을 존중할 때병목 대역폭과 최소 RTT를 추정하고 pacing으로 큐를 짧게 유지하려 한다.
장거리, 무선 손실, 고BDP 경로에서 CUBIC 회복이 느릴 때선택 기준은 “최신 알고리즘을 켠다”가 아니다. 먼저 병목을 분류해야 한다.
| 상황 | 더 자연스러운 선택 | 이유 |
|---|---|---|
| 데이터센터 내부, RTT가 매우 낮고 손실이 적다 | CUBIC 기본값 유지 | 튜닝 이득보다 회귀 위험이 클 수 있다 |
| 리전 간 파일 전송, RTT 30~100ms, 대역폭이 큰데 처리량이 낮다 | BDP 계산 후 버퍼와 알고리즘 검토 | cwnd/버퍼가 파이프를 못 채울 수 있다 |
| 모바일/무선 손실이 잦다 | BBR 검토 | 손실을 혼잡으로만 보지 않는 모델이 유리할 수 있다 |
| p99 RTT가 늘고 큐잉 지연이 의심된다 | BBR 또는 pacing 검토 | bufferbloat를 줄이는 방향이 중요하다 |
| 상대 서비스나 네트워크가 policer를 쓴다 | 신중한 canary | 처리량만 보면 BBR 회귀를 놓칠 수 있다 |
Nagle 알고리즘은 작은 TCP 세그먼트가 너무 많이 생기는 문제를 줄이기 위해 송신 측에서 데이터를 잠깐 모아 보내는 규칙이다. 작은 패킷을 흔히 tinygram이라고 부른다.
Nagle의 단순 규칙:
1. 보낼 데이터가 MSS 이상이면 즉시 보낸다.2. 아직 ACK를 받지 못한 in-flight 데이터가 없으면 즉시 보낸다.3. in-flight 데이터가 있고 새 데이터가 MSS보다 작으면 ACK가 올 때까지 모은다.Delayed ACK는 수신자가 ACK만 담긴 작은 패킷을 줄이기 위해 ACK 전송을 잠깐 미루는 방식이다. Linux 환경에서 흔히 40ms 수준의 지연으로 관찰된다. 목적은 ACK를 다른 데이터와 함께 보내거나 여러 ACK를 합치는 것이다.
각각은 효율을 위한 장치다. 문제는 둘이 만나면 서로를 기다릴 수 있다는 점이다.
클라이언트(Nagle ON) 서버(Delayed ACK ON) | | |-- write(header, 100B) -------------> | 수신, ACK 지연 타이머 시작 | | |-- write(body, 50B) | | Nagle: 이전 데이터 ACK가 없고 | | body가 MSS보다 작으므로 대기 | | | | 서버: 더 올 데이터가 있나 기다림 | | | ~ 40ms 대기 ~ | | | | <------------------------------ ACK | | | |-- body 전송 -----------------------> |이 현상은 평균 지연보다 tail latency에서 먼저 보인다. p50은 괜찮은데 p95나 p99가 40ms 단위로 튀면, 작은 write 패턴과 Nagle/Delayed ACK 조합을 의심할 수 있다.
TCP_NODELAY는 Nagle 알고리즘을 비활성화하는 소켓 옵션이다. 이름은 “TCP 지연 없음”처럼 보이지만, 모든 지연을 없애는 마법이 아니다. 작은 write를 즉시 세그먼트로 내보내도록 허용할 뿐이다.
| 상황 | 판단 | 이유 |
|---|---|---|
| 실시간 채팅, 게임, SSH, 요청/응답이 작고 지연 민감 | 켜는 쪽이 자연스럽다 | 40ms 대기가 사용자 경험에 직접 보인다 |
| DB/MySQL/Redis 클라이언트의 작은 왕복 요청 | 켜거나 드라이버 기본값 확인 | 작은 패킷 왕복이 많다 |
| HTTP/2, gRPC | 보통 켜는 쪽이 자연스럽다 | 스트림 단위 지연이 중요하고 구현체가 이미 조절한다 |
| 대용량 파일 전송 | 끄거나 기본값 존중 | 작은 패킷보다 throughput과 batching 효율이 중요하다 |
| 애플리케이션이 이미 한 번에 큰 버퍼를 write | 영향이 작다 | MSS 이상 데이터는 Nagle과 무관하게 바로 나간다 |
반례를 기억하자. TCP_NODELAY를 켠다고 대역폭이 늘어나는 것은 아니다. 작은 요청의 대기 시간을 줄이는 선택이지, 고BDP 링크의 파이프를 채우는 선택은 아니다. 고BDP 처리량 문제는 cwnd, rwnd, 소켓 버퍼, 알고리즘, BDP를 봐야 한다.
Nagle 문제는 소켓 옵션만의 문제가 아니다. 애플리케이션이 아주 작은 조각을 여러 번 write()하면 문제를 만들 확률이 높다.
Bad pattern: write(header 100B) write(body 50B) flush()
Better pattern: write(header + body 150B) flush()즉, 선택 순서는 보통 다음이다.
TCP_NODELAY를 기본으로 켜는지 본다.TCP_NODELAY를 적용하고 요청 수, 패킷 수, tail latency를 같이 본다.TIME_WAIT는 연결을 먼저 닫은 쪽이 일정 시간 기다리는 TCP 상태다. MSL(Maximum Segment Lifetime)은 세그먼트가 네트워크에 살아 있을 수 있는 최대 시간이고, 2MSL은 그 두 배다.
TIME_WAIT가 필요한 이유는 두 가지다.
이유 1: 마지막 ACK 유실 대비
클라이언트가 서버의 FIN에 ACK를 보냈다.그 ACK가 네트워크에서 사라졌다.서버는 FIN을 다시 보낸다.클라이언트가 TIME_WAIT에 남아 있으면 다시 ACK할 수 있다.
CLOSED로 바로 사라졌다면: 서버는 예상치 못한 RST(reset)를 볼 수 있다.이유 2: 오래된 세그먼트가 새 연결에 섞이는 것 방지
이전 연결: src IP:port A -> dst IP:port B
같은 4-tuple로 새 연결이 너무 빨리 생김: 지연된 이전 세그먼트가 새 연결에 도착할 수 있다.
2MSL 대기: 이전 세그먼트가 네트워크에서 사라질 시간을 준다.따라서 TIME_WAIT가 많다는 사실만으로 장애는 아니다. 연결을 많이 만들고 닫는 서비스에서는 정상적으로 많을 수 있다. 문제는 로컬 ephemeral port가 고갈될 정도로 단기 연결을 반복하는 경우다.
ephemeral port는 클라이언트가 아웃바운드 연결을 만들 때 임시로 쓰는 로컬 포트다. Linux에서는 설정에 따라 범위가 달라지지만, 넓혀 잡아도 대략 수만 개 수준의 유한한 자원이다. 같은 대상 IP:port로 짧은 연결을 매우 많이 만들면 TIME_WAIT가 이 포트를 점유해 새 연결을 만들지 못할 수 있다.
| 상황 | 해석 | 더 먼저 할 일 |
|---|---|---|
| TIME_WAIT가 많지만 에러가 없다 | 정상일 수 있다 | 연결 생성률과 포트 사용률을 같이 본다 |
cannot assign requested address가 난다 | ephemeral port 고갈 의심 | 커넥션 풀, keep-alive, 로컬 포트 범위 확인 |
| 서버 인바운드 TIME_WAIT가 많다 | 서버가 active close를 많이 한다 | 프로토콜 종료 방향과 idle timeout 확인 |
| CLOSE_WAIT가 계속 늘어난다 | 애플리케이션이 close를 안 한 버그 가능성 | 코드의 socket close 누락을 먼저 본다 |
tw_recycle을 켜고 싶다 | 위험한 선택 | NAT 환경 패킷 드롭 때문에 사용하지 않는다 |
tcp_tw_reuse와 tcp_tw_recycle은 이름이 비슷하지만 경계가 다르다. tw_reuse는 조건이 맞는 아웃바운드 TIME_WAIT 소켓 재사용을 돕는 설정으로 설명되는 경우가 많다. 반면 tw_recycle은 NAT 뒤 여러 클라이언트의 timestamp가 단조 증가하지 않는 문제로 패킷 드롭을 일으킬 수 있어 Linux 4.12에서 제거됐다. 학습 기준은 간단하다. TIME_WAIT를 없애려 하지 말고, 단기 연결을 줄이는 구조를 먼저 본다.
TCP Keep-Alive는 유휴 연결이 살아 있는지 확인하기 위해 probe를 보내는 커널 기능이다. HTTP Keep-Alive는 같은 TCP 연결로 여러 HTTP 요청을 보내는 애플리케이션/프로토콜 수준의 연결 재사용이다. 이름은 같지만 층이 다르다.
TCP Keep-Alive: 유휴 TCP 연결이 죽었는지 확인한다. NAT, 방화벽, 미들박스가 조용히 끊은 연결을 감지하는 데 쓰인다.
HTTP Keep-Alive: 같은 TCP 연결로 여러 HTTP 요청/응답을 처리한다. handshake, TLS, Slow Start 반복을 줄인다.기본 TCP Keep-Alive 감지 시간은 매우 길 수 있다.
예시 기본값:tcp_keepalive_time = 7200초tcp_keepalive_intvl = 75초tcp_keepalive_probes = 9
총 감지 시간 = 7200 + 75 x 9 = 7875초 ~= 2.2시간클라우드 로드밸런서, NAT Gateway, 방화벽은 이보다 훨씬 짧은 idle timeout을 가질 수 있다. 그래서 “연결이 살아 있다고 믿고 썼는데 이미 중간 장비가 버렸다”는 실패가 생긴다.
ALB(Application Load Balancer)의 기본 idle timeout을 60초라고 하자. Node.js HTTP 서버의 keepAliveTimeout이 5초라면 서버가 먼저 연결을 닫을 수 있다. ALB는 아직 재사용 가능한 연결이라고 생각하고 새 요청을 보냈는데, 서버 쪽에서는 이미 닫힌 연결이면 502가 산발적으로 발생할 수 있다.
ALB idle timeout = 60초서버 keepAliveTimeout = 5초
0초: 요청 처리 완료, 연결 idle
5초: 서버가 연결 종료
30초: ALB는 연결이 살아 있다고 보고 재사용 시도
결과: 서버는 닫힌 연결로 보거나 RST(reset)를 보냄 ALB 502가 산발적으로 발생해결의 핵심은 “무조건 timeout을 길게”가 아니다. 연결의 양 끝과 중간 장비의 idle timeout 순서를 맞추는 것이다. 일반적으로 백엔드 서버 timeout은 ALB idle timeout보다 약간 길게 두어 ALB가 먼저 연결 생애주기를 관리하게 만든다.
커넥션 풀은 DB, Redis, 외부 API 같은 대상과 TCP 연결을 재사용한다. 이득은 세 가지다.
하지만 풀이 크다고 항상 좋은 것은 아니다.
| 풀 설정 | 너무 작을 때 | 너무 클 때 |
|---|---|---|
| DB pool max | 요청이 pool 대기열에서 기다린다 | DB max_connections와 메모리를 소모한다 |
| HTTP agent max sockets | 외부 API 호출이 직렬화된다 | 상대 서비스 rate limit과 로컬 포트를 압박한다 |
| idle timeout | 연결 재사용 이득이 줄어든다 | 죽은 연결을 오래 붙잡거나 중간 장비 timeout과 충돌한다 |
| pre-ping/recycle | 죽은 연결 감지가 늦다 | 너무 자주 검사하면 요청 전 오버헤드가 늘어난다 |
첫 기준은 “동시 요청 수와 대상 서비스의 처리량”이다. TCP 관점에서 풀은 빠른 마법이 아니라 연결 수, handshake 비용, TIME_WAIT, 대상 서버 부하를 한꺼번에 조절하는 backpressure 장치다.
송신 버퍼(SO_SNDBUF)는 애플리케이션이 write()한 데이터가 네트워크로 나가기 전 머무는 공간이다. 수신 버퍼(SO_RCVBUF)는 네트워크에서 도착한 데이터가 애플리케이션 read() 전까지 머무는 공간이다.
송신 애플리케이션 write() -> send buffer -> 네트워크 -> recv buffer -> read()수신 애플리케이션수신 버퍼가 작으면 수신자는 작은 rwnd를 알린다. 그러면 송신자는 cwnd가 충분히 커도 rwnd 때문에 멈춘다.
rwnd = recv buffer 여유 공간cwnd = 경로 혼잡 기준 전송 허용량
실제 전송 가능량 = min(rwnd, cwnd)서울과 도쿄 사이에 10Gbps 링크가 있고 RTT가 30ms라고 하자.
BDP = 10,000,000,000 bits/s x 0.030s = 300,000,000 bits = 37.5MB이론적으로 파이프를 꽉 채우려면 수십 MB 수준의 in-flight 데이터가 필요하다. TCP 수신 버퍼 최대값이 6MB라면 단순 계산상 다음과 같은 상한이 생긴다.
throughput ~= window / RTT
6MB / 0.030s = 200MB/s ~= 1.6Gbps
10Gbps 링크를 샀어도,윈도우가 6MB 수준이면 10Gbps를 채우기 어렵다.이 계산은 “버퍼를 무조건 128MB로 올려라”가 아니다. 먼저 현재 경로의 목표 처리량과 RTT를 넣어 필요한 윈도우 크기를 계산하라는 뜻이다.
버퍼 확대가 의미 있는 신호는 다음처럼 조합으로 나타난다.
retrans가 크지 않고, RTT도 폭발적으로 증가하지 않는다.rwnd 또는 수신 버퍼 한계가 BDP보다 작다.이 경우에는 tcp_rmem, tcp_wmem, rmem_max, wmem_max, 자동 수신 버퍼 조정(tcp_moderate_rcvbuf)을 검토할 수 있다. 하지만 값 자체는 환경 의존적이다. 첫 독서에서는 “BDP보다 작은 버퍼가 throughput 상한이 될 수 있다”는 원리를 잡으면 충분하다.
버퍼가 너무 크면 큐가 길어진다. 손실은 줄어들 수 있지만 지연이 커진다. 특히 CUBIC 같은 손실 기반 알고리즘은 손실이 나기 전까지 큐가 길어지는 것을 늦게 알아차릴 수 있다.
작은 버퍼: 손실은 빨리 보일 수 있다. 지연은 상대적으로 짧다.
큰 버퍼: 손실은 늦게 보일 수 있다. 큐잉 지연이 커져 p95/p99 RTT가 나빠질 수 있다.따라서 버퍼 튜닝의 관측값은 throughput 하나가 아니다.
| 목표 | 같이 봐야 할 값 | 실패 신호 |
|---|---|---|
| 단일 흐름 처리량 증가 | delivery rate, cwnd, rwnd, BDP | 처리량은 올랐지만 p99 RTT가 2배 이상 증가 |
| 손실 감소 | retrans, duplicate ACK | 손실은 줄었지만 큐잉 지연이 늘어 사용자 지연 악화 |
| 수신자 병목 완화 | rcv_space, 애플리케이션 read 지연 | 버퍼만 키우고 read 루프 병목은 그대로 |
| 리전 간 전송 개선 | 동일 시간대 baseline 비교 | 다른 트래픽의 latency가 악화 |
운영 절차를 외우기 전에 순서를 외우는 편이 낫다.
rwnd, cwnd, delivery_rate, retrans, rtt를 관찰한다.이 순서는 튜닝을 느리게 만들려는 것이 아니다. TCP는 공유 자원을 다루기 때문에 한 노드의 처리량 개선이 다른 흐름의 지연 악화로 바뀔 수 있다.
| 증상 | 의미 | 먼저 의심할 개념 |
|---|---|---|
retrans가 계속 증가한다 | 세그먼트 손실 또는 혼잡 신호 | 혼잡 제어, MTU/링크 손실 |
rtt 평균보다 편차가 크게 증가한다 | 큐잉 또는 경로 불안정 | bufferbloat, pacing |
cwnd가 낮은 값에서 자주 리셋된다 | 손실/RTO로 회복이 반복된다 | Reno/CUBIC 회복, 경로 손실 |
rcv_space가 작다 | 수신자 버퍼 여유가 작다 | 흐름 제어, 애플리케이션 read |
| TIME_WAIT가 포트 범위에 가깝다 | 단기 아웃바운드 연결 과다 | keep-alive, connection pool |
| CLOSE_WAIT가 누적된다 | 앱이 close를 호출하지 않았다 | 소켓 생애주기 버그 |
| p99만 40ms 단위로 튄다 | 작은 패킷 지연 가능성 | Nagle + Delayed ACK |
| ALB 502가 산발적이고 재시도 성공 | idle timeout 불일치 가능성 | Keep-Alive, 연결 종료 방향 |
관찰: 서울 -> 도쿄 전송이 10Gbps 링크인데 5MB/s 수준 RTT ~= 32ms cwnd가 수십 MSS 수준
손계산: BDP = 10Gbps x 32ms = 40MB cwnd 45 MSS ~= 65KB
해석: 파이프를 채우려면 수십 MB in-flight가 필요한데, 현재 cwnd/버퍼가 그보다 훨씬 작다.
대응 방향: 애플리케이션 송신량, rwnd, 소켓 버퍼, 혼잡 제어 알고리즘을 순서대로 본다. 바로 BBR을 켜기 전에 CUBIC에서도 버퍼 상한이 병목인지 확인한다.관찰: p50은 낮지만 일부 요청이 40ms 근처로 튄다. 요청 payload가 작고 write가 둘 이상으로 쪼개진다.
해석: Nagle이 두 번째 작은 write를 잡고, 서버 Delayed ACK가 첫 write의 ACK를 미루는 교착 가능성.
대응 방향: write를 합칠 수 있는지 본다. 드라이버의 TCP_NODELAY 기본값을 확인한다. 적용 전후 p95/p99와 패킷 수를 같이 본다.관찰: ALB access log에는 502가 산발적으로 보인다. 재시도하면 성공한다. 애플리케이션 에러 로그는 빈약하다.
해석: 서버가 ALB보다 먼저 keep-alive 연결을 닫고, ALB가 그 연결을 재사용하려 했을 가능성.
대응 방향: ALB idle timeout과 서버 keepAliveTimeout/headersTimeout의 순서를 맞춘다. 연결을 오래 살리는 것만 보지 말고 pool 크기와 idle connection 수를 같이 본다.최소 관계식은 headersTimeout > keepAliveTimeout이고, 외부 프록시나 ALB의 idle timeout과도 맞아야 한다. 서버가 프록시보다 먼저 연결을 닫으면 재사용 시점에 502가 생기고, 반대로 너무 오래 유지하면 idle connection과 메모리 사용량이 늘 수 있다.
TCP 튜닝의 성공 기준은 단일 메트릭이 아니다.
| 바꾼 것 | 좋아졌다고 말하려면 | 나빠졌다고 봐야 할 신호 |
|---|---|---|
| CUBIC -> BBR | 처리량 증가와 RTT 안정이 같이 보인다 | 처리량은 늘었지만 retrans나 p99 RTT가 크게 증가 |
| 버퍼 확대 | BDP 상한이 완화되고 tail latency가 유지된다 | 큐잉 지연과 메모리 압박이 증가 |
| TCP_NODELAY | 작은 요청 p95/p99가 줄고 패킷 폭증이 없다 | 패킷 수만 늘고 latency 개선이 없다 |
| Keep-Alive 증가 | handshake 반복과 TIME_WAIT가 줄어든다 | 죽은 연결 재사용, idle connection 과다 |
| Pool 확대 | pool 대기 시간이 줄고 대상 서버가 버틴다 | DB 연결 수, 메모리, lock 경합이 증가 |
BBR은 좋은 도구지만, 손실 기반 알고리즘이 항상 나쁘다는 뜻은 아니다. 낮은 RTT, 낮은 손실, 충분한 버퍼, 안정적인 데이터센터 내부 경로에서는 CUBIC 기본값이 더 예측 가능할 수 있다.
BBR을 검토할 때는 최소한 다음을 같이 본다.
qdisc(queueing discipline)는 Linux 네트워크 스택에서 패킷 큐를 관리하는 규칙이다. BBR은 pacing과 함께 동작할 때 의도한 효과가 잘 나오므로, 알고리즘 이름만 보고 판단하면 안 된다.
BDP보다 작은 버퍼는 throughput 상한이 될 수 있다. 그러나 버퍼를 키우면 큐잉 지연도 커질 수 있다. 처리량이 중요한 백업 전송과 지연이 중요한 API 트래픽은 같은 튜닝값을 공유하면 안 된다.
반례:
리전 간 백업 전송: 목표: 큰 파일을 빨리 보낸다. 버퍼 확대와 BBR이 도움이 될 수 있다.
사용자 API 트래픽: 목표: p99 latency를 낮게 유지한다. 큰 큐는 tail latency를 악화할 수 있다.TIME_WAIT는 정상 종료의 안전 대기다. 줄여야 할 것은 TIME_WAIT 자체가 아니라, 불필요한 단기 연결 생성일 때가 많다. 풀링과 keep-alive가 먼저고, 커널 knob는 그다음이다.
반대로 CLOSE_WAIT는 다르다. CLOSE_WAIT는 상대가 FIN을 보냈는데 로컬 애플리케이션이 소켓을 닫지 않은 상태다. 시간이 지나도 계속 쌓이면 코드 버그나 리소스 누수를 의심해야 한다.
Keep-Alive는 handshake와 Slow Start를 줄인다. 하지만 너무 긴 idle connection은 죽은 연결을 오래 붙잡거나, 중간 장비 timeout과 충돌하거나, 대상 서버의 연결 슬롯을 낭비할 수 있다.
좋은 설정은 길이 자체가 아니라 timeout의 정렬이다.
클라이언트 pool idle timeout < 중간 장비 idle timeout < 서버가 기대하는 keep-alive 생애주기실제 값은 환경마다 다르다. 중요한 것은 어느 쪽이 먼저 닫는지, 닫힌 연결을 누가 재사용하려 하는지다.
SSR 응답의 첫 바이트가 늦을 때 TCP를 보면 도움이 된다. 하지만 이 문서는 React SSR 튜닝 문서가 아니다. TTFB를 DNS, TCP, TLS, 서버 처리, Slow Start, CDN 경로로 분해할 수 있으면 충분하다.
TTFB ~= DNS + TCP handshake + TLS handshake + 서버 처리 + 첫 응답 전송초기 HTML이 14.6KB 근처인지, 연결 재사용이 되는지, CDN과 origin 사이에 새 연결이 반복되는지는 TCP 관점의 좋은 질문이다. 하지만 컴포넌트 렌더링, hydration, route splitting은 별도 프론트엔드 성능 주제다.
TCP 문제를 만나면 다음 순서로 좁힌다.
1. 애플리케이션이 충분히 보내고/읽고 있는가? 아니면 TCP가 아니라 앱 처리량 문제일 수 있다.
2. rwnd가 작은가? 수신자 버퍼나 read 루프 병목을 먼저 본다.
3. cwnd가 작은가? 손실, RTT, 혼잡 제어 상태를 본다.
4. RTT가 기준보다 계속 높아지는가? 큐잉 지연과 bufferbloat를 본다.
5. retrans가 증가하는가? 손실, MTU, 경로 품질, policer를 본다.
6. 새 연결이 너무 많은가? Keep-Alive, pooling, TIME_WAIT, ephemeral port를 본다.| 질문 | CUBIC 유지 쪽 신호 | BBR 검토 쪽 신호 |
|---|---|---|
| RTT | 매우 낮고 안정적 | 리전 간, 모바일, 위성처럼 크거나 변동 큼 |
| 손실 | 거의 없고 baseline 안정 | 손실이 있는데 혼잡이 아닐 가능성 큼 |
| 처리량 | 목표를 이미 만족 | BDP 대비 단일 흐름 처리량이 낮음 |
| 지연 | p99가 중요하고 안정적 | 큐잉 지연을 줄여야 함 |
| 운영 위험 | 기본값을 바꿀 이유 부족 | canary와 rollback 기준이 준비됨 |
| 조치 | 먼저 만족해야 할 조건 | 피해야 할 적용 |
|---|---|---|
| 소켓 버퍼 확대 | BDP가 현재 버퍼보다 크고 앱이 충분히 빠름 | p99 지연이 더 중요한 공유 API 경로에 무턱대고 적용 |
| Pool 확대 | pool 대기 시간이 병목이고 대상 서버 여유 있음 | DB 연결 수 한계와 메모리 압박을 무시 |
| Keep-Alive 유지 시간 조정 | 중간 장비 timeout과 순서가 맞지 않음 | 죽은 연결을 오래 붙잡는 방향 |
| TCP_NODELAY | 작은 요청 tail latency가 문제 | 대용량 전송 throughput 개선 목적으로 적용 |
| BBR 전환 | 고BDP/손실/큐잉 지연 문제가 관측됨 | baseline 없이 전체 노드에 즉시 적용 |
TCP 튜닝은 시간대와 트래픽 패턴의 영향을 크게 받는다. 비교 조건을 맞추지 않으면 “좋아진 것처럼 보이는” 착시가 많다.
같은 대상: 같은 리전, 같은 endpoint, 같은 path
같은 workload: 같은 payload 크기, 같은 동시성, 같은 전송 시간
같은 관측 창: p50/p95/p99, retrans, RTT, delivery rate, error rate
같은 rollback 기준: 처리량 +10%라도 p99 RTT 2배면 실패로 본다min(rwnd, cwnd)다. 수신자 병목과 네트워크 병목을 먼저 나눈다.BDP = bandwidth x RTT는 고대역폭 경로를 채우는 데 필요한 in-flight 데이터 감각을 준다.rwnd가 64KB이고 cwnd가 4MB라면 병목은 어느 쪽에 더 가까운가?TCP_NODELAY가 throughput 튜닝이 아니라 tail latency 튜닝에 가까운 이유는 무엇인가?rwnd가 더 작으므로 수신자 버퍼 또는 애플리케이션 read 병목에 가깝다.1,000,000,000 bits/s x 0.1s = 100,000,000 bits = 12.5MB다.cwnd를 줄이고 RTT마다 대략 1 MSS씩 선형 회복하기 때문이다.본문의 개념을 이해하는 데 아래 명령이 필수는 아니다. 실제 환경을 볼 때 “어떤 신호를 어디서 보나”를 확인하는 짧은 관찰용이다.
ss -ti dst 10.0.0.1관찰 포인트:
cwnd: 혼잡 윈도우rtt: 평균 RTT와 편차retrans: 재전송delivery_rate: 최근 전달률rcv_space: 수신 공간 추정해석은 단일 필드가 아니라 조합으로 한다. cwnd가 작아도 애플리케이션이 적게 보내는 상황이면 혼잡이라고 단정하지 않는다.
sysctl net.ipv4.tcp_congestion_controlsysctl net.ipv4.tcp_available_congestion_control관찰 포인트:
주의:
# 변경 전후 같은 대상, 같은 시간 창에서 비교ss -ti dst api.partner.example.com | grep -E "bbr|cubic|rtt|retrans|send|delivery_rate"성공 신호:
delivery_rate 또는 실제 처리량 증가retrans 급증 없음회귀 신호:
ss -tan state time-wait | wc -lss -tan state close-wait | wc -lss -tan | awk '{print $1}' | sort | uniq -c | sort -rn관찰 포인트:
# Linux TCP keepalive 기본값sysctl net.ipv4.tcp_keepalive_timesysctl net.ipv4.tcp_keepalive_intvlsysctl net.ipv4.tcp_keepalive_probes관찰 포인트:
sudo tcpdump -i eth0 -nn port 3306 -w /tmp/mysql.pcap관찰 포인트:
TCP_NODELAY 적용 전후 p95/p99가 줄었는지주의:
BDP(bytes) = bandwidth(bits/s) x RTT(seconds) / 8
예: bandwidth = 10Gbps RTT = 30ms = 0.030s
10,000,000,000 x 0.030 / 8 = 37,500,000 bytes ~= 37.5MB비교:
cwnd x MSSrwndtcp_rmem/tcp_wmem 최대값sysctl net.core.rmem_max net.core.wmem_max net.ipv4.tcp_rmem net.ipv4.tcp_wmem관찰 포인트:
tcp_rmem/tcp_wmem 최대값이 지나치게 낮지 않은가?tw_reuse, tw_recycle 경계