- Published on
Linux 네트워크 스택 Deep Dive — sk_buff, NAPI, Netfilter, Traffic Control, GRO/TSO 완전 정복 (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- Linux 네트워크 스택은 NIC → sk_buff 할당 → NAPI polling → Netfilter → 라우팅 → TCP/UDP → 소켓 → 유저 복사의 파이프라인.
sk_buff: 패킷을 표현하는 구조체. 수백 바이트의 메타데이터. 레이어를 거치며 헤더가 추가/제거됨.- NAPI (New API): 고부하에서 인터럽트 대신 polling. "Receive livelock"을 해결한 2003년 설계. 10+ Mpps를 가능하게 함.
- Netfilter: 5개 훅(
PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING). iptables/nftables의 기반. - Traffic Control (tc): 큐 관리. qdisc (pfifo_fast, fq_codel, fq, cake)로 AQM, rate limit, QoS.
- Offloads: TSO(송신), GSO(소프트웨어 세그먼트), LRO(수신 하드웨어), GRO(소프트웨어 수신 merge). CPU 절감.
- RPS/RFS: 수신 패킷을 여러 CPU에 분산. 멀티 큐 NIC와 결합하면 초당 수백만 pps.
- Connection Tracking (conntrack): Netfilter의 stateful 모듈. NAT, stateful firewall 기반.
- XDP, io_uring, AF_XDP: 전통 스택 우회 경로. 성능 극한.
- 디버깅:
tcpdump,ss,ip,ethtool,perf,bcc/bpftrace로 각 단계 프로파일링.
1. 패킷의 여정 — 전체 그림
1.1 수신 경로
하드웨어 프레임 도착
↓
NIC가 DMA로 RX 링 버퍼에 기록
↓
NIC가 인터럽트 발생
↓
커널: NAPI 스케줄
↓
소프트 IRQ (NET_RX_SOFTIRQ)
↓
NAPI poll 함수가 RX 링에서 프레임 수집
↓
각 프레임에 sk_buff 할당
↓
GRO (Generic Receive Offload) — 작은 패킷 병합
↓
netif_receive_skb() — 프로토콜 핸들러에게 전달
↓
Netfilter PREROUTING hook
↓
IP layer — ip_rcv()
↓
라우팅 결정 (로컬 or 포워드)
↓
[로컬]
↓
Netfilter INPUT hook
↓
ip_local_deliver()
↓
TCP/UDP layer — tcp_v4_rcv()
↓
소켓 버퍼에 큐
↓
프로세스 wake up (recv/epoll)
↓
userspace copy (syscall)
1.2 송신 경로
유저 프로세스 send()
↓
sock_sendmsg()
↓
TCP/UDP layer
↓
TCP: 세그먼트 생성, sk_buff 할당
↓
IP layer — ip_queue_xmit()
↓
라우팅 (dst cache)
↓
Netfilter OUTPUT hook
↓
Netfilter POSTROUTING hook (NAT)
↓
Traffic Control (qdisc)
↓
dev_hard_start_xmit() → driver's ndo_start_xmit
↓
TSO (하드웨어 세그먼트)
↓
NIC의 TX 링에 descriptor
↓
NIC가 DMA로 전송 → 와이어
1.3 복잡한 이유
각 단계가:
- 자체 자료구조.
- Lock, memory allocation.
- 레이어별 헤더 조작.
- 확장성을 위한 per-CPU 처리.
단순한 "packet in, packet out"이 수천 줄의 커널 코드. 이 글은 주요 부분을 상세 설명한다.
2. sk_buff — 패킷의 표현
2.1 구조
struct sk_buff {
struct sk_buff *next, *prev; // 큐 링크
struct sock *sk; // 관련 소켓
ktime_t tstamp; // 타임스탬프
struct net_device *dev; // 관련 디바이스
unsigned int len; // 실제 데이터 길이
unsigned int data_len; // paged 데이터 길이
__u16 mac_len;
__u16 hdr_len;
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
unsigned char *head; // 버퍼 시작
unsigned char *data; // 현재 데이터 시작
sk_buff_data_t tail; // 현재 데이터 끝
sk_buff_data_t end; // 버퍼 끝
atomic_t users; // 참조 카운트
// ... 수십 개 필드
};
크기: 약 256 바이트 (하드웨어, 빌드에 따라 다름). 데이터 자체가 아닌 메타데이터.
2.2 헤드/데이터/테일/엔드
┌─────────────────────────────────────────────┐
│ headroom data tailroom │
│ (add headers (payload (add trailers
│ down the goes here) down the
│ stack) stack)
└─────────────────────────────────────────────┘
↑ ↑ ↑ ↑
head data tail end
Headroom: 아래 레이어가 헤더 추가할 공간 (송신 시). Tailroom: 트레일러 추가 공간. data ~ tail: 실제 패킷 내용.
레이어를 내려가며 헤더 추가 → data 포인터를 앞으로 이동 (skb_push).
레이어를 올라가며 헤더 제거 → data 포인터를 뒤로 이동 (skb_pull).
2.3 왜 복잡한가
Metadata가 데이터보다 큰 경우도 있다. 64-byte 프레임의 메타데이터가 256 바이트. 비효율.
Linked list: 큐에 있는 skb들은 이중 링크드 리스트. 각 추가/제거가 포인터 조작.
Reference counting: clone, copy 시 참조 관리. 버그 원인.
Scatter-gather: 대형 패킷은 여러 page fragment로 분산. 복잡한 iterate.
2.4 XDP가 sk_buff를 회피하는 이유
XDP(eXpress Data Path)는 sk_buff 할당 전에 실행된다:
드라이버 → XDP → sk_buff 할당 → 기존 스택
↓
DROP/TX/REDIRECT
sk_buff 할당 + 메타데이터 처리만으로도 수백 ns. XDP는 이걸 건너뛴다 → 단일 코어로 수천만 pps.
2.5 skb_clone vs skb_copy
Clone: 메타데이터만 복사, 데이터는 공유. Copy: 전체 복사 (COW 없음).
Clone은 빠르지만 데이터 수정 불가. Netfilter의 "tee" target이 clone 사용.
3. NIC와 드라이버 레벨
3.1 NIC 구조
현대 NIC는 멀티 큐 지원:
- Rx queues: 수신용 ring buffer.
- Tx queues: 송신용.
- 각 queue마다 별도 descriptor ring.
NIC
├── RX queue 0 (descriptors + DMA buffers)
├── RX queue 1
├── RX queue 2
├── ...
├── TX queue 0
└── TX queue 1
큐 수는 ethtool -l <iface>로 확인.
3.2 DMA와 Ring Buffer
드라이버가 시작 시:
- 각 RX queue에 ring buffer 할당.
- 각 descriptor에 DMA 주소 설정 (pre-allocated sk_buff 또는 page).
- NIC에 ring의 base, size 전달.
패킷 도착 시:
- NIC가 DMA로 직접 descriptor의 주소에 쓰기.
- Descriptor에 "written" 플래그.
- 인터럽트 (또는 NAPI 중이면 생략).
커널:
- Ring에서 새 descriptor 찾기.
- 해당 sk_buff를 네트워크 스택에 전달.
- Ring에 새 버퍼 재할당.
3.3 Interrupt의 문제
초기 (NAPI 이전):
패킷 도착 → 인터럽트 → ISR 실행 → 패킷 처리 → ISR 종료
초당 100만 패킷 → 초당 100만 인터럽트 → CPU가 인터럽트 처리에만 몰두 → 실제 작업 불가능. "Receive livelock".
3.4 NAPI — 해결책
New API (Linux 2.4.20, 2003년).
아이디어: 인터럽트 + polling 하이브리드.
첫 패킷: 인터럽트 발생 → 커널이 NAPI 스케줄
→ 인터럽트 비활성화
Soft IRQ: NAPI poll 함수 실행
→ ring에서 한 배치씩 패킷 가져오기
→ 모두 처리할 때까지 반복
→ 패킷 없으면 polling 종료, 인터럽트 재활성화
이점:
- 저부하: 인터럽트 한 번에 패킷 하나.
- 고부하: 한 번의 인터럽트 후 수백 패킷을 배치로 처리. 인터럽트 오버헤드 분산.
net/core/dev.c의 napi_poll():
static int napi_poll(struct napi_struct *n, struct list_head *repoll) {
int work = n->poll(n, n->budget); // 드라이버의 poll 함수
// work <= budget이면 완료, 다시 인터럽트
// work == budget이면 더 있음, 다음 softirq에 계속
}
budget(기본 64)만큼 배치 처리. 공정성을 위해 한 번에 너무 많이 안 함.
3.5 GRO — 수신 패킷 병합
Generic Receive Offload: NAPI가 여러 작은 TCP 패킷을 하나의 큰 패킷으로 병합.
Before GRO:
[TCP 1460B] [TCP 1460B] [TCP 1460B] [TCP 1460B]
After GRO:
[TCP 5840B]
이후 스택 레이어는 큰 패킷 하나만 처리. TCP를 거슬러 올라가며 마치 점보 프레임처럼.
결과: 처리 오버헤드 감소 (4개 → 1개).
소프트웨어 GRO는 NAPI poll 중에, 하드웨어 LRO는 NIC가 직접.
3.6 LRO vs GRO
- LRO (Large Receive Offload): 하드웨어 병합. 빠르지만 약간 부정확 (일부 플래그 손실).
- GRO: 소프트웨어 병합. 조금 느리지만 정확. Linux 기본.
대부분 GRO 사용. LRO는 방화벽 등에서 문제 가능.
4. Protocol Handler 단계
4.1 netif_receive_skb
NAPI가 모은 skb가 netif_receive_skb()에 들어온다:
int netif_receive_skb(struct sk_buff *skb) {
// 1. Packet taps (tcpdump 같은 raw socket)
// 2. Protocol handler 찾기 (ETH_P_IP, ETH_P_IPV6, etc.)
// 3. 해당 handler 호출
}
4.2 IP Layer — ip_rcv
IPv4 패킷이면 ip_rcv():
int ip_rcv(struct sk_buff *skb, ...) {
// 1. Basic sanity check (length, version, checksum)
// 2. Netfilter PREROUTING hook
// 3. ip_rcv_finish()
}
static int ip_rcv_finish(struct sk_buff *skb) {
// 1. Routing lookup (ip_route_input)
// 2. 결과에 따라:
// - local delivery: ip_local_deliver
// - forward: ip_forward
}
4.3 Routing
FIB (Forwarding Information Base) 조회:
- 출발지/목적지 IP.
- 인터페이스.
- TOS.
결과: dst_entry — 어떻게 전달할지의 정보.
ip_route_input은 routing cache를 먼저 본다. 캐시 미스면 FIB 조회. 매우 hot path → 최적화가 많이 됨.
4.4 Local Delivery vs Forward
Local:
ip_local_deliver()
↓
Netfilter INPUT hook
↓
ip_local_deliver_finish()
↓
transport layer (tcp_v4_rcv, udp_rcv, etc.)
Forward:
ip_forward()
↓
Netfilter FORWARD hook
↓
ip_forward_finish()
↓
dst_output()
↓
Netfilter POSTROUTING hook
↓
실제 전송 (dev_queue_xmit)
라우터 역할을 하는 Linux 박스 (예: iptables gateway)는 대부분 forward path.
5. Netfilter
5.1 5개 Hook
┌─────────────┐
패킷 → │PREROUTING │ → ┌──────┐
└─────────────┘ │Routing
└──┬───┘
↓
┌──────────┐
│ Local? │
└┬────────┬┘
↓ ↓
┌──────┐ ┌─────────┐
│ INPUT│ │ FORWARD │
└───┬──┘ └────┬────┘
↓ ↓
Local process │
↓ │
┌──────┐ │
│OUTPUT│ │
└───┬──┘ │
↓ ↓
┌─────────────┐
│POSTROUTING │ → 네트워크
└─────────────┘
각 hook에서 iptables/nftables 규칙이 실행된다.
5.2 iptables vs nftables
iptables (legacy):
- 고전적, 잘 알려짐.
- 테이블별 코드 (
ip_tables,ip6_tables,arp_tables,ebtables). - 명령어 문법 복잡.
nftables (current):
- 2014년 도입. 단일 커널 프레임워크.
- VM 기반 — 룰이 바이트코드로 컴파일.
- 더 나은 성능, 더 단순한 문법.
2020+ 배포판이 nftables 기본. iptables-nft compatibility layer 있음.
5.3 예제
iptables:
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -j DROP
nftables:
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0 \; }
nft add rule inet filter input tcp dport { 22, 80 } accept
nft add rule inet filter input drop
5.4 Connection Tracking (conntrack)
Netfilter의 stateful 모듈. 각 연결을 추적 → NAT과 stateful firewall의 기반.
Packet → PREROUTING → conntrack (lookup/create entry)
Conntrack entry:
- Source/dest IP + port.
- Protocol.
- State (NEW, ESTABLISHED, RELATED, INVALID).
- Timeout.
NAT은 conntrack entry에 번역 규칙 저장. 이후 패킷이 같은 흐름이면 자동 적용.
단점: 메모리 사용 (대규모 연결 추적). /proc/sys/net/nf_conntrack_max로 한계 설정.
6. Socket Layer
6.1 sock 구조
struct sock {
struct sock_common __sk_common; // hash 등 공통 필드
atomic_t sk_rmem_alloc; // 수신 큐 메모리
atomic_t sk_wmem_alloc; // 송신 큐 메모리
struct sk_buff_head sk_receive_queue; // 수신 큐
struct sk_buff_head sk_write_queue; // 송신 큐
struct proto *sk_prot; // 프로토콜 (tcp_prot, udp_prot)
// 콜백
void (*sk_data_ready)(struct sock *);
void (*sk_write_space)(struct sock *);
// ... 수백 필드
};
각 소켓 fd가 struct sock과 연결.
6.2 sk_receive_queue
수신된 패킷이 이 큐에 쌓임. recv() syscall이 큐에서 꺼냄.
Backpressure: 큐가 가득 차면 새 패킷 드롭. sk_rmem_alloc > sk_rcvbuf 체크.
6.3 TCP 수신
int tcp_v4_rcv(struct sk_buff *skb) {
// 1. Header sanity
// 2. 해당 연결의 sock 찾기 (hash lookup)
// 3. 상태에 따라:
// - ESTABLISHED: tcp_rcv_established
// - LISTEN: tcp_v4_do_rcv (새 연결)
// - 기타: tcp_rcv_state_process
}
tcp_rcv_established: ESTABLISHED 상태의 빠른 경로. 대부분 패킷.
- Sequence number 체크.
- ACK 처리.
- 데이터를 receive queue 또는 out-of-order queue에.
sk_data_ready()콜백 → epoll 또는 recv 대기 중인 프로세스 깨우기.
6.4 TCP 송신
TCP의 혼잡 제어와 흐름 제어가 여기서 적용:
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size) {
// 1. 데이터를 sk_write_queue에 추가 (skb에 복사 또는 copy_from_iter)
// 2. tcp_push() — 실제 전송 가능한지 확인
// - 송신 윈도우 확인
// - Congestion window 확인
// - Nagle (TCP_NODELAY)
// 3. tcp_write_xmit() — 세그먼트 전송
}
tcp_write_xmit는 여러 세그먼트를 한 번에 네트워크에 내보낼 수 있다 (SO_SNDBUF만큼).
6.5 TCP State Machine
CLOSED
↓ LISTEN
LISTEN ──────────────────────────────┐
↓ SYN received │
SYN_RECV │
↓ ↓
ESTABLISHED ↔ 데이터 교환 ← SYN_SENT ← connect()
↓ close() ↓ CLOSED
FIN_WAIT1 CLOSE_WAIT
↓ ACK ↓ close()
FIN_WAIT2 LAST_ACK
↓ FIN ↓ ACK
TIME_WAIT CLOSED
↓ 2*MSL
CLOSED
커널의 tcp_rcv_state_process()가 상태 전이 처리. RFC 793 + 확장들 (RFC 1122, 5681, ...).
7. Traffic Control (tc)
7.1 역할
송신 시 패킷을 큐잉해서 어떻게 내보낼지 제어. AQM(Active Queue Management), QoS, rate limiting.
IP layer
↓
qdisc (queueing discipline)
↓
NIC TX ring
기본 qdisc는 각 TX queue마다 하나.
7.2 pfifo_fast (기본)
3-band priority queue. ToS 기반 분류:
- Band 0: 긴급 (Interactive).
- Band 1: 일반.
- Band 2: Bulk.
간단하지만 bufferbloat 문제.
7.3 fq_codel
Fair Queueing + Controlled Delay. Linux 4+ 기본(일부 배포판).
- 연결별 분리 큐 → 한 flow가 다른 걸 굶기지 못함.
- CoDel AQM으로 bufferbloat 완화.
대부분 일반 사용자 환경에 좋음.
7.4 fq (Fair Queue)
Google의 BBR congestion control과 함께 등장. Pacing 지원:
- 각 패킷에 정확한 전송 시각.
- BBR이 지정한 rate 준수.
7.5 cake
Common Applications Kept Enhanced. 홈 라우터용 고급 qdisc. fq_codel의 개선:
- 자동 대역폭 제한.
- ATM overhead 인식.
- Multi-queue.
7.6 htb (Hierarchical Token Bucket)
대역폭 계층적 공유:
Root 100 Mbps
├── Video 50 Mbps (max 80)
├── Web 30 Mbps (max 60)
└── Bulk 20 Mbps (max 50)
각 클래스가 자기 몫을 보장받으면서 나머지 대역폭은 공유.
7.7 tc 명령어
# 현재 qdisc 확인
tc qdisc show dev eth0
# fq_codel 설정
tc qdisc add dev eth0 root fq_codel
# htb로 rate limit
tc qdisc add dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit
7.8 Ingress Control
기본적으로 tc는 egress. Ingress qdisc로 수신도:
tc qdisc add dev eth0 ingress
tc filter add dev eth0 parent ffff: protocol ip u32 \
match ip src 1.2.3.4/32 police rate 10kbit burst 10k drop
제한적이지만 DDoS mitigation에 유용.
8. 송신 경로 심층
8.1 dev_queue_xmit
IP 레이어가 패킷을 보낼 준비가 되면:
int dev_queue_xmit(struct sk_buff *skb) {
// 1. TX queue 선택 (XPS, skb->hash 기반)
// 2. qdisc에 enqueue
// 3. qdisc가 schedule 허락 시 driver 호출
}
8.2 Transmit Packet Steering (XPS)
여러 TX queue 중 어느 걸 쓸지. 보통 패킷의 CPU → TX queue 매핑 — 캐시 지역성.
echo 0x1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
# TX queue 0은 CPU 0에서만
8.3 ndo_start_xmit
드라이버의 전송 함수:
netdev_tx_t ndo_start_xmit(struct sk_buff *skb, struct net_device *dev) {
// 1. 드라이버가 sk_buff를 descriptor로 변환
// 2. DMA 주소 설정
// 3. NIC에 "새 descriptor 있어" 알림 (doorbell)
// 4. 성공 또는 NETDEV_TX_BUSY 반환
}
8.4 TSO — TCP Segmentation Offload
큰 데이터를 한 번에 커널 → NIC에 주고, NIC가 MTU 단위로 분할:
커널: TCP 65 KB 패킷 하나 (sk_buff)
NIC: 1460 바이트 세그먼트 44개로 분할 송신
이점:
- 커널에서 44개 skb 처리 안 해도 됨 → CPU 절감.
- TCP header는 NIC가 계산 (seq number, checksum).
ethtool -k eth0 | grep segmentation으로 지원 확인.
8.5 GSO — Generic Segmentation Offload
하드웨어 TSO가 없으면 커널이 세그먼트. 하지만 늦게 — qdisc 이후, 드라이버 직전에.
이점: 대부분 스택이 큰 sk_buff 하나로 처리, 마지막에만 분할 → 오버헤드 감소.
8.6 Checksum Offload
NIC가 TCP/UDP/IP checksum 계산. 커널은 "partial checksum"만 만들고, NIC가 완료.
대부분 NIC 지원. ethtool로 확인/설정.
9. 멀티 코어 확장 (RSS/RPS/RFS)
9.1 RSS — Receive Side Scaling
하드웨어 기능. NIC가 패킷을 해시해서 여러 RX queue에 분산:
hash(src_ip, dst_ip, src_port, dst_port) → RX queue i
각 RX queue가 다른 CPU의 인터럽트 → 멀티 코어 확장.
ethtool -x eth0로 RSS 설정 확인.
9.2 RPS — Receive Packet Steering
RSS 없는 NIC 또는 추가 분산 필요 시. 소프트웨어로 패킷을 CPU에 분산:
echo fffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# rx-0에서 받은 패킷을 모든 CPU 0-19에 분산
해시로 CPU 선택 → 해당 CPU의 backlog queue → softirq.
9.3 RFS — Receive Flow Steering
RPS의 개선: "이 flow를 처리한 CPU"를 기억해서 같은 CPU로 → 캐시 지역성:
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
특정 소켓에 recv()한 CPU가 있으면 그 CPU로 패킷 전달.
9.4 Accelerated RFS
하드웨어 지원 — NIC가 직접 "이 flow는 CPU X로" 지정. Intel NIC 등 지원.
9.5 효과
단일 큐 NIC + RSS 없음:
- 초당 ~500 Kpps (단일 코어 병목).
멀티 큐 NIC + RSS + RFS:
- 초당 수십 Mpps (멀티 코어 확장).
XDP까지 가면 100 Mpps 이상.
10. Offloads 정리
10.1 수신 Offloads
LRO (Large Receive Offload): NIC가 작은 TCP 세그먼트들을 큰 것 하나로 병합. 스택이 처리할 패킷 수 감소.
GRO (Generic Receive Offload): 소프트웨어 LRO. 더 정확, Linux 기본.
RX Checksum: NIC가 수신 시 체크섬 검증. 커널은 skip.
VLAN stripping: NIC가 VLAN 태그 제거 → sk_buff 메타데이터로.
10.2 송신 Offloads
TSO (TCP Segmentation Offload): 큰 TCP 패킷을 NIC가 분할.
UFO (UDP Fragmentation Offload): 큰 UDP 패킷 분할. 현재는 거의 폐기.
GSO (Generic Segmentation Offload): 소프트웨어 세그먼트. TSO 지원 안 하는 NIC에.
TX Checksum: NIC가 체크섬 계산.
Scatter/Gather: sk_buff의 여러 조각을 한 번의 DMA로.
10.3 확인 / 제어
ethtool -k eth0
# tcp-segmentation-offload: on
# generic-segmentation-offload: on
# generic-receive-offload: on
# tx-checksumming: on
# rx-checksumming: on
# scatter-gather: on
ethtool -K eth0 tso off # TSO 끄기 (디버깅)
10.4 언제 offload를 끄나
- 방화벽/proxy 박스: LRO는 패킷 정보 손실 → 정확한 포워딩 문제.
- 패킷 캡처: LRO로 합쳐진 건 원본과 다름.
- 낮은 레이턴시: TSO가 약간의 지연 추가.
일반 서버는 모두 켜는 것이 최적.
11. 우회 경로 — XDP, AF_XDP, io_uring
11.1 기본 스택의 한계
위에서 본 전통 스택은:
- 256+ 바이트 sk_buff 할당.
- 여러 레이어의 함수 호출.
- Netfilter hook 순회.
- lock 경쟁.
초당 수십만 pps까지는 OK. 수백만 pps는 병목.
11.2 XDP
이전 eBPF 포스트에서 설명. 여기서는 간단히:
- 드라이버가 패킷을 받자마자 XDP 프로그램 실행.
- sk_buff 할당 전.
- DROP / TX / REDIRECT / PASS 가능.
- 수백만 pps per core.
11.3 AF_XDP
XDP + 유저 공간 socket. XDP가 패킷을 sk_buff 없이 유저 공간 버퍼에 직접 전달:
int fd = socket(AF_XDP, SOCK_RAW, 0);
// ring buffer 설정
// XDP 프로그램이 해당 fd로 REDIRECT
DPDK 유사한 성능 + Linux 네이티브.
11.4 io_uring for Network
io_uring은 원래 파일 I/O용이지만 소켓도 지원:
// SQE 준비
io_uring_prep_recv(sqe, fd, buf, len, 0);
io_uring_submit(ring);
// 완료 대기
io_uring_wait_cqe(ring, &cqe);
이점:
- Syscall 오버헤드 감소 (batch submit).
- Zero-copy 지원 (registered buffers).
- Multi-shot (한 번 submit, 여러 번 받음).
io_uring recv multishot: 하나의 SQE로 여러 패킷 계속 받기. 매번 syscall 안 함.
11.5 DPDK
커널 우회 완전히. 드라이버를 유저 공간에서 실행.
- Poll mode driver (PMD): 항상 polling. 인터럽트 없음.
- Huge pages: TLB 미스 감소.
- Dedicated CPU: 한 코어 완전 점유.
초당 수천만 pps per core. 통신 장비, 고성능 서버에.
단점: 커널 우회라 기존 도구 (iptables, tcpdump) 사용 불가. 별도 구현 필요.
12. Connection Tracking과 NAT
12.1 Conntrack Entry
각 연결이 hash table의 entry:
struct nf_conn {
struct nf_conntrack_tuple_hash tuplehash[2]; // original + reply
atomic_t use;
unsigned long status; // state flags
struct timer_list timeout;
// Extensions: NAT, helper, timeout, ...
};
Tuple: (src_ip, src_port, dst_ip, dst_port, proto).
Original: 첫 패킷 방향. Reply: 응답 방향.
12.2 Timeouts
상태별 다른 timeout:
- TCP ESTABLISHED: 5일 (기본).
- TCP TIME_WAIT: 120초.
- UDP: 30초.
/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_*
12.3 conntrack 확인
conntrack -L # 모든 연결
conntrack -S # 통계
cat /proc/net/nf_conntrack
12.4 NAT
SNAT (Source NAT): masquerading. Outgoing 패킷의 source IP 변경.
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
DNAT (Destination NAT): port forwarding.
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to 10.0.0.5:8080
Conntrack이 첫 패킷에 변환을 기록, 이후 패킷은 자동 적용.
12.5 Full Cone vs Symmetric
NAT 종류:
- Full Cone: (inside IP, inside port)만으로 mapping. 모든 외부에서 같은 mapping.
- Symmetric: (inside, outside pair)로 mapping. 외부가 다르면 다른 mapping.
Linux의 기본 SNAT은 symmetric. P2P (WebRTC)에 문제 → STUN/TURN 필요.
13. 디버깅과 진단
13.1 tcpdump
가장 기본 도구. BPF 필터로 패킷 캡처:
tcpdump -i eth0 'tcp port 80' -nn -vv
tcpdump -i any host 1.2.3.4 -w out.pcap
주의: LRO/GRO가 패킷을 합쳐서 tcpdump에 다르게 보일 수 있음. ethtool -K eth0 gro off로 임시 끔.
13.2 ss (socket stats)
ss -tulnp # TCP/UDP listening ports + process
ss -ti # TCP 연결 상세 (cwnd, rtt, etc.)
ss -s # 요약 통계
netstat의 현대적 후임. 훨씬 빠름.
13.3 ip
ip addr
ip route
ip link show
ip neigh # ARP 테이블
13.4 ethtool
ethtool eth0 # 링크 상태
ethtool -S eth0 # 드라이버 통계
ethtool -g eth0 # ring buffer 크기
ethtool -l eth0 # 채널 수 (큐)
ethtool -k eth0 # offload 상태
ethtool -c eth0 # interrupt coalescing
13.5 /proc/net/*
cat /proc/net/dev # 인터페이스 통계
cat /proc/net/snmp # 프로토콜 통계
cat /proc/net/netstat # 확장 통계
cat /proc/net/softnet_stat # softirq 처리량
cat /proc/net/sockstat # 소켓 통계
13.6 bpftrace
고급 런타임 추적:
# 모든 TCP 재전송
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { printf("%s\n", comm); }'
# 드롭 원인
bpftrace -e 'kprobe:kfree_skb_reason { printf("reason %d\n", arg1); }'
# 대기열 정체
bpftrace -e 'tracepoint:net:net_dev_queue { @[args->name] = count(); }'
13.7 bcc tools
BCC (BPF Compiler Collection)에 유용 도구:
tcpconnect # 새 TCP 연결
tcpaccept # 새 accept
tcpretrans # 재전송
tcplife # 연결 수명
tcptracer # 연결 이벤트
softirqs # softirq 시간 분포
hardirqs # hardirq 시간
13.8 perf
perf record -g -p <pid>
perf report
perf stat -e 'skb:*' -a sleep 10 # sk_buff 이벤트 통계
13.9 흔한 문제
Packet drop:
cat /proc/net/softnet_stat
# 각 CPU의 dropped packets
# 많으면 RPS/RFS 조정 또는 netdev_max_backlog 증가
TIME_WAIT 쌓임:
ss -tan state time-wait | wc -l
# 많으면 tcp_tw_reuse, ephemeral port range 확인
Connection tracking full:
cat /proc/sys/net/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
# max에 도달하면 NEW connection drop
Retransmission 많음:
ss -ti # retrans, rto 확인
nstat -z # TCP 통계
14. 튜닝 팁
14.1 Buffer 크기
sysctl net.core.rmem_max=134217728 # 128 MB
sysctl net.core.wmem_max=134217728
sysctl net.ipv4.tcp_rmem='4096 87380 134217728'
sysctl net.ipv4.tcp_wmem='4096 65536 134217728'
대역폭이 높고 지연이 있으면 큰 buffer가 필요 (BDP).
14.2 Backlog
sysctl net.core.netdev_max_backlog=300000 # RPS backlog
sysctl net.ipv4.tcp_max_syn_backlog=8192 # SYN backlog
sysctl net.core.somaxconn=65535 # listen backlog 상한
14.3 TIME_WAIT
sysctl net.ipv4.tcp_tw_reuse=1 # 재사용 허용
sysctl net.ipv4.tcp_fin_timeout=15
tcp_tw_recycle은 폐기됨 (NAT와 호환 X). 사용하지 말 것.
14.4 Interrupt Coalescing
ethtool -C eth0 rx-usecs 50 rx-frames 32
# 50μs 또는 32 프레임 후 인터럽트
고부하에서 interrupt 감소 → CPU 절감. 지연 약간 증가.
14.5 CPU Affinity
# IRQ affinity
echo 1 > /proc/irq/32/smp_affinity
# 프로세스 affinity
taskset -cp 0-3 <pid>
네트워크 트래픽과 앱을 같은 NUMA 노드에.
15. 요약 — 한 장 정리
┌─────────────────────────────────────────────────────┐
│ Linux Network Stack Cheat Sheet │
├─────────────────────────────────────────────────────┤
│ 수신 경로: │
│ NIC → DMA → ring → 인터럽트 → NAPI │
│ → sk_buff 할당 → GRO → netif_receive_skb │
│ → IP → Netfilter → 라우팅 │
│ → TCP/UDP → socket queue → recv() │
│ │
│ 송신 경로: │
│ send() → TCP 혼잡 제어 → IP 라우팅 │
│ → Netfilter → qdisc │
│ → driver (TSO) → NIC │
│ │
│ sk_buff: │
│ 패킷 메타데이터 (~256 바이트) │
│ head/data/tail/end 포인터 │
│ headroom (헤더 추가), tailroom (트레일러) │
│ skb_push / skb_pull / skb_clone │
│ │
│ NAPI: │
│ Interrupt + polling 하이브리드 │
│ 인터럽트 → poll 시작 → 소진까지 반복 │
│ budget (기본 64)로 공정성 │
│ Receive livelock 해결 │
│ │
│ Netfilter 5 hooks: │
│ PREROUTING / INPUT / FORWARD / OUTPUT / │
│ POSTROUTING │
│ iptables (legacy) → nftables │
│ Conntrack: NAT + stateful firewall │
│ │
│ Traffic Control (tc): │
│ qdisc: pfifo_fast, fq_codel, fq, cake, htb │
│ fq_codel: 기본, bufferbloat 완화 │
│ fq: BBR용 pacing │
│ htb: 계층적 bandwidth share │
│ │
│ Offloads: │
│ 수신: GRO, LRO, RX checksum │
│ 송신: TSO, GSO, TX checksum, SG │
│ ethtool -K로 제어 │
│ │
│ 멀티 코어: │
│ RSS (하드웨어 queue 분산) │
│ RPS (소프트웨어 CPU 분산) │
│ RFS (flow affinity로 캐시 지역성) │
│ XPS (송신 queue 선택) │
│ │
│ 우회 경로: │
│ XDP (드라이버 직후, sk_buff 전) │
│ AF_XDP (유저 공간 직접) │
│ io_uring (async socket) │
│ DPDK (커널 완전 우회) │
│ │
│ 디버깅: │
│ tcpdump, ss, ip, ethtool │
│ bpftrace, bcc tools │
│ perf, /proc/net/* │
│ │
│ 튜닝: │
│ Buffer (rmem, wmem) │
│ Backlog (netdev_max_backlog, somaxconn) │
│ Interrupt coalescing │
│ CPU affinity │
└─────────────────────────────────────────────────────┘
16. 퀴즈
Q1. NAPI가 해결한 "Receive Livelock" 문제는?
A. 초당 수백만 인터럽트 상황에서 시스템이 실제 작업을 못 하는 현상. 전통 모델은 패킷 하나당 인터럽트 하나 — 10 Mpps면 10M 인터럽트/초. CPU가 인터럽트 핸들러만 실행하다가 실제 패킷 처리는 못하고, 더 나아가 유저 프로세스도 실행 못함. 시스템이 외견상 멈춘 것처럼 됨. NAPI의 해결: 첫 인터럽트가 오면 인터럽트를 비활성화하고 polling 모드로 진입. Budget만큼(기본 64 패킷) 처리한 후 다시 인터럽트 활성화. 저부하에서는 인터럽트(낮은 지연), 고부하에서는 polling(높은 처리량) — 자동 전환. 이 단순한 설계가 2003년 이후 Linux 네트워크 성능의 기반이 됐고, 현대에는 10+ Mpps가 commodity hardware에서 가능.
Q2. sk_buff가 왜 XDP에 의해 회피되는가?
A. 할당 비용과 메타데이터 크기. sk_buff는 약 256 바이트의 메타데이터를 포함하고 많은 필드 초기화가 필요. 작은 프레임(64 바이트)의 경우 메타데이터가 데이터보다 더 크다. 초당 수천만 패킷을 처리할 때 이 할당과 초기화만으로 CPU가 소진. XDP는 NIC 드라이버 직후, sk_buff 할당 전에 실행 — raw 버퍼 포인터만 받아 처리. 결과: DROP/REDIRECT 같은 간단한 결정을 단일 코어로 수천만 pps 가능. 전통 스택은 200-500 Kpps per core. 40-50배 차이. XDP가 "너무 빠른" 이유는 Linux 네트워크 스택을 실제로 우회하기 때문이고, sk_buff는 그 우회의 중심에 있는 구조체. 같은 원리가 AF_XDP, DPDK에도 적용 — sk_buff 할당을 피하는 것이 초고성능 패킷 처리의 공통 기법.
Q3. Netfilter의 5개 hook 중 NAT 규칙은 어디에 들어가는가?
A. PREROUTING (DNAT, destination rewrite) 과 POSTROUTING (SNAT/masquerade, source rewrite). 이유: (1) DNAT는 라우팅 결정 전에 변경되어야 함 — "어디로 전달할지"가 변경된 목적지 기준으로 결정되어야 함. PREROUTING이 라우팅보다 먼저. (2) SNAT는 라우팅 후 마지막 순간에 적용 — source IP만 바꾸고 다른 것은 그대로. POSTROUTING이 전송 직전 마지막 hook. Conntrack과 결합해서 첫 패킷의 변환 규칙을 기록 → 같은 flow의 후속 패킷은 자동 적용. iptables의 -t nat가 이 두 hook에 걸림. FILTER 테이블은 INPUT/FORWARD/OUTPUT에, MANGLE은 5개 모두에. 설계의 우아함: 각 hook이 명확한 목적을 가지고, NAT과 방화벽이 독립적으로 추가 가능.
Q4. Generic Receive Offload (GRO)가 성능을 어떻게 개선하는가?
A. 작은 TCP 세그먼트들을 큰 것 하나로 병합해서 스택 상위 레이어의 처리 횟수를 줄임. 예: 1460 바이트 세그먼트 10개가 연속으로 도착 → GRO가 이를 14600 바이트 "슈퍼 세그먼트" 하나로 합침. 이후 IP, TCP 레이어는 패킷 하나만 처리 — 함수 호출 10번 → 1번, sk_buff 조작 10번 → 1번, TCP 상태 업데이트 10번 → 1번. CPU 오버헤드 ~5배 감소 실측. "자연스러운 점보 프레임" 효과 — 하드웨어는 표준 프레임을 보내지만 소프트웨어 스택은 큰 프레임처럼 처리. GRO vs LRO: LRO는 하드웨어가 병합(빠르지만 일부 정보 손실), GRO는 소프트웨어(약간 느리지만 정확). Linux는 기본 GRO — 라우터/방화벽 같은 정확성 필요한 곳에 안전. ethtool -K gro off로 끌 수 있음, 패킷 캡처 시 원본 보려면 필요.
Q5. RSS, RPS, RFS의 차이는?
A. 누가 어떻게 CPU를 선택하는가. RSS (Receive Side Scaling): 하드웨어 기능. NIC가 패킷의 5-tuple을 해시해서 여러 RX queue에 분산, 각 queue가 다른 CPU의 인터럽트 대상. "하드웨어 로드 밸런싱". RPS (Receive Packet Steering): RSS가 없거나 추가 분산 필요 시. 소프트웨어로 단일 queue에서 받은 패킷을 여러 CPU에 분배. 해시 기반. 간단하지만 캐시 지역성 고려 없음. RFS (Receive Flow Steering): RPS 개선. "이 flow를 마지막에 recv()한 CPU"를 기억 → 같은 CPU로 라우팅. 캐시 지역성 최적 — 소켓 버퍼, TCP state가 L1 캐시에 있을 가능성. RSS는 하드웨어 단, RPS는 커널 단, RFS는 애플리케이션 인식. 대형 서버는 셋 모두 조합 사용 — RSS로 queue 분산 + RPS로 추가 CPU 확장 + RFS로 캐시 친화성.
Q6. fq_codel이 bufferbloat을 어떻게 완화하는가?
A. Fair Queueing + Controlled Delay 결합. Bufferbloat: 라우터/호스트의 큐가 너무 커서 패킷이 수백 ms 대기 → TCP 혼잡 제어가 제대로 작동 못함. Fair Queueing: 각 flow(5-tuple)별로 별도 큐 → 한 대역폭-hog flow가 다른 flow를 굶기지 못함. Round-robin 스케줄링으로 공정 분배. CoDel (Controlled Delay): 큐에 들어온 패킷의 체류 시간을 측정. 5ms 이상이 100ms 이상 지속되면 "혼잡" 판단 → 큐 head에서 패킷 드롭 (ECN 지원 시 마킹). TCP가 드롭/ECN을 혼잡 신호로 해석 → 송신 속도 낮춤 → 큐 감소. 결과: 큐가 커지기 전에 TCP가 자제 → 지연 제한. 2014년 Linux 기본(일부 배포판). 가정용 라우터에도 점차 채택 — 실질적 "Netflix가 Zoom을 죽이는" 현상 해결. 단순한 아이디어가 전 세계 네트워크 지연을 개선한 사례.
Q7. io_uring이 네트워크 성능에 주는 이점은?
A. Syscall 오버헤드 제거와 async I/O 통합. 전통 epoll + recv 패턴: (1) epoll_wait 대기, (2) recv syscall, (3) 처리, (4) 반복 — 각 recv가 syscall 경계 crossing(~200ns). 초당 100만 패킷이면 syscall만 200ms/s — 무시 못할 오버헤드. io_uring의 접근: (1) Submission Queue에 여러 recv 작업을 batch로 제출 (1 syscall), (2) 커널이 async로 실행, (3) Completion Queue에서 결과 batch로 수집. Multi-shot recv: 하나의 submission으로 여러 패킷 연속 수신 — 매번 재제출 불필요. Registered buffers: 미리 등록된 버퍼로 zero-copy 가능. Registered files: fd 조회 오버헤드 제거. 실측: epoll 대비 2-5배 throughput. Nginx, Redis, TFTP 서버 등이 실험적 채택. 2024+ 프로덕션 워크로드에 점진 적용. 커널 5.1+ 지원, 6.x에 상당히 개선. "syscall-heavy"가 병목이었던 워크로드의 게임 체인저.
이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:
- "TCP Congestion Control BBR/Cubic/Reno" — TCP 레이어의 상세.
- "eBPF Deep Dive" — XDP와 tc-bpf의 기반.
- "Zero-Copy I/O (sendfile, splice, mmap)" — 효율적 I/O 경로.
- "Envoy Proxy Internals" — L7 프록시가 이 스택 위에.