TCP 프로토콜 심화
Linux TCP 프로토콜: tcp_sock, 상태 머신, 혼잡 제어(CUBIC/BBR), kTLS, 재전송 메커니즘, Zero-Copy, 성능 튜닝 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
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 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, 전체 재전송 */
};
혼잡 제어 심화
혼잡 제어 기초 — 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 유지)
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 디버깅과 모니터링
# === 연결 상태 모니터링 ===
# 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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.