Skip to content

필사 모드: Linux 네트워크 스택 Deep Dive — sk_buff, NAPI, Netfilter, Traffic Control, GRO/TSO 완전 정복 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

드라이버가 시작 시:

1. 각 RX queue에 **ring buffer** 할당.

2. 각 descriptor에 **DMA 주소** 설정 (pre-allocated sk_buff 또는 page).

3. NIC에 ring의 base, size 전달.

패킷 도착 시:

1. NIC가 **DMA로 직접** descriptor의 주소에 쓰기.

2. Descriptor에 "written" 플래그.

3. **인터럽트** (또는 NAPI 중이면 생략).

커널:

1. Ring에서 새 descriptor 찾기.

2. 해당 sk_buff를 네트워크 스택에 전달.

3. 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. 퀴즈

**A.** **초당 수백만 인터럽트 상황에서 시스템이 실제 작업을 못 하는 현상**. 전통 모델은 패킷 하나당 인터럽트 하나 — 10 Mpps면 10M 인터럽트/초. CPU가 인터럽트 핸들러만 실행하다가 실제 패킷 처리는 못하고, 더 나아가 **유저 프로세스도 실행 못함**. 시스템이 외견상 멈춘 것처럼 됨. NAPI의 해결: 첫 인터럽트가 오면 인터럽트를 **비활성화**하고 polling 모드로 진입. Budget만큼(기본 64 패킷) 처리한 후 다시 인터럽트 활성화. 저부하에서는 인터럽트(낮은 지연), 고부하에서는 polling(높은 처리량) — 자동 전환. 이 단순한 설계가 2003년 이후 Linux 네트워크 성능의 기반이 됐고, 현대에는 10+ Mpps가 commodity hardware에서 가능.

**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 할당을 피하는 것이 초고성능 패킷 처리의 공통 기법.

**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과 방화벽이 독립적으로 추가 가능.

**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`로 끌 수 있음, 패킷 캡처 시 원본 보려면 필요.

**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로 캐시 친화성.

**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을 죽이는" 현상 해결. 단순한 아이디어가 전 세계 네트워크 지연을 개선한 사례.

**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 프록시가 이 스택 위에.

현재 단락 (1/632)

- Linux 네트워크 스택은 **NIC → sk_buff 할당 → NAPI polling → Netfilter → 라우팅 → TCP/UDP → 소켓 → 유저 복사**의 파이프라인...

작성 글자: 0원문 글자: 20,240작성 단락: 0/632