Skip to content
Published on

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

Authors

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 수신 경로

하드웨어 프레임 도착
NICDMARX 링 버퍼에 기록
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 (하드웨어 세그먼트)
NICTX 링에 descriptor
NICDMA로 전송 → 와이어

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.cnapi_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_inputrouting 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 모듈. 각 연결을 추적 → NATstateful firewall의 기반.

PacketPREROUTINGconntrack (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_SENTconnect()
close()CLOSED
   FIN_WAIT1                          CLOSE_WAIT
ACKclose()
   FIN_WAIT2                            LAST_ACK
FINACK
   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├─────────────────────────────────────────────────────┤
│ 수신 경로:NICDMA → ring → 인터럽트 → NAPI│   → sk_buff 할당 → GRO → netif_receive_skb          │
│   → IPNetfilter → 라우팅                          │
│   → 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 /POSTROUTINGiptables (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 프록시가 이 스택 위에.