TCP 프로토콜 심화
Linux TCP 프로토콜: tcp_sock, 상태 머신, 혼잡 제어(CUBIC/BBR), kTLS, 재전송 메커니즘, Zero-Copy, 성능 튜닝 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
TCP 심화 — 커널 내부 메커니즘
TCP는 11개의 상태로 구성된 유한 상태 머신(FSM)으로 동작합니다. 모든 TCP 소켓은 아래 다이어그램의 상태 중 하나에 있으며, 각 상태 전이는 커널의 tcp_rcv_state_process()와 tcp_set_state()에서 처리됩니다:
/* include/net/tcp_states.h — TCP 소켓 상태 열거 */
enum {
TCP_ESTABLISHED = 1, /* 데이터 송수신 가능 */
TCP_SYN_SENT = 2, /* SYN 전송, SYN+ACK 대기 */
TCP_SYN_RECV = 3, /* SYN+ACK 전송, ACK 대기 */
TCP_FIN_WAIT1 = 4, /* FIN 전송, ACK 대기 */
TCP_FIN_WAIT2 = 5, /* FIN ACK 수신, 상대 FIN 대기 */
TCP_TIME_WAIT = 6, /* 2×MSL 대기 (경량 소켓) */
TCP_CLOSE = 7, /* 소켓 미사용 */
TCP_CLOSE_WAIT = 8, /* 상대 FIN 수신, 앱 close() 대기 */
TCP_LAST_ACK = 9, /* FIN 전송, 마지막 ACK 대기 */
TCP_LISTEN = 10, /* 연결 대기 */
TCP_CLOSING = 11, /* 동시 종료: 양쪽 FIN, ACK 대기 */
};
/* 상태 전이 핵심 함수: net/ipv4/tcp_input.c */
/*
* tcp_rcv_state_process():
* → ESTABLISHED 이외 상태에서의 패킷 처리
* → SYN_SENT → ESTABLISHED (클라이언트)
* → SYN_RECV → ESTABLISHED (서버)
* → FIN_WAIT_1/2, CLOSE_WAIT, LAST_ACK 등 종료 처리
*
* tcp_rcv_established():
* → ESTABLISHED 상태의 고속 데이터 처리 (Fast Path)
*
* tcp_set_state():
* → 상태 변경 + tracepoint 발생 (tcp:tcp_set_state)
* → /proc/net/tcp에 반영
*/
3-Way Handshake와 커널 구조체
- TCP 연결 수립 과정의 커널 내부 */
- 1단계: 클라이언트 SYN 전송 */
- tcp_v4_connect() → tcp_connect() → tcp_transmit_skb()
- → TCP_SYN_SENT 상태 전이
- skb에 SYN 플래그 + 초기 시퀀스 번호(ISN) + MSS/Window Scale 옵션 설정
- ISN: secure_tcp_seq() → siphash 기반 (예측 불가)
- 2단계: 서버 SYN+ACK 수신 */
- tcp_v4_rcv() → tcp_v4_do_rcv() → tcp_rcv_state_process()
- LISTEN 소켓에서 수신 → request_sock (미니 소켓) 생성
- → inet_csk_reqsk_queue_hash_add()로 SYN 큐에 추가
- → SYN+ACK 응답 전송
- 3단계: 클라이언트 ACK 수신 */
- 서버: tcp_check_req() → 전체 struct sock 생성
- → inet_csk_complete_hashdance()로 accept 큐에 이동
- → TCP_ESTABLISHED 상태
- Listen 소켓의 큐 구조:
- SYN 큐 (반개방 연결): request_sock으로 관리
- → 크기: /proc/sys/net/ipv4/tcp_max_syn_backlog
- Accept 큐 (완전 연결): listen() backlog 인자로 제한
- → 크기: min(backlog, /proc/sys/net/core/somaxconn)
SYN Cookie 메커니즘
SYN Flood 공격 시 SYN 큐가 가득 차면 SYN Cookie가 발동합니다. 서버 상태를 저장하지 않고 SYN+ACK의 시퀀스 번호에 연결 정보를 인코딩합니다:
/* net/ipv4/syncookies.c */
/* SYN Cookie ISN 생성: */
/* ISN = hash(saddr, daddr, sport, dport, count) + (count << 24)
* + (MSS 인덱스 << 접근자)
*
* 인코딩 정보:
* - 타임스탬프 (분 단위 카운터, 상위 비트)
* - MSS 값 (8개 고정 값 중 하나로 양자화)
* - 상대방 IP/포트 해시
*
* ACK 수신 시: ISN 검증 → request_sock 없이 직접 sock 생성
*/
/* 제약사항:
* - Window Scale, SACK, Timestamp 옵션 정보 손실
* → TCP 성능 저하 가능 (큰 윈도우, SACK 불가)
* - 커널 4.4+: TCP_SAVED_SYN으로 일부 완화
* - tcp_syncookies = 1: SYN 큐 overflow 시에만 활성화 (권장)
* - tcp_syncookies = 2: 항상 활성화 (성능 저하 감수)
*/
/* sysctl 설정 */
/* net.ipv4.tcp_syncookies = 1 (기본: overflow 시 활성화) */
/* net.ipv4.tcp_max_syn_backlog = 4096 (SYN 큐 크기) */
/* net.core.somaxconn = 4096 (accept 큐 크기) */
Window Scaling과 수신 윈도우
/* TCP 윈도우: 16비트 필드 → 최대 65535바이트 */
/* Window Scale 옵션 (RFC 7323): 3-way handshake 시 협상 */
/* 실제 윈도우 = header의 window × 2^(scale factor) */
/* 최대 scale factor = 14 → 최대 윈도우 = 65535 × 16384 ≈ 1GB */
/* net/ipv4/tcp_output.c */
static u16 tcp_select_window(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 가용 수신 버퍼 크기를 윈도우로 광고 */
u32 cur_win = tcp_receive_window(tp);
/* 윈도우 축소 방지 (RFC 규칙) */
if (new_win < cur_win)
new_win = cur_win;
/* Window Scale 적용 */
return new_win >> tp->rx_opt.rcv_wscale;
}
/* 커널 자동 튜닝 (tcp_rmem) */
/* net.ipv4.tcp_rmem = 4096 131072 6291456
* min default max
* 커널이 RTT와 BDP(Bandwidth-Delay Product)에 따라
* 수신 버퍼를 default~max 범위에서 자동 조절
* → tcp_moderate_rcvbuf=1 (기본) 일 때 활성화
*/
SACK (Selective Acknowledgment)
/* SACK: 수신자가 비연속적으로 받은 블록을 명시적으로 알림 */
/* → 송신자가 손실된 세그먼트만 정확히 재전송 가능 */
/* TCP 헤더 옵션으로 SACK 블록 전달 (최대 4블록) */
/* [Kind=5] [Length] [Left Edge 1][Right Edge 1] [Left Edge 2][Right Edge 2] ... */
/* net/ipv4/tcp_input.c */
static int tcp_sacktag_write_queue(struct sock *sk,
const struct sk_buff *ack_skb, u32 prior_snd_una, ...)
{
/* SACK 블록을 파싱하여 재전송 큐의 skb에 마킹 */
/* TCPCB_SACKED_ACKED: 상대가 수신 확인한 블록 */
/* TCPCB_SACKED_RETRANS: 재전송된 블록 */
/* TCPCB_LOST: 손실로 판단된 블록 → 재전송 대상 */
}
/* SACK 관련 sysctl */
/* net.ipv4.tcp_sack = 1 (기본 활성화) */
/* net.ipv4.tcp_dsack = 1 (D-SACK: 중복 수신 알림) */
/* net.ipv4.tcp_fack = 0 (FACK: 6.x에서 제거됨) */
/* SACK 없이 3-duplicate ACK만 사용하면:
* 연속 손실 시 하나씩 재전송 → 복구 느림
* SACK 활성화 시:
* 손실된 세그먼트를 한 RTT 내에 모두 재전송 가능
*/
TCP Timestamps와 PAWS
TCP Timestamp 옵션(RFC 7323)은 세 가지 핵심 기능을 제공합니다: 정밀 RTT 측정, PAWS(Protection Against Wrapped Sequences), TIME_WAIT 소켓 재사용. 현대 TCP에서 거의 필수적인 옵션입니다:
/* TCP Timestamp 옵션 (Kind=8, Length=10) */
/* +-------+-------+-----------+-----------+
* | Kind | Len | TSval | TSecr |
* | 8 | 10 | (4 bytes) | (4 bytes) |
* +-------+-------+-----------+-----------+
*
* TSval (Timestamp Value): 송신자의 현재 타임스탬프
* TSecr (Timestamp Echo Reply): 수신한 TSval을 그대로 에코
*
* 3-way handshake 시 양쪽 모두 Timestamp 옵션을 포함해야 활성화
*/
/* === 1. 정밀 RTT 측정 === */
/*
* 기존 방식: 세그먼트 전송 시각을 기록하고 ACK 수신 시 차이 계산
* → 재전송된 세그먼트의 RTT는 모호함 (어떤 전송에 대한 ACK인지?)
* → Karn의 알고리즘: 재전송 세그먼트의 RTT 샘플 무시
*
* Timestamp 방식:
* → 송신: 세그먼트에 TSval = 현재 시각
* → 수신: ACK에 TSecr = 수신한 TSval (에코)
* → 송신: RTT = 현재 시각 - TSecr
* → 재전송 여부와 무관하게 정확한 RTT 측정
* → 모든 ACK에서 RTT 샘플 수집 가능 (기존은 비행 중 1개만)
*
* 커널 구현: tcp_rtt_estimator()에서 Timestamp 기반 RTT 우선 사용
*/
/* === 2. PAWS (Protection Against Wrapped Sequences) === */
/*
* 문제: 고속 링크에서 시퀀스 번호(32비트)가 짧은 시간에 순환
* → 10Gbps: ~3.4초면 4GB 순환
* → 이전 연결의 지연 패킷이 현재 연결의 유효 시퀀스로 해석될 수 있음
*
* PAWS 해결:
* → 타임스탬프를 시퀀스 번호의 "확장"으로 사용
* → 수신 시: TSecr이 최근 타임스탬프보다 오래되면 폐기
* → tcp_paws_check(): ts_recent - rcv_tsval > 24일 → 폐기
*
* 커널 구현 (net/ipv4/tcp_input.c):
* tcp_validate_incoming() → tcp_paws_check()
* → 타임스탬프가 역행하면 세그먼트 폐기 (오래된 패킷)
*/
/* net/ipv4/tcp_input.c */
static bool tcp_paws_check(const struct tcp_options_received *rx_opt,
int paws_win)
{
/* rx_opt→ts_recent: 최근 수신한 타임스탬프
* rx_opt→rcv_tsval: 현재 세그먼트의 타임스탬프
*
* ts_recent > rcv_tsval이면 → 오래된 세그먼트 → 폐기
* 단, ts_recent_stamp가 24일 이상 지나면 → 타임스탬프 리셋 허용
*/
if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win)
return true; /* 유효 */
if (get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)
return true; /* 24일 지나면 리셋 */
return false; /* 오래된 세그먼트 — 폐기 */
}
/* === 3. TIME_WAIT 소켓 재사용 === */
/*
* tcp_tw_reuse 옵션이 활성화되면:
* → TIME_WAIT 소켓의 타임스탬프보다 큰 TSval을 가진 SYN만 허용
* → 이전 연결의 지연 패킷과 새 연결의 패킷을 타임스탬프로 구분
* → 타임스탬프 없이는 재사용 불가 (안전하지 않음)
*
* 설정:
* net.ipv4.tcp_tw_reuse = 1 (활성화)
* net.ipv4.tcp_tw_reuse = 2 (loopback만, 기본값 커널 5.7+)
* → tcp_timestamps = 1 필수 (비활성화 시 tw_reuse 무효)
*/
/* sysctl */
/* net.ipv4.tcp_timestamps = 1 (기본: 활성화)
* → 비활성화 시: RTT 측정 정밀도 감소, PAWS 보호 없음,
* tw_reuse 불가, 각 세그먼트에 10바이트 절약
* → 일부 환경에서 비활성화하면 오히려 성능 저하
* → 비활성화는 권장하지 않음
*/
tcp_timestamps=0으로 설정하면 PAWS 보호가 해제되어 고속 링크에서 데이터 손상 위험이 있고, tcp_tw_reuse도 동작하지 않아 TIME_WAIT 소켓이 더 오래 남습니다. 헤더 10바이트 절약을 위해 비활성화하는 것은 대부분의 환경에서 부적절합니다.
TCP Keepalive
/* TCP Keepalive: 유휴 연결의 생존 여부 확인 */
/* net/ipv4/tcp_timer.c */
/* keepalive 타이머 동작:
* 1. 마지막 데이터 이후 tcp_keepalive_time 경과 → 첫 probe 전송
* 2. 응답 없으면 tcp_keepalive_intvl 간격으로 반복
* 3. tcp_keepalive_probes 회 응답 없으면 연결 종료 (RST)
*/
/* sysctl 기본값 */
/* net.ipv4.tcp_keepalive_time = 7200 (2시간 유휴 후 시작) */
/* net.ipv4.tcp_keepalive_intvl = 75 (75초 간격 probe) */
/* net.ipv4.tcp_keepalive_probes = 9 (9회 실패 시 종료) */
/* → 총 ~2시간 11분 후 연결 종료 */
/* 소켓별 설정 (sysctl 기본값 오버라이드) */
int optval = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
int idle = 60; /* 60초 유휴 후 시작 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
int interval = 10; /* 10초 간격 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
int maxpkt = 3; /* 3회 실패 시 종료 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &maxpkt, sizeof(maxpkt));
TCP Fast Open (TFO)
/* TCP Fast Open: SYN 패킷에 데이터를 포함하여 1-RTT 절감 */
/* RFC 7413, 커널 3.7+ */
/* 동작 원리:
* 1. 최초 연결: 일반 3-way handshake + 서버가 TFO 쿠키 발급
* 2. 이후 연결: SYN + 쿠키 + 데이터 → 서버 즉시 응답 가능
* → HTTP 요청/응답에서 1-RTT 절감
*/
/* sysctl 설정 */
/* net.ipv4.tcp_fastopen = 3
* 비트 0: 클라이언트 TFO 활성화
* 비트 1: 서버 TFO 활성화
* 비트 2: 쿠키 없이 TFO 허용 (보안 위험)
*/
/* 서버 측 */
int qlen = 5;
setsockopt(fd, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
/* qlen: TFO 대기열 크기 */
/* 클라이언트 측 */
sendto(fd, data, len, MSG_FASTOPEN,
(struct sockaddr *)&addr, sizeof(addr));
/* connect() 없이 첫 sendto()에서 SYN+데이터 전송 */
TCP 성능 관련 sysctl 종합
| sysctl | 기본값 | 설명 |
|---|---|---|
tcp_wmem |
4096 16384 4194304 | 전송 버퍼 (min/default/max). 자동 튜닝 범위 |
tcp_rmem |
4096 131072 6291456 | 수신 버퍼 (min/default/max). 자동 튜닝 범위 |
tcp_mem |
(시스템 메모리 기반) | TCP 전체 메모리 제한 (페이지 단위: low/pressure/high) |
tcp_moderate_rcvbuf |
1 | 수신 버퍼 자동 조절 활성화 |
tcp_window_scaling |
1 | Window Scale 옵션 (비활성화 시 최대 64KB) |
tcp_timestamps |
1 | Timestamp 옵션 (RTT 측정, PAWS 보호) |
tcp_tw_reuse |
2 | TIME_WAIT 소켓 재사용 (2: loopback+timestamp 조건부) |
tcp_fin_timeout |
60 | FIN_WAIT2 타임아웃 (초) |
tcp_max_tw_buckets |
262144 | TIME_WAIT 소켓 최대 수 (초과 시 즉시 종료) |
tcp_slow_start_after_idle |
1 | 유휴 후 슬로 스타트 재시작 (0: cwnd 유지) |
tcp_notsent_lowat |
UINT_MAX | 미전송 데이터 한계. epoll 통지 기준 (값 설정 시 쓰기 효율↑) |
tcp_ecn |
2 | ECN (0:비활성, 1:활성, 2:서버측만 응답) |
tcp_sock 핵심 구조체
struct tcp_sock은 struct inet_connection_sock을 확장하며, TCP 연결의 모든 상태를 관리합니다. 소켓 하나당 약 2KB 이상의 메모리를 차지합니다:
/* include/linux/tcp.h */
struct tcp_sock {
struct inet_connection_sock inet_conn;
/* === 시퀀스 번호 관리 === */
u32 snd_una; /* 전송 완료 확인된 첫 바이트 (send unacknowledged) */
u32 snd_nxt; /* 다음 전송할 시퀀스 번호 */
u32 snd_wnd; /* 상대방이 광고한 수신 윈도우 크기 */
u32 rcv_nxt; /* 다음 수신 기대 시퀀스 번호 */
u32 rcv_wnd; /* 광고할 수신 윈도우 크기 */
u32 write_seq; /* 유저가 write()한 마지막 바이트 다음 */
u32 copied_seq; /* 유저가 read()한 마지막 바이트 다음 */
/* === 혼잡 제어 상태 === */
u32 snd_cwnd; /* 혼잡 윈도우 (cwnd) — 패킷 단위 */
u32 snd_ssthresh; /* 슬로 스타트 임계값 */
u32 prior_cwnd; /* 손실 복구 전 cwnd (undo용) */
u32 prr_delivered; /* PRR 알고리즘: 복구 중 전달된 세그먼트 */
u32 prr_out; /* PRR: 복구 중 전송한 세그먼트 */
/* === RTT 측정 === */
u32 srtt_us; /* smoothed RTT (마이크로초 × 8) */
u32 mdev_us; /* RTT 편차 (마이크로초 × 4) */
u32 rttvar_us; /* RTT 분산 (RTO 계산용) */
u32 rto; /* 재전송 타임아웃 (jiffies) */
/* === 재전송 관리 === */
u32 retrans_out; /* 현재 네트워크에 있는 재전송 세그먼트 수 */
u32 lost_out; /* 손실로 판단된 세그먼트 수 */
u32 sacked_out; /* SACK 확인된 세그먼트 수 */
u8 reordering; /* 현재 관측된 재정렬 수준 */
/* === TCP 옵션 === */
struct tcp_options_received rx_opt;
/* .rcv_wscale 수신 Window Scale factor
* .snd_wscale 전송 Window Scale factor
* .tstamp_ok Timestamp 옵션 협상 여부
* .sack_ok SACK 옵션 협상 여부
* .wscale_ok Window Scale 협상 여부
*/
/* === Pacing === */
u64 tcp_mstamp; /* 가장 최근 전송 시각 */
u32 sk_pacing_rate; /* bytes/sec 단위 전송 속도 */
/* === 혼잡 제어 알고리즘 private 데이터 === */
u64 ca_priv[104 / sizeof(u64)];
/* CUBIC: bic_K, bic_origin_point, cnt 등
* BBR: bw[], min_rtt_us, mode, cycle_idx 등
*/
};
struct sock → struct inet_sock → struct inet_connection_sock → struct tcp_sock.
tcp_sk(sk) 매크로로 struct sock *에서 struct tcp_sock *로 캐스팅합니다. 각 계층이 프로토콜 독립적인 필드를 추가하는 상속 구조입니다.
TCP 연결 종료와 TIME_WAIT
TCP 연결 종료는 4-Way Handshake 또는 동시 종료(simultaneous close)로 진행됩니다. TIME_WAIT 상태는 지연 패킷 처리와 연결 식별자 재사용 방지를 위해 핵심적인 역할을 합니다:
/* TCP 4-Way Handshake 연결 종료 */
/* 능동 종료자 (Active Close) — close() 호출 측 */
/*
* ESTABLISHED → FIN_WAIT1 → FIN_WAIT2 → TIME_WAIT → CLOSED
*
* 1. close()/shutdown() 호출
* → tcp_close() → tcp_send_fin()
* → FIN 세그먼트 전송, 상태 → FIN_WAIT1
*
* 2. 상대방 ACK 수신
* → tcp_rcv_state_process() → FIN_WAIT2
* → tcp_fin_timeout (기본 60초) 타이머 시작
*
* 3. 상대방 FIN 수신
* → tcp_fin() → ACK 전송
* → TIME_WAIT 상태 전이
*
* 4. TIME_WAIT: 2 × MSL (Maximum Segment Lifetime) 동안 대기
* → Linux: 60초 고정 (TCP_TIMEWAIT_LEN)
* → 이유: 지연 패킷 흡수 + 마지막 ACK 재전송 보장
*/
/* 수동 종료자 (Passive Close) — FIN 수신 측 */
/*
* ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
*
* 1. 상대방 FIN 수신
* → ACK 자동 전송, 상태 → CLOSE_WAIT
* → 애플리케이션에 EOF(read()=0) 전달
*
* 2. 애플리케이션 close() 호출
* → tcp_send_fin(), 상태 → LAST_ACK
*
* 3. 마지막 ACK 수신
* → CLOSED, 소켓 해제
*/
/* TIME_WAIT 소켓 최적화 */
struct tcp_timewait_sock {
struct inet_timewait_sock tw_sk;
u32 tw_rcv_nxt; /* 기대 수신 시퀀스 */
u32 tw_snd_nxt; /* 마지막 전송 시퀀스 */
u32 tw_rcv_wnd; /* 수신 윈도우 */
u32 tw_ts_recent; /* 최근 타임스탬프 */
long tw_ts_recent_stamp;
};
/* → 전체 tcp_sock (~2KB) 대신 경량 구조체 (~240B) 사용
* → TIME_WAIT 소켓 수만 개에도 메모리 절약
*/
close()를 호출하지 않으면 CLOSE_WAIT 상태가 무한히 쌓입니다. 이는 애플리케이션 버그(FD 누수)이며, ss -s로 모니터링해야 합니다. 커널은 이 상태를 강제로 정리하지 않습니다.
# TIME_WAIT 관련 sysctl 튜닝
# TIME_WAIT 소켓 재사용 (동일 4-tuple에 한해)
net.ipv4.tcp_tw_reuse = 1
# 조건: Timestamp 옵션 활성화 + 이전 타임스탬프보다 큰 값
# 2: loopback에서만 활성화 (기본값, 커널 5.7+)
# TIME_WAIT 소켓 최대 수
net.ipv4.tcp_max_tw_buckets = 262144
# 초과 시 새 TIME_WAIT 즉시 종료 (로그: "time wait bucket table overflow")
# FIN_WAIT2 타임아웃 (orphan 소켓)
net.ipv4.tcp_fin_timeout = 30
# 기본 60초 → 고부하 서버에서 30초로 단축 권장
# orphan 소켓 최대 수
net.ipv4.tcp_max_orphans = 65536
# close() 후 아직 FIN 교환 중인 소켓
TCP 재전송 메커니즘
TCP의 신뢰성 보장 핵심은 재전송입니다. 커널은 타이머 기반 재전송(RTO)과 빠른 재전송(Fast Retransmit) 두 가지 메커니즘을 사용합니다:
/* === RTO (Retransmission Timeout) 계산 — RFC 6298 === */
/* net/ipv4/tcp_input.c: tcp_rtt_estimator() */
/* RTT 샘플 수집 (Timestamp 옵션 또는 전송 시각 기록) */
/*
* SRTT = (1 - α) × SRTT + α × RTT_sample (α = 1/8)
* RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample| (β = 1/4)
* RTO = SRTT + max(G, 4 × RTTVAR) (G = clock granularity)
*
* 커널 구현 (정수 연산, 스케일링):
* tp→srtt_us = srtt × 8 (마이크로초)
* tp→mdev_us = rttvar × 4 (마이크로초)
* tp→rto = jiffies 단위
*/
static void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
struct tcp_sock *tp = tcp_sk(sk);
long m = mrtt_us; /* 새 RTT 샘플 */
u32 srtt = tp->srtt_us;
if (srtt != 0) {
m -= (srtt >> 3); /* m = sample - srtt/8 */
srtt += m; /* srtt = 7/8 × srtt + 1/8 × sample */
if (m < 0) m = -m;
m -= (tp->mdev_us >> 2); /* mdev 갱신 */
tp->mdev_us += m;
} else {
/* 첫 번째 RTT 샘플 */
srtt = m << 3; /* srtt = sample × 8 */
tp->mdev_us = m << 1; /* mdev = sample × 2 */
tp->rttvar_us = max(tp->mdev_us, tcp_rto_min_us(sk));
}
tp->srtt_us = max(1U, srtt);
}
/* RTO 범위 제한 */
/* 최소: TCP_RTO_MIN = 200ms (HZ/5)
* 최대: TCP_RTO_MAX = 120초 (120*HZ)
* 초기: TCP_TIMEOUT_INIT = 1초 (SYN 재전송 시작값)
*/
/* === 재전송 타이머와 지수 백오프 === */
/* net/ipv4/tcp_timer.c: tcp_retransmit_timer() */
/*
* RTO 만료 시 동작:
* 1. snd_una 이후 첫 번째 미확인 skb를 재전송
* 2. RTO를 2배로 증가 (지수 백오프: exponential backoff)
* 3. snd_cwnd = 1 MSS (혼잡 윈도우 최소화)
* 4. snd_ssthresh = max(flight_size/2, 2)
* 5. 재전송 횟수 카운터 증가
*
* 최대 재전송 횟수:
* tcp_retries1 = 3 → 이 횟수 초과 시 라우팅 테이블 갱신 시도
* tcp_retries2 = 15 → 이 횟수 초과 시 연결 종료 (RST)
* → ~13~30분 (RTO 백오프에 따라 변동)
*
* SYN 재전송 횟수:
* tcp_syn_retries = 6 → 약 127초
* tcp_synack_retries = 5 → 약 63초
*/
void tcp_retransmit_timer(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
if (!tp->packets_out) /* 미확인 패킷 없으면 무시 */
return;
/* 재전송 실행 */
tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1);
/* 지수 백오프: RTO × 2 */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
min(tp->rto << 1, TCP_RTO_MAX), TCP_RTO_MAX);
}
/* === Fast Retransmit / Fast Recovery (RFC 5681, RFC 6675) === */
/*
* 빠른 재전송 (Fast Retransmit):
* 3개의 중복 ACK (duplicate ACK) 수신 시
* → RTO 만료를 기다리지 않고 즉시 재전송
* → SACK 기반: 3개 이상의 세그먼트가 SACK 확인되면 gap을 손실로 간주
*
* 빠른 복구 (Fast Recovery):
* → ssthresh = max(flight_size / 2, 2)
* → cwnd = ssthresh + 3 (중복 ACK 수만큼)
* → 중복 ACK마다 cwnd++ (새 세그먼트 전송 가능)
* → 새 ACK(snd_una 전진) 수신 시 cwnd = ssthresh, 복구 종료
*
* PRR (Proportional Rate Reduction, RFC 6937):
* → 커널 기본 복구 알고리즘 (3.2+)
* → 기존 Fast Recovery의 버스트 문제 해결
* → 손실 복구 중에도 일정한 비율로 세그먼트 전송
* → prr_delivered, prr_out으로 전송량 조절
*/
/* 손실 감지 상태 머신 (tcp_ca_state) */
enum tcp_ca_state {
TCP_CA_Open = 0, /* 정상 동작 (cwnd 증가) */
TCP_CA_Disorder = 1, /* 중복 ACK/SACK 감지 (아직 손실 미확정) */
TCP_CA_CWR = 2, /* ECN-Echo 수신 → cwnd 감소 중 */
TCP_CA_Recovery = 3, /* Fast Retransmit 진입 (SACK 기반 복구) */
TCP_CA_Loss = 4, /* RTO 만료 → cwnd=1, 전체 재전송 */
};
TLP (Tail Loss Probe)
TLP(Tail Loss Probe, 커널 3.10+)는 연결의 마지막 세그먼트가 손실된 경우를 빠르게 감지하는 메커니즘입니다. 기존 방식은 3 dupACK을 기다리거나 RTO 만료를 기다려야 하지만, 마지막 세그먼트 손실 시에는 추가 데이터가 없으므로 dupACK이 발생하지 않고 긴 RTO 대기만 남습니다. TLP는 이 상황을 1~2 RTT 안에 해결합니다:
/* net/ipv4/tcp_output.c — tcp_send_loss_probe() */
/* TLP (Tail Loss Probe) — RFC 8985, 커널 3.10+
*
* 트리거 조건:
* → 1개 이상의 미확인 세그먼트가 있음
* → 추가 전송 데이터 없음 (tail 상태)
* → cwnd에 여유 없음 (새 세그먼트 전송 불가)
*
* PTO (Probe Timeout) 계산:
* PTO = max(2 × SRTT + (delayed_ack_time ? : 1), 10ms)
* → RTO보다 훨씬 짧음 (보통 2~20ms vs 200ms+)
*
* TLP 동작:
* 1. PTO 만료 시 마지막 전송 세그먼트를 재전송 (probe)
* 2. probe에 대한 응답으로:
* a) 새 ACK(snd_una 전진) → 정상 (손실 아니었음)
* b) dupACK → 손실 확인 → Fast Retransmit 진입
* c) SACK → RACK으로 정확한 손실 감지
* 3. 응답 없으면 → RTO 전환 (기존 경로)
*
* 효과:
* → 마지막 세그먼트 손실 복구 시간: RTO(200ms+) → PTO(수 ms)
* → 짧은 HTTP 요청/응답에서 체감 성능 대폭 개선
* → TcpExtTCPLossProbes 카운터로 모니터링
*/
void tcp_send_loss_probe(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
/* 전송 큐의 마지막 skb를 probe로 재전송 */
skb = tcp_rtx_queue_tail(sk);
if (tcp_retransmit_skb(sk, skb, 1) > 0) {
/* 재전송 실패: RTO로 폴백 */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
tp->rto, TCP_RTO_MAX);
return;
}
/* TLP 카운터 증가 */
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPLOSSPROBES);
}
/* sysctl 설정 */
/* net.ipv4.tcp_early_retrans = 3 (기본, TLP 활성화)
* 비트 0: TLP 활성화
* 비트 1: Delay 제거 (ER-delay 없음)
* 0: TLP 비활성화
* 3: TLP + delay 없음 (기본값, 권장)
*/
혼잡 제어 심화
흐름 제어 vs 혼잡 제어
TCP에는 두 가지 독립적인 전송량 제한 메커니즘이 있습니다. 이 둘을 혼동하면 성능 튜닝이나 디버깅에서 잘못된 결론을 내리게 됩니다:
/* 실제 전송 가능 윈도우 = min(cwnd, rwnd)
*
* net/ipv4/tcp_output.c — tcp_cwnd_test() + tcp_snd_wnd_test()
*
* tcp_write_xmit()에서:
* flight_size = packets_out - (sacked_out + lost_out)
* effective_window = min(tp→snd_cwnd, tp→snd_wnd)
* if (flight_size < effective_window)
* → 전송 가능
*
* 디버깅 팁:
* ss -tnpi 출력에서:
* "cwnd:10" → 혼잡 윈도우 (10 MSS)
* "send 77Mbps" → min(cwnd, rwnd) × MSS / RTT
* "rcv_space:X" → 수신 윈도우 관련
* cwnd << rwnd → 혼잡 제어가 병목 (네트워크 문제)
* rwnd << cwnd → 흐름 제어가 병목 (수신자 버퍼 부족)
*/
혼잡 제어 기초 — Slow Start와 Congestion Avoidance
TCP 혼잡 제어의 핵심 목표는 네트워크 용량을 넘지 않으면서 가능한 한 빠르게 데이터를 전송하는 것입니다. 이를 위해 cwnd(congestion window)라는 변수가 전송 가능한 최대 바이트 수를 제한합니다.
Slow Start 단계에서 cwnd는 초기값 initcwnd(보통 10 MSS, RFC 6928)에서 시작하여, ACK를 받을 때마다 지수적으로 증가합니다. 하나의 ACK가 돌아오면 cwnd에 1 MSS를 더하므로, 한 RTT 동안 cwnd가 대략 2배가 됩니다:
RTT 0: cwnd = 10 MSS (10개 세그먼트 전송)
RTT 1: cwnd = 20 MSS (각 ACK마다 +1 MSS, 10개 ACK → +10)
RTT 2: cwnd = 40 MSS
RTT 3: cwnd = 80 MSS ← ssthresh 도달 시 Congestion Avoidance 전환
cwnd가 ssthresh(slow start threshold)에 도달하면 Congestion Avoidance 단계로 전환합니다. 이 단계에서 cwnd는 RTT당 약 1 MSS씩 선형적으로 증가합니다. 이것이 AIMD(Additive Increase / Multiplicative Decrease) 원리의 "AI" 부분입니다:
- Additive Increase: 손실 없으면 RTT당 cwnd += 1 MSS
- Multiplicative Decrease: 손실 감지 시 cwnd를 절반(또는 알고리즘별 비율)으로 감소
커널에서 이 로직은 tcp_cong_avoid_ai()에 구현되어 있으며, tcp_congestion_ops.cong_avoid 콜백을 통해 호출됩니다. ssthresh 초기값은 TCP_INFINITE_SSTHRESH(0x7FFFFFFF)로, 첫 손실 전까지 Slow Start가 계속됩니다.
ip route change default ... initcwnd 20으로 초기 cwnd를 늘릴 수 있습니다.
짧은 연결(HTTP 요청 등)에서는 Slow Start 단계에서 대부분의 데이터를 전송하므로, initcwnd 증가가 체감 성능에 큰 영향을 줍니다.
단, 과도한 값은 네트워크 혼잡을 유발할 수 있으므로 대역폭과 RTT를 고려하여 설정하세요.
손실 기반 혼잡 감지
전통적 TCP 혼잡 제어(Reno, CUBIC 등)는 패킷 손실을 혼잡의 신호로 해석합니다. 손실을 감지하는 세 가지 주요 메커니즘이 있습니다:
1. Fast Retransmit (3 dupACK)
수신자가 기대하는 순서와 다른 세그먼트를 받으면 중복 ACK를 전송합니다. 송신자가 동일한 ACK 번호를 3회 연속 수신하면, 해당 세그먼트가 손실되었다고 판단하고 RTO를 기다리지 않고 즉시 재전송합니다. 이때 Fast Recovery에 진입하여 cwnd를 절반으로 줄이고(tcp_ca_state = TCP_CA_Recovery), SACK 정보를 기반으로 선택적 재전송합니다.
2. RTO (Retransmission Timeout)
ACK가 일정 시간(RTO) 내에 도착하지 않으면 심각한 손실로 판단합니다. 이 경우 cwnd를 1 MSS로 리셋하고 Slow Start부터 재시작합니다(tcp_ca_state = TCP_CA_Loss). RTO는 SRTT(smoothed RTT)와 RTTVAR(RTT 변동)로 계산됩니다:
RTO = SRTT + max(G, 4 × RTTVAR) (RFC 6298)
최솟값: 200ms (net.ipv4.tcp_rto_min)
최댓값: 120s (net.ipv4.tcp_retries2 기반)
3. RACK (Recent ACKnowledgment)
커널 4.15+에서 기본 활성화된 시간 기반 손실 감지 알고리즘입니다. 가장 최근 ACK된 세그먼트의 전송 시각을 기준으로, min_rtt/4 이상 지난 미확인 세그먼트를 손실로 판단합니다. 3 dupACK 규칙보다 정확하며, reordering에도 강건합니다:
/* net/ipv4/tcp_recovery.c — tcp_rack_detect_loss() */
/* 판정 기준: 최근 ACK 수신 세그먼트 전송 시각 - 미확인 세그먼트 전송 시각
* > rack_rtt + rack_reo_wnd (= min_rtt/4)
* → 시간 기반이므로 순서 뒤바뀜(reordering)에 강건
* → net.ipv4.tcp_recovery = 1 (기본 활성화)
*/
손실 감지 방식에 따른 cwnd 반응:
| 감지 방식 | cwnd 변화 | tcp_ca_state | 심각도 |
|---|---|---|---|
| Fast Retransmit (3 dupACK) | cwnd × 0.5 (Reno) 또는 × 0.7 (CUBIC) | TCP_CA_Recovery | 중간 |
| RTO 만료 | cwnd = 1 MSS, ssthresh = cwnd/2 | TCP_CA_Loss | 심각 |
| RACK | Fast Recovery와 동일 | TCP_CA_Recovery | 중간 |
| ECN (CE 마킹) | 알고리즘별 상이 (보통 × 0.5~0.7) | TCP_CA_CWR | 경미 |
혼잡 제어 프레임워크 — tcp_congestion_ops
Linux의 혼잡 제어 프레임워크는 struct tcp_congestion_ops 인터페이스를 통해 알고리즘을 플러그인으로 교체할 수 있습니다. 이 구조체의 콜백 함수를 구현하면 커널 모듈로 자체 혼잡 제어 알고리즘을 등록할 수 있습니다:
/* include/net/tcp.h */
struct tcp_congestion_ops {
/* 필수 콜백 */
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
/* → ACK 수신 시 cwnd 조절 (Slow Start / Congestion Avoidance) */
u32 (*ssthresh)(struct sock *sk);
/* → 손실 감지 시 새 ssthresh 계산 */
/* 선택적 콜백 */
void (*init)(struct sock *sk);
void (*release)(struct sock *sk);
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
u32 (*undo_cwnd)(struct sock *sk);
u32 (*sndbuf_expand)(struct sock *sk);
/* BBR 등에서 사용 */
u32 (*min_tso_segs)(struct sock *sk);
void (*cong_control)(struct sock *sk, const struct rate_sample *rs);
/* → cong_control이 정의되면 cong_avoid/ssthresh 대신 호출
* BBR은 이 콜백에서 cwnd와 pacing_rate를 직접 설정 */
char name[TCP_CA_NAME_MAX];
struct module *owner;
};
CUBIC 알고리즘 내부
CUBIC(커널 2.6.19+)은 Linux의 기본 혼잡 제어 알고리즘으로, cwnd 증가를 3차 함수(cubic function)로 모델링합니다. 손실 직전 cwnd(Wmax)에 빠르게 접근하고, 그 지점을 넘어서면 천천히 탐색하는 오목→볼록(concave→convex) 패턴이 특징입니다.
/* net/ipv4/tcp_cubic.c — Linux 기본 혼잡 제어 (커널 2.6.19+) */
/* CUBIC 윈도우 함수:
* W(t) = C × (t - K)³ + Wmax
*
* C = 0.4 (스케일링 상수)
* K = ³√(Wmax × β / C) — 원점에서 Wmax까지 도달 시간
* β = 0.7 (손실 시 cwnd 감소 비율: new_cwnd = Wmax × 0.7)
* t = 마지막 손실 이후 경과 시간
*/
struct bictcp {
u32 cnt; /* cwnd 증가 속도 (ACK당 1/cnt MSS) */
u32 last_max_cwnd; /* Wmax: 마지막 손실 시점 cwnd */
u32 last_cwnd; /* 직전 cwnd 값 */
u32 last_time; /* 직전 갱신 시각 */
u32 bic_origin_point; /* CUBIC 함수 원점 */
u32 bic_K; /* Wmax 도달 시간 K */
u32 epoch_start; /* 현재 에포크 시작 시각 */
u32 ack_cnt; /* 에포크 내 ACK 카운트 */
u32 tcp_cwnd; /* Reno 모드 cwnd (하이브리드용) */
};
/* CUBIC 슬로 스타트: Hystart++
* → 표준 슬로 스타트의 과도한 오버슈트 방지
* → ACK 지연 변화량으로 BDP 근처 감지
* → 탐지 시 ssthresh 설정하고 congestion avoidance 전환
* → net.ipv4.tcp_hystart = 1 (기본 활성화)
*/
BBR 알고리즘 내부
BBR(Bottleneck Bandwidth and Round-trip propagation time, 커널 4.9+)은 Google이 개발한 혼잡 제어 알고리즘으로, 손실 기반이 아닌 대역폭과 RTT 측정에 기반합니다. 네트워크의 BDP(Bandwidth-Delay Product)를 추정하여 최적 전송률을 결정합니다:
/* net/ipv4/tcp_bbr.c — Google BBR (커널 4.9+) */
/* BBR 핵심 원리:
* 손실이 아닌 "대역폭(BtlBw)"과 "최소 RTT(RTprop)"를 측정하여
* 최적 전송 속도를 결정
*
* pacing_rate = BtlBw × pacing_gain
* cwnd = BDP × cwnd_gain = BtlBw × RTprop × cwnd_gain
*/
struct bbr {
u32 min_rtt_us; /* 관측된 최소 RTT (10초 윈도우) */
u32 min_rtt_stamp; /* min_rtt 측정 시각 */
u32 bw[2]; /* 최대 대역폭 샘플 (windowed max) */
u32 mode:3, /* 현재 상태: STARTUP/DRAIN/PROBE_BW/PROBE_RTT */
prev_ca_state:3,
round_start:1,
idle_restart:1,
probe_rtt_round_done:1;
u32 cycle_idx; /* PROBE_BW 사이클 위치 (0~7) */
u32 pacing_gain; /* 현재 pacing gain (× BBR_UNIT) */
u32 cwnd_gain; /* 현재 cwnd gain */
};
BBR은 4개의 상태를 순환하는 상태 머신으로 동작합니다:
- STARTUP: pacing_gain = 2.89 (= 2/ln2)로 대역폭을 빠르게 탐색. 3 라운드 연속 BW 증가율이 25% 미만이면 DRAIN 전환
- DRAIN: pacing_gain ≈ 0.35로 STARTUP에서 쌓인 큐 배출. inflight ≤ BDP이면 PROBE_BW 전환
- PROBE_BW: 정상 상태. 8-phase 사이클 [1.25, 0.75, 1.0×6]으로 대역폭 변화 탐지
- PROBE_RTT: 10초간 min_rtt가 갱신되지 않으면 진입. cwnd=4로 축소하여 큐를 비우고 순수 전파 지연 측정 (200ms 유지)
BBRv2/v3 진화
BBRv1은 높은 처리량을 달성하지만, 패킷 손실을 무시하고 CUBIC 플로우와의 공정성 문제가 있었습니다. BBRv2(커널 5.18+)와 BBRv3(커널 6.x)는 이를 체계적으로 개선합니다:
/* === BBRv2 주요 개선 (커널 5.18+) === */
/* 1. 손실 기반 신호 통합
* BBRv1: 패킷 손실 무시 → 과도한 재전송, CUBIC과 불공정
* BBRv2: inflight_hi/inflight_lo 경계를 추적하여 손실 반응
* → 손실 발생 시 inflight_hi를 낮춤 (β ≈ 0.7)
* → 손실 없으면 inflight_hi를 점진적으로 높임
* → CUBIC과 유사한 공정성 달성
*/
/* 2. ECN 지원
* BBRv1: ECN 마킹 무시
* BBRv2: ECN CE 마킹에도 inflight_hi를 조정
* → DCTCP와 유사한 정밀 혼잡 반응
* → 데이터센터 환경에서도 사용 가능
*/
/* 3. ProbeRTT 개선
* BBRv1: cwnd=4로 급격히 축소 → 200ms 처리량 급감
* BBRv2: 보다 점진적인 cwnd 축소
* → min_rtt 갱신 간격도 조정 가능
*/
/* 4. STARTUP 오버슈트 제어
* BBRv1: STARTUP에서 2×BDP까지 큐 쌓임 → 지연 급증
* BBRv2: loss_round_start 감지로 STARTUP 조기 종료
* → 큐잉 지연 대폭 감소
*/
/* === BBRv3 추가 개선 (커널 6.x) === */
/* 1. PROBE_BW 상태 세분화
* PROBE_UP → 대역폭 증가 탐색
* PROBE_DOWN → inflight 축소
* PROBE_CRUISE → BDP 근처 유지
* PROBE_REFILL → cwnd/inflight 복원
*
* 2. full_bw_reached 판단 개선
* → 3 round 연속 25% 미만 증가 → 더 정확한 임계값
*
* 3. 공정성 모델 (fairness model)
* → Reno/CUBIC 플로우와 대역폭 공유 시
* inflight_hi를 CUBIC 수준으로 수렴하도록 조정
* → 장기 연결에서 안정적 공존
*/
| 특성 | BBRv1 (4.9+) | BBRv2 (5.18+) | BBRv3 (6.x) |
|---|---|---|---|
| 손실 반응 | 무시 | inflight_hi 조정 | 정밀 inflight 모델 |
| ECN 지원 | 없음 | CE 기반 반응 | CE 기반 + 비율 추적 |
| CUBIC 공정성 | 불공정 (과점유) | 개선 | 수렴 모델 적용 |
| STARTUP 오버슈트 | 2×BDP 큐잉 | 조기 종료 | 정밀 BDP 추적 |
| PROBE_RTT 영향 | 200ms 처리량 급감 | 점진적 축소 | 최소화 |
tc qdisc replace dev eth0 root fq로 FQ 스케줄러를 설정하세요.
ECN (Explicit Congestion Notification)
전통적 혼잡 제어는 패킷 손실을 혼잡 신호로 사용하지만, ECN(RFC 3168)은 라우터가 패킷을 드롭하기 전에 혼잡을 알려줍니다. 이를 통해 불필요한 재전송 없이 혼잡에 대응할 수 있습니다.
ECN 비트 (IP TOS 필드 하위 2비트):
| ECT(1) | ECT(0) | 의미 |
|---|---|---|
| 0 | 0 | Non-ECT: ECN 미지원 패킷 |
| 0 | 1 | ECT(0): ECN 지원 전송 |
| 1 | 0 | ECT(1): ECN 지원 전송 |
| 1 | 1 | CE (Congestion Experienced): 라우터가 혼잡 마킹 |
TCP ECN 핸드셰이크: 연결 수립 시 SYN에 ECE+CWR 플래그를 설정하고, 상대방이 SYN-ACK에 ECE를 설정하면 양쪽 모두 ECN을 지원합니다. 데이터 전송 중 수신자가 CE 마킹된 패킷을 받으면 ACK에 ECE 플래그를 설정하고, 송신자는 cwnd를 감소시킨 후 CWR 플래그로 응답합니다:
# ECN 활성화 (0=비활성, 1=요청 시 사용, 2=항상 사용)
sysctl -w net.ipv4.tcp_ecn=1
# 소켓별 ECN 비활성화
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, ...); /* ECN은 sysctl 전역 설정 */
DCTCP (Data Center TCP)는 ECN을 극대화한 데이터센터 전용 혼잡 제어 알고리즘입니다. CE 마킹된 패킷의 비율에 비례하여 cwnd를 감소시키므로, 적은 혼잡에는 약간만 줄이고 심한 혼잡에는 크게 줄입니다. 이를 통해 데이터센터 내의 낮은 큐 지연과 높은 처리량을 동시에 달성합니다:
/* net/ipv4/tcp_dctcp.c
* α = (1 - g) × α + g × F (F = CE 마킹 비율, g = 감쇠 계수)
* cwnd = cwnd × (1 - α/2) (α가 크면 많이 감소, 작으면 적게 감소)
*
* → 일반 TCP: 손실 시 cwnd 절반 (0 or 1)
* → DCTCP: 혼잡 정도에 비례한 정밀 제어 (0.0 ~ 1.0)
*/
혼잡 제어 알고리즘 비교와 선택
Linux에서 사용 가능한 주요 혼잡 제어 알고리즘의 특성을 비교합니다:
| 알고리즘 | 혼잡 신호 | cwnd 감소 | BDP 활용 | 공정성 | 적합 환경 |
|---|---|---|---|---|---|
| Reno | 패킷 손실 | × 0.5 | 낮음 (선형 증가) | 높음 | 저지연, 소규모 BDP |
| CUBIC | 패킷 손실 | × 0.7 | 중간 (3차 함수) | 높음 | 일반 인터넷 (기본값) |
| BBR | BW/RTT 측정 | pacing 기반 | 높음 (BDP 추정) | 낮음 (v1) | 고지연 WAN, 무선, 위성 |
| DCTCP | ECN (CE 비율) | × (1-α/2) | 높음 | 높음 (DC 내) | 데이터센터 (ECN 필수) |
- CUBIC (기본): 대부분의 환경에서 안정적. 별도 설정 없이 잘 동작합니다
- BBR: 높은 패킷 손실률(무선, 위성), 고지연(WAN), 버퍼블로트 환경에서 유리합니다. 단, BBRv1은 손실을 무시하므로 CUBIC 플로우와 공존 시 대역폭을 불공평하게 점유할 수 있습니다. BBRv2/v3(커널 6.x)에서 개선 진행 중
- DCTCP: 데이터센터 내부 전용. 스위치의 ECN 지원이 필수이며, 인터넷 트래픽과 혼용하면 안 됩니다
- Reno: 교육/비교 목적. 실환경에서는 CUBIC이 상위 호환입니다
# 시스템 기본 혼잡 제어 알고리즘 확인/변경
sysctl net.ipv4.tcp_congestion_control
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 사용 가능한 알고리즘 확인
sysctl net.ipv4.tcp_available_congestion_control
# BBR 사용 시 fq (fair queue) 스케줄러 권장 (pacing 지원)
tc qdisc replace dev eth0 root fq
# 소켓별 설정 (애플리케이션 코드)
/* setsockopt(fd, SOL_TCP, TCP_CONGESTION, "bbr", 3); */
/* setsockopt(fd, SOL_TCP, TCP_CONGESTION, "cubic", 5); */
TCP 데이터 전송/수신 경로
유저 프로세스의 send()/recv() 호출이 커널 내부에서 처리되는 전체 경로입니다:
/* === 전송 경로 (send → wire) === */
/* 1. 시스템 콜 진입 */
/* send(fd, buf, len, flags)
* → sys_sendto() → sock_sendmsg() → tcp_sendmsg()
*/
/* 2. tcp_sendmsg() — net/ipv4/tcp.c */
/*
* a) 유저 데이터를 sk_buff 체인으로 복사 (send buffer)
* → sk->sk_write_queue에 추가
* → 가능하면 기존 skb의 남은 공간에 append (coalescence)
* → copy_from_iter()로 유저 → 커널 복사
*
* b) write_seq 갱신
*
* c) 전송 조건 확인 후 tcp_push() 호출
* → Nagle 알고리즘 확인 (TCP_NODELAY 아니면)
* → 혼잡 윈도우 / 수신 윈도우 확인
*/
/* 3. tcp_write_xmit() — 실제 세그먼트 전송 루프 */
/*
* while (cwnd에 여유 && 전송 대기 skb 있음) {
* tcp_transmit_skb():
* a) TCP 헤더 구성 (seq, ack, window, options)
* b) 체크섬 계산 (또는 hw checksum offload 설정)
* c) IP 계층 전달: ip_queue_xmit()
* d) retransmit queue에 skb 유지 (ACK 대기)
*
* pacing 적용: sk→sk_pacing_rate에 따라 전송 간격 조절
* TSO 적용: 대형 세그먼트를 NIC에서 분할하도록 설정
* }
*/
/* 4. IP → 디바이스 → NIC */
/* ip_queue_xmit() → ip_local_out() → NF_INET_LOCAL_OUT
* → dst_output() → ip_output() → NF_INET_POST_ROUTING
* → ip_finish_output() → neigh_output() → dev_queue_xmit()
* → qdisc → NIC 드라이버 → 하드웨어 전송
*/
- === 수신 경로 (wire → recv) === */
- NIC → NAPI → IP */
- NIC 인터럽트 → NAPI poll → napi_gro_receive()
- → netif_receive_skb() → ip_rcv()
- → NF_INET_PRE_ROUTING → ip_rcv_finish()
- → ip_local_deliver() → NF_INET_LOCAL_IN
- → tcp_v4_rcv()
- tcp_v4_rcv() — TCP 수신 핵심 */
- a) 4-tuple (src_ip, dst_ip, src_port, dst_port)로 소켓 조회
- → inet_lookup_established() 또는 inet_lookup_listener()
- → Early Demux 최적화 적용 가능
- b) 체크섬 검증 (HW offload 또는 SW)
- c) tcp_v4_do_rcv() → tcp_rcv_established() (대부분의 경우)
- tcp_rcv_established() — Fast Path / Slow Path */
- Fast Path (예측 기반 — 일반적 경우):
- → 다음 기대 시퀀스와 일치하는 순서 데이터
- → ACK 번호가 유효
- → 윈도우 변화 없음
- → 직접 sk→sk_receive_queue에 추가
- → 매우 빠름 (헤더 예측으로 분기 최소화)
- Slow Path:
- → 비순서 데이터 → Out-of-Order 큐에 추가
- → SACK 처리, 윈도우 업데이트, URG 등
- → tcp_data_queue() → tcp_ofo_queue() (재정렬)
- 유저 read() */
- recv()/read() → tcp_recvmsg()
- → sk_receive_queue에서 데이터를 유저 버퍼로 복사
- → copied_seq 갱신
- → 수신 윈도우 갱신 → ACK 전송 (조건부)
- Delayed ACK: 즉시 ACK 대신 최대 40ms 지연 (tcp_delack_timer)
- → 데이터 응답에 ACK를 피기백(piggyback)하여 패킷 수 절감
- → 2번째 세그먼트마다 즉시 ACK (quick_ack)
TCP 메모리 관리와 TSQ
/* === TCP 메모리 관리 3단계 === */
/* net.ipv4.tcp_mem = low pressure high (페이지 단위)
*
* 전체 TCP 소켓이 사용하는 메모리를 3단계로 관리:
*
* 1. low 미만: 정상 동작
* → 소켓별 버퍼 자동 튜닝 정상 작동
*
* 2. pressure (low < 현재 < high):
* → tcp_memory_pressure 플래그 설정
* → 소켓별 버퍼 축소 시작
* → 새 버퍼 할당에 제한
* → sk_stream_moderate_sndbuf()로 전송 버퍼 감소
*
* 3. high 이상:
* → 새 메모리 할당 거부 (소켓 write 블로킹)
* → 기존 연결의 전송도 지연될 수 있음
* → OOM 방지를 위한 최후 방어선
*
* 현재 사용량 확인: cat /proc/net/sockstat
* TCP: inuse 1234 orphan 0 tw 56 alloc 1234 mem 789
* (mem = 현재 사용 페이지 수)
*/
/* === 소켓별 메모리 관리 === */
/*
* sk→sk_wmem_queued: 전송 버퍼에 쌓인 바이트
* sk→sk_rmem_alloc: 수신 버퍼에 쌓인 바이트
* sk→sk_sndbuf: 전송 버퍼 상한 (tcp_wmem 기반 자동 조절)
* sk→sk_rcvbuf: 수신 버퍼 상한 (tcp_rmem 기반 자동 조절)
*
* sk_wmem_queued ≥ sk_sndbuf 이면:
* → sk_stream_wait_memory(): write() 블로킹
* → epoll: EPOLLOUT 해제
*/
- === TSQ (TCP Small Queues) — net/ipv4/tcp_output.c === */
- 커널 3.6+ (commit 46d3ceab) */
- 문제: 대량의 TCP 세그먼트가 qdisc 큐에 쌓이면
- → 지연 시간 증가 (버퍼블로트)
- → 다른 플로우에 대한 공정성 저하
- → 혼잡 제어의 피드백 루프가 느려짐
- 해결: 소켓당 qdisc/NIC에 대기 중인 바이트 수를 제한
- → sk→sk_pacing_status로 추적
- → 제한: sysctl net.ipv4.tcp_limit_output_bytes (기본 1MB)
- 동작:
- tcp_write_xmit()에서 전송 전 확인:
- if (sk→sk_wmem_queued - sk→sk_wmem_alloc > limit)
- → 전송 보류, tasklet으로 나중에 재시도
- TSQ tasklet:
- NIC 드라이버가 skb 전송 완료 → skb_orphan()
- → sk→sk_wmem_alloc 감소
- → tcp_tsq_handler() → tcp_write_xmit() 재개
- 효과:
- → qdisc 큐 깊이 감소 → 지연 시간 대폭 개선
- → BBR의 pacing과 함께 사용하면 버퍼블로트 근본 해결
TCP Segmentation Offload (TSO/GSO)
TSO/GSO는 TCP 전송 성능의 핵심입니다. 커널이 MSS보다 훨씬 큰 대형 skb를 생성하여 네트워크 스택을 한 번만 통과시킨 후, 최종 단계에서 분할합니다.
/* === TSO (TCP Segmentation Offload) — 하드웨어 오프로드 === */
/*
* 일반 전송 (오프로드 없음):
* write(fd, buf, 64000) → 커널이 MSS(1460) 단위로 44개 skb 생성
* → 각 skb마다: TCP 헤더 생성, IP 헤더, 체크섬, qdisc, NIC DMA
* → 매우 높은 per-packet CPU 오버헤드
*
* TSO 활성:
* → 커널이 64KB 대형 skb 1개 생성
* → 네트워크 스택(IP, Netfilter, TC, qdisc) 1번 통과
* → NIC 하드웨어가 MSS 단위 분할:
* - 각 세그먼트에 TCP 헤더 복사 (seq 증가, PSH/FIN 조정)
* - IP 헤더 복사 (total_length, ID 증가)
* - 체크섬 계산 (TCP pseudo-header + payload)
* → CPU 부하 대폭 절감 — 10Gbps+ 환경에서 매우 중요한 최적화
*
* 확인: ethtool -k eth0 | grep tcp-segmentation
* tcp-segmentation-offload: on
*/
/* === GSO (Generic Segmentation Offload) — 소프트웨어 fallback === */
/*
* TSO의 소프트웨어 일반화 (커널 2.6.18+, Herbert Xu):
* → TSO와 동일하게 대형 skb를 생성하여 스택 통과
* → validate_xmit_skb()에서 분할 결정:
* NIC가 TSO 지원 (NETIF_F_TSO) → 그대로 NIC에 전달
* NIC가 TSO 미지원 → skb_gso_segment()로 소프트웨어 분할
*
* GSO의 핵심 가치:
* 1. TSO 미지원 NIC에서도 중간 계층 처리 비용 절감
* 2. 터널(VXLAN, GRE), 가상화(veth, bridge) 환경에서 동작
* 3. TCP 외 프로토콜 지원: UDP GSO, SCTP GSO, ESP GSO
* 4. GRO ↔ GSO 대칭: 포워딩 시 GRO로 병합된 skb를 GSO로 재분할
*
* 전송 경로:
* tcp_sendmsg() → tcp_write_xmit() [대형 skb 생성, gso_size=MSS]
* → ip_queue_xmit() → __dev_queue_xmit() → qdisc (1개 skb만 처리)
* → validate_xmit_skb() → NIC feature 확인
* ├→ HW TSO 가능: skb 그대로 NIC 전달
* └→ HW 미지원: skb_gso_segment() → N개 세그먼트로 분할
*/
/* GSO 유형 (skb_shinfo→gso_type 비트마스크):
* SKB_GSO_TCPV4 IPv4 TCP (기본 TSO/GSO)
* SKB_GSO_TCPV6 IPv6 TCP
* SKB_GSO_UDP_L4 UDP L4 세그먼트 (4.18+, QUIC/WireGuard)
* SKB_GSO_UDP UDP IP 단편화 (UFO)
* SKB_GSO_GRE GRE 터널 내부 GSO
* SKB_GSO_UDP_TUNNEL VXLAN/Geneve 내부 GSO
* SKB_GSO_PARTIAL 부분 GSO: 외부 HW + 내부 SW
* SKB_GSO_ESP IPsec ESP GSO
* SKB_GSO_SCTP SCTP 청크 GSO
* SKB_GSO_FRAGLIST frag_list 기반 (GRO→포워딩→GSO)
* SKB_GSO_TCP_ECN ECN 활성 TCP GSO
* SKB_GSO_DODGY 신뢰할 수 없는 GSO (VM 전달 등)
*/
struct sk_buff *skb;
/* TSO/GSO 관련 skb 필드 (skb_shared_info) */
skb_shinfo(skb)->gso_size; /* 분할 단위 크기 (MSS)
* TCP: MSS (예: 1460)
* UDP GSO: 데이터그램 크기 (예: 1472)
* 0이면 GSO 미사용 */
skb_shinfo(skb)->gso_segs; /* 예상 세그먼트 수 (힌트)
* DIV_ROUND_UP(payload_len, gso_size)
* BQL(Byte Queue Limit) 계산에 활용 */
skb_shinfo(skb)->gso_type; /* SKB_GSO_* 비트마스크 (OR 조합)
* 예: SKB_GSO_TCPV4 | SKB_GSO_TCP_ECN */
/* 예: 64KB 데이터 + MSS=1460인 경우
* → gso_size = 1460 (분할 단위)
* → gso_segs = 64000/1460 ≈ 44개 세그먼트
* → 커널은 1개의 skb만 처리:
* - ip_queue_xmit() 1회, qdisc 1회
* - Netfilter/conntrack/NAT 1회
* → 최종 분할:
* NIC TSO → 하드웨어가 44개 와이어 프레임 생성
* SW GSO → validate_xmit_skb()에서 44개 skb 분할
*
* GSO 최대 크기:
* net_device→gso_max_size (기본 65536)
* BIG TCP (6.3+): IPv6에서 ~185KB까지 확장 가능
* ip link set dev eth0 gso_max_size 185000
*/
/* 유용한 헬퍼 함수 */
skb_is_gso(skb); /* gso_size != 0이면 true */
skb_gso_network_seglen(skb); /* 세그먼트의 실제 와이어 크기 */
skb_gso_segment(skb, features);/* SW GSO 분할 수행 → skb 리스트 반환 */
GSO/GRO 심화: GSO 전송 경로 상세, skb_gso_segment() 내부 동작, GSO_PARTIAL 터널 처리, GRO 병합 기준/flush 메커니즘, HW-GRO, 성능 튜닝(sysctl) 등은 GSO/GRO — 심화 섹션을 참고하세요.
Nagle 알고리즘, TCP_NODELAY, TCP_CORK
/* === Nagle 알고리즘 (RFC 896) === */
/*
* 목적: 작은 패킷(tinygram) 과다 전송 방지
*
* 규칙:
* 미확인 데이터(unACKed)가 있는 경우:
* → 새 데이터가 MSS 미만이면 전송 보류 (버퍼에 합침)
* → ACK 도착하면 합쳐진 데이터를 한 번에 전송
* 미확인 데이터가 없으면: 즉시 전송
*
* 효과: 작은 write() 여러 번 → 하나의 세그먼트로 합침
* 부작용: 지연 시간 증가 (ACK 대기 필요)
*/
/* net/ipv4/tcp_output.c */
static bool tcp_nagle_check(bool partial,
const struct tcp_sock *tp, int nonagle)
{
return partial && /* MSS 미만 세그먼트 */
((nonagle & TCP_NAGLE_CORK) ||
(!nonagle && tp->packets_out && /* 미확인 패킷 존재 */
!tcp_minshall_check(tp)));
}
/* === TCP_NODELAY — Nagle 비활성화 === */
/*
* 사용 시나리오:
* → 대화형 프로토콜 (SSH, 게임, 실시간 통신)
* → 작은 메시지라도 즉시 전송 필요
* → 이미 애플리케이션에서 버퍼링하는 경우
*/
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
/* === TCP_CORK — 명시적 코르크 === */
/*
* 코르크를 끼우면: 모든 전송을 보류 (MSS 미만 세그먼트 억제)
* 코르크를 빼면: 합쳐진 데이터를 한꺼번에 전송
*
* Nagle과 차이:
* Nagle: ACK 도착 시 자동 전송
* CORK: 명시적으로 코르크를 뺄 때만 전송
*
* 사용 시나리오:
* → sendfile() + 헤더/트레일러 조합
* → 여러 write()를 하나의 세그먼트로 합치고 싶을 때
* → 200ms 타임아웃으로 자동 해제 (안전장치)
*/
int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
/* ... 여러 write() 호출 ... */
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
/* → 합쳐진 데이터가 한 번에 전송됨 */
| 옵션 | 동작 | 지연 | 사용 사례 |
|---|---|---|---|
| 기본 (Nagle ON) | 작은 세그먼트 합침, ACK 시 전송 | 중간 (~RTT) | 일반적 벌크 전송 |
TCP_NODELAY |
즉시 전송, 합침 없음 | 최소 | 대화형, 실시간 |
TCP_CORK |
코르크 해제 시까지 전부 보류 | 제어 가능 | sendfile 조합, 배치 전송 |
TCP_NODELAY + MSG_MORE |
MSG_MORE 동안 보류, 마지막 전송 시 즉시 | 최소 | 프레임워크 내부 최적화 |
Zero-Copy 전송
커널 ↔ 유저 공간 간 데이터 복사를 제거하여 CPU 사용률과 지연을 줄이는 기법들입니다:
/* === sendfile() — 커널 2.2+ === */
/*
* 파일 → 소켓 전송 시 유저 공간 버퍼를 거치지 않음
*
* 일반 경로: read(file, buf) → write(sock, buf)
* 디스크 → 커널 버퍼 → 유저 버퍼 → 커널 소켓 버퍼 → NIC
* (2번의 유저↔커널 복사)
*
* sendfile(): sendfile(sock_fd, file_fd, offset, count)
* 디스크 → 커널 Page Cache → 커널 소켓 버퍼 → NIC
* (유저 공간 복사 제거 — 복사 1회로 줄임)
*
* NIC가 scatter-gather 지원 시:
* Page Cache 페이지를 직접 NIC DMA에 매핑
* → 복사 0회 (진정한 zero-copy)
*/
ssize_t sendfile(int out_fd, int in_fd,
off_t *offset, size_t count);
/* === MSG_ZEROCOPY — 커널 4.14+ === */
/*
* 유저 버퍼를 직접 NIC DMA에 매핑 (복사 없이 전송)
*
* 동작 원리:
* 1. 유저 페이지를 pin (get_user_pages)
* 2. skb의 frag으로 유저 페이지를 참조
* 3. NIC가 DMA로 직접 유저 메모리 읽기
* 4. 전송 완료 통지: SO_EE_ORIGIN_ZEROCOPY (errqueue)
* 5. 유저가 통지 받은 후에야 버퍼 수정/해제 가능
*
* 적합한 경우:
* → 대용량 전송 (10KB+ per send, 권장 수십KB~)
* → 높은 처리량 필요 (10Gbps+)
* → 복사 비용 > pin + 통지 오버헤드
*
* 부적합한 경우:
* → 작은 메시지 (오버헤드 > 이득)
* → 전송 완료 전 버퍼를 빠르게 재사용해야 하는 경우
*/
/* 설정 */
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));
/* 전송 */
send(fd, buf, len, MSG_ZEROCOPY);
/* 완료 통지 수신 (errqueue에서) */
struct msghdr msg = {};
struct sock_extended_err *serr;
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* serr→ee_origin == SO_EE_ORIGIN_ZEROCOPY
* serr→ee_data == 전송 완료된 send() 호출의 카운터
* → 이 통지 이후 유저 버퍼 안전하게 재사용 가능
*/
/* === splice() / vmsplice() — 파이프 기반 zero-copy === */
/*
* splice(): 파일 → 파이프 → 소켓 (또는 역방향)
* → 파이프를 중개자로 사용하여 커널 내부 페이지 이동
* → 유저 공간 복사 없음
*
* vmsplice(): 유저 버퍼 → 파이프
* → 유저 페이지를 파이프에 zero-copy로 연결
*
* 사용 예: 프록시 서버
* splice(client_fd → pipe) + splice(pipe → upstream_fd)
* → 프록시가 데이터를 한 번도 복사하지 않음
*/
ssize_t splice(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
Device Memory TCP (v6.16+)
커널 6.16에서 도입된 device memory TCP TX 경로는 GPU/DPU 등의 디바이스 메모리(DMABUF)에서 호스트 메모리를 거치지 않고 직접 TCP 송신을 수행합니다. 이는 기존 MSG_ZEROCOPY보다 한 단계 더 나아간 제로카피 방식으로, 디바이스 간 데이터 전송 시 호스트 메모리 대역폭 병목을 완전히 제거합니다.
/*
* Device Memory TCP TX (v6.16+)
*
* 기존 zero-copy:
* GPU 메모리 → 호스트 메모리 (DMA) → NIC DMA → 네트워크
*
* devmem TCP TX:
* GPU 메모리 → NIC DMA → 네트워크 (호스트 메모리 우회)
*
* 요구사항:
* - NIC가 device memory direct access 지원
* - DMABUF를 통한 디바이스 메모리 공유
* - TCP 소켓에 dmabuf binding
*/
/* 사용 흐름 */
/* 1. DMABUF fd를 NIC에 바인딩 */
/* 2. TCP 소켓에 SO_DEVMEM_DONTNEED 설정 */
/* 3. sendmsg()에 DMABUF offset/length 전달 */
/* → NIC가 디바이스 메모리에서 직접 DMA 읽기 수행 */
TX 하드웨어 셰이핑 API (v6.13+)
커널 6.13에서 net_shaper API가 도입되어, NIC 하드웨어의 TX 트래픽 셰이핑 기능을 통합된 인터페이스로 제어할 수 있게 되었습니다. 기존에는 벤더별 ethtool 확장이나 devlink 커맨드로 파편화되어 있던 H/W 레이트 리미팅을 표준화합니다.
# NIC H/W shaper 조회 (iproute2, v6.13+)
# NIC의 큐/포트 레벨 하드웨어 셰이핑 설정 확인
ip link show dev eth0 # shaper 관련 속성 표시
TCP Pacing
/* === TCP Pacing — 균일한 전송 속도 제어 === */
/* 커널 3.12+ (FQ qdisc 기반), 4.20+ (내장 pacing) */
/* 문제: TCP가 cwnd만큼 한꺼번에 전송 (burst)
* → 네트워크 버퍼에 순간적 과부하
* → 패킷 드롭 → 재전송 → 성능 저하
* → 특히 고대역폭 환경에서 심각
*
* Pacing: 세그먼트를 일정한 시간 간격으로 전송
* → 버스트 제거 → 큐잉 지연 감소
* → BBR 알고리즘의 핵심 구성요소
*/
/* 커널 내부 pacing 구현 */
/*
* sk→sk_pacing_rate: bytes/sec 단위 전송 속도
* sk→sk_pacing_status:
* SK_PACING_NONE pacing 미사용
* SK_PACING_NEEDED pacing 활성화됨
* SK_PACING_FQ FQ qdisc가 pacing 수행
*
* 방법 1: FQ (Fair Queue) qdisc 사용 (권장)
* tc qdisc add dev eth0 root fq
* → qdisc 레벨에서 per-flow pacing
* → sk→sk_pacing_rate를 읽어 패킷 전송 간격 조절
* → EDT (Earliest Departure Time) 모델: skb→tstamp에 전송 시각 기록
*
* 방법 2: 내장 pacing (FQ 없을 때)
* → tcp_internal_pacing() → hrtimer 기반
* → FQ보다 정밀도 낮고 CPU 오버헤드 높음
*
* BBR + FQ 조합:
* BBR이 sk_pacing_rate = BtlBw × pacing_gain으로 설정
* → FQ가 해당 속도에 맞춰 패킷 간격 조절
* → 버퍼블로트 없는 고성능 전송
*/
# FQ qdisc 설정 (BBR + pacing 최적 조합)
tc qdisc replace dev eth0 root fq
# EDT 기반 pacing 확인
tc -s qdisc show dev eth0
# → flows 127 (gcflows 0) throttled 45231
# throttled: pacing에 의해 지연된 패킷 수
# 소켓별 전송 속도 제한 (SO_MAX_PACING_RATE)
# → 혼잡 제어와 별도로 상한선 설정 가능
Early Demux 최적화
/* === Early Demux — IP 계층에서의 사전 소켓 조회 === */
/* net/ipv4/ip_input.c, 커널 3.6+ */
/* 일반 경로:
* ip_rcv() → ip_rcv_finish() → ip_local_deliver()
* → tcp_v4_rcv() → 소켓 조회 (inet_lookup)
*
* Early Demux 경로:
* ip_rcv_finish()에서 미리 소켓 조회 수행
* → skb→sk에 소켓 캐싱
* → 라우팅 조회를 소켓의 캐시된 dst_entry로 대체 (FIB 조회 스킵)
* → tcp_v4_rcv()에서 중복 조회 회피
*
* 성능 효과:
* → ESTABLISHED 연결의 수신 경로에서 ~5% CPU 절감
* → FIB 조회 비용이 높은 대규모 라우팅 테이블 환경에서 효과 극대화
*/
/* net/ipv4/ip_input.c */
static int ip_rcv_finish_core(struct net *net,
struct sock *sk, struct sk_buff *skb, ...)
{
/* Early Demux: 소켓과 연관된 dst 캐시 활용 */
if (net->ipv4.sysctl_ip_early_demux &&
!skb_dst(skb) && !skb->sk) {
tcp_v4_early_demux(skb);
/* → skb→sk = 매칭된 소켓
* → skb→dst = 소켓의 캐시된 라우팅 엔트리
*/
}
/* skb→dst가 설정되어 있으면 FIB 조회 스킵 */
if (!skb_valid_dst(skb))
ip_route_input_noref(skb, ...); /* FIB 조회 */
}
/* sysctl 제어 */
/* net.ipv4.ip_early_demux = 1 (기본: 활성화) */
/* net.ipv4.tcp_early_demux = 1 (TCP용, 커널 4.15+) */
/* net.ipv4.udp_early_demux = 1 (UDP용, 커널 4.15+) */
/* 비활성화 고려:
* → 라우터/포워더: ESTABLISHED 소켓이 적고 포워딩이 대부분
* → Early Demux의 소켓 조회가 불필요한 오버헤드
* → 대규모 서버: 소켓 수 많으면 조회 비용 > 캐시 이득
* → 벤치마크로 확인 필요
*/
TCP 인증 (MD5 Signature / TCP-AO)
/* === TCP MD5 Signature (RFC 2385) === */
/*
* BGP 세션 보호를 위해 설계된 TCP 세그먼트 인증
* → TCP 옵션(Kind=19)에 16바이트 MD5 해시 추가
* → 해시 입력: TCP pseudo-header + 세그먼트 데이터 + 비밀 키
* → 잘못된 해시의 세그먼트는 조용히 폐기
*
* 사용: BGP 피어 간 RST injection/hijacking 방어
*/
/* 커널 API (소켓 옵션) */
struct tcp_md5sig md5sig = {
.tcpm_addr = { /* 피어 주소 */
.ss_family = AF_INET,
},
.tcpm_keylen = 16,
};
memcpy(md5sig.tcpm_key, "secret_key_here!", 16);
setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5sig, sizeof(md5sig));
/* === TCP-AO (Authentication Option, RFC 5925) === */
/*
* TCP MD5의 후속 — 커널 6.7+
* 개선점:
* - 알고리즘 선택 가능 (HMAC-SHA1, AES-128-CMAC 등)
* - 키 롤오버 지원 (키 ID 기반으로 무중단 교체)
* - 주소 바인딩 유연 (prefix 매칭)
* - MD5보다 강력한 암호화 지원
*
* 커널 구조체:
* struct tcp_ao_key: 키 정보 (알고리즘, ID, 키 데이터)
* struct tcp_ao_info: 소켓별 TCP-AO 정보
*
* 사용: BGP, LDP 등 장시간 연결 보호
* 설정: TCP_AO_ADD_KEY, TCP_AO_DEL_KEY, TCP_AO_INFO setsockopt
*/
TCP Metrics Cache
커널은 목적지별 TCP 연결 성능 정보를 캐싱하여 새 연결의 초기 성능을 최적화합니다. 이전 연결에서 학습한 RTT, ssthresh 등을 새 연결에 적용하므로, 동일 목적지로의 반복 연결이 빠르게 안정됩니다:
/* net/ipv4/tcp_metrics.c */
/* TCP Metrics Cache: 목적지별 성능 정보 캐싱
*
* 저장 항목 (struct tcp_metrics_block):
* - RTT (srtt_us): smoothed RTT → 새 연결의 초기 RTO 계산
* - RTTVAR (rttvar_us): RTT 분산
* - ssthresh: 이전 연결의 최종 ssthresh → 새 연결 초기값
* - cwnd: 이전 연결의 cwnd 힌트
* - reordering: 관측된 재정렬 수준
*
* 캐시 키: 목적지 IP 주소 (src 무관)
* 저장소: 해시 테이블 (net→ipv4.tcp_metrics_hash)
* 만료: tcp_metrics_timeout (기본 없음, 수동 관리)
*/
/* 새 연결에서 Metrics 적용 */
void tcp_init_metrics(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_metrics_block *tm;
tm = tcp_get_metrics(sk, __sk_dst_get(sk), true);
/* 캐시된 ssthresh 적용 */
if (tm->tcpm_vals[TCP_METRIC_SSTHRESH])
tp->snd_ssthresh = tm->tcpm_vals[TCP_METRIC_SSTHRESH];
/* 캐시된 RTT로 초기 RTO 개선 */
if (tm->tcpm_vals[TCP_METRIC_RTT])
tp->srtt_us = tm->tcpm_vals[TCP_METRIC_RTT];
/* 캐시된 reordering 수준 적용 */
if (tm->tcpm_vals[TCP_METRIC_REORDERING])
tp->reordering = tm->tcpm_vals[TCP_METRIC_REORDERING];
}
/* 연결 종료 시 Metrics 저장 */
void tcp_update_metrics(struct sock *sk)
{
/* 조건: 유효한 RTT 샘플이 있고, 연결이 정상 종료된 경우
* → ssthresh, RTT, cwnd를 캐시에 갱신
* → 연결이 너무 짧으면 (세그먼트 < 4) 갱신하지 않음
*/
}
# Metrics Cache 조회
ip tcp_metrics show
# 출력 예:
# 10.0.0.1 age 1234sec cwnd 10 rtt 1500us rttvar 750us ssthresh 20
# 특정 목적지 Metrics 삭제 (디버깅 시 유용)
ip tcp_metrics delete 10.0.0.1
# 전체 Metrics 플러시
ip tcp_metrics flush all
# 주의: Metrics 캐시가 오래된 ssthresh를 유지하면
# 새 연결이 불필요하게 작은 cwnd로 시작할 수 있음
# → 네트워크 환경 변경 후 flush 권장
TCP Busy Polling
일반적으로 네트워크 수신은 인터럽트 → softirq → 프로세스 깨우기 경로를 거칩니다. Busy Polling은 프로세스가 직접 NIC 큐를 폴링하여 수신 지연을 수 마이크로초 수준으로 줄입니다. 금융 거래, HPC 등 극저지연이 필요한 환경에서 사용됩니다:
/* === Busy Polling 설정 === */
/* 시스템 전역 설정 (sysctl) */
/* net.core.busy_poll = 50
* → recv()/poll()/epoll_wait()에서 busy poll할 마이크로초
* → 0: 비활성화 (기본)
* → 50~100μs 권장 (환경에 따라 조정)
*
* net.core.busy_read = 50
* → read()/recvmsg()에서 busy poll할 마이크로초
*
* net.core.netdev_budget_usecs = 2000
* → busy poll에서 NIC당 처리 시간 제한
*/
/* 소켓별 설정 */
int busy_poll_us = 50;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &busy_poll_us, sizeof(busy_poll_us));
/* SO_PREFER_BUSY_POLL (커널 5.11+) */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &prefer, sizeof(prefer));
/* → NAPI가 인터럽트 모드 대신 busy poll 모드를 선호하도록 힌트
* → busy poll 소켓이 있으면 NAPI가 인터럽트를 비활성화하고
* 프로세스의 폴링에 의존
*/
/* SO_BUSY_POLL_BUDGET (커널 5.11+) */
int budget = 8;
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET, &budget, sizeof(budget));
/* → 한 번의 busy poll에서 처리할 최대 패킷 수
* → 기본: 8, 높은 처리량에는 증가
*/
/* 커널 내부 동작 (net/core/dev.c) */
/*
* sk_busy_loop():
* 1. 소켓에 연결된 NAPI 인스턴스를 찾음
* 2. napi_busy_loop()로 해당 NIC 큐를 직접 폴링
* 3. 패킷 수신 → 프로토콜 스택 처리 → 소켓 수신 큐에 추가
* 4. timeout(busy_poll μs) 초과 또는 데이터 도착 시 반환
*
* 요구사항:
* - NIC 드라이버가 busy poll 지원 (ndo_busy_poll 또는 NAPI 기반)
* - CAP_NET_ADMIN 또는 sysctl 전역 허용
* - epoll: EPOLLEXCLUSIVE와 함께 사용 권장
*/
MPTCP (Multipath TCP)
MPTCP(RFC 8684)는 하나의 TCP 연결을 여러 네트워크 경로(서브플로우)에 걸쳐 동시 전송할 수 있게 하는 TCP 확장입니다. 리눅스 커널 5.6에서 처음 도입되었으며, 스마트폰(Wi-Fi↔LTE 전환), 다중 NIC 서버, 고가용성 네트워크에서 활용됩니다:
/* === MPTCP 커널 구조 === */
/* net/mptcp/ — MPTCP 서브시스템 (커널 5.6+) */
/* mptcp_sock: MPTCP 연결의 최상위 구조체 */
struct mptcp_sock {
struct inet_connection_sock sk;
u64 snd_nxt; /* MPTCP 레벨 다음 전송 시퀀스 */
u64 write_seq; /* 애플리케이션 기록 시퀀스 */
u64 ack_seq; /* MPTCP 레벨 수신 시퀀스 */
struct list_head conn_list; /* 서브플로우 리스트 */
struct socket *subflow; /* 첫 번째 서브플로우 */
/* DSS (Data Sequence Signal): 데이터→서브플로우 매핑
* MPTCP 시퀀스 번호 → TCP 서브플로우 시퀀스 번호
* → 수신 측에서 서브플로우 데이터를 올바른 순서로 재조립
*/
};
/* 경로 관리자 (Path Manager): 서브플로우 생성/제거 정책
*
* 내장 경로 관리자:
* - "in-kernel" (기본): ip mptcp endpoint 명령으로 관리
* - "netlink": 유저스페이스 데몬이 경로 결정 (mptcpd)
*
* 서브플로우 추가 모드:
* - subflow: 이 주소에서 서브플로우 시작
* - signal: 이 주소를 상대방에게 알림 (ADD_ADDR)
* - backup: 백업 경로로 사용 (주 경로 실패 시)
*/
/* 스케줄러: 어떤 서브플로우로 데이터를 전송할지 결정
*
* 내장 스케줄러 (커널 6.4+):
* - "default": 라운드로빈 + 혼잡 윈도우 고려
* - "redundant": 모든 서브플로우에 중복 전송 (신뢰성 우선)
* - "roundrobin": 서브플로우 간 순차 분배
* - BPF 스케줄러: sched_ext 방식으로 커스텀 가능 (6.5+)
*/
# === MPTCP 설정 (커널 5.6+) ===
# MPTCP 전역 활성화
sysctl -w net.mptcp.enabled=1
# 엔드포인트(로컬 주소) 추가
ip mptcp endpoint add 10.0.0.1 dev eth0 subflow
ip mptcp endpoint add 192.168.1.5 dev wlan0 subflow signal
ip mptcp endpoint add 10.0.1.1 dev eth1 backup
# 엔드포인트 확인
ip mptcp endpoint show
# 서브플로우 제한 설정
ip mptcp limits set subflow 4 add_addr_accepted 2
# MPTCP 소켓 모니터링
ss -M # MPTCP 소켓만 표시
# 스케줄러 변경 (커널 6.4+)
ip mptcp endpoint change id 1 sched roundrobin
# 애플리케이션에서 MPTCP 사용
# 방법 1: IPPROTO_MPTCP로 직접 소켓 생성
# socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP)
# 방법 2: eBPF cgroup/setsockopt으로 투명 전환
# 방법 3: mptcpize 래퍼 (mptcpd 패키지)
# mptcpize run curl https://example.com
- 대역폭 결합: 여러 경로의 대역폭을 합산하여 단일 연결의 처리량 증가
- 경로 전환: 한 경로(예: Wi-Fi)가 끊겨도 다른 경로(LTE)로 연결 유지 → seamless handover
- 호환성: MPTCP 미지원 중간 장비(방화벽, NAT)에서는 자동으로 일반 TCP로 폴백
- Android: MPTCP를 Wi-Fi↔모바일 전환에 활용 (커널 5.6+, Android 12+)
kTLS (Kernel TLS) 연동
kTLS(커널 4.13+)는 TLS 레코드 계층 처리를 커널로 이동하여 sendfile()과 같은 zero-copy 전송을 TLS 연결에서도 가능하게 합니다. 유저 공간 TLS 라이브러리(OpenSSL 등)가 핸드셰이크를 완료한 후, 암호화 키를 커널에 주입하면 커널이 직접 암호화/복호화를 수행합니다:
/* === kTLS 설정 API === */
/* 전제: TLS 핸드셰이크가 유저 공간에서 완료된 상태 */
/* 1. TLS ULP(Upper Layer Protocol) 설정 */
setsockopt(fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
/* 2. TX 키 주입 (전송 방향) */
struct tls12_crypto_info_aes_gcm_128 crypto_info;
crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(crypto_info.key, tx_key, TLS_CIPHER_AES_GCM_128_KEY_SIZE);
memcpy(crypto_info.iv, tx_iv, TLS_CIPHER_AES_GCM_128_IV_SIZE);
memcpy(crypto_info.salt, tx_salt, TLS_CIPHER_AES_GCM_128_SALT_SIZE);
memcpy(crypto_info.rec_seq, tx_seq, TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE);
setsockopt(fd, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));
/* 3. RX 키 주입 (수신 방향, 커널 4.17+) */
/* 동일한 구조체에 rx_key/iv/salt/seq 설정 */
setsockopt(fd, SOL_TLS, TLS_RX, &rx_crypto_info, sizeof(rx_crypto_info));
/* 4. 이후 send()/sendfile()이 자동으로 TLS 레코드 암호화 */
sendfile(fd, file_fd, NULL, file_size); /* kTLS 없이는 불가능! */
/* 지원 암호 스위트 (커널 버전별):
* TLS_CIPHER_AES_GCM_128 (4.13+)
* TLS_CIPHER_AES_GCM_256 (5.1+)
* TLS_CIPHER_CHACHA20_POLY1305 (5.11+)
* TLS_CIPHER_AES_CCM_128 (5.11+)
* TLS_CIPHER_SM4_GCM (6.0+, 중국 국가 표준)
* TLS_CIPHER_SM4_CCM (6.0+)
* TLS_CIPHER_ARIA_GCM_128 (6.2+, 한국 국가 표준)
* TLS_CIPHER_ARIA_GCM_256 (6.2+)
*
* HW TLS Offload 지원 NIC:
* Mellanox/NVIDIA ConnectX-6+, Intel E810 (ice 드라이버)
* → ethtool -k eth0 | grep tls
* → tls-hw-tx-offload: on / tls-hw-rx-offload: on
*/
ssl_conf_command Options KTLS; 옵션으로 활성화하면, 정적 파일 서빙 시 sendfile()을 TLS 연결에서도 사용하여 CPU 사용률을 크게 줄일 수 있습니다.
상세 내용은 kTLS 심화 문서를 참고하세요.
TCP Sockmap과 BPF 가속
TCP Sockmap(커널 4.14+)은 eBPF 프로그램이 TCP 소켓 간 데이터를 커널 내부에서 직접 전달하는 메커니즘입니다. 프록시 서버에서 유저 공간 복사를 완전히 제거하여 처리량을 크게 향상시킵니다:
/* === Sockmap/Sockhash 사용 예 (BPF C) === */
/* 1. Sockmap 정의 */
struct {
__uint(type, BPF_MAP_TYPE_SOCKHASH);
__uint(max_entries, 65535);
__type(key, struct sock_key); /* 4-tuple */
__type(value, __u32);
} sock_ops_map SEC(".maps");
/* 2. SOCK_OPS 프로그램: 연결 이벤트에서 Sockmap에 소켓 등록 */
SEC("sockops")
int bpf_sockmap(struct bpf_sock_ops *skops)
{
if (skops->op == BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB ||
skops->op == BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB) {
struct sock_key key = {
.sip4 = skops->local_ip4,
.dip4 = skops->remote_ip4,
.sport = skops->local_port,
.dport = bpf_ntohl(skops->remote_port),
};
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_ANY);
}
return 1;
}
/* 3. SK_MSG 프로그램: 메시지를 대상 소켓으로 리디렉션 */
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
struct sock_key key = { /* 역방향 4-tuple 구성 */ };
bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
return SK_PASS;
}
TCP Connection Repair
TCP Connection Repair(커널 3.5+)는 TCP 연결의 내부 상태를 직렬화/역직렬화하여, 프로세스 간 또는 호스트 간 연결을 이전(migration)할 수 있게 합니다. CRIU(Checkpoint/Restore In Userspace) 프로젝트에서 컨테이너 라이브 마이그레이션에 핵심적으로 사용됩니다:
/* === TCP Connection Repair (커널 3.5+) === */
/* net/ipv4/tcp.c — TCP_REPAIR 소켓 옵션 */
/* TCP_REPAIR 모드: 소켓을 "수술 모드"로 전환
* → 모든 상태를 읽고 쓸 수 있음
* → 데이터 전송 없이 내부 상태만 조작
* → CAP_NET_ADMIN 권한 필요
*/
/* === 연결 상태 저장 (체크포인트) === */
/* 1. Repair 모드 진입 */
int repair = TCP_REPAIR_ON;
setsockopt(fd, SOL_TCP, TCP_REPAIR, &repair, sizeof(repair));
/* 2. 시퀀스 번호 읽기 */
struct tcp_repair_opt opt;
int queue = TCP_SEND_QUEUE;
setsockopt(fd, SOL_TCP, TCP_REPAIR_QUEUE, &queue, sizeof(queue));
getsockopt(fd, SOL_TCP, TCP_QUEUE_SEQ, &snd_seq, &len);
/* → snd_seq: write_seq (전송 시퀀스) */
queue = TCP_RECV_QUEUE;
setsockopt(fd, SOL_TCP, TCP_REPAIR_QUEUE, &queue, sizeof(queue));
getsockopt(fd, SOL_TCP, TCP_QUEUE_SEQ, &rcv_seq, &len);
/* → rcv_seq: rcv_nxt (수신 시퀀스) */
/* 3. TCP 옵션 읽기 (Timestamp, Window Scale 등) */
struct tcp_repair_window window;
getsockopt(fd, SOL_TCP, TCP_REPAIR_WINDOW, &window, &len);
/* 4. 미전송 데이터 읽기 */
recv(fd, send_queue_data, ..., MSG_PEEK); /* repair 모드에서 큐 데이터 읽기 */
/* === 연결 복원 (리스토어) === */
/* 1. 새 소켓 생성 + Repair 모드 진입 */
int new_fd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(new_fd, SOL_TCP, TCP_REPAIR, &repair, sizeof(repair));
/* 2. 바인드 + 시퀀스 번호 설정 */
bind(new_fd, &local_addr, ...);
setsockopt(new_fd, SOL_TCP, TCP_REPAIR_QUEUE, &send_queue, ...);
setsockopt(new_fd, SOL_TCP, TCP_QUEUE_SEQ, &snd_seq, ...);
/* 3. connect() — repair 모드에서는 SYN 없이 상태만 설정 */
connect(new_fd, &remote_addr, ...);
/* → 실제 패킷 전송 없음, 소켓 상태만 ESTABLISHED로 전환 */
/* 4. TCP 옵션/윈도우 복원 */
setsockopt(new_fd, SOL_TCP, TCP_REPAIR_WINDOW, &window, ...);
/* 5. 미전송 데이터 복원 */
send(new_fd, send_queue_data, ..., 0); /* repair 모드에서 큐에만 추가 */
/* 6. Repair 모드 해제 → 정상 TCP 동작 재개 */
repair = TCP_REPAIR_OFF;
setsockopt(new_fd, SOL_TCP, TCP_REPAIR, &repair, sizeof(repair));
/* → 이 시점부터 정상적으로 ACK, 데이터 전송 시작 */
- CRIU: 컨테이너 체크포인트/리스토어 — Docker/Podman의 라이브 마이그레이션
- 서버 업그레이드: 커널 업데이트 시 기존 TCP 연결을 새 프로세스로 이전
- 로드 밸런싱: 활성 연결을 다른 서버로 무중단 이전
- 제한사항: 양쪽 호스트의 타이밍/시퀀스가 맞아야 하며, 중간에 패킷 손실 시 복구가 복잡해질 수 있음
TCP 디버깅과 모니터링
# === 연결 상태 모니터링 ===
# ss: 소켓 통계 (netstat 대체, 커널 정보 직접 조회)
ss -tnpi
# -t: TCP, -n: 숫자 표시, -p: 프로세스, -i: 내부 TCP 정보
# 출력 예:
# cubic wscale:7,7 rto:204 rtt:1.5/0.75 ato:40
# cwnd:10 ssthresh:7 send 77.9Mbps retrans:0/3
# → cwnd=10, rtt=1.5ms, 재전송 3회(현재 0 in-flight)
# 상태별 연결 수
ss -s
# TCP: 1234 (estab 890, closed 12, orphaned 0, timewait 332)
# 특정 상태 필터
ss -tn state time-wait
ss -tn state established '( dport = 443 )'
# === TCP 내부 통계 ===
# /proc/net/snmp — MIB-II 카운터
cat /proc/net/snmp | grep Tcp
# 주요 필드:
# ActiveOpens: connect() 성공 수
# PassiveOpens: accept() 성공 수
# RetransSegs: 재전송된 세그먼트 총수
# InErrs: 수신 오류 (체크섬, 길이 등)
# OutRsts: 전송된 RST 수
# /proc/net/netstat — 확장 TCP 통계
nstat -az | grep -i tcp
# 주요 카운터:
# TcpExtTCPTimeouts RTO 타임아웃 횟수
# TcpExtTCPLossProbes TLP (Tail Loss Probe) 전송 수
# TcpExtTCPFastRetrans Fast Retransmit 횟수
# TcpExtTCPSACKRecovery SACK 기반 복구 횟수
# TcpExtTCPMemoryPressures 메모리 pressure 진입 횟수
# TcpExtTCPBacklogDrop backlog 큐 드롭 수
# TcpExtListenOverflows accept 큐 오버플로우
# TcpExtListenDrops listen 드롭 총수
# === 패킷 수준 디버깅 ===
# tcpdump: TCP 핸드셰이크, 재전송, 윈도우 분석
tcpdump -i eth0 -nn tcp port 443 -v
# -v: TCP 옵션 (MSS, SACK, Window Scale, Timestamp) 표시
# === ftrace: 커널 함수 추적 ===
# TCP 재전송 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_retransmit_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe
# → sport, dport, saddr, daddr, state, 재전송 시퀀스 번호 출력
# TCP probe (혼잡 제어 디버깅)
echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_probe/enable
# → cwnd, ssthresh, snd_wnd, srtt 실시간 추적
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { printf("%s:%d → %s:%d state=%d\n", ntop(args->saddr), args->sport, ntop(args->daddr), args->dport, args->state); }'
— TCP 재전송 발생 시 실시간으로 소스/목적지와 상태를 출력합니다.
관련 문서
TCP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.