네트워크 패킷(Packet) 흐름 & 디버깅(Debugging)

Linux 네트워크 스택(Network Stack) 고급 주제를 다룹니다. 멀티코어 NIC 환경에서 RSS/RPS/RFS/XPS/aRFS가 실제로 트래픽을 어떻게 분산하는지, GRO/GSO가 CPU 사용률과 레이턴시에 미치는 영향, conntrack과 Netfilter 경로의 오버헤드(Overhead), 패킷 드롭·큐 적체·reorder 원인을 추적하는 방법, NUMA/IRQ affinity/busy-poll 기반 튜닝 전략까지 실전 중심으로 설명합니다.

전제 조건: 커널 아키텍처 문서를 먼저 읽으세요. CPU, 메모리, 인터럽트(Interrupt)의 기본 흐름을 알고 있으면 본 문서를 더 빠르게 이해할 수 있습니다.
일상 비유: 이 개념은 작업 순서표를 따라 문제를 해결하는 과정과 비슷합니다. 핵심 용어를 먼저 잡고, 실제 동작 순서를 단계별로 확인하면 복잡한 커널 내부 동작을 안정적으로 이해할 수 있습니다.

핵심 요약

  • 핵심 객체 — 이 문서의 중심이 되는 자료구조/API를 먼저 파악합니다.
  • 실행 경로 — 요청이 들어와 처리되고 종료되는 흐름을 확인합니다.
  • 병목(Bottleneck) 지점 — 지연(Latency)이나 처리량(Throughput) 저하가 발생하는 구간을 점검합니다.
  • 동기화 지점 — 경합(Contention)과 경쟁 조건(Race Condition)이 생길 수 있는 구간을 구분합니다.
  • 운영 포인트 — 관측 지표와 튜닝 항목을 함께 확인합니다.

단계별 이해

  1. 구성요소 확인
    핵심 자료구조와 주요 API를 먼저 식별합니다.
  2. 요청 흐름 추적
    입력부터 완료까지의 호출 경로를 순서대로 따라갑니다.
  3. 예외 경로 점검
    실패 처리, 재시도, 타임아웃 등 경계 조건을 확인합니다.
  4. 성능/안정성 점검
    잠금 경합(Lock Contention), 큐 적체, 병목 지점을 측정하고 조정합니다.
기초 개념: 네트워크 스택에서 기본 개념(sk_buff, 소켓(Socket) 계층, TCP)을 먼저 확인하세요.

NAPI — 성능 튜닝과 주의사항

NAPI 상태 전이와 Budget 관리

DISABLED SCHED (대기) POLLING COMPLETE enable softirq done<budget done==budget (계속 폴링) IRQ → napi_schedule()
매개변수기본값설명튜닝 가이드
budget (per-NAPI) 64 poll() 한 번 호출에서 처리할 최대 패킷 수 증가 시 처리량↑, 지연↑. NIC 드라이버에서 설정
netdev_budget 300 softirq 한 사이클에서 모든 NAPI의 총 처리량 10G+ 환경에서 600~1200으로 증가 고려
netdev_budget_usecs 2000 (2ms) softirq 한 사이클의 시간 제한 지연 민감 환경에서 감소, 처리량 중시에서 증가
busy_poll 0 (비활성) 소켓 busy polling 시간 (μs) 50~100μs 설정 시 지연 감소 (CPU 사용률 증가)
busy_read 0 (비활성) 소켓 읽기 busy polling 시간 (μs) busy_poll과 함께 설정

GRO (Generic Receive Offload)

GRO는 NAPI poll 내에서 수신된 여러 패킷을 하나의 대형 skb로 병합하여 상위 스택 호출 횟수를 줄입니다. LRO(Large Receive Offload)의 소프트웨어 대체로, 원본 헤더 정보를 보존하여 포워딩 환경에서도 안전합니다.

GRO 패킷 병합 과정 NAPI poll() 수신 패킷 pkt 1: seq 1000 pkt 2: seq 2460 pkt 3: seq 3920 pkt 4: seq 5380 각 1460B (MSS) napi_gro_receive() 1. rxhash로 gro_hash 버킷 검색 2. 동일 flow 존재? (src/dst 비교) 병합 기준 검증: - TCP seq 연속? - ACK만 설정? (SYN/FIN 거부) - 윈도우/타임스탬프 일관? 3. frag[] 또는 frag_list 방식 병합 4. MAX_GRO_SKBS(8) 초과 시 flush GRO_MERGED 기존 skb에 병합 GRO_HELD gro_list에 보관 (새 flow) GRO_NORMAL 병합 불가 → 일반 경로 Super Packet seq 1000~6839 5840B (4 × MSS) GRO OFF 1M pps → 1M회 netif_receive_skb() 호출 포워딩: 43개 패킷 × routing/conntrack/NAT CPU 사용률 높음 GRO ON 1M pps → ~15K회 호출 (64KB super-packet) 포워딩: 1개 대형 skb × routing/conntrack/NAT CPU 사용률 대폭 감소 (~43배 효율)
/* === GRO 수신 경로 ===
 *
 * NIC IRQ → napi_schedule()
 *  └→ NAPI poll()
 *      └→ napi_gro_receive(napi, skb)
 *          └→ dev_gro_receive()
 *              └→ inet_gro_receive()        (L3: IP)
 *                  └→ tcp4_gro_receive()    (L4: TCP)
 *                      ├→ 동일 flow 검색 (rxhash → gro_hash 버킷)
 *                      ├→ 병합 기준 검증:
 *                      │   - 동일 src/dst IP + port
 *                      │   - TCP seq 연속 (이전 끝 + 1)
 *                      │   - ACK만 설정 (SYN/FIN/RST → 거부)
 *                      │   - 윈도우 크기 동일
 *                      │   - TCP 타임스탬프 일관성
 *                      └→ 결과:
 *                          GRO_MERGED     : 기존 skb에 병합
 *                          GRO_HELD       : gro_list에 보관 (새 flow)
 *                          GRO_NORMAL     : 병합 불가 → 일반 경로
 */

/* NAPI poll 함수에서 GRO 사용 패턴 */
napi_gro_receive(napi, skb);   /* 일반적: 완전한 skb를 GRO 처리 */
napi_gro_frags(napi);          /* 페이지 기반 수신 시 (헤더/데이터 분리)
                                 * 고성능 NIC 드라이버에서 선호:
                                 *   napi->skb에 헤더(선형) + 페이로드(frag)
                                 *   → 메모리 복사 최소화 */

/* GRO 데이터 병합 방식 */
/* 1. frag 기반: skb_shinfo→frags[]에 페이지 추가 (MAX_SKB_FRAGS=17 제한)
 * 2. frag_list 기반: skb_shinfo→frag_list에 skb 체인 (제한 없음)
 *    → frag 공간 부족 시 자동 전환 */

/* GRO flush 조건:
 * 1. napi_complete_done() 호출 시 (budget 미만 처리)
 * 2. gro_hash 버킷에 MAX_GRO_SKBS(8)개 초과 시
 * 3. 비연속 패킷 수신 시 (seq 불연속, 다른 플래그)
 * 4. gro_flush_timeout 타이머 만료 시
 *    → sysctl net.core.gro_flush_timeout (기본 0 = 즉시 flush)
 *    → net.core.napi_defer_hard_irqs와 함께 사용하면 GRO 효율↑ */

/* GRO 성능 효과 예시 (1500 MTU, TCP):
 *   GRO OFF: 1M pps → 1M번 netif_receive_skb() 호출
 *   GRO ON:  1M pps → ~15K번 호출 (64KB super-packet 생성)
 *   → CPU 사용률 대폭 감소, 처리량 증가
 *
 * 포워딩 환경 (라우터, 브리지):
 *   GRO OFF: 43개 패킷 × routing/conntrack/NAT
 *   GRO ON:  1개 대형 skb × routing/conntrack/NAT → ~43배 효율 */

/* HW-GRO (커널 5.19+) — NIC가 GRO 수행하되 헤더 보존 */
/* # ethtool -K eth0 rx-gro-hw on
 * → LRO와 달리 원본 헤더 정보 유지 → 포워딩에도 안전
 * → NIC의 RSC(Receive Side Coalescing) 기능 활용 */

/* GRO 제어 및 확인 */
/* # ethtool -K eth0 gro on|off           # SW GRO 전환 */
/* # ethtool -K eth0 rx-gro-hw on|off     # HW GRO 전환 (5.19+) */
/* # ethtool -k eth0 | grep gro           # 상태 확인 */
/* # ethtool -S eth0 | grep gro           # GRO 통계 확인 */
GRO 상세 분석: 병합 기준, 프로토콜별 콜백(Callback) 체인, flush 메커니즘, HW-GRO 등 내용은 GSO/GRO — 섹션을 참고하세요.
NAPI 폴링(Polling) 루프, busy polling, 적응형 인터럽트 조절, GRO 통합 아키텍처, 멀티큐 스케일링 등 NAPI 전반에 대한 내용은 NAPI 문서를 참고하세요.

NAPI 드라이버 구현 주의사항

NAPI 구현 시 흔한 실수:
  1. budget 미준수 — poll 함수가 budget 이상 처리하면 안 됨. 정확히 budget만큼 처리했으면 budget 반환, 적게 처리하면 실제 수를 반환
  2. napi_complete_done 누락 — work_done < budget일 때 반드시 호출해야 다음 IRQ에서 재스케줄 가능
  3. IRQ 재활성화 순서napi_complete_done() 이후에 HW 인터럽트를 재활성화해야 함. 순서가 반대면 race condition
  4. 멀티큐 미고려 — RSS/멀티큐 NIC에서는 큐마다 별도의 NAPI 인스턴스 필요. CPU affinity 설정 중요
  5. RX 링 고갈 — poll에서 버퍼(Buffer) refill을 하지 않으면 RX 링이 비어서 패킷 드롭 발생

RSS, RPS, RFS — 멀티코어 네트워크 분산

기법계층설명설정
RSS Hardware NIC가 flow hash로 큐 분배 (H/W 인터럽트 분산) ethtool -L eth0 combined 8
RPS Software 커널에서 패킷을 CPU로 분배 (RSS 미지원 NIC용) echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
RFS Software 패킷을 해당 소켓을 처리하는 CPU로 전달 (캐시(Cache) 친화) echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
XPS Software TX 큐를 CPU에 매핑 (TX 측 분산) echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
aRFS HW+SW NIC가 flow를 올바른 RX 큐로 직접 스티어링 NIC ntuple filter + RFS 조합

Toeplitz Hash — RSS 해시(Hash) 알고리즘

RSS의 핵심은 NIC 하드웨어가 패킷 헤더로부터 해시 값을 계산하여 수신 큐를 결정하는 것입니다. 대부분의 NIC는 Microsoft가 정의한 Toeplitz 해시를 사용합니다. Toeplitz 해시는 XOR 기반의 비트 연산으로, 하드웨어 구현이 매우 단순하면서도 트래픽 분산 특성이 우수합니다.

해시 입력 (Hash Input)

NIC는 패킷 유형에 따라 해시 입력 필드를 선택합니다:

해시 타입입력 필드적용 대상
4-tuple src IP, dst IP, src port, dst port TCP, UDP, SCTP
2-tuple src IP, dst IP non-TCP/UDP IPv4, IPv6 (포트 없는 프로토콜)
확장 src IP, dst IP, SPI (Security Parameter Index) IPsec (ESP/AH)
참고: UDP의 경우, 단편화(fragmentation) 시 첫 번째 단편만 포트 정보를 포함하므로 후속 단편은 2-tuple로 해싱됩니다. 이로 인해 동일 플로우의 단편이 다른 큐로 분배될 수 있습니다. ethtool -N eth0 rx-flow-hash udp4 sd로 UDP를 2-tuple로 고정하면 이 문제를 완화할 수 있습니다.

Toeplitz 해시 알고리즘

Toeplitz 해시는 해시 키(Key)입력 데이터를 비트 단위로 XOR 누적하여 32비트 해시를 생성합니다:

/*
 * Toeplitz Hash 의사코드
 *
 * input[]:  해시 입력 (예: src_ip + dst_ip + src_port + dst_port)
 *           IPv4 4-tuple = 12바이트 (96비트)
 * key[]:    해시 키 (40바이트 = 320비트, 네트워크 바이트 순서)
 * 결과:     32비트 해시 값
 */
uint32_t toeplitz_hash(uint8_t *input, int input_len, uint8_t *key)
{
    uint32_t result = 0;
    int i, j;

    for (i = 0; i < input_len; i++) {
        for (j = 0; j < 8; j++) {
            if (input[i] & (1 << (7 - j))) {
                /* key의 (i*8+j) 위치에서 시작하는 32비트를 XOR */
                result ^= get_unaligned_be32(key + i) << j
                        | (uint32_t)get_unaligned_be32(key + i + 4) >> (32 - j);
            }
        }
    }
    return result;
}
핵심 원리: 입력 데이터의 각 비트가 1이면, 해시 키의 해당 위치에서 시작하는 32비트 윈도우를 결과에 XOR합니다. 입력 비트가 0이면 건너뛴다. 즉, 입력 비트가 키 윈도우를 선택(select)하는 구조로, 하드웨어에서 시프트 레지스터(Register)와 XOR 게이트만으로 구현 가능합니다.

커널 내부의 소프트웨어 구현은 include/linux/netdevice.h의 인라인과 lib/toeplitz.c에 위치합니다. RPS가 사용하는 소프트웨어 해시도 동일한 Toeplitz를 사용하며, net/core/flow_dissector.c__skb_get_hash()에서 호출됩니다:

/* include/linux/netdevice.h — 커널 소프트웨어 Toeplitz */
static inline __u32
__toeplitz_hash(const __u32 *key_cache, int nkeys,
                const __u32 *data, int ndata)
{
    __u32 hash = 0;
    int i;

    for (i = 0; i < ndata; i++)
        hash ^= toeplitz_byte(data[i], key_cache + i);
    return hash;
}

해시 키 (Hash Key)

Toeplitz 해시 키는 일반적으로 40바이트 (320비트)입니다. NIC 드라이버가 초기화 시 기본 키를 설정하며, ethtool로 조회·변경할 수 있습니다:

# 현재 RSS 해시 키 조회
$ ethtool -x eth0
RX flow hash indirection table for eth0 with 4 RX ring(s):
    0:      0     1     2     3     0     1     2     3
    8:      0     1     2     3     0     1     2     3
  ...
RSS hash key:
6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:ca:2b:cb:...

# 커스텀 해시 키 설정 (대칭 키 예시 — src/dst 교환 시 동일 해시)
$ ethtool -X eth0 hkey \
  6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:ca:2b:cb:...

# 해시 함수 종류 확인 (toeplitz / xor / crc32)
$ ethtool -x eth0 | grep "RSS hash function"
RSS hash function:
    toeplitz: on
    xor: off
    crc32: off

# 해시 함수 변경 (NIC 지원 시)
$ ethtool -X eth0 hfunc toeplitz
대칭 해시 (Symmetric Hash): 기본 Toeplitz 키에서는 (A→B)(B→A) 트래픽이 다른 해시 값을 가져 서로 다른 큐로 분배될 수 있습니다. 연결 추적(Connection Tracking)이나 양방향 플로우 모니터링이 필요한 경우, 대칭 키를 사용하면 src/dst를 교환해도 동일한 해시가 생성됩니다. 일부 NIC(Intel ixgbe 등)는 symmetric-xor 해시 함수를 별도로 지원합니다.

해시 필드 설정 (ethtool -N)

프로토콜별로 해시에 사용할 필드를 세밀하게 제어할 수 있습니다:

# TCP4: 4-tuple 해시 (기본값)
$ ethtool -N eth0 rx-flow-hash tcp4 sdfn
# s=src IP, d=dst IP, f=src port, n=dst port

# UDP4: 2-tuple로 변경 (단편화 이슈 방지)
$ ethtool -N eth0 rx-flow-hash udp4 sd

# 현재 설정 확인
$ ethtool -n eth0 rx-flow-hash tcp4
TCP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
L4 bytes 0 & 1 [TCP/UDP src port]
L4 bytes 2 & 3 [TCP/UDP dst port]

RETA — Redirection Table (인다이렉션 테이블)

Toeplitz 해시가 32비트 해시 값을 생성하면, NIC는 이 값의 하위 N비트를 인덱스로 사용하여 RETA(Redirection Table)를 참조합니다. RETA의 각 엔트리는 실제 수신 큐 번호를 가리킵니다.

수신 패킷 헤더 src/dst IP + port Toeplitz Hash + 40-byte Key 32-bit Hash 0x7A3B...F2 하위 N비트 추출 index = hash & (size-1) RETA (128 entries) [0]=Q0 [1]=Q1 [2]=Q2 [3]=Q3 [4]=Q0 [5]=Q1 [6]=Q2 [7]=Q3 ... ... ... ... [124]=Q0 [125]=Q1 [126]=Q2 [127]=Q3 RX Queue 0 RX Queue 1 RX Queue 2 → CPU 0 → CPU 1 → CPU 2

RETA 구조와 크기

NIC 계열RETA 크기인덱스 비트비고
Intel i350, 82576 128 엔트리 하위 7비트 GbE 서버용
Intel 82599 (ixgbe) 128 엔트리 하위 7비트 10GbE, SR-IOV 지원
Intel X710 (i40e) 512 엔트리 하위 9비트 더 세밀한 분배 가능
Intel E810 (ice) 2048 엔트리 하위 11비트 100GbE, ADQ 지원
Mellanox ConnectX-5/6 가변 (최대 4096) 가변 TIR (Transport Interface Receive) 기반
Broadcom BCM57xx 128 엔트리 하위 7비트 bnxt 드라이버

RETA의 각 엔트리는 0부터 시작하는 수신 큐 번호를 저장합니다. 기본적으로 라운드 로빈(RETA[i] = i % num_queues)으로 초기화되며, 이렇게 하면 트래픽이 모든 큐에 균등하게 분배됩니다.

RETA 조회 및 설정

# RETA 인다이렉션 테이블 조회
$ ethtool -x eth0
RX flow hash indirection table for eth0 with 4 RX ring(s):
    0:      0     1     2     3     0     1     2     3
    8:      0     1     2     3     0     1     2     3
   16:      0     1     2     3     0     1     2     3
   24:      0     1     2     3     0     1     2     3
   ...

# 균등 분배 (기본값) — N개 큐에 라운드 로빈
$ ethtool -X eth0 equal 4
# RETA = [0,1,2,3,0,1,2,3,...] → 4개 큐 균등 분배

# 가중치 분배 — 큐별 비율 지정
$ ethtool -X eth0 weight 3 1 1 1
# Queue 0에 50%, Queue 1~3에 각 16.7%
# RETA = [0,0,0,1,0,0,0,2,...] 등으로 채워짐

# 특정 큐만 사용 (큐 0, 1만 활성)
$ ethtool -X eth0 weight 1 1 0 0
# Queue 2, 3은 RSS 트래픽 수신 안 함
NUMA 최적화: 큐를 특정 NUMA 노드의 CPU에만 매핑하면 캐시 효율이 향상됩니다. 예를 들어 8큐 NIC에서 NUMA 노드 0의 CPU 0~3만 사용하려면: ethtool -X eth0 weight 1 1 1 1 0 0 0 0으로 큐 0~3만 활성화하고, /proc/irq/<IRQ>/smp_affinity로 해당 큐의 IRQ를 같은 CPU에 고정합니다.

커널 내부: RETA 설정 경로

RETA 설정은 ethtool_ops 콜백을 통해 드라이버로 전달됩니다:

/* include/linux/ethtool.h — 드라이버가 구현하는 콜백 */
struct ethtool_ops {
    /* RETA 인다이렉션 테이블 조회/설정 */
    int (*get_rxfh_indir_size)(struct net_device *);
    int (*get_rxfh)(struct net_device *,
                    struct ethtool_rxfh_param *);
    int (*set_rxfh)(struct net_device *,
                    struct ethtool_rxfh_param *,
                    struct netlink_ext_ack *);
    /* ... */
};

/* ethtool_rxfh_param — RETA + 해시 키 + 해시 함수를 한 번에 전달 */
struct ethtool_rxfh_param {
    u32 *indir;          /* RETA 테이블 (큐 번호 배열) */
    u8  *key;            /* Toeplitz 해시 키 */
    u8   hfunc;          /* 해시 함수 (ETH_RSS_HASH_*) */
    u32  indir_size;     /* RETA 엔트리 수 */
    u32  key_size;       /* 해시 키 바이트 수 */
};

예를 들어 Intel ixgbe 드라이버에서의 RETA 프로그래밍:

/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
 * RETA를 하드웨어 레지스터에 기록 */
static void ixgbe_store_reta(struct ixgbe_adapter *adapter)
{
    u32 reta_entries = ixgbe_rss_indir_tbl_entries(adapter);  /* 128 */
    u32 i, reta = 0;

    for (i = 0; i < reta_entries; i++) {
        reta |= (u32)adapter->rss_indir_tbl[i] <<
                (i & 0x3) * 8;        /* 4개 엔트리를 32비트에 패킹 */
        if ((i & 3) == 3) {
            IXGBE_WRITE_REG(hw, IXGBE_RETA(i >> 2), reta);
            reta = 0;
        }
    }
}

RSS 전체 흐름 요약

단계위치동작
1. 패킷 수신 NIC H/W 패킷 헤더에서 해시 입력 필드 추출 (IP, port)
2. 해시 계산 NIC H/W Toeplitz(input, key) → 32비트 해시 값
3. RETA 참조 NIC H/W queue = RETA[hash & (reta_size - 1)]
4. DMA 전송 NIC → Memory 패킷을 해당 큐의 RX 링 버퍼(Ring Buffer)에 DMA
5. 인터럽트 NIC → CPU 해당 큐에 바인딩된 CPU로 MSI-X 인터럽트 발생
6. NAPI poll 커널 해당 CPU에서 큐의 패킷 처리 (softirq)

RSS 디버깅 및 모니터링

# 큐별 패킷 수 확인 — 분배가 균등한지 검증
$ ethtool -S eth0 | grep rx_queue
     rx_queue_0_packets: 1523847
     rx_queue_1_packets: 1518293
     rx_queue_2_packets: 1521056
     rx_queue_3_packets: 1519834

# 큐별 IRQ 확인
$ grep eth0 /proc/interrupts
 128:   152384    0    0    0  IR-PCI-MSI eth0-TxRx-0
 129:        0  151829    0    0  IR-PCI-MSI eth0-TxRx-1
 130:        0    0  152105    0  IR-PCI-MSI eth0-TxRx-2
 131:        0    0    0  151983  IR-PCI-MSI eth0-TxRx-3

# IRQ affinity 설정 (큐 0 → CPU 0)
$ echo 1 > /proc/irq/128/smp_affinity

# 활성 큐 수 변경
$ ethtool -L eth0 combined 8     # combined RX+TX 8큐로
$ ethtool -l eth0                 # 현재 설정 확인
Channel parameters for eth0:
Pre-set maximums:
RX:     0
TX:     0
Other:  1
Combined:    63
Current hardware settings:
RX:     0
TX:     0
Other:  1
Combined:    8

# sk_buff의 해시 값 확인 (BPF로)
$ bpftrace -e 'kprobe:netif_receive_skb {
    printf("hash=0x%x queue=%d\n",
           ((struct sk_buff *)arg0)->hash,
           ((struct sk_buff *)arg0)->queue_mapping);
}'
큐 불균형 진단: ethtool -S에서 특정 큐에 트래픽이 집중되면: (1) 소수의 플로우가 대부분의 트래픽을 차지하는지 확인 (elephant flow), (2) 해시 키를 변경하여 분포 개선 시도, (3) Flow Director(ntuple filter)로 특정 플로우를 지정된 큐로 스티어링하는 것을 고려합니다.

Flow Director — 정밀 플로우 스티어링

RSS의 해시 기반 분배로 충분하지 않을 때, Flow Director (Intel의 fdir / ntuple filter)로 특정 플로우를 원하는 큐에 직접 매핑할 수 있습니다. Flow Director 규칙은 RSS보다 높은 우선순위(Priority)를 가집니다:

# 특정 목적지 포트의 트래픽을 큐 3으로 스티어링
$ ethtool -N eth0 flow-type tcp4 dst-port 80 action 3

# 특정 5-tuple 매칭
$ ethtool -N eth0 flow-type tcp4 \
    src-ip 10.0.0.1 dst-ip 10.0.0.2 \
    src-port 12345 dst-port 443 action 2

# 현재 규칙 목록
$ ethtool -n eth0
4 RX rings available
Total 2 rules
Filter: 1023
    Rule Type: TCP over IPv4
    Src IP addr: 0.0.0.0 mask: 255.255.255.255
    Dest IP addr: 0.0.0.0 mask: 255.255.255.255
    TOS: 0x0 mask: 0xff
    Src port: 0 mask: 0xffff
    Dest port: 80 mask: 0x0
    Action: Direct to queue 3

# 규칙 삭제
$ ethtool -N eth0 delete 1023

RPS (Receive Packet Steering) — 소프트웨어 RSS

RPS는 커널 소프트웨어에서 수신 패킷을 여러 CPU로 분배하는 메커니즘입니다. RSS를 지원하지 않는 NIC나, 큐 수가 CPU 수보다 적은 환경에서 유용합니다. NIC의 하드웨어 큐에서 패킷을 받은 CPU가 해시를 계산하고, 그 결과에 따라 다른 CPU의 backlog 큐에 패킷을 넣어 처리를 분산시킵니다.

RPS 아키텍처

NIC (single queue) CPU 0 (IRQ 처리) netif_receive_skb() → get_rps_cpu() Hash 계산 skb_get_hash() IPI (Inter-Processor Interrupt) CPU 1 backlog process_backlog() CPU 2 backlog process_backlog() CPU 3 backlog process_backlog() IP → TCP 프로토콜 처리 IP → TCP 프로토콜 처리 IP → TCP 프로토콜 처리

RPS 커널 구현

RPS의 핵심 로직은 net/core/dev.cget_rps_cpu() 함수에 있습니다. 이 함수는 패킷의 해시 값을 계산하고, rps_map을 참조하여 대상 CPU를 결정합니다:

/* net/core/dev.c — RPS CPU 선택 핵심 로직 */
static int get_rps_cpu(struct net_device *dev,
                       struct sk_buff *skb,
                       struct rps_dev_flow **rflowp)
{
    struct netdev_rx_queue *rxqueue;
    struct rps_map *map;
    struct rps_sock_flow_table *sock_flow_table;
    int cpu = -1;
    u32 hash;

    rxqueue = dev->_rx + skb_get_rx_queue(skb);
    map = rcu_dereference(rxqueue->rps_map);
    if (!map)
        return -1;

    /* 패킷의 flow hash 계산 (Toeplitz 기반) */
    hash = skb_get_hash(skb);
    if (!hash)
        return -1;

    /* RFS (sock_flow_table)가 설정된 경우, 소켓을 처리하는 CPU 우선 */
    sock_flow_table = rcu_dereference(rps_sock_flow_table);
    if (sock_flow_table) {
        /* RFS 로직: 소켓의 마지막 처리 CPU를 참조 */
        /* ... (아래 RFS 섹션 참조) */
    }

    /* 해시 기반 CPU 선택: hash를 rps_map의 CPU 배열 인덱스로 변환 */
    cpu = map->cpus[reciprocal_scale(hash, map->len)];

    return cpu;
}

대상 CPU가 결정되면, enqueue_to_backlog()를 통해 해당 CPU의 per-CPU backlog 큐(softnet_data.input_pkt_queue)에 패킷을 삽입하고, IPI(Inter-Processor Interrupt)로 대상 CPU를 깨운다:

/* net/core/dev.c — 대상 CPU의 backlog에 패킷 삽입 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                              unsigned int *qtail)
{
    struct softnet_data *sd = &per_cpu(softnet_data, cpu);

    rps_lock_irqsave(sd, &flags);
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
        __skb_queue_tail(&sd->input_pkt_queue, skb);
        rps_unlock_irq_restore(sd, &flags);

        /* 대상 CPU에 NET_RX_SOFTIRQ 스케줄링 (IPI 발생) */
        ____napi_schedule(sd, &sd->backlog);
        return NET_RX_SUCCESS;
    }

    /* backlog 큐 초과 → 패킷 드롭 */
    sd->dropped++;
    rps_unlock_irq_restore(sd, &flags);
    kfree_skb(skb);
    return NET_RX_DROP;
}

RPS 핵심 자료구조

/* include/linux/netdevice.h — rps_map: 큐별 대상 CPU 목록 */
struct rps_map {
    unsigned int   len;          /* 활성 CPU 수 */
    struct rcu_head rcu;
    u16            cpus[];      /* CPU 번호 배열 */
};

/* softnet_data: per-CPU 네트워크 처리 구조체 */
struct softnet_data {
    struct list_head     poll_list;       /* NAPI poll 리스트 */
    struct sk_buff_head  input_pkt_queue; /* RPS backlog 큐 */
    struct sk_buff_head  process_queue;   /* 처리 중인 큐 */
    struct napi_struct   backlog;         /* backlog NAPI */
    unsigned int         dropped;         /* 드롭 카운터 */
    /* ... */
};

RPS 설정 방법

# RPS 설정: 특정 RX 큐에서 어떤 CPU로 패킷을 분산할지 결정
# rps_cpus: CPU 비트맵 (16진수)

# 8-core 시스템에서 모든 CPU 활성화
$ echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# ff = 11111111(2) → CPU 0~7 모두 사용

# NUMA 노드 0의 CPU(0~3)만 사용
$ echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
# f = 00001111(2) → CPU 0~3만

# 32-core 시스템: 모든 CPU
$ echo ffffffff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# backlog 큐 크기 조절 (기본 1000)
$ echo 5000 > /proc/sys/net/core/netdev_budget
$ echo 10000 > /proc/sys/net/core/netdev_max_backlog

# RPS flow hash 엔트리 수 (전역, RFS와 함께 사용)
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 큐별 flow 엔트리 수
$ echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

# 현재 설정 확인
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
000000ff
$ cat /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
0
RPS 사용 시 주의사항:
  1. RSS가 가능하면 RSS를 먼저 사용 — RPS는 소프트웨어 처리이므로 IRQ 처리 CPU에 추가 부하가 발생합니다. NIC가 RSS를 지원하면 하드웨어 분산이 더 효율적입니다.
  2. IPI 오버헤드 — 패킷마다 IPI를 발생시키므로 cache line bouncing이 생길 수 있습니다. 대량 트래픽에서는 RSS 대비 성능이 낮습니다.
  3. IRQ CPU 제외 — IRQ를 처리하는 CPU를 rps_cpus 비트맵(Bitmap)에서 제외하면, 해당 CPU의 부하를 줄이고 다른 CPU로만 분산시킬 수 있습니다.
  4. NUMA 경계 고려 — 원격 NUMA 노드의 CPU로 패킷을 보내면 메모리 접근 지연이 증가합니다. NIC가 연결된 NUMA 노드의 CPU로 제한하는 것이 좋습니다.

RSS vs RPS 비교

특성RSS (Hardware)RPS (Software)
분산 시점 NIC 하드웨어에서 DMA 전 드라이버의 NAPI poll 후, 프로토콜 스택 진입 전
해시 계산 NIC 하드웨어 (Toeplitz) 커널 소프트웨어 (skb_get_hash)
CPU 오버헤드 없음 (H/W) 해시 계산 + IPI + backlog 큐잉
NIC 요구사항 멀티큐 + RSS 지원 필수 싱글큐 NIC도 가능
동적 재설정 ethtool (드라이버 리셋 가능) sysfs 즉시 반영 (무중단)
캐시 효율 높음 (DMA부터 같은 CPU) 보통 (IRQ CPU → 대상 CPU 이동)
주요 사용 사례 고성능 서버, 10G+ NIC 가상머신 (virtio), 싱글큐 NIC, 큐 < CPU 수

RFS (Receive Flow Steering) — 캐시 친화적 분배

RFS는 RPS를 확장하여 패킷을 해당 소켓을 마지막으로 처리한 CPU로 전달합니다. RPS가 해시 기반으로 아무 CPU나 선택하는 것과 달리, RFS는 애플리케이션의 CPU 위치를 추적하여 L1/L2 캐시 히트율을 극대화합니다.

RFS 동작 원리

RFS는 두 개의 해시 테이블(Hash Table)을 사용합니다:

테이블위치내용갱신 시점
rps_sock_flow_table 전역 (per-net) flow hash → 소켓을 마지막으로 처리한 CPU (desired CPU) recvmsg(), sendmsg() 등 소켓 시스템 콜(System Call) 시
rps_dev_flow_table per-queue flow hash → 패킷이 마지막으로 전달된 CPU (current CPU) get_rps_cpu()에서 패킷 처리 시
/* include/linux/netdevice.h — RFS 자료구조 */
struct rps_sock_flow_table {
    u32   mask;             /* 엔트리 수 - 1 (power of 2) */
    u32   ents[];           /* flow hash → desired CPU */
};

struct rps_dev_flow {
    u16   cpu;              /* 패킷이 마지막으로 전달된 CPU */
    u16   filter;           /* aRFS에서 사용하는 필터 ID */
    unsigned int last_qtail; /* 마지막 삽입 시 큐 tail 위치 */
};

struct rps_dev_flow_table {
    unsigned int          mask;  /* 엔트리 수 - 1 */
    struct rcu_head       rcu;
    struct rps_dev_flow   flows[];
};

RFS CPU 선택 로직

RFS의 CPU 선택은 get_rps_cpu() 내부에서 다음 우선순위로 진행됩니다:

/* get_rps_cpu() 내부 RFS 로직 (단순화) */

/* 1. 전역 sock_flow_table에서 desired CPU 조회 */
desired_cpu = sock_flow_table->ents[hash & sock_flow_table->mask];

/* 2. per-queue dev_flow_table에서 current CPU 조회 */
rflow = &flow_table->flows[hash & flow_table->mask];
current_cpu = rflow->cpu;

/* 3. CPU 선택 결정 */
if (desired_cpu == current_cpu) {
    /* 동일 CPU → 그대로 사용 (최적) */
    cpu = desired_cpu;
} else if (current_cpu_unset || current_cpu_offline ||
         unlikely(qtail - rflow->last_qtail >= backlog_len)) {
    /* current CPU가 미설정/오프라인/backlog 소진됨
     * → desired CPU로 전환 (out-of-order 방지 후) */
    cpu = desired_cpu;
    rflow->cpu = cpu;
} else {
    /* current CPU의 backlog에 아직 이전 패킷이 있음
     * → 순서 보장을 위해 current CPU 유지 */
    cpu = current_cpu;
}
순서 보장 (Out-of-Order 방지): RFS가 CPU를 변경할 때, 이전 CPU의 backlog에 아직 처리되지 않은 패킷이 있으면 동일 플로우의 패킷 순서가 뒤바뀔 수 있습니다. 이를 방지하기 위해 last_qtail을 추적하여, 이전 CPU의 backlog가 해당 지점을 넘어서 처리될 때까지 CPU 전환을 지연시킵니다.

RFS 설정 방법

# 1. 전역 sock_flow_table 크기 설정 (power of 2 권장)
# 활성 연결 수의 2배 이상으로 설정
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 2. 큐별 dev_flow_table 크기 설정
# rps_sock_flow_entries / N (N = RX 큐 수)
$ echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
$ echo 2048 > /sys/class/net/eth0/queues/rx-1/rps_flow_cnt
# ... 모든 RX 큐에 대해 반복

# 3. RPS도 함께 활성화해야 동작함 (RFS는 RPS 위에서 동작)
$ echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 한 번에 모든 큐 설정 (스크립트)
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo ff > $rxq/rps_cpus
    echo 2048 > $rxq/rps_flow_cnt
done

소켓 측 CPU 갱신

소켓의 desired CPU는 sock_rps_record_flow()를 통해 갱신됩니다. 이 함수는 recvmsg(), sendmsg(), tcp_v4_rcv() 등 소켓 처리 경로에서 호출됩니다:

/* include/net/sock.h — 소켓 처리 시 CPU 기록 */
static inline void sock_rps_record_flow(const struct sock *sk)
{
    struct rps_sock_flow_table *table;

    table = rcu_dereference(rps_sock_flow_table);
    if (table) {
        u32 hash = sk->sk_rxhash;
        if (hash) {
            u32 index = hash & table->mask;
            /* 현재 CPU를 desired CPU로 기록 */
            if (table->ents[index] != raw_smp_processor_id())
                table->ents[index] = raw_smp_processor_id();
        }
    }
}

XPS (Transmit Packet Steering) — 송신 측 CPU-큐 매핑

XPS는 송신(TX) 패킷을 보내는 CPU에 최적화된 TX 큐를 선택하는 메커니즘입니다. 멀티큐 NIC에서 TX 큐를 CPU에 적절히 매핑하면 lock contention 감소캐시 효율 향상을 얻을 수 있습니다.

XPS가 해결하는 문제

XPS 없이 멀티큐 NIC에서 패킷을 전송하면, 커널은 skb_tx_hash()를 사용해 해시 기반으로 TX 큐를 선택합니다. 이 경우 여러 CPU가 같은 TX 큐를 사용하여 TX 큐 락 경합이 발생할 수 있습니다:

/* XPS 미설정 시: 해시 기반 TX 큐 선택 */
static u16 skb_tx_hash(const struct net_device *dev,
                       const struct sk_buff *skb)
{
    /* 여러 CPU가 같은 큐를 선택할 수 있음 → lock contention */
    return reciprocal_scale(skb_get_hash(skb),
                           dev->real_num_tx_queues);
}

XPS 동작 원리

XPS를 설정하면, 각 TX 큐에 대해 어떤 CPU가 사용할 수 있는지를 매핑합니다. 패킷 전송 시 현재 CPU에 매핑된 TX 큐 중 하나를 선택하여 lock contention을 최소화합니다:

/* include/linux/netdevice.h — XPS 매핑 구조체 */
struct xps_map {
    unsigned int   len;          /* 큐 수 */
    unsigned int   alloc_len;
    struct rcu_head rcu;
    u16            queues[];    /* 이 CPU가 사용할 TX 큐 번호 배열 */
};

struct xps_dev_maps {
    struct rcu_head rcu;
    unsigned int   nr_ids;      /* CPU 수 또는 RX 큐 수 */
    s16            num_tc;      /* Traffic Class 수 */
    struct xps_map __rcu *attr_map[];  /* per-CPU 또는 per-RX-queue 매핑 */
};
/* net/core/dev.c — XPS가 활성화된 경우의 TX 큐 선택 */
static int __netdev_pick_tx(struct net_device *dev,
                           struct sk_buff *skb,
                           struct net_device *sb_dev)
{
    struct xps_dev_maps *dev_maps;
    struct xps_map *map;
    int queue_index = -1;

    /* 1. XPS RX-queue 매핑 시도 (수신 큐 → 송신 큐) */
    dev_maps = rcu_dereference(dev->xps_maps[XPS_RXQS]);
    if (dev_maps) {
        map = rcu_dereference(dev_maps->attr_map[skb_get_rx_queue(skb)]);
        if (map)
            queue_index = map->queues[reciprocal_scale(
                skb_get_hash(skb), map->len)];
    }

    /* 2. XPS CPU 매핑 시도 (현재 CPU → 송신 큐) */
    if (queue_index < 0) {
        dev_maps = rcu_dereference(dev->xps_maps[XPS_CPUS]);
        if (dev_maps) {
            map = rcu_dereference(
                dev_maps->attr_map[raw_smp_processor_id()]);
            if (map)
                queue_index = map->queues[
                    reciprocal_scale(skb_get_hash(skb), map->len)];
        }
    }

    /* 3. XPS 미설정 시 fallback: skb_tx_hash() */
    if (queue_index < 0)
        queue_index = skb_tx_hash(dev, skb);

    return queue_index;
}

XPS 두 가지 모드

모드매핑 기준설정 파일사용 사례
XPS (CPU) CPU → TX 큐 /sys/class/net/<dev>/queues/tx-<N>/xps_cpus CPU별 전용 TX 큐 할당으로 lock contention 제거
XPS (RXQ) RX 큐 → TX 큐 /sys/class/net/<dev>/queues/tx-<N>/xps_rxqs 수신-송신 큐 페어링, 같은 CPU에서 처리하여 캐시 효율 극대화

XPS 설정 방법

# === XPS CPU 모드: CPU → TX 큐 1:1 매핑 ===

# TX Queue 0 → CPU 0 전용
$ echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
# 1 = 00000001(2) → CPU 0만

# TX Queue 1 → CPU 1 전용
$ echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus
# 2 = 00000010(2) → CPU 1만

# TX Queue 2 → CPU 2 전용
$ echo 4 > /sys/class/net/eth0/queues/tx-2/xps_cpus

# TX Queue 3 → CPU 3 전용
$ echo 8 > /sys/class/net/eth0/queues/tx-3/xps_cpus

# 8-queue NIC에서 CPU 1:1 매핑 스크립트
for i in $(seq 0 7); do
    printf '%x' $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done

# NUMA 인식 매핑: NUMA 0 CPU(0~3) → TX Queue 0~3
#                 NUMA 1 CPU(4~7) → TX Queue 4~7
$ echo 0f > /sys/class/net/eth0/queues/tx-0/xps_cpus  # CPU 0~3
$ echo 0f > /sys/class/net/eth0/queues/tx-1/xps_cpus
$ echo 0f > /sys/class/net/eth0/queues/tx-2/xps_cpus
$ echo 0f > /sys/class/net/eth0/queues/tx-3/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-4/xps_cpus  # CPU 4~7
$ echo f0 > /sys/class/net/eth0/queues/tx-5/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-6/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-7/xps_cpus

# === XPS RXQ 모드: RX 큐 → TX 큐 매핑 ===
# 커널 4.18+ 필요, RSS/RPS로 수신한 큐와 동일 TX 큐 사용

# TX Queue 0 → RX Queue 0에서 수신한 패킷의 응답 전송
$ echo 1 > /sys/class/net/eth0/queues/tx-0/xps_rxqs
# TX Queue 1 → RX Queue 1
$ echo 2 > /sys/class/net/eth0/queues/tx-1/xps_rxqs

# 현재 설정 확인
$ cat /sys/class/net/eth0/queues/tx-0/xps_cpus
00000001
$ cat /sys/class/net/eth0/queues/tx-0/xps_rxqs
0
XPS 최적 구성: CPU 1:1 매핑이 가장 효과적입니다. 각 CPU가 전용 TX 큐를 가지면 qdisc 락 경합이 완전히 제거됩니다. CPU 수 > TX 큐 수인 경우, 같은 NUMA 노드의 CPU 그룹을 하나의 TX 큐에 매핑합니다. xps_rxqs 모드는 TCP처럼 요청-응답 패턴에서 수신과 송신이 같은 CPU에서 처리되도록 하여 캐시 효율을 극대화합니다.

XPS 모니터링

# TX 큐별 전송 통계
$ ethtool -S eth0 | grep tx_queue
     tx_queue_0_packets: 982341
     tx_queue_0_bytes: 587204160
     tx_queue_1_packets: 978892
     tx_queue_1_bytes: 585023408
     tx_queue_2_packets: 981204
     tx_queue_2_bytes: 586921600
     tx_queue_3_packets: 979563
     tx_queue_3_bytes: 585425376

# TX 큐 락 경합 확인 (perf로)
$ perf stat -e 'lock:contention_begin' -a -- sleep 5

# BPF로 TX 큐 선택 과정 추적
$ bpftrace -e 'kretprobe:__netdev_pick_tx {
    printf("cpu=%d txq=%d\n", cpu, retval);
}'

aRFS (Accelerated RFS) — 하드웨어 가속 RFS

aRFS는 RFS의 결정을 NIC 하드웨어에 반영하여, 패킷이 DMA 단계에서부터 올바른 CPU의 RX 큐로 전달되도록 합니다. RFS가 소프트웨어로 패킷을 재분배하는 것과 달리, aRFS는 NIC의 ntuple filter (Flow Director)를 동적으로 프로그래밍하여 하드웨어 수준에서 스티어링합니다.

aRFS 동작 흐름

Application recvmsg() on CPU 2 RFS 테이블 갱신 desired_cpu = 2 get_rps_cpu() desired(CPU2) != current(CPU0) → ndo_rx_flow_steer() 호출 NIC 드라이버 ndo_rx_flow_steer() ntuple filter 추가/갱신 NIC H/W Flow Director 규칙: flow X → RX Queue 2 (CPU 2에 바인딩된 큐) 이후 패킷 H/W가 직접 CPU 2의 큐로 DMA

aRFS 커널 API

aRFS를 지원하려면 NIC 드라이버가 ndo_rx_flow_steer 콜백을 구현해야 합니다:

/* include/linux/netdevice.h — aRFS 드라이버 콜백 */
struct net_device_ops {
    /* ... */
    int (*ndo_rx_flow_steer)(struct net_device *dev,
                             const struct sk_buff *skb,
                             u16 rxq_index,
                             u32 flow_id);
    /* rxq_index: 대상 RX 큐 번호 */
    /* flow_id:   고유 플로우 식별자 */
    /* 반환값:     NIC에 설정된 필터 ID */
};

/* 예: Intel ixgbe 드라이버의 aRFS 구현 */
static int ixgbe_rx_flow_steer(struct net_device *dev,
                               const struct sk_buff *skb,
                               u16 rxq_index, u32 flow_id)
{
    struct ixgbe_adapter *adapter = netdev_priv(dev);
    struct ixgbe_fdir_filter *input;

    /* 패킷의 5-tuple로 Flow Director 필터 생성 */
    input = kzalloc(sizeof(*input), GFP_ATOMIC);
    /* skb에서 src/dst IP, port 추출 → ATR 필터 설정 */
    ixgbe_fdir_write_perfect_filter(adapter, input, rxq_index);

    return filter_id;
}

aRFS 설정

# aRFS 요구사항:
# 1. NIC가 ntuple filter (Flow Director) 지원
# 2. NIC 드라이버가 ndo_rx_flow_steer 구현
# 3. RFS가 활성화되어 있어야 함

# ntuple filter 활성화
$ ethtool -K eth0 ntuple on

# RFS 설정 (aRFS의 전제 조건)
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo 2048 > $rxq/rps_flow_cnt
done

# aRFS 지원 여부 확인
$ ethtool -k eth0 | grep ntuple
ntuple-filters: on

# 현재 aRFS/Flow Director 규칙 수 확인
$ ethtool -S eth0 | grep fdir
     fdir_match: 28745
     fdir_miss: 312
     fdir_overflow: 0
aRFS 지원 NIC: Intel ixgbe (82599, X540), i40e (X710, XL710), ice (E810), Mellanox mlx4/mlx5 (ConnectX-3/4/5/6), Broadcom bnxt (BCM57xxx), Chelsio cxgb4 (T5/T6) 등이 aRFS를 지원합니다. 확인 방법: grep ndo_rx_flow_steer drivers/net/ethernet/로 드라이버 소스에서 구현 여부를 검색합니다.

ICE aRFS 운영 주의사항

Intel E810(ice) 드라이버의 aRFS는 다음과 같은 제약사항이 있으므로 운영 시 주의가 필요합니다.

항목ICE aRFS 제약
지원 프로토콜TCP/UDP over IPv4 및 IPv6만 지원 (SCTP, ICMP 등 미지원)
단편화 패킷IP 단편화(fragmented) 패킷은 aRFS 스티어링 대상에서 제외
ntuple 충돌ethtool -N으로 추가한 수동 ntuple 규칙과 aRFS 자동 규칙이 충돌 가능 — 같은 플로우에 대해 둘 다 설정하면 예측 불가
필터 수 제한FDIR 테이블 용량에 따라 동시 활성 규칙 수 제한
# ICE aRFS 활성화 (ntuple + RFS 동시 설정)
ethtool -K eth0 ntuple on
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/eth0/queues/rx-*; do
    echo 4096 > "$q/rps_flow_cnt"
done

# FDIR 통계로 aRFS 동작 확인
ethtool -S eth0 | grep fdir
# fdir_match: aRFS 규칙 매칭 횟수
# fdir_miss: 규칙 미매칭 (RSS 폴백)
# fdir_overflow: 테이블 초과 (규칙 수 부족)

멀티코어 네트워크 분산 전체 비교

기법계층방향분배 기준장점단점
RSS H/W RX 해시 → RETA → 큐 CPU 부하 없음, DMA부터 분산 NIC 지원 필요, 정적 매핑
RPS S/W RX 해시 → CPU backlog 어떤 NIC든 사용 가능 IPI 오버헤드, 캐시 비효율
RFS S/W RX 소켓의 CPU 추적 캐시 친화적, 앱-패킷 같은 CPU RPS 위에서만 동작, 테이블 메모리
aRFS H/W+S/W RX RFS 결정을 H/W 필터에 반영 RFS + H/W 가속, 최적 성능 ntuple 지원 NIC 필요, 필터 수 제한
XPS S/W TX CPU → TX 큐 매핑 TX 락 경합 제거, 설정 간단 송신 전용, 멀티큐 NIC 필요
Flow Dir. H/W RX 관리자가 수동 규칙 설정 정밀 제어, RSS보다 높은 우선순위. ICE는 Flex Byte(user-def) 지원 수동 관리, 규칙 수 제한
권장 구성 가이드:
  • 고성능 서버 (RSS NIC): RSS + IRQ affinity + XPS (CPU 1:1) + aRFS
  • 가상머신 (virtio): RPS + RFS (virtio는 RSS 미지원이 많으므로)
  • 큐 수 < CPU 수: RSS + RPS (S/W로 추가 분산) + RFS
  • 단순 구성: RSS + XPS만으로도 대부분의 경우 충분

RSS (Receive Side Scaling) 구현 상세

RSS는 NIC 하드웨어가 수신 패킷의 헤더 필드를 해싱하여 여러 RX 큐에 분배하는 기술입니다. 위 개요에서 Toeplitz 해시와 RETA를 설명했으므로, 이 섹션에서는 NIC 벤더별 RSS 구현 차이, 해시 키 선택 전략, 그리고 하드웨어 큐 매핑의 실전 튜닝을 다룹니다.

하드웨어 큐와 MSI-X 인터럽트 매핑

RSS의 핵심은 RX 큐 → MSI-X 인터럽트 벡터 → CPU 매핑입니다. NIC 드라이버는 초기화 시 큐 수만큼 MSI-X 벡터를 할당하고, 각 벡터를 특정 CPU에 바인딩합니다. 이 매핑이 올바르지 않으면 RSS의 분산 효과가 제대로 발휘되지 않습니다.

드라이버 초기화 시 MSI-X 벡터 할당 (일반적 패턴):

  1. pci_alloc_irq_vectors() → num_queues개 MSI-X 벡터 할당
  2. request_irq(vector[i], handler, 0, "eth0-TxRx-i", q[i])
  3. irq_set_affinity_hint(irq, cpu_mask) → 각 큐의 인터럽트를 특정 CPU에 affinity 설정

큐 수 결정 기준:

큐-CPU affinity 확인:

# cat /proc/interrupts | grep eth0
 128:  152384    0    0    0  IR-PCI-MSI eth0-TxRx-0   → CPU 0
 129:       0  151829    0    0  IR-PCI-MSI eth0-TxRx-1   → CPU 1
 130:       0    0  152105    0  IR-PCI-MSI eth0-TxRx-2   → CPU 2
 131:       0    0    0  151983  IR-PCI-MSI eth0-TxRx-3   → CPU 3

NIC 벤더별 RSS 지원

NIC / 드라이버최대 큐 수RETA 크기해시 함수특이 사항
Intel E810 (ice) 256 2048 Toeplitz, Symmetric Toeplitz, XOR ADQ(Application Device Queues)로 앱별 전용 큐 할당 가능. VLAN/tunneling 내부 해싱 지원
Intel X710 (i40e) 64 (PF) 512 Toeplitz, XOR PCTYPEs로 프로토콜별 세밀한 해시 필드 제어. ATR(Application Targeted Routing) 내장
Mellanox ConnectX-5/6 (mlx5) 256 최대 4096 Toeplitz, XOR TIR(Transport Interface Receive) 기반 유연한 분배. Inner header 해싱(VXLAN, GRE) 기본 지원
Broadcom BCM57xxx (bnxt) 128 128~512 Toeplitz RSS context 다중 지원으로 VF별 독립 RSS 설정 가능
Chelsio T6 (cxgb4) 128 2048 Toeplitz, CRC32 하드웨어 GRO/TOE 통합. RSS + 필터 우선순위 계층 구조

RSS 키 선택 전략

RSS 해시 키의 품질은 트래픽 분산 균등성에 직접 영향을 미친다. 잘못된 키는 특정 큐로 트래픽이 편중되는 해시 충돌(hash collision)을 일으킬 수 있습니다.

키 유형설명사용 사례
Microsoft 기본 키 Microsoft RSS 스펙에 정의된 40바이트 키. 대부분의 NIC 드라이버가 기본값으로 사용 범용. 특별한 요구사항이 없을 때
대칭 키 (Symmetric) (A->B)(B->A) 트래픽이 동일 해시를 생성하는 키 conntrack, 양방향 플로우 모니터링, IDS/IPS
랜덤 키 /dev/urandom에서 생성한 키. 공격자가 의도적으로 해시 충돌을 유도하기 어려움 보안이 중요한 환경, DDoS 방어
# 대칭 키 생성 및 적용 예시
# (src_ip XOR dst_ip, src_port XOR dst_port가 동일 해시를 만들도록 설계)
$ python3 -c "
import os
key = os.urandom(40)
# 대칭 속성을 위해 key[i] == key[i+20] 조건 적용
sym_key = key[:20] + key[:20]
print(':'.join(f'{b:02x}' for b in sym_key))
" | xargs -I{} ethtool -X eth0 hkey {}

# Intel NIC에서 symmetric-xor 해시 함수 사용 (키 변경 불필요)
$ ethtool -X eth0 hfunc symmetric-xor

# 랜덤 키 적용
$ KEY=$(python3 -c "import os; print(':'.join(f'{b:02x}' for b in os.urandom(40)))")
$ ethtool -X eth0 hkey "$KEY"

ethtool RSS 관리 명령 종합

# === RSS 상태 조회 ===

# RETA 테이블 + 해시 키 + 해시 함수 조회
$ ethtool -x eth0

# 큐 수 조회/변경
$ ethtool -l eth0             # 현재 및 최대 큐 수
$ ethtool -L eth0 combined 8  # combined 큐 8개로 변경

# 프로토콜별 해시 필드 조회
$ ethtool -n eth0 rx-flow-hash tcp4
$ ethtool -n eth0 rx-flow-hash udp4

# === RSS 설정 변경 ===

# RETA 균등 분배 (4큐)
$ ethtool -X eth0 equal 4

# RETA 가중치 분배 (큐 0에 50%, 나머지 균등)
$ ethtool -X eth0 weight 3 1 1 1

# 해시 함수 변경
$ ethtool -X eth0 hfunc toeplitz   # 또는 xor, crc32

# 해시 필드 변경
$ ethtool -N eth0 rx-flow-hash tcp4 sdfn  # 4-tuple
$ ethtool -N eth0 rx-flow-hash udp4 sd    # 2-tuple (단편화 방지)

# === 큐별 통계 확인 ===
$ ethtool -S eth0 | grep rx_queue
$ ethtool -S eth0 | grep tx_queue
Intel ice ADQ (Application Device Queues): ice 드라이버는 RSS를 넘어 tc(Traffic Control) 기반으로 애플리케이션별 전용 큐 세트를 할당할 수 있습니다. 예를 들어 웹 서버 트래픽(port 443)과 DB 트래픽(port 3306)을 별도의 큐 세트로 분리하여 상호 간섭 없이 처리할 수 있습니다: tc qdisc add dev eth0 root mqprio num_tc 2 map 0 0 0 0 1 1 1 1 queues 4@0 4@4 hw 1 mode channeltc filter add dev eth0 protocol ip parent 1: flower dst_port 443 hw_tc 1로 설정합니다.

RPS (Receive Packet Steering) 구현 상세

RPS는 위 개요에서 설명한 것처럼 소프트웨어 기반 수신 분산입니다. 이 섹션에서는 softirq 컨텍스트에서의 해시 계산, per-CPU backlog 큐 삽입 과정, 그리고 운영 환경에서의 최적 설정을 상세히 다룹니다.

softirq 컨텍스트의 소프트웨어 해시

RPS에서 패킷 해시는 NIC가 제공한 하드웨어 해시를 우선 사용하고, 없으면 소프트웨어로 계산합니다. 소프트웨어 해시는 __skb_get_hash()에서 Flow Dissector를 통해 패킷 헤더를 파싱하고, Toeplitz 또는 jhash를 사용하여 32비트 해시를 생성합니다.

/* net/core/flow_dissector.c — RPS 소프트웨어 해시 계산 */

/* skb_get_hash() 호출 경로:
 *   netif_receive_skb()
 *     → __netif_receive_skb()
 *       → get_rps_cpu()
 *         → skb_get_hash(skb)
 *           → __skb_get_hash(skb)
 *             → ___skb_get_hash(skb, &keys, &flow_keys_dissector_symmetric)
 *               → __flow_hash_from_keys(&keys, &hashrnd)
 */

void __skb_get_hash(struct sk_buff *skb)
{
    struct flow_keys keys;
    u32 hash;

    /* NIC가 이미 해시를 계산했으면 그것을 사용 */
    if (skb->l4_hash || skb->sw_hash)
        return;

    /* Flow Dissector로 L3/L4 헤더 파싱 */
    if (!skb_flow_dissect_flow_keys(skb, &keys, 0))
        return;

    /* jhash2로 해시 계산 (소프트웨어 Toeplitz 대신 jhash 사용) */
    hash = __flow_hash_from_keys(&keys, &hashrnd);
    __skb_set_sw_hash(skb, hash, flow_keys_have_l4(&keys));
}

enqueue_to_backlog 상세

get_rps_cpu()가 대상 CPU를 결정하면, 패킷은 enqueue_to_backlog()을 통해 해당 CPU의 softnet_data.input_pkt_queue에 삽입됩니다. 이 과정에서 발생하는 동기화와 오버플로 처리를 살펴봅니다.

/* net/core/dev.c — enqueue_to_backlog 상세 분석 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                              unsigned int *qtail)
{
    struct softnet_data *sd;
    unsigned long flags;

    sd = &per_cpu(softnet_data, cpu);

    /* per-CPU backlog 큐에 대한 스핀락 획득
     * (IRQ 비활성화 — 하드 인터럽트 안전) */
    rps_lock_irqsave(sd, &flags);

    /* 큐 길이 검사: netdev_max_backlog 초과 여부 */
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
        /* 큐에 여유가 있으면 삽입 */
        if (skb_queue_len(&sd->input_pkt_queue)) {
            /* 큐가 비어있지 않으면 — 이미 softirq 스케줄됨 */
            goto enqueue;
        }
        /* 큐가 비어있으면 — softirq 스케줄 필요 */
        if (!__test_and_set_bit(NAPI_STATE_SCHED,
                                &sd->backlog.state)) {
            /* backlog NAPI를 poll_list에 추가하고
             * NET_RX_SOFTIRQ를 raise (IPI 발생) */
            ____napi_schedule(sd, &sd->backlog);
        }
        goto enqueue;
    }

    /* === 큐 오버플로 ===
     * netdev_max_backlog 초과 → 패킷 드롭
     * /proc/net/softnet_stat의 두 번째 컬럼(dropped)이 증가 */
    sd->dropped++;
    rps_unlock_irq_restore(sd, &flags);
    kfree_skb_reason(skb, SKB_DROP_REASON_CPU_BACKLOG);
    return NET_RX_DROP;

enqueue:
    __skb_queue_tail(&sd->input_pkt_queue, skb);
    /* RFS용 qtail 기록 (순서 보장에 사용) */
    if (qtail)
        *qtail = sd->input_queue_head +
                 skb_queue_len(&sd->input_pkt_queue);
    rps_unlock_irq_restore(sd, &flags);
    return NET_RX_SUCCESS;
}

RPS sysfs 설정 상세

# === /sys/class/net/<dev>/queues/rx-N/rps_cpus ===
# CPU 비트맵 (16진수). 각 비트가 하나의 CPU를 나타냄

# 예: 16코어 시스템, NUMA 노드 0 = CPU 0~7, NUMA 노드 1 = CPU 8~15
# NIC가 NUMA 0에 연결된 경우:

# 방법 1: IRQ CPU를 제외한 같은 NUMA 노드 CPU만 사용
# (큐 0의 IRQ가 CPU 0에 고정이면, CPU 1~7만 사용)
$ echo 00fe > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 00fe = 11111110(2) → CPU 1~7

# 방법 2: 모든 CPU 사용 (NUMA 경계 무시 — 비추천)
$ echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# === /proc/sys/net/core/netdev_max_backlog ===
# per-CPU backlog 큐의 최대 패킷 수 (기본: 1000)
$ echo 10000 > /proc/sys/net/core/netdev_max_backlog

# 오버플로 확인: softnet_stat의 두 번째 컬럼
$ cat /proc/net/softnet_stat
# 형식: processed  dropped  time_squeeze  ...  (16진수, per-CPU 행)
# 00a1b2c3 00000000 00000005 ...   ← CPU 0: 0 dropped
# 009f8d21 0000002a 00000003 ...   ← CPU 1: 42 dropped → 문제!
RPS 성능 트레이드오프: RPS는 IPI(Inter-Processor Interrupt) 비용이 핵심 병목입니다. 패킷당 IPI 발생은 아니지만(backlog에 이미 패킷이 있으면 IPI 생략), 새 burst 시작마다 IPI가 발생합니다. 10Gbps 이상 환경에서 RPS만으로는 RSS 대비 20~30% 낮은 처리량을 보입니다. RPS는 RSS 미지원 NIC(virtio, 가상환경)이나 큐 수가 CPU 수보다 적은 환경에서 사용하고, RSS가 가능하면 RSS를 우선 사용하라.

RFS (Receive Flow Steering) 구현 상세

RFS는 단순한 해시 기반 분배를 넘어 애플리케이션 locality를 활용합니다. 패킷을 해당 소켓을 마지막으로 처리한 CPU로 전달하여 L1/L2/L3 캐시 히트율을 극대화합니다.

이중 테이블 메커니즘

RFS가 두 개의 테이블을 사용하는 이유는 패킷 순서 보장(out-of-order prevention) 때문입니다. 단일 테이블로 CPU를 즉시 변경하면, 이전 CPU의 backlog에 아직 처리되지 않은 패킷이 있을 때 동일 플로우의 패킷 순서가 뒤바뀔 수 있습니다.

RFS 이중 테이블 동작 흐름 Application recvmsg() on CPU 3 → sock_rps_record_flow() rps_sock_flow_table (전역, per-net) ents[hash] = desired_cpu (3) 갱신: recvmsg/sendmsg/tcp_v4_rcv 패킷 수신 NIC IRQ → CPU 0 get_rps_cpu() 1. sock_flow → desired = CPU 3 2. dev_flow → current = CPU 1 3. backlog 소진 여부 확인 rps_dev_flow_table (per-queue) flows[hash].cpu = current_cpu flows[hash].last_qtail = N backlog 소진? Yes CPU 전환 → desired CPU 3으로 전달 No CPU 유지 → current CPU 1 계속 사용 last_qtail 추적으로 이전 CPU backlog 미처리 패킷이 있으면 CPU 전환을 지연 → 순서 보장

rps_may_expire_flow — 플로우 만료

RFS 테이블의 엔트리는 무한히 유지되지 않습니다. rps_may_expire_flow()는 플로우가 만료되었는지 판단하여, 오래된 플로우 엔트리가 새 플로우에 의해 재사용될 수 있도록 합니다.

/* net/core/dev.c — RFS 플로우 만료 판단 */
static bool rps_may_expire_flow(struct net_device *dev,
                               u16 rxq_index,
                               u32 flow_id, u16 filter_id)
{
    struct netdev_rx_queue *rxqueue;
    struct rps_dev_flow_table *flow_table;
    struct rps_dev_flow *rflow;
    bool expire = true;

    rxqueue = dev->_rx + rxq_index;
    flow_table = rcu_dereference(rxqueue->rps_flow_table);
    if (flow_table && flow_id <= flow_table->mask) {
        rflow = &flow_table->flows[flow_id];
        /* 필터 ID가 일치하고 CPU가 온라인이면 만료하지 않음 */
        if (rflow->filter == filter_id &&
            cpu_online(rflow->cpu))
            expire = false;
    }
    return expire;
}

/* aRFS가 이 함수를 사용하여 NIC의 ntuple 필터를
 * 정리할지 결정합니다. 만료된 플로우의 하드웨어 필터는
 * 삭제되어 FDIR 테이블 공간을 확보합니다. */

RFS 설정 상세 가이드

# === RFS 전역 설정 ===

# rps_sock_flow_entries: 전역 sock_flow_table 크기
# 권장: 예상 동시 활성 연결 수의 2배 (power of 2)
# 예: 16K 동시 연결 → 32768
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# === RFS per-queue 설정 ===

# rps_flow_cnt: per-queue dev_flow_table 크기
# 권장: rps_sock_flow_entries / RX 큐 수
# 예: 32768 / 8 큐 = 4096
NUM_QUEUES=$(ls -d /sys/class/net/eth0/queues/rx-* | wc -l)
FLOW_CNT=$((32768 / NUM_QUEUES))
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo $FLOW_CNT > "$rxq/rps_flow_cnt"
done

# === RPS도 반드시 활성화해야 RFS가 동작 ===
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo ff > "$rxq/rps_cpus"
done

# === 설정 검증 ===
$ cat /proc/sys/net/core/rps_sock_flow_entries
32768
$ cat /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
4096
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
000000ff
RFS 메모리 비용: rps_sock_flow_table은 엔트리당 4바이트(u32), rps_dev_flow_table은 엔트리당 8바이트(rps_dev_flow 구조체(Struct))를 사용합니다. 32768 엔트리의 sock_flow_table은 약 128KB, 8큐 x 4096 엔트리의 dev_flow_table은 약 256KB로, 메모리 비용은 매우 적습니다.

XPS (Transmit Packet Steering) 구현 상세

위 개요에서 XPS의 기본 개념과 설정을 다루었습니다. 이 섹션에서는 NUMA 노드 기반 설정, TX 큐 선택의 내부 로직, 그리고 XPS가 qdisc 락 경합에 미치는 실질적 영향을 분석합니다.

NUMA 노드 기반 XPS 설정

NUMA 시스템에서 XPS의 핵심은 NIC가 연결된 PCIe 슬롯의 NUMA 노드TX 큐를 사용하는 CPU의 NUMA 노드를 일치시키는 것입니다. NUMA 노드가 일치하지 않으면 DMA 전송 시 원격 메모리 접근이 발생하여 레이턴시가 증가합니다.

# NIC의 NUMA 노드 확인
$ cat /sys/class/net/eth0/device/numa_node
0

# NUMA 노드별 CPU 목록 확인
$ lscpu | grep NUMA
NUMA node0 CPU(s):  0-7
NUMA node1 CPU(s):  8-15

# NUMA 인지 XPS 설정 스크립트
# NIC가 NUMA 0에 연결 → CPU 0~7만 사용
NIC_NUMA=$(cat /sys/class/net/eth0/device/numa_node)
CPUS=$(lscpu -p=CPU,NODE | grep ",$NIC_NUMA" | cut -d, -f1)

# 각 CPU에 전용 TX 큐 매핑
i=0
for cpu in $CPUS; do
    TXQ="/sys/class/net/eth0/queues/tx-$i/xps_cpus"
    if [ -f "$TXQ" ]; then
        printf '%x' $((1 << cpu)) > "$TXQ"
    fi
    i=$((i + 1))
done

# 설정 결과 확인
for txq in /sys/class/net/eth0/queues/tx-*/xps_cpus; do
    echo "$txq: $(cat $txq)"
done

XPS와 TX 큐 락 경합

멀티큐 NIC에서 각 TX 큐는 qdisc 락으로 보호됩니다. XPS 없이 여러 CPU가 같은 TX 큐를 사용하면 spinlock 경합이 발생합니다. XPS로 CPU-큐를 1:1 매핑하면 이 경합이 완전히 제거됩니다.

/* net/sched/sch_generic.c — qdisc 전송 시 락 경합 */
static inline int __dev_xmit_skb(struct sk_buff *skb,
                                  struct Qdisc *q,
                                  struct net_device *dev,
                                  struct netdev_queue *txq)
{
    /* 이 spinlock이 XPS 미설정 시 경합 지점!
     * 여러 CPU가 같은 txq를 사용하면
     * spin_lock(&q->busylock)에서 대기 시간 발생 */
    spin_lock(root_lock);

    /* qdisc에 skb 삽입 */
    rc = q->enqueue(skb, q, &to_free);
    if (rc == NET_XMIT_SUCCESS) {
        /* 직접 전송 시도 */
        qdisc_run(q);
    }

    spin_unlock(root_lock);
    return rc;
}

/* XPS로 CPU 1:1 매핑 시:
 *   CPU 0 → TX Queue 0 (전용) → 락 경합 없음
 *   CPU 1 → TX Queue 1 (전용) → 락 경합 없음
 *   ...
 * 결과: spin_lock 대기 시간 ≈ 0 */
# XPS 전후 TX 락 경합 측정

# 방법 1: perf lock contention 분석
$ perf lock record -a -- sleep 10
$ perf lock report
# qdisc_lock 또는 root_lock의 contention이 줄어드는지 확인

# 방법 2: bpftrace로 TX 큐 선택 모니터링
$ bpftrace -e '
kretprobe:__netdev_pick_tx {
    @txq[cpu] = lhist(retval, 0, 16, 1);
}
END { print(@txq); }
' -- sleep 5
# 각 CPU가 고유한 TX 큐를 선택하는지 확인

aRFS (Accelerated RFS) 구현 상세

aRFS는 RFS의 소프트웨어 결정을 NIC 하드웨어에 반영하여, 패킷이 DMA 단계에서부터 올바른 CPU의 RX 큐로 전달되도록 합니다. 이 섹션에서는 ndo_rx_flow_steer 콜백의 동작, ntuple 필터와의 관계, 그리고 ethtool을 통한 설정을 다룹니다.

ndo_rx_flow_steer 콜백 동작

커널의 get_rps_cpu()가 desired CPU와 current CPU의 불일치를 감지하면, NIC 드라이버의 ndo_rx_flow_steer 콜백을 호출하여 하드웨어 필터를 설정합니다.

/* net/core/dev.c — aRFS 트리거 로직 (get_rps_cpu 내부) */

/* desired_cpu != current_cpu이고 CPU 전환이 결정된 경우: */
if (rflow->filter != rflow_to_cpu_id) {
    /* NIC에 flow steering 규칙 설정 요청 */
    rflow->filter = dev->netdev_ops->ndo_rx_flow_steer(
        dev, skb,
        rxq_index,    /* desired CPU에 바인딩된 RX 큐 */
        flow_id       /* 해시 기반 플로우 ID */
    );
    /* 반환값: NIC에 설정된 필터 ID
     * → rflow->filter에 저장하여 나중에 만료 시 삭제에 사용 */
}

/* 드라이버 구현 예시: mlx5 (Mellanox ConnectX) */
static int mlx5e_rx_flow_steer(struct net_device *dev,
                               const struct sk_buff *skb,
                               u16 rxq_index, u32 flow_id)
{
    struct mlx5e_priv *priv = netdev_priv(dev);
    struct mlx5e_arfs_tables *arfs = &priv->fs->arfs;
    struct arfs_rule *arfs_rule;

    /* 패킷의 5-tuple 추출 */
    /* 기존 규칙 검색 → 없으면 새 규칙 생성 */
    arfs_rule = arfs_find_rule(arfs, skb);
    if (!arfs_rule) {
        arfs_rule = arfs_alloc_rule(priv, skb, rxq_index, flow_id);
        /* workqueue를 통해 비동기적으로 H/W 규칙 설정
         * (softirq 컨텍스트에서 직접 H/W 프로그래밍 불가) */
        queue_work(priv->wq, &arfs_rule->arfs_work);
    }

    return arfs_rule->filter_id;
}

ntuple 필터와 aRFS의 관계

항목수동 ntuple (ethtool -N)aRFS (자동)
규칙 생성 관리자가 수동으로 설정 커널이 RFS 결정에 따라 자동 생성/삭제
우선순위 높음 (RSS보다 우선) 높음 (RSS보다 우선, 수동 ntuple과 동급)
만료/삭제 수동 삭제 필요 rps_may_expire_flow()에 의해 자동 만료
필터 테이블 NIC FDIR/Flow Table 공유 NIC FDIR/Flow Table 공유
충돌 가능성 - 같은 플로우에 수동 + aRFS 규칙이 공존하면 예측 불가
# === aRFS 활성화 전제 조건 ===

# 1. NIC ntuple 지원 확인
$ ethtool -k eth0 | grep ntuple
ntuple-filters: on  # off이면 -K로 활성화

# 2. ntuple 활성화
$ ethtool -K eth0 ntuple on

# 3. rx-flow-hash 설정 (해시 필드 확인)
$ ethtool -n eth0 rx-flow-hash tcp4
# sdfn (4-tuple) 권장

# 4. RFS 설정 (aRFS의 전제 조건)
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo 4096 > "$rxq/rps_flow_cnt"
done

# === aRFS 동작 확인 ===

# FDIR 통계로 확인
$ ethtool -S eth0 | grep fdir
     fdir_match: 28745      # aRFS 규칙 매칭 성공
     fdir_miss: 312          # 미매칭 → RSS 폴백
     fdir_overflow: 0        # FDIR 테이블 초과 (0이어야 정상)

# 현재 활성 ntuple 규칙 목록 (수동 + aRFS 포함)
$ ethtool -n eth0
aRFS 규칙 수 제한: NIC의 FDIR/Flow Table 크기는 유한합니다. Intel ixgbe는 약 8K 규칙, ice(E810)는 최대 16K 규칙을 지원합니다. 동시 활성 플로우가 이 한도를 초과하면 fdir_overflow 카운터가 증가하고, 초과 플로우는 RSS 폴백으로 처리됩니다. ethtool -S eth0 | grep fdir_overflow를 모니터링하여 테이블 고갈을 감지하십시오.

Flow Dissector — 패킷 파싱 엔진

Flow Dissector는 패킷 헤더를 파싱하여 flow_keys 구조체로 추출하는 커널의 범용 패킷 파싱 엔진입니다. RPS/RFS의 해시 계산, TC(Traffic Control)의 패킷 분류, Netfilter의 conntrack 등 다양한 서브시스템에서 사용됩니다.

skb_flow_dissect와 flow_keys

/* include/net/flow_dissector.h — flow_keys 구조체 */
struct flow_keys {
    struct flow_dissector_key_control control;
    struct flow_dissector_key_basic   basic;
    /* L3 */
    union {
        struct flow_dissector_key_ipv4_addrs ipv4;
        struct flow_dissector_key_ipv6_addrs ipv6;
    } addrs;
    /* L4 */
    struct flow_dissector_key_ports    ports;
    struct flow_dissector_key_tags     tags;
    struct flow_dissector_key_vlan     vlan;
    struct flow_dissector_key_keyid    keyid;
    /* ... 기타 키 타입 (MPLS, GRE, 암호화 등) */
};

/* flow_dissector_key_basic — 프로토콜 정보 */
struct flow_dissector_key_basic {
    __be16 n_proto;    /* L3 프로토콜 (ETH_P_IP 등) */
    u8     ip_proto;   /* L4 프로토콜 (IPPROTO_TCP 등) */
};

/* 핵심 파싱 함수 */
bool __skb_flow_dissect(
    const struct net *net,
    const struct sk_buff *skb,
    struct flow_dissector *flow_dissector,
    void *target_container,     /* flow_keys를 채울 대상 */
    const void *data,
    __be16 proto,
    int nhoff, int hlen,
    unsigned int flags);

/* 파싱 파이프라인:
 *   1. Ethernet 헤더 → n_proto (VLAN 태그 스킵)
 *   2. IP 헤더 → src/dst IP, ip_proto
 *      (터널링: VXLAN/GRE/IPIP → 내부 헤더 재귀 파싱)
 *   3. TCP/UDP/SCTP 헤더 → src/dst port
 *   4. 결과를 flow_keys에 저장 → 해시 계산에 사용
 */

BPF Flow Dissector

커널 5.3부터 BPF 프로그램으로 Flow Dissector를 교체할 수 있습니다. 커스텀 프로토콜의 해싱이나, 독자적인 터널(Tunnel) 프로토콜의 inner header 파싱이 필요할 때 유용합니다.

/* BPF Flow Dissector 프로그램 예시 (libbpf) */
SEC("flow_dissector")
int custom_flow_dissector(struct __sk_buff *skb)
{
    struct bpf_flow_keys *keys =
        (struct bpf_flow_keys *)skb->flow_keys;

    /* 커스텀 프로토콜 파싱 로직 */
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return BPF_OK;

    keys->n_proto = eth->h_proto;
    keys->nhoff = sizeof(*eth);

    /* IP 헤더 파싱 */
    if (keys->n_proto == bpf_htons(ETH_P_IP)) {
        struct iphdr *iph = data + keys->nhoff;
        if ((void *)(iph + 1) > data_end)
            return BPF_OK;

        keys->ipv4_src = iph->saddr;
        keys->ipv4_dst = iph->daddr;
        keys->ip_proto = iph->protocol;
        keys->thoff = keys->nhoff + (iph->ihl * 4);
    }

    return BPF_OK;
}
# BPF Flow Dissector 로드 (네트워크 네임스페이스에 연결)
$ bpftool prog load flow_dissector.bpf.o /sys/fs/bpf/flow_dissector
$ bpftool net attach flow_dissector \
    pinned /sys/fs/bpf/flow_dissector

# 확인
$ bpftool net list
flow_dissector:
  netns(id:1) flow_dissector id 42

# 해제
$ bpftool net detach flow_dissector
Flow Dissector 사용처: Flow Dissector는 RPS/RFS 해시뿐 아니라 tc flower 필터, nft flow 테이블, cls_bpf, act_ct (conntrack), skb_get_hash() 전체에서 사용됩니다. BPF Flow Dissector를 교체하면 이 모든 서브시스템의 패킷 파싱 동작이 함께 변경됩니다.

per-CPU backlog 큐 — softnet_data

리눅스 네트워크 스택의 수신 경로에서 softnet_data 구조체는 CPU마다 하나씩 존재하는 네트워크 처리 허브입니다. RPS가 패킷을 다른 CPU로 전달할 때 사용하는 backlog 큐, NAPI poll 리스트, 그리고 각종 통계 카운터가 이 구조체에 포함됩니다.

softnet_data 구조체 상세

/* include/linux/netdevice.h — per-CPU 네트워크 처리 구조체 */
struct softnet_data {
    /* === NAPI poll 관련 === */
    struct list_head     poll_list;
    /* NET_RX_SOFTIRQ에서 처리할 NAPI 인스턴스 리스트
     * 드라이버의 NAPI + backlog NAPI가 여기에 등록됨 */

    /* === RPS backlog 큐 === */
    struct sk_buff_head  input_pkt_queue;
    /* 다른 CPU에서 RPS/enqueue_to_backlog()을 통해
     * 이 CPU로 전달된 패킷이 대기하는 큐
     * 최대 크기: /proc/sys/net/core/netdev_max_backlog */

    struct sk_buff_head  process_queue;
    /* process_backlog() NAPI poll에서 실제 처리하는 큐
     * input_pkt_queue에서 splice로 옮겨온 패킷들 */

    struct napi_struct   backlog;
    /* backlog 처리용 가상 NAPI 인스턴스
     * poll 함수: process_backlog()
     * input_pkt_queue의 패킷을 프로토콜 스택으로 전달 */

    /* === 통계 카운터 === */
    unsigned int         processed;
    /* 이 CPU에서 처리한 총 패킷 수 */

    unsigned int         dropped;
    /* backlog 오버플로로 드롭된 패킷 수
     * /proc/net/softnet_stat 두 번째 컬럼 */

    unsigned int         time_squeeze;
    /* softirq 시간/budget 제한으로 처리 못한 횟수
     * /proc/net/softnet_stat 세 번째 컬럼 */

    unsigned int         received_rps;
    /* RPS를 통해 이 CPU로 전달된 패킷 수 */

    /* ... 기타 필드 */
};

process_backlog NAPI poll 함수

/* net/core/dev.c — backlog NAPI poll 함수 */
static int process_backlog(struct napi_struct *napi, int quota)
{
    struct softnet_data *sd = container_of(napi,
        struct softnet_data, backlog);
    int work = 0;

    /* input_pkt_queue → process_queue로 일괄 이동 (splice)
     * splice 중에만 spinlock 필요 → 락 보유 시간 최소화 */
    rps_lock_irqsave(sd, &flags);
    skb_queue_splice_tail_init(&sd->input_pkt_queue,
                               &sd->process_queue);
    rps_unlock_irq_restore(sd, &flags);

    /* process_queue에서 패킷을 하나씩 꺼내 프로토콜 스택에 전달 */
    while (work < quota) {
        struct sk_buff *skb = __skb_dequeue(&sd->process_queue);
        if (!skb) {
            /* 처리할 패킷이 없으면 input_pkt_queue 재확인 */
            rps_lock_irqsave(sd, &flags);
            if (skb_queue_empty(&sd->input_pkt_queue)) {
                __napi_complete(napi);
                rps_unlock_irq_restore(sd, &flags);
                break;
            }
            skb_queue_splice_tail_init(
                &sd->input_pkt_queue,
                &sd->process_queue);
            rps_unlock_irq_restore(sd, &flags);
            skb = __skb_dequeue(&sd->process_queue);
        }

        /* 프로토콜 스택으로 전달 */
        __netif_receive_skb(skb);
        work++;
    }

    return work;
}

backlog 큐 오버플로 처리와 모니터링

# === /proc/net/softnet_stat 해독 ===
# 각 행은 하나의 CPU에 대응 (CPU 0부터 순서대로)
# 컬럼 (16진수): processed  dropped  time_squeeze  ...  received_rps  flow_limit_count

$ cat /proc/net/softnet_stat
00a1b2c3 00000000 00000005 00000000 00000000 00000000 00000000 00000000 00000000 001a2b3c 00000000
009f8d21 0000002a 00000003 00000000 00000000 00000000 00000000 00000000 00000000 00198abc 00000000
# CPU 0: dropped=0, time_squeeze=5 → 정상
# CPU 1: dropped=42 → backlog 오버플로 42회 발생!

# === 오버플로 대응 ===

# 1. backlog 큐 크기 증가 (기본: 1000)
$ echo 10000 > /proc/sys/net/core/netdev_max_backlog

# 2. RPS CPU 맵 조정으로 부하 재분배
# (특정 CPU에 과부하 → 다른 CPU 추가)

# 3. time_squeeze가 높으면 → softirq budget 증가
$ echo 600 > /proc/sys/net/core/netdev_budget
$ echo 4000 > /proc/sys/net/core/netdev_budget_usecs

# === 실시간 모니터링 스크립트 ===
$ watch -d -n 1 'cat /proc/net/softnet_stat | \
  awk "{printf \"CPU%d: processed=%d dropped=%d squeeze=%d rps_recv=%d\\n\", \
  NR-1, strtonum(\"0x\"$1), strtonum(\"0x\"$2), strtonum(\"0x\"$3), strtonum(\"0x\"$10)}"'

netif_receive_skb 경로와 backlog 큐 진입

/* net/core/dev.c — 패킷 수신의 분기점 */

/* netif_receive_skb() 호출 경로:
 *
 * NAPI poll()
 *   └→ napi_gro_receive() 또는 netif_receive_skb()
 *       └→ netif_receive_skb_internal()
 *           ├→ RPS 미설정: __netif_receive_skb() (현재 CPU에서 직접 처리)
 *           └→ RPS 설정:
 *               ├→ get_rps_cpu() → 대상 CPU 결정
 *               ├→ 대상 == 현재 CPU: __netif_receive_skb()
 *               └→ 대상 != 현재 CPU: enqueue_to_backlog()
 *                   └→ 대상 CPU의 softnet_data.input_pkt_queue에 삽입
 *                       └→ ____napi_schedule() → IPI → NET_RX_SOFTIRQ
 *                           └→ process_backlog() → __netif_receive_skb()
 */

/* netif_receive_skb_internal 핵심 분기 */
static int netif_receive_skb_internal(struct sk_buff *skb)
{
    int cpu = get_rps_cpu(skb->dev, skb, &rflow);

    if (cpu >= 0 && cpu != smp_processor_id()) {
        /* 다른 CPU로 전달 */
        return enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
    }

    /* 현재 CPU에서 직접 처리 */
    return __netif_receive_skb(skb);
}

멀티코어 네트워크 성능 튜닝 실전

지금까지 다룬 RSS, RPS, RFS, XPS, aRFS를 실전 환경에서 조합하여 최적 성능을 달성하는 방법을 설명합니다. NUMA 인지 IRQ affinity, busy polling, GRO/GSO 최적화, softirq 부하 분석, 그리고 perf/bpftrace를 활용한 병목 진단을 다룹니다.

NUMA 인지 IRQ Affinity 설정

IRQ affinity는 네트워크 성능의 가장 기본적인 튜닝 포인트입니다. NIC가 연결된 PCIe 슬롯의 NUMA 노드와 일치하는 CPU에 IRQ를 고정하면 캐시 미스와 원격 메모리 접근을 최소화할 수 있습니다.

# === irqbalance vs 수동 설정 ===

# irqbalance: 자동 IRQ 분산 데몬
# 장점: 설정 간편, 동적 재조정
# 단점: NUMA 인지가 불완전할 수 있음, 네트워크 집약 환경에서 부적합
$ systemctl status irqbalance

# 고성능 환경에서는 irqbalance 비활성화 + 수동 설정 권장
$ systemctl stop irqbalance
$ systemctl disable irqbalance

# === NIC NUMA 노드 확인 ===
$ cat /sys/class/net/eth0/device/numa_node
0

# NUMA 노드 0의 CPU 목록
$ cat /sys/devices/system/node/node0/cpulist
0-7

# === IRQ Affinity 수동 설정 ===
# 각 큐의 IRQ를 같은 NUMA 노드의 CPU에 1:1 매핑

# IRQ 번호 확인
$ grep eth0 /proc/interrupts | awk '{print $1, $NF}'
128: eth0-TxRx-0
129: eth0-TxRx-1
130: eth0-TxRx-2
131: eth0-TxRx-3
132: eth0-TxRx-4
133: eth0-TxRx-5
134: eth0-TxRx-6
135: eth0-TxRx-7

# IRQ를 NUMA 0의 CPU에 1:1 매핑
$ echo 1   > /proc/irq/128/smp_affinity   # CPU 0
$ echo 2   > /proc/irq/129/smp_affinity   # CPU 1
$ echo 4   > /proc/irq/130/smp_affinity   # CPU 2
$ echo 8   > /proc/irq/131/smp_affinity   # CPU 3
$ echo 10  > /proc/irq/132/smp_affinity   # CPU 4
$ echo 20  > /proc/irq/133/smp_affinity   # CPU 5
$ echo 40  > /proc/irq/134/smp_affinity   # CPU 6
$ echo 80  > /proc/irq/135/smp_affinity   # CPU 7

# 설정 확인
$ for irq in 128 129 130 131 132 133 134 135; do
    echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity)"
done

# === Intel ice 드라이버 자동 IRQ affinity ===
# ice 드라이버는 irq_set_affinity_hint()로 NUMA 인지 힌트를 자동 설정
# /proc/irq/N/affinity_hint를 참조하여 설정할 수 있음
$ cat /proc/irq/128/affinity_hint

Busy Polling (SO_BUSY_POLL) 튜닝

Busy polling은 소켓 읽기 시 softirq를 기다리지 않고 직접 NIC의 NAPI poll을 호출하여 레이턴시를 줄이는 기법입니다. CPU 사용률이 증가하지만, 지연 민감 워크로드(HFT, 실시간(Real-time) 서비스)에서 효과적입니다.

# === 전역 busy polling 설정 ===

# busy_poll: poll()/select()/epoll_wait() 시 busy-poll 시간 (μs)
$ echo 50 > /proc/sys/net/core/busy_poll
# 50μs 동안 NAPI를 직접 폴링 → softirq 대기 없이 패킷 수신

# busy_read: 소켓 recvmsg() 시 busy-poll 시간 (μs)
$ echo 50 > /proc/sys/net/core/busy_read

# === 소켓별 설정 (SO_BUSY_POLL) ===
# C 코드에서:
# int val = 50;  // 50μs
# setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &val, sizeof(val));

# === NAPI defer + busy polling 조합 (커널 5.11+) ===
# IRQ를 지연시키고 busy polling으로 패킷을 가져오는 하이브리드 모드
$ echo 1 > /sys/class/net/eth0/napi_defer_hard_irqs
$ echo 200000 > /sys/class/net/eth0/gro_flush_timeout
# → IRQ 발생을 200μs 지연, 그 사이에 busy polling으로 수신
# → 인터럽트 수 대폭 감소 + 레이턴시 개선

# === 효과 확인 ===
# busy polling으로 처리한 패킷 수
$ cat /proc/net/softnet_stat | awk '{print "CPU"NR-1": busy_poll="strtonum("0x"$11)}'
Busy Polling 주의사항:
  • CPU 사용률 증가 — busy polling 중에는 CPU가 100% 점유됩니다. CPU 코어가 충분하지 않으면 다른 워크로드에 영향을 줄 수 있습니다.
  • NIC 지원 필요 — NIC 드라이버가 napi_busy_loop을 지원해야 합니다. 대부분의 현대 드라이버(ixgbe, i40e, ice, mlx5)가 지원합니다.
  • epoll 호환epoll_wait()에서도 busy polling이 작동하지만, 많은 수의 소켓을 모니터링하면 각 소켓의 NAPI를 순회하여 오버헤드가 증가합니다.

GRO/GSO 설정과 성능 영향

# === GRO/GSO 상태 확인 ===
$ ethtool -k eth0 | grep -E "(gro|gso|tso)"
generic-receive-offload: on
generic-segmentation-offload: on
tcp-segmentation-offload: on
rx-gro-hw: off         # HW-GRO (커널 5.19+)
rx-gro-list: off        # GRO list mode

# === GRO 튜닝 ===

# GRO 활성화 (기본: on)
$ ethtool -K eth0 gro on

# HW-GRO 활성화 (NIC 지원 시)
$ ethtool -K eth0 rx-gro-hw on

# GRO flush timeout (커널 4.6+)
# 기본 0 = poll 종료 시 즉시 flush
# 값을 늘리면 더 큰 super-packet 생성 → 처리량↑, 지연↑
$ echo 20000 > /sys/class/net/eth0/gro_flush_timeout
# 20μs 동안 GRO 패킷 병합 대기

# === GSO/TSO 튜닝 ===

# TSO (TCP Segmentation Offload) — NIC H/W에 세그멘테이션 위임
$ ethtool -K eth0 tso on

# GSO (Generic Segmentation Offload) — S/W 세그멘테이션
$ ethtool -K eth0 gso on

# === 효과 측정 ===
# GRO 통계
$ ethtool -S eth0 | grep gro
# 처리량 비교: GRO on vs off
$ iperf3 -c <TARGET> -t 30 -P 4   # 멀티스레드 처리량 측정

코어별 softirq 부하 분석

# === /proc/softirqs — softirq 유형별 CPU별 카운터 ===
$ cat /proc/softirqs | grep NET
                    CPU0       CPU1       CPU2       CPU3
      NET_TX:       1523       1487       1501       1498
      NET_RX:    1523847    1518293    1521056    1519834
# NET_RX가 균등하게 분배되는지 확인
# 특정 CPU에 집중 → RSS/RPS 재설정 필요

# === /proc/net/softnet_stat 분석 ===
$ cat /proc/net/softnet_stat
# 주요 확인 포인트:
# - 컬럼 2 (dropped) > 0  → netdev_max_backlog 증가 필요
# - 컬럼 3 (time_squeeze) > 0  → netdev_budget 증가 필요
# - 특정 CPU의 컬럼 1 (processed)가 비정상 높음 → IRQ affinity 재조정

# === mpstat으로 softirq CPU 사용률 확인 ===
$ mpstat -P ALL 1 5 | grep -E "CPU|all"
# %soft 컬럼: softirq에 사용된 CPU 비율
# %soft > 50% → 해당 CPU가 네트워크 처리로 과부하

# === sar로 네트워크 인터럽트 추이 확인 ===
$ sar -I ALL 1 5
# IRQ별 초당 발생 횟수 확인

perf/bpftrace 활용 네트워크 병목 분석

# === perf: 네트워크 스택 핫스팟 프로파일링 ===

# CPU별 네트워크 함수 프로파일
$ perf record -g -a -C 0-7 -- sleep 10
$ perf report --sort=symbol | head -30
# netif_receive_skb, __netif_receive_skb_core,
# tcp_v4_rcv 등이 상위에 위치하면 네트워크 바운드

# 패킷 드롭 위치 추적
$ perf record -e skb:kfree_skb -a -- sleep 30
$ perf script | head -20
# kfree_skb가 호출된 스택 트레이스로 드롭 원인 파악

# === bpftrace: 실시간 네트워크 분석 ===

# RPS CPU 선택 분포 확인
$ bpftrace -e '
kretprobe:get_rps_cpu {
    @rps_cpu = lhist(retval, -1, 16, 1);
}
interval:s:5 { print(@rps_cpu); clear(@rps_cpu); }
'

# NAPI poll 시간 측정 (μs)
$ bpftrace -e '
kprobe:napi_poll { @start[tid] = nsecs; }
kretprobe:napi_poll /@start[tid]/ {
    @poll_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

# backlog 큐 길이 히스토그램
$ bpftrace -e '
kprobe:enqueue_to_backlog {
    $sd = (struct softnet_data *)arg1;
    @backlog_len = lhist($sd->input_pkt_queue.qlen, 0, 10000, 100);
}
'

# per-flow 처리 레이턴시 (IP → 소켓 전달까지)
$ bpftrace -e '
kprobe:ip_rcv { @ip_start[arg0] = nsecs; }
kprobe:tcp_queue_rcv /@ip_start[arg0]/ {
    @latency_us = hist((nsecs - @ip_start[arg0]) / 1000);
    delete(@ip_start[arg0]);
}
'

환경별 튜닝 레시피

환경RSSRPS/RFSXPSaRFSBusy Poll기타
고처리량 웹 서버 큐 = NUMA CPU 수 RFS on CPU 1:1 on (NIC 지원 시) off GRO on, netdev_budget=600, conntrack NOTRACK(port 80/443)
저지연 금융 (HFT) 전용 큐 isolate off (RPS 불필요) CPU 1:1 on 50~100μs napi_defer_hard_irqs=1, isolcpus, nohz_full
가상머신 (virtio) 지원 시 on RPS+RFS on CPU 1:1 미지원 off netdev_max_backlog=10000, vhost-net 사용
네트워크 라우터/방화벽(Firewall) 큐 = CPU 수 off CPU 1:1 off off conntrack 최적화 또는 비활성화, GRO on, ip_forward sysctl
컨테이너(Container) 호스트 큐 = CPU 수 RFS on NUMA 그룹 on off tc mqprio, cgroup net_cls, conntrack_max 증가
종합 튜닝 체크리스트:
  1. NUMA 확인: cat /sys/class/net/<dev>/device/numa_node로 NIC NUMA 노드 확인
  2. 큐 수 설정: ethtool -L <dev> combined N (N = NUMA 노드의 CPU 수)
  3. IRQ affinity: irqbalance 중지 후 수동 설정 (큐 IRQ → 같은 NUMA CPU)
  4. RSS 키/RETA: ethtool -X <dev> equal N (균등) 또는 weight (가중치)
  5. XPS: xps_cpus로 CPU 1:1 매핑
  6. RFS + aRFS: rps_sock_flow_entries, rps_flow_cnt, ntuple on
  7. backlog: netdev_max_backlog, netdev_budget 조정
  8. 검증: ethtool -S, /proc/net/softnet_stat, mpstat으로 확인

네트워크 패킷 흐름 (Packet Flow)

전체 네트워크 스택 RX/TX 경로

Linux 네트워크 스택은 NIC 하드웨어부터 유저스페이스 애플리케이션까지 여러 계층을 통과합니다. 다음 다이어그램은 패킷이 수신(RX)되고 송신(TX)되는 전체 경로를 보여줍니다.

네트워크 스택 개요 NIC/NAPI L2/L3/L4 Netfilter/라우팅 Socket/Qdisc RX: NIC → 소켓 전달 TX: 소켓 → qdisc/NIC 전송 아래 상세 다이어그램에서 훅/함수 단위를 확인
큰 상세도에 들어가기 전, 패킷이 통과하는 핵심 계층만 먼저 파악하는 개요판입니다.
Linux 네트워크 스택 전체 플로우 ← 수신 경로 (RX) 1. NIC 하드웨어 2. DMA → Ring Buffer 3. 인터럽트/NAPI Poll 4. netif_receive_skb() 5. L2/L3/L4 프로토콜 스택 (Ethernet → IP → TCP/UDP) Netfilter: PREROUTING 라우팅 결정 local Netfilter: INPUT 6. 소켓 수신 큐 7. 유저 프로세스 송신 경로 (TX) → 1. 유저 프로세스 2. 소켓 송신 (send/write) 3. L4/L3 프로토콜 처리 (TCP/UDP → IP → Ethernet) Netfilter: OUTPUT 라우팅 선택 Netfilter: POSTROUTING 4. TC/Qdisc (QoS) 5. 디바이스 드라이버 6. DMA → TX Ring Buffer 7. NIC 하드웨어 송신 핵심 구조체 struct sk_buff struct net_device struct sock struct dst_entry XDP/eBPF Hook 드라이버 레벨 (RX 최초 처리)
Linux 네트워크 스택 전체 플로우: 수신(RX)과 송신(TX) 경로

수신 경로 (RX Path) 상세

/* 1. NIC 하드웨어가 패킷 수신 → DMA로 메모리에 복사 */
/* 2. 인터럽트 발생 → NAPI softirq로 처리 전환 */

/* net/core/dev.c — 수신 처리 핵심 */
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                                   struct packet_type **ppt_prev)
{
    struct sk_buff *skb = *pskb;
    struct net_device *orig_dev = skb->dev;

    /* 3. XDP 프로그램 실행 (드라이버 레벨) */
    /* 4. Generic XDP (드라이버가 native XDP 미지원 시) */
    if (static_branch_unlikely(&generic_xdp_needed_key)) {
        int ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
        if (ret2 != XDP_PASS) return NET_RX_DROP;
    }

    /* 5. 프로토콜 핸들러 실행 (eth_type_trans → ip_rcv → tcp_v4_rcv) */
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (ptype->dev && ptype->dev != skb->dev)
            continue;
        deliver_skb(skb, ptype, orig_dev);
    }

    return NET_RX_SUCCESS;
}

/* net/ipv4/ip_input.c — IP 계층 수신 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
         struct packet_type *pt, struct net_device *orig_dev)
{
    /* IP 헤더 검증 (체크섬, 버전, 길이) */
    if (!pskb_may_pull(skb, sizeof(struct iphdr)))
        goto inhdr_error;

    /* Netfilter PREROUTING 훅 실행 → 라우팅 결정 */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
                    net(dev), NULL, skb, dev, NULL,
                    ip_rcv_finish);
}

/* net/ipv4/tcp_ipv4.c — TCP 계층 수신 */
int tcp_v4_rcv(struct sk_buff *skb)
{
    struct sock *sk = __inet_lookup_skb(&tcp_hashinfo, skb, ...);

    if (sk->sk_state == TCP_LISTEN)
        return tcp_v4_do_rcv(sk, skb);  /* SYN 처리 */

    /* 소켓 수신 큐에 추가 → 유저 프로세스가 read/recv */
    tcp_queue_rcv(sk, skb, &fragstolen);
    return 0;
}

송신 경로 (TX Path) 상세

/* 유저 프로세스: write() → sys_sendto() → sock->ops->sendmsg() */

/* net/ipv4/tcp.c — TCP 송신 시작 */
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    /* sk_buff 할당 및 데이터 복사 */
    skb = tcp_write_queue_tail(sk);

    /* TCP 헤더 구성 (시퀀스 번호, ACK, 윈도우 크기 등) */
    tcp_push(sk, flags, mss_now, TCP_NAGLE_OFF);

    return copied;
}

/* net/ipv4/ip_output.c — IP 계층 송신 */
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    /* 라우팅 테이블 검색 → 출력 인터페이스 결정 */
    rt = (struct rtable *)__sk_dst_check(sk, 0);

    /* IP 헤더 구성 (TTL, 체크섬, src/dst IP) */
    ip_copy_addrs(iph, fl4);
    iph->ttl = ip_select_ttl(inet, dst);

    /* Netfilter OUTPUT 훅 실행 */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
                    net, sk, skb, NULL, rt->dst.dev,
                    ip_output);
}

/* net/core/dev.c — 디바이스 큐 송신 */
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    struct netdev_queue *txq = netdev_pick_tx(dev, skb, sb_dev);
    struct Qdisc *q = rcu_dereference_bh(txq->qdisc);

    /* TC/Qdisc 통과 (트래픽 셰이핑, 우선순위 큐 등) */
    if (q->enqueue) {
        rc = __dev_xmit_skb(skb, q, dev, txq);
    }

    /* 드라이버의 ndo_start_xmit() 호출 → DMA → NIC */
    return dev_hard_start_xmit(skb, dev, txq, &rc);
}
💡
성능 최적화 지점:
  • RX 경로: XDP/eBPF를 사용하여 드라이버 레벨에서 조기 필터링 (DDoS 방어, 로드밸런싱)
  • NAPI: 인터럽트 병합으로 CPU 사용률 감소 (ethtool -C eth0 rx-usecs 50)
  • TX 경로: TSO/GSO로 대용량 패킷을 NIC에 오프로드
  • Qdisc: 고성능 환경에서는 pfifo_fast 대신 fq_codel 또는 noqueue 사용

Netfilter 체인과 패킷 경로

NIC (RX) PREROUTING 라우팅 INPUT Local Process FORWARD POSTROUTING OUTPUT NIC (TX) local forward output routing DROP ✕ DROP ✕ DROP ✕ DROP ✕ DROP ✕

패킷 경로별 상세 흐름

경로Netfilter 훅 순서설명주요 처리
LOCAL_IN PREROUTING → INPUT 외부 → 로컬 프로세스(Process) DNAT(PREROUTING), conntrack, 방화벽(INPUT), 소켓 전달
FORWARD PREROUTING → FORWARD → POSTROUTING 외부 → 라우팅(Routing) → 외부 DNAT, 포워딩 정책, SNAT(POSTROUTING), TTL 감소
LOCAL_OUT OUTPUT → POSTROUTING 로컬 프로세스 → 외부 출력 필터링(OUTPUT), SNAT/MASQUERADE(POSTROUTING)
DROP 어느 훅에서든 패킷 폐기 NF_DROP 반환, kfree_skb(), 드롭 카운터 증가

Connection Tracking (conntrack)

/* conntrack은 PREROUTING/OUTPUT 훅에서 패킷의 연결 상태를 추적 */
/* 모든 netfilter 기반 NAT, stateful 방화벽의 기초 */

/* conntrack 엔트리 상태 */
IP_CT_NEW           /* 첫 번째 패킷 (SYN) */
IP_CT_ESTABLISHED   /* 양방향 트래픽 확인됨 */
IP_CT_RELATED       /* 기존 연결과 관련 (FTP data, ICMP error) */
IP_CT_INVALID       /* 상태 추적 실패 */

/* conntrack 해시 테이블 크기 — 성능에 직접 영향 */
/* /proc/sys/net/netfilter/nf_conntrack_max = 262144 */
/* /proc/sys/net/netfilter/nf_conntrack_buckets (readonly) */
/* 최적: max = buckets × 4 (체인 길이 ~4 유지) */
conntrack 테이블 포화 문제: 고트래픽 환경에서 conntrack 테이블이 가득 차면 nf_conntrack: table full, dropping packet 에러와 함께 패킷이 무작위로 드롭됩니다.
  • nf_conntrack_max 증가 (메모리 비용: 엔트리당 ~300바이트)
  • 타임아웃 조정: nf_conntrack_tcp_timeout_established (기본 432000초 = 5일)
  • conntrack 불필요한 트래픽은 NOTRACK (raw 테이블)으로 바이패스
  • 초고성능 라우터에서는 conntrack 자체를 비활성화 고려

패킷 드롭 디버깅

# 드롭 모니터 — 패킷이 어디서 드롭되는지 추적
perf record -e skb:kfree_skb -a sleep 10
perf script

# dropwatch 도구 활용
dropwatch -l kas
> start
# 출력: drop at: tcp_v4_rcv+0x1a (sobjects hit: 15)

# nftables/iptables 카운터로 규칙별 드롭 확인
iptables -L -v -n | grep DROP
nft list ruleset | grep drop

# 인터페이스 통계로 드롭 위치 파악
ethtool -S eth0 | grep -i drop
cat /proc/net/softnet_stat  # 컬럼: processed, dropped, time_squeeze

# netstat 프로토콜별 에러 통계
netstat -s | grep -i -E "drop|error|overflow|pruned"

# BPF 기반 패킷 추적 (bcc/bpftrace)
bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }'

패킷 흐름 설계 시 고려사항

고성능 패킷 처리 설계 포인트:
  • PREROUTING에서 조기 드롭 — 불필요한 패킷은 가능한 일찍 드롭하여 후속 처리 비용 절감
  • conntrack 바이패스 — 상태 추적 불필요한 트래픽(DNS 캐시, CDN)은 raw 테이블에서 NOTRACK
  • FORWARD 최적화 — IP forwarding 시 bridge vs routing 성능 차이 고려. nf_conntrack 비활성화 검토
  • LOCAL_OUT 경로 — 로컬 소켓의 출력 경로도 netfilter를 거침. 컨테이너 환경에서 iptables 규칙 수 관리 중요
  • XDP 조기 처리 — netfilter보다 앞단(드라이버 레벨)에서 XDP로 패킷 필터링/리다이렉트 가능
  • nftables 선호 — iptables 대비 nftables는 단일 패스 처리로 체인이 많을 때 성능 우위

TC (Traffic Control) 아키텍처

Linux Traffic Control(TC)은 커널 네트워크 스택의 송신(egress) 및 수신(ingress) 경로에서 패킷의 대기열 관리, 스케줄링, 분류, 폴리싱, 셰이핑을 담당하는 프레임워크입니다. TC는 세 가지 핵심 컴포넌트로 구성됩니다: qdisc(Queuing Discipline), class(클래스), filter(분류기).

TC 아키텍처: qdisc, class, filter 계층 구조 패킷 입력 filter (분류기) u32, flower, bpf... class 1:1 rate 100Mbit ceil 200Mbit class 1:2 rate 50Mbit ceil 100Mbit leaf qdisc fq_codel / cake leaf qdisc netem / pfifo root qdisc (htb / hfsc / prio) Egress (송신 경로) dev_queue_xmit() -> qdisc->enqueue() -> dequeue_skb() -> dev_hard_start_xmit() 셰이핑: 토큰 버킷 (HTB), 커브 (HFSC) AQM: CoDel, RED, PIE 스케줄링: WRR, WFQ, DRR Ingress (수신 경로) sch_handle_ingress() 호출 clsact qdisc: 양방향 지원 (6.0+) 폴리싱: 속도 제한 (drop/pass) 리다이렉트: mirred, connmark eBPF: TC BPF direct-action

qdisc, class, filter 계층 구조

TC의 세 컴포넌트는 계층적 트리 구조를 형성합니다. root qdisc가 최상위에 위치하고, 그 아래에 class가 배치되며, filter가 패킷을 적절한 class로 분류합니다.

컴포넌트역할커널 구조체핵심 콜백
qdisc 패킷 큐잉 및 스케줄링 정책 struct Qdisc enqueue(), dequeue(), reset()
class 대역폭(Bandwidth) 할당 단위 (classful qdisc만) struct Qdisc_class_common graft(), leaf(), dump()
filter 패킷 분류 (class 매핑) struct tcf_proto classify(), init(), destroy()
/* net/sched/sch_api.c — TC 핵심 구조체 */

struct Qdisc {
    int                     (*enqueue)(struct sk_buff *skb,
                                       struct Qdisc *sch,
                                       struct sk_buff **to_free);
    struct sk_buff *        (*dequeue)(struct Qdisc *sch);
    const struct Qdisc_ops  *ops;
    struct qdisc_size_table *stab;
    struct hlist_node       hash;
    u32                     handle;     /* major:minor 핸들 */
    u32                     parent;
    struct netdev_queue     *dev_queue;
    struct net_rate_estimator *rate_est;
    struct gnet_stats_basic_sync bstats;
    struct gnet_stats_queue      qstats;
    unsigned long           state;
    struct Qdisc            *next_sched;
    struct sk_buff_head     gso_skb;    /* GSO 세그먼트 재큐잉용 */
    struct sk_buff_head     skb_bad_txq;
    long                    privdata[];  /* qdisc별 사적 데이터 */
};

/* TC filter 프레임워크 */
struct tcf_proto {
    struct tcf_proto        *next;
    void                    *root;       /* 필터 데이터 */
    int                     (*classify)(struct sk_buff *,
                                        const struct tcf_proto *,
                                        struct tcf_result *);
    __be16                  protocol;
    u32                     prio;
    struct Qdisc            *q;
    const struct tcf_proto_ops *ops;
    struct tcf_chain        *chain;
    struct rcu_head         rcu;
};

tc flower 오프로드

tc flower는 커널 5.1+에서 도입된 고급 필터로, 하드웨어 오프로드를 통해 NIC에서 직접 필터링과 액션을 수행할 수 있습니다. SmartNIC 환경에서 수백만 flow를 라인레이트로 처리하는 핵심 메커니즘입니다.

# tc flower 기본 사용 예시

# 1. clsact qdisc 추가 (ingress + egress)
tc qdisc add dev eth0 clsact

# 2. 특정 VLAN + IP 트래픽을 특정 큐로 리다이렉트
tc filter add dev eth0 ingress protocol 802.1Q \
    flower vlan_id 100 vlan_prio 3 \
    dst_ip 192.168.1.0/24 \
    action skbedit queue_mapping 4

# 3. 하드웨어 오프로드 (skip_sw: 소프트웨어 우회)
tc filter add dev eth0 ingress protocol ip \
    flower skip_sw \
    src_ip 10.0.0.0/8 \
    dst_port 80 \
    action drop

# 4. 터널 매칭 (VXLAN 내부 필터링)
tc filter add dev eth0 ingress protocol ip \
    flower \
    enc_dst_ip 192.168.1.1 \
    enc_key_id 100 \
    enc_dst_port 4789 \
    src_ip 10.0.0.1 \
    action mirred egress redirect dev veth0

# 5. 오프로드 통계 확인
tc -s filter show dev eth0 ingress
/* net/sched/cls_flower.c — flower 오프로드 핵심 구조체 */

struct fl_flow_key {
    struct flow_dissector_key_meta      meta;
    struct flow_dissector_key_control   control;
    struct flow_dissector_key_basic     basic;
    struct flow_dissector_key_eth_addrs eth;
    struct flow_dissector_key_vlan      vlan;
    struct flow_dissector_key_ipv4_addrs ipv4;
    struct flow_dissector_key_ipv6_addrs ipv6;
    struct flow_dissector_key_ports     tp;
    struct flow_dissector_key_enc_keyid enc_key_id;
    /* ... 터널, MPLS, CT 등 확장 필드 */
};

/* 오프로드 콜백 체인:
 * 1. cls_flower → flow_cls_offload 구성
 * 2. ndo_setup_tc() 호출 (드라이버 콜백)
 * 3. 드라이버가 HW 규칙 설치/삭제
 * 4. 실패 시 소프트웨어 폴백 (skip_sw 미지정 시) */

/* 드라이버 측 오프로드 처리 (mlx5 예시) */
static int mlx5e_setup_tc(struct net_device *dev,
                          enum tc_setup_type type,
                          void *type_data)
{
    switch (type) {
    case TC_SETUP_CLSFLOWER:
        return mlx5e_setup_tc_cls_flower(
            mlx5e_priv(dev), type_data,
            mlx5e_rep_indr_setup_tc_cb);
    case TC_SETUP_CLSMATCHALL:
        return mlx5e_setup_tc_cls_matchall(dev, type_data);
    default:
        return -EOPNOTSUPP;
    }
}
오프로드 모드 비교:
  • skip_sw — 하드웨어에서만 실행 (실패 시 에러 반환)
  • skip_hw — 소프트웨어에서만 실행 (오프로드 시도 안 함)
  • 기본(둘 다 미지정) — HW 오프로드 시도, 실패 시 SW 폴백

qdisc 종류별

Linux 커널은 다양한 qdisc를 제공하며, 각각 다른 트래픽 관리 요구사항에 최적화되어 있습니다. 이 섹션에서는 실무에서 가장 많이 사용되는 qdisc들을 커널 내부 동작과 함께 설명합니다.

qdisc 종류별 비교: classless vs classful Classless qdisc fq_codel 공정 큐잉 + CoDel AQM cake 통합 셰이핑+AQM netem 지연/손실 시뮬레이션 pfifo_fast 3-band 우선순위 tbf 토큰 버킷 필터 red / pie AQM 알고리즘 fq Fair Queue (pacing) sfq Stochastic Fair Queue Classful qdisc htb 계층적 토큰 버킷 hfsc 계층적 Fair Service mqprio HW 큐 매핑 taprio TSN 시간 스케줄링 prio 우선순위 분류 drr 결손 라운드 로빈 cbq 클래스 기반 큐잉 mq 멀티큐 NIC 기본

fq_codel (Fair Queuing Controlled Delay)

fq_codel은 Linux 3.5+에서 기본 qdisc로 사용되는 AQM(Active Queue Management) 알고리즘입니다. 플로우별 공정 큐잉과 CoDel 알고리즘을 결합하여 버퍼블로트(bufferbloat) 문제를 효과적으로 해결합니다.

/* net/sched/sch_fq_codel.c — fq_codel 핵심 구조 */

struct fq_codel_sched_data {
    u32     perturbation;      /* 해시 섭동 (플로우 분산 균등화) */
    u32     limit;             /* 전체 큐 최대 패킷 수 (기본 10240) */
    u32     flows_cnt;         /* 플로우 버킷 수 (기본 1024) */
    u32     quantum;           /* DRR 양자 (기본 MTU) */
    u32     drop_batch_size;   /* 배치 드롭 크기 */
    u32     memory_limit;      /* 메모리 제한 (기본 32MB) */
    struct codel_params cparams;   /* CoDel 매개변수 */
    struct codel_stats  cstats;   /* 전역 통계 */
    struct fq_codel_flow *flows; /* 플로우 버킷 배열 */
    struct fq_codel_flow *backlogs; /* 백로그 포인터 */
    u32     *drop_overlimit;
};

/* CoDel AQM 핵심 매개변수:
 * target   = 5ms   (목표 지연 시간)
 * interval = 100ms (측정 간격)
 * → 패킷 체류 시간이 target을 초과하는 기간이
 *   interval을 넘으면 드롭 시작 */

/* fq_codel 동작 흐름:
 * enqueue:
 *   1. skb_get_hash()로 플로우 해시 계산
 *   2. hash % flows_cnt → 플로우 버킷 선택
 *   3. 버킷에 패킷 큐잉
 *   4. 메모리/패킷 제한 초과 시 최대 백로그 플로우에서 드롭
 *
 * dequeue:
 *   1. DRR(Deficit Round Robin)로 플로우 선택
 *   2. 선택된 플로우에서 CoDel 알고리즘으로 dequeue
 *   3. CoDel이 drop 결정 시 ECN 마킹 우선 시도 */
# fq_codel 설정 예시

# 기본 설정으로 적용 (대부분의 경우 충분)
tc qdisc replace dev eth0 root fq_codel

# 세밀한 튜닝
tc qdisc replace dev eth0 root fq_codel \
    limit 10240 \
    flows 1024 \
    quantum 1514 \
    target 5ms \
    interval 100ms \
    ecn

# 통계 확인
tc -s qdisc show dev eth0
# → maxpacket, drop_overlimit, new_flow_count,
#   ecn_mark, memory_used 등 확인

CAKE (Common Applications Kept Enhanced)

CAKE는 fq_codel의 발전형으로, 셰이핑 + AQM + 공정 큐잉을 단일 qdisc에 통합합니다. 가정/소규모 네트워크에서 최적의 성능을 제공하도록 설계되었습니다.

# CAKE 기본 설정 (WAN 인터페이스)
tc qdisc replace dev eth0 root cake bandwidth 100Mbit

# 상세 설정: NAT 인식 + 호스트별 공정성
tc qdisc replace dev eth0 root cake \
    bandwidth 100Mbit \
    nat \
    dual-srchost \
    diffserv4 \
    wash \
    ingress

# CAKE 모드별 특징:
# besteffort  — 단일 tin (기본)
# diffserv3   — Bulk, Best Effort, Voice 3단계
# diffserv4   — Bulk, Best Effort, Video, Voice 4단계
# diffserv8   — 8단계 세분화
# precedence  — IP precedence 기반

# 통계 확인
tc -s qdisc show dev eth0

mqprio / taprio (TSN 스케줄링)

mqprio는 TC 우선순위를 하드웨어 TX 큐에 매핑하고, taprio는 IEEE 802.1Qbv Time-Aware Shaper를 구현하여 시간 기반 스케줄링을 제공합니다. TSN(Time-Sensitive Networking) 환경의 핵심 qdisc입니다.

# mqprio: TC-큐 매핑 설정
tc qdisc add dev eth0 root mqprio \
    num_tc 4 \
    map 0 0 0 0 1 1 2 2 3 3 3 3 3 3 3 3 \
    queues 2@0 2@2 2@4 2@6 \
    hw 1
# map: skb priority → TC 매핑
# queues: TC별 큐 개수@시작 오프셋
# hw 1: 하드웨어 오프로드 활성화

# taprio: TSN 시간 기반 스케줄링
tc qdisc replace dev eth0 parent root handle 100 taprio \
    num_tc 3 \
    map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
    queues 1@0 1@1 2@2 \
    base-time 200 \
    sched-entry S 01 300000 \
    sched-entry S 02 300000 \
    sched-entry S 04 400000 \
    flags 0x2
# sched-entry: S <gate-mask> <interval-ns>
# gate-mask 비트: TC0=0x01, TC1=0x02, TC2=0x04
# → 주기적으로 게이트를 열어 TC별 시간 슬롯 할당

netem (Network Emulation)

netem은 네트워크 지연, 패킷 손실, 중복, 재정렬, 대역폭 제한 등을 시뮬레이션하는 qdisc입니다. 분산 시스템 테스트, WAN 시뮬레이션, 프로토콜 강건성 검증에 필수적입니다.

# netem 기본 예시

# 100ms 지연 + 10ms 지터 (정규분포)
tc qdisc add dev eth0 root netem delay 100ms 10ms distribution normal

# 1% 패킷 손실 + 25% 상관 (연속 손실 경향)
tc qdisc change dev eth0 root netem loss 1% 25%

# Gilbert-Elliott 모델 (버스트 손실 시뮬레이션)
tc qdisc change dev eth0 root netem loss gemodel p 5% r 90% 1-h 0.1% 1-k 0%

# 패킷 재정렬: 25%를 10ms 지연, 나머지 즉시 전송
tc qdisc change dev eth0 root netem delay 10ms reorder 25% 50%

# 대역폭 제한과 함께 사용 (tbf + netem)
tc qdisc add dev eth0 root handle 1: tbf rate 10Mbit burst 32kbit latency 400ms
tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 50ms 5ms loss 0.5%

# 패킷 중복 + 손상
tc qdisc change dev eth0 root netem duplicate 1% corrupt 0.1%

# 슬롯 기반 전송 제어 (커널 4.12+)
tc qdisc change dev eth0 root netem \
    slot 800us 200us \
    slot-dist uniform \
    packets 27 bytes 0

네트워크 네임스페이스(Namespace)

네트워크 네임스페이스는 Linux 네트워크 스택의 완전한 격리(Isolation)를 제공합니다. 각 네임스페이스는 독립적인 라우팅 테이블(Routing Table), iptables 규칙, 인터페이스, 소켓을 가집니다. 컨테이너 네트워킹의 핵심 기반 기술입니다.

네트워크 네임스페이스: veth, macvlan, ipvlan 패킷 경로 호스트 네트워크 네임스페이스 (default) eth0 (물리) br0 (bridge) L2 포워딩 veth-host macvlan0 ipvlan0 호스트 라우팅 테이블 + iptables/nftables netns: container1 veth-c1 IP: 172.17.0.2/16 독립 라우팅/iptables 독립 lo 인터페이스 독립 소켓 포트 공간 netns: container2 (macvlan) macvlan1 IP: 192.168.1.101 고유 MAC 주소 부모 NIC 직접 송수신 bridge 불필요 netns: container3 (ipvlan) ipvlan1 IP: 192.168.1.102 부모 MAC 공유 L2/L3 모드 선택 802.1X 호환

veth 패킷 경로

veth(Virtual Ethernet) 쌍은 네트워크 네임스페이스 간 패킷 전달의 가장 일반적인 메커니즘입니다. Docker, Kubernetes의 기본 네트워킹에 사용됩니다.

/* drivers/net/veth.c — veth 패킷 전송 경로 */

/* veth_xmit: 한쪽 veth에서 보내면 반대쪽으로 전달
 *
 * Container netns         Host netns
 * ┌──────────────┐       ┌──────────────┐
 * │  veth-c1     │──────→│  veth-host   │
 * │  (TX)        │       │  (RX)        │
 * └──────────────┘       └──────────────┘
 *      dev_queue_xmit()       netif_rx() / NAPI
 */

static netdev_tx_t veth_xmit(struct sk_buff *skb,
                              struct net_device *dev)
{
    struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
    struct veth_rq *rq = NULL;
    struct net_device *rcv;
    int length = skb->len;

    rcu_read_lock();
    rcv = rcu_dereference(priv->peer);
    if (unlikely(!rcv) || !pskb_may_pull(skb, ETH_HLEN)) {
        kfree_skb(skb);
        goto drop;
    }

    /* XDP 프로그램이 장착된 경우 XDP 먼저 실행 */
    rcv_priv = netdev_priv(rcv);
    rq = &rcv_priv->rq[veth_select_rxq(rcv)];
    if (rq->xdp_prog) {
        /* XDP_REDIRECT, XDP_TX, XDP_DROP 등 처리 */
    }

    /* NAPI 기반 수신 (커널 5.0+, GRO 지원) */
    if (likely(veth_forward_skb(rcv, skb, rq, use_napi))) {
        /* 성공: 상대측 NAPI poll에서 패킷 처리 */
    }
    rcu_read_unlock();
    return NETDEV_TX_OK;
drop:
    rcu_read_unlock();
    return NETDEV_TX_OK;
}

macvlan vs ipvlan 비교

특성veth + bridgemacvlanipvlan
MAC 주소 컨테이너별 고유 컨테이너별 고유 부모 NIC 공유
L2 격리 bridge 기반 NIC 직접 (bridge 불필요) L3 모드에서 완전 격리
성능 보통 (bridge 오버헤드) 우수 (직접 송수신) 우수 (MAC 처리 최소)
802.1X 호환 제한적 비호환 (다중 MAC) 호환 (단일 MAC)
호스트-컨테이너 통신 가능 bridge 모드만 L3 모드 가능
사용 사례 Docker 기본, 범용 직접 네트워크 연결 클라우드/스위치 제한 환경
# 네임스페이스 + veth 쌍 설정
ip netns add ns1
ip link add veth-host type veth peer name veth-ns1
ip link set veth-ns1 netns ns1
ip addr add 10.0.0.1/24 dev veth-host
ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth-ns1
ip link set veth-host up
ip netns exec ns1 ip link set veth-ns1 up

# macvlan 설정 (bridge 모드)
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns ns1
ip netns exec ns1 ip addr add 192.168.1.101/24 dev macvlan0
ip netns exec ns1 ip link set macvlan0 up

# ipvlan 설정 (L3 모드)
ip link add ipvlan0 link eth0 type ipvlan mode l3
ip link set ipvlan0 netns ns1
ip netns exec ns1 ip addr add 192.168.1.102/24 dev ipvlan0
ip netns exec ns1 ip link set ipvlan0 up

# macvlan 모드 비교:
# private — 동일 부모의 다른 macvlan과 통신 차단
# vepa    — 외부 스위치 경유 통신 (hairpin)
# bridge  — 동일 부모의 macvlan 간 직접 통신
# passthru — 부모 NIC 독점 (VF passthrough 유사)

네트워크 디버깅 체크리스트

네트워크 스택 주요 버그 사례

리눅스 커널 네트워크 스택은 수십만 줄의 코드와 수백 개의 프로토콜 구현으로 구성된 복잡한 서브시스템입니다. 이 섹션에서는 실제 발생한 주요 버그와 취약점(Vulnerability) 사례를 분석하여 커널 네트워크 개발 시 주의해야 할 패턴을 살펴봅니다.

AF_PACKET 경쟁 조건 (CVE-2016-8655)

AF_PACKET 소켓의 TPACKET_V3 링 버퍼 설정 과정에서 타이머(Timer) 핸들러(Handler)와 소켓 종료 사이의 경쟁 조건(race condition)이 발견되었습니다. packet_set_ring() 함수에서 링 버퍼를 해제하는 동안 타이머 콜백이 이미 해제된 메모리에 접근하여 use-after-free가 발생했습니다.

⚠️

CVE-2016-8655 (CVSS 7.8): 로컬 비권한 사용자가 AF_PACKET 소켓의 TPACKET_V3 타이머 경쟁 조건을 악용하여 use-after-free를 트리거하고, 커널 코드 실행을 통해 root 권한 상승이 가능합니다. Linux 4.8.14 이전 커널이 영향을 받습니다.

/* net/packet/af_packet.c — 취약한 코드 (단순화) */

/* Thread A: packet_set_ring() — 링 버퍼 해제 */
static int packet_set_ring(struct sock *sk, ...)
{
    /* 문제: 타이머가 아직 실행 중일 수 있음 */
    if (closing) {
        kfree(rb->prb_bdqc);      /* 메모리 해제 */
        rb->prb_bdqc = NULL;
    }
    return 0;
}

/* Thread B: 타이머 콜백 — 해제된 메모리 접근! */
static void prb_retire_rx_blk_timer_expired(struct timer_list *t)
{
    /* pkc가 이미 kfree()된 메모리를 가리킴 → use-after-free! */
    prb_retire_current_block(pkc);  /* BOOM */
}

/* 수정된 코드: del_timer_sync()로 타이머 완전 취소 후 해제 */
static int packet_set_ring(struct sock *sk, ...)
{
    mutex_lock(&fanout_mutex);
    if (closing && po->tp_version == TPACKET_V3) {
        del_timer_sync(&rb->prb_bdqc->retire_blk_timer);
        kfree(rb->prb_bdqc);
        rb->prb_bdqc = NULL;
    }
    mutex_unlock(&fanout_mutex);
    return 0;
}
💡

교훈: 소켓 종료 경로에서 타이머, 워크큐, tasklet 등 비동기 핸들러가 모두 완료되었는지 반드시 확인해야 합니다. del_timer_sync(), cancel_work_sync() 등의 동기식 취소 함수를 사용하세요. 단순 del_timer()는 이미 실행 중인 콜백을 기다리지 않으므로 race condition에 취약합니다.

SYN Flood는 TCP 3-way handshake의 설계를 악용한 대표적인 DoS 공격입니다. 공격자가 위조된 소스 IP로 대량의 SYN 패킷을 전송하면 서버의 SYN 큐가 가득 차 정상적인 연결 요청도 거부됩니다.

SYN Cookie는 서버가 SYN 큐에 상태를 저장하지 않고도 정상적인 연결을 수립할 수 있게 해주는 방어 메커니즘입니다. SYN-ACK의 ISN(Initial Sequence Number)에 연결 정보를 암호학적으로 인코딩합니다:

/* net/ipv4/syncookies.c — SYN Cookie ISN 구성:
 *
 *  31      27 26  24 23                0
 * ┌──────────┬──────┬───────────────────┐
 * │ t(5bits) │ MSS  │    hash(24bits)   │
 * │(타이머)  │(3bit)│(HMAC 기반 검증값) │
 * └──────────┴──────┴───────────────────┘
 * hash = SHA-1(saddr, daddr, sport, dport, t, secret)
 */

static __u32 cookie_v4_init_sequence(
    const struct sk_buff *skb, __u16 *mssp)
{
    /* 클라이언트 MSS를 사전 정의된 테이블에서 가장 가까운 값으로 매핑 */
    for (mssind = ARRAY_SIZE(msstab) - 1; mssind; mssind--)
        if (mss >= msstab[mssind])
            break;
    return secure_tcp_syn_cookie(
        iph->saddr, iph->daddr,
        th->source, th->dest,
        ntohl(th->seq), mssind);
}
# SYN Cookie 활성화 (기본값: 1)
sysctl -w net.ipv4.tcp_syncookies=1

# SYN 큐 크기 증가
sysctl -w net.ipv4.tcp_max_syn_backlog=65536

# SYN-ACK 재전송 횟수 감소 (빠른 타임아웃)
sysctl -w net.ipv4.tcp_synack_retries=2

# 확인
cat /proc/net/netstat | grep "TCPReqQFullDoCookies"

Netfilter nf_conntrack 테이블 고갈

Netfilter의 연결 추적 모듈인 nf_conntrack은 NAT, stateful 방화벽의 핵심입니다. conntrack 테이블이 가득 차면 새로운 패킷이 드롭되어 심각한 서비스 장애가 발생합니다:

# 현재 conntrack 사용량 확인
cat /proc/sys/net/netfilter/nf_conntrack_count   # 현재 추적 중인 연결 수
cat /proc/sys/net/netfilter/nf_conntrack_max     # 최대 허용 연결 수

# 최대 연결 수 증가
sysctl -w net.netfilter.nf_conntrack_max=1048576

# 타임아웃 감소로 만료된 연결 빠르게 제거
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

# 특정 트래픽의 conntrack 제외 (NOTRACK)
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK

TCP SACK Panic (CVE-2019-11477)

2019년 Netflix 보안팀이 발견한 취약점입니다. TCP SACK 처리 과정에서 정수 오버플로(Integer Overflow)우가 발생하여, 원격 공격자가 특수 조작된 SACK 시퀀스를 전송하는 것만으로 커널 패닉(Kernel Panic)을 유발할 수 있었습니다.

⚠️

CVE-2019-11477 — SACK Panic (CVSS 7.5): SACK 블록 처리로 SKB가 과도하게 분할되면 tcp_gso_segs(17-bit 필드)에서 정수 오버플로우가 발생하여 BUG_ON()이 트리거되어 커널 패닉이 일어납니다. Linux 2.6.29 이후 모든 커널이 영향을 받습니다.

# 임시 완화: SACK 비활성화 (성능 저하 주의)
sysctl -w net.ipv4.tcp_sack=0

# 대안: 비정상적으로 작은 MSS 차단
iptables -A INPUT -p tcp -m tcpmss --mss 1:500 -j DROP

# 영구 수정: 커널 업데이트
# Linux 4.4.182, 4.9.182, 4.14.127, 4.19.52, 5.1.11 이후 수정됨
uname -r   # 현재 커널 버전 확인
💡

TCP 스택 보안 점검: (1) 커널을 최신 보안 패치(Patch) 버전으로 유지합니다. (2) SACK을 비활성화하기보다 커널 업데이트로 대응합니다. (3) iptables/nftables에서 비정상 MSS 값을 차단합니다. (4) /proc/net/netstat의 비정상 카운터를 모니터링합니다.

io_uring 네트워크 I/O

io_uring은 Linux 5.1+에서 도입된 비동기 I/O 프레임워크로, 네트워크 소켓 I/O에서도 epoll 대비 현저한 성능 향상을 제공합니다. SQ(Submission Queue)와 CQ(Completion Queue)의 링 버퍼 구조로 시스템 콜 오버헤드를 극소화합니다.

io_uring 네트워크 I/O 구조: SQ/CQ 링과 네트워크 소켓 유저 공간 (User Space) SQ (제출 큐) send/recv/accept connect/close CQ (완료 큐) 결과 + 바이트 수 에러 코드 mmap 공유 메모리 (zero-copy 가능) fixed buffers registered files SQPOLL 모드: 커널 스레드가 SQ 폴링 -> 시스템 콜 0회로 I/O 처리 커널 공간 (Kernel Space) io_uring worker io-wq 스레드풀 소켓 계층 TCP/UDP/Unix 네트워크 스택 (L3/L4) tcp_sendmsg / tcp_recvmsg NIC 드라이버 / NAPI zero-copy TX (6.0+) multishot accept/recv: 한 번 제출 -> 여러 완료 이벤트 수신 제출 완료

io_uring 네트워크 오퍼레이션

오퍼레이션커널 버전설명특징
IORING_OP_SEND 5.6+ 소켓 데이터 전송 fixed buffer 지원, zero-copy (6.0+)
IORING_OP_RECV 5.6+ 소켓 데이터 수신 multishot 모드 (6.0+)
IORING_OP_ACCEPT 5.5+ 연결 수락 multishot: 한 번 제출로 무한 accept
IORING_OP_CONNECT 5.5+ 소켓 연결 비동기 TCP connect
IORING_OP_SEND_ZC 6.0+ Zero-copy 전송 MSG_ZEROCOPY 대체, notification CQE
IORING_OP_RECV_ZC 6.1+ (예정) Zero-copy 수신 제공 버퍼(provided buffers) 활용
IORING_OP_SENDMSG 5.3+ sendmsg 비동기 ancillary data, cmsg 지원
IORING_OP_RECVMSG 5.3+ recvmsg 비동기 multishot + 제공 버퍼 그룹
/* io_uring 네트워크 서버 패턴 (간략화) */

/* 1. io_uring 인스턴스 생성 */
struct io_uring ring;
struct io_uring_params params = {
    .flags = IORING_SETUP_SQPOLL |     /* SQ 폴링 모드 */
             IORING_SETUP_SINGLE_ISSUER,  /* 단일 스레드 최적화 */
    .sq_thread_idle = 2000,            /* 2초 유휴 후 커널 스레드 슬립 */
};
io_uring_queue_init_params(4096, &ring, &params);

/* 2. 파일 디스크립터 사전 등록 (syscall 최적화) */
int fds[1024];
io_uring_register_files(&ring, fds, 1024);

/* 3. Multishot accept 제출 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0);
sqe->flags |= IOSQE_FIXED_FILE;

/* 4. Multishot recv (제공 버퍼 그룹 사용) */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sqe, client_fd, NULL, 0, 0);
sqe->flags |= IOSQE_BUFFER_SELECT;
sqe->buf_group = 0;  /* 미리 등록한 버퍼 그룹 ID */

/* 5. 이벤트 루프 */
while (1) {
    io_uring_submit_and_wait(&ring, 1);
    struct io_uring_cqe *cqe;
    unsigned head;
    io_uring_for_each_cqe(&ring, head, cqe) {
        if (cqe->res < 0) { /* 에러 처리 */ }
        /* user_data로 요청 구분, res로 결과 확인 */
    }
    io_uring_cq_advance(&ring, count);
}
io_uring vs epoll 성능 비교:
  • 시스템 콜 수: epoll은 이벤트당 1회 이상, io_uring SQPOLL은 0회
  • 배치 처리: io_uring은 submit/complete를 배치로 처리
  • zero-copy TX: io_uring SEND_ZC는 데이터 복사 없이 NIC에 직접 전달
  • 멀티샷: accept/recv를 한 번 제출하면 여러 이벤트를 자동 수신
  • 실측 결과: HTTP 서버 벤치마크에서 epoll 대비 10~40% QPS 향상 (워크로드 의존)

eBPF 소켓 프로그래밍

eBPF는 소켓 계층에서 다양한 프로그래밍 모델을 제공합니다. sockmap/sockhash를 통한 소켓 간 직접 리다이렉트, cgroup BPF를 통한 소켓 정책 적용, sk_msg를 통한 메시지 레벨 처리 등을 지원합니다.

eBPF sockmap: 소켓 간 직접 리다이렉트 경로 일반 경로 (sockmap 미사용) Socket A TCP/IP 스택 유저 공간 TCP/IP 스택 Socket B 6단계: 2x 커널-유저 복사 + 2x TCP/IP 처리 sockmap 경로 (eBPF 리다이렉트) Socket A sk_msg BPF 프로그램 bpf_msg_redirect_hash() Socket B 2단계: 커널 내부에서 직접 리다이렉트 (유저 복사 없음) sockmap fd -> sock

sockmap/sockhash 구현

sockmap은 BPF 맵의 일종으로, 소켓 파일 디스크립터(File Descriptor)를 키로 소켓 구조체를 저장합니다. sk_msg 프로그램이 이 맵을 참조하여 패킷을 다른 소켓으로 직접 리다이렉트합니다.

/* eBPF sockmap 프록시 예시 (BPF 프로그램) */

/* sockmap: 소켓 fd → sock 구조체 매핑 */
struct {
    __uint(type, BPF_MAP_TYPE_SOCKHASH);
    __uint(max_entries, 65535);
    __type(key, struct sock_key);  /* 5-tuple 키 */
    __type(value, int);
} sock_hash SEC(".maps");

/* sk_msg 프로그램: 소켓 메시지 리다이렉트 */
SEC("sk_msg")
int bpf_redir_proxy(struct sk_msg_md *msg)
{
    struct sock_key key = {};

    /* 대상 소켓 키 구성 (src/dst 교환) */
    key.sip4 = msg->remote_ip4;
    key.dip4 = msg->local_ip4;
    key.sport = msg->remote_port;
    key.dport = bpf_htonl(msg->local_port);
    key.family = msg->family;

    /* sockhash에서 대상 소켓 찾아서 리다이렉트 */
    return bpf_msg_redirect_hash(msg, &sock_hash, &key,
                                  BPF_F_INGRESS);
}

/* sk_skb 프로그램: stream parser + verdict */
SEC("sk_skb/stream_verdict")
int bpf_verdict(struct __sk_buff *skb)
{
    /* L7 프로토콜 파싱 후 라우팅 결정 */
    return bpf_sk_redirect_hash(skb, &sock_hash, &key,
                                 BPF_F_INGRESS);
}
BPF 프로그램 타입부착점용도핵심 헬퍼
BPF_PROG_TYPE_SK_MSG sendmsg/sendpage 경로 메시지 레벨 리다이렉트 bpf_msg_redirect_hash()
BPF_PROG_TYPE_SK_SKB 소켓 수신 경로 스트림 파싱 + verdict bpf_sk_redirect_hash()
BPF_PROG_TYPE_SOCK_OPS 소켓 이벤트 콜백 sockmap 항목 관리 bpf_sock_hash_update()
BPF_PROG_TYPE_CGROUP_SOCK 소켓 생성/바인드 소켓 정책 (허용/거부) bpf_setsockopt()
BPF_PROG_TYPE_CGROUP_SKB cgroup ingress/egress 패킷 필터링 bpf_skb_cgroup_id()
sockmap 활용 사례:
  • Cilium — Kubernetes 서비스 메시에서 사이드카 프록시 우회 (소켓 레벨 리다이렉트)
  • Katran — Facebook L4 로드 밸런서의 소켓 리다이렉트
  • Envoy + sockmap — L7 프록시의 localhost 트래픽 가속
  • TLS 종단 — kTLS + sockmap으로 프록시 없이 TLS 오프로드

Generic Netlink(genl)는 Netlink 프로토콜 패밀리 번호의 부족 문제를 해결하기 위해 도입된 프레임워크입니다. 단일 Netlink 패밀리(NETLINK_GENERIC) 위에 다중 서브 패밀리를 동적으로 등록할 수 있어, 새로운 커널-유저 통신 인터페이스를 쉽게 추가할 수 있습니다.

/* include/net/genetlink.h — Generic Netlink 핵심 구조체 */

/* 패밀리 정의 */
struct genl_family {
    unsigned int            id;         /* 자동 할당 */
    unsigned int            hdrsize;    /* 사용자 헤더 크기 */
    char                    name[GENL_NAMSIZ]; /* 패밀리 이름 */
    unsigned int            version;
    unsigned int            maxattr;    /* 최대 속성 수 */
    const struct nla_policy  *policy;    /* 속성 유효성 정책 */
    const struct genl_ops   *ops;       /* 오퍼레이션 배열 */
    int                     n_ops;
    const struct genl_multicast_group *mcgrps;
    int                     n_mcgrps;
    struct module           *module;
};

/* 오퍼레이션 정의 */
struct genl_ops {
    int                    (*doit)(struct sk_buff *skb,
                                    struct genl_info *info);
    int                    (*dumpit)(struct sk_buff *skb,
                                      struct netlink_callback *cb);
    u8                     cmd;        /* 명령 ID */
    u8                     flags;      /* GENL_ADMIN_PERM 등 */
    const struct nla_policy *policy;
};

/* 패밀리 등록 예시 (커널 모듈) */
static const struct nla_policy my_policy[MY_ATTR_MAX + 1] = {
    [MY_ATTR_NAME]  = { .type = NLA_NUL_STRING, .len = 64 },
    [MY_ATTR_VALUE] = { .type = NLA_U32 },
    [MY_ATTR_DATA]  = { .type = NLA_BINARY, .len = 4096 },
};

static const struct genl_ops my_ops[] = {
    {
        .cmd    = MY_CMD_GET,
        .doit   = my_cmd_get,
        .dumpit = my_cmd_dump,
        .flags  = GENL_CMD_CAP_DO | GENL_CMD_CAP_DUMP,
        .policy = my_policy,
    },
    {
        .cmd    = MY_CMD_SET,
        .doit   = my_cmd_set,
        .flags  = GENL_ADMIN_PERM,
        .policy = my_policy,
    },
};

static struct genl_family my_family = {
    .name    = "MY_GENL",
    .version = 1,
    .maxattr = MY_ATTR_MAX,
    .ops     = my_ops,
    .n_ops   = ARRAY_SIZE(my_ops),
    .module  = THIS_MODULE,
};

/* 모듈 초기화 */
static int __init my_init(void)
{
    return genl_register_family(&my_family);
}

/* 패밀리 해제 */
static void __exit my_exit(void)
{
    genl_unregister_family(&my_family);
}
Generic Netlink 사용 사례:
  • nl80211 — Wi-Fi 설정 (cfg80211/mac80211)
  • devlink — NIC 디바이스 관리
  • OVS (Open vSwitch) — 데이터패스 관리
  • TaskStats — 프로세스별 리소스 통계
  • wireguard — VPN 터널 설정
  • ethtool — 커널 6.0+ netlink 기반 인터페이스

Netlink는 커널-유저 공간 간 IPC의 핵심 메커니즘으로, 네트워크 설정의 거의 모든 영역에서 사용됩니다. rtnetlink(NETLINK_ROUTE)는 라우팅, 인터페이스, 주소 관리를 담당하는 가장 중요한 Netlink 패밀리입니다.

/* include/linux/netlink.h — Netlink 메시지 구조 */

/*
 * Netlink 메시지 레이아웃:
 * ┌─────────────────────────────────────┐
 * │  nlmsghdr (16 bytes)                │
 * │  ┌─ nlmsg_len   (전체 메시지 길이)  │
 * │  ├─ nlmsg_type  (메시지 타입)       │
 * │  ├─ nlmsg_flags (NLM_F_REQUEST 등)  │
 * │  ├─ nlmsg_seq   (시퀀스 번호)       │
 * │  └─ nlmsg_pid   (발신자 PID)        │
 * ├─────────────────────────────────────┤
 * │  프로토콜별 헤더 (예: ifinfomsg)    │
 * ├─────────────────────────────────────┤
 * │  Netlink Attribute (NLA) 배열       │
 * │  ┌─ nla_len + nla_type + payload    │
 * │  ├─ nla_len + nla_type + payload    │
 * │  └─ ...                             │
 * └─────────────────────────────────────┘
 */

struct nlmsghdr {
    __u32 nlmsg_len;    /* 헤더 포함 전체 길이 */
    __u16 nlmsg_type;   /* 메시지 타입 (RTM_NEWLINK 등) */
    __u16 nlmsg_flags;  /* NLM_F_REQUEST, NLM_F_DUMP 등 */
    __u32 nlmsg_seq;    /* 요청 시퀀스 번호 */
    __u32 nlmsg_pid;    /* 발신 프로세스 포트 ID */
};

/* rtnetlink 주요 메시지 타입 */
/* RTM_NEWLINK / RTM_DELLINK / RTM_GETLINK  — 인터페이스 */
/* RTM_NEWADDR / RTM_DELADDR / RTM_GETADDR  — IP 주소 */
/* RTM_NEWROUTE / RTM_DELROUTE / RTM_GETROUTE — 라우팅 */
/* RTM_NEWNEIGH / RTM_DELNEIGH / RTM_GETNEIGH — ARP/NDP */
/* RTM_NEWRULE / RTM_DELRULE                — 정책 라우팅 */
/* RTM_NEWQDISC / RTM_DELQDISC               — TC qdisc */
Netlink 패밀리상수용도주요 사용자
NETLINK_ROUTE 0 라우팅, 인터페이스, 주소, TC ip, bridge, tc, ss
NETLINK_NETFILTER 12 conntrack, nftables, 로그 conntrack, nft
NETLINK_GENERIC 16 범용 (genl 프레임워크) nl80211, devlink, OVS
NETLINK_XFRM 6 IPsec SA/SP 관리 ip xfrm, strongSwan
NETLINK_AUDIT 9 감사 로그 auditd
NETLINK_KOBJECT_UEVENT 15 디바이스 이벤트 udevd
# Netlink 소켓 모니터링

# rtnetlink 이벤트 모니터링 (인터페이스/주소/라우팅 변경)
ip monitor all

# 특정 이벤트만 모니터링
ip monitor link    # 인터페이스 변경
ip monitor address # IP 주소 변경
ip monitor route   # 라우팅 변경
ip monitor neigh   # ARP/NDP 변경

# Netlink 소켓 통계
cat /proc/net/netlink
# sk   Eth Pid   Groups  Rmem  Wmem  Dump  Locks  Drops

# Netlink 메시지 디버깅 (strace)
strace -e trace=sendmsg,recvmsg ip link show

NAPI Threaded 모드와 PREEMPT_RT

커널 5.12+에서 도입된 Threaded NAPI는 기존 softirq 기반 NAPI를 커널 스레드(Kernel Thread)에서 실행하는 모드입니다. PREEMPT_RT 환경에서 네트워크 처리의 결정론적 지연 보장이 핵심 목표입니다.

NAPI 전통 방식 vs Threaded NAPI 비교 전통 NAPI (softirq) Hard IRQ napi_schedule() NET_RX_SOFTIRQ ksoftirqd 또는 IRQ 복귀 시 net_rx_action() -> poll() 문제: softirq는 선점 불가 -> RT 태스크 지연 발생 -> 우선순위 역전 가능 Threaded NAPI (5.12+) Hard IRQ napi_schedule() napi/<ifname>-<queue> 전용 커널 스레드 (SCHED_OTHER) napi_threaded_poll() 장점: 선점 가능 (RT 호환) -> chrt로 우선순위 조절 -> CPU affinity 세밀 제어
/* net/core/dev.c — Threaded NAPI 구현 */

/* Threaded NAPI 활성화 */
/*
 * # 전체 디바이스에 대해 threaded NAPI 활성화
 * echo 1 > /sys/class/net/eth0/threaded
 *
 * → 각 NAPI 인스턴스마다 "napi/eth0-" 커널 스레드 생성
 * → softirq 대신 해당 스레드에서 poll 함수 실행
 */

static int napi_threaded_poll(void *data)
{
    struct napi_struct *napi = data;
    void (*gro_flush_timeout_fn)(struct napi_struct *);

    while (!napi_thread_wait(napi)) {
        unsigned long last_busy = jiffies;
        int work;

        for (;;) {
            work = __napi_poll(napi, &repoll);
            if (!repoll)
                break;
            /* 선점 가능: cond_resched()로 다른 태스크에 양보 */
            if (need_resched() || signal_pending(current))
                cond_resched();
        }
    }
    return 0;
}

/* PREEMPT_RT 환경에서의 NAPI 스레드 우선순위 설정 */
/*
 * # FIFO 정책, 우선순위 50으로 설정
 * chrt -f -p 50 $(pgrep -f "napi/eth0-0")
 *
 * # CPU affinity 설정 (NUMA 노드 고려)
 * taskset -pc 0-3 $(pgrep -f "napi/eth0-0")
 *
 * # 모든 NAPI 스레드 확인
 * ps aux | grep "napi/"
 */
특성전통 NAPI (softirq)Threaded NAPI
실행 컨텍스트 softirq (IRQ 복귀 시 또는 ksoftirqd) 전용 커널 스레드
선점(Preemption) 가능 여부 불가 (softirq는 선점 안 됨) 가능 (일반 스레드(Thread))
우선순위 제어 불가 chrt로 RT 우선순위 설정 가능
CPU affinity IRQ affinity에 종속 독립적 affinity 설정 가능
GRO 지원 지원 지원
busy polling 지원 지원
처리량 (벌크) 약간 우위 (softirq 오버헤드 적음) 스레드 스케줄링 약간의 오버헤드
지연 (RT) 비결정론적 결정론적 (우선순위 기반)
활성화 기본 /sys/class/net/<dev>/threaded = 1
PREEMPT_RT 네트워크 튜닝:
  • Threaded NAPI 활성화 후 NAPI 스레드 우선순위를 RT 태스크(Task)보다 낮게 설정
  • IRQ 스레드(irq/<N>-<name>) 우선순위를 NAPI 스레드보다 높게 설정
  • net.core.busy_poll은 RT 환경에서 비활성화 권장 (폴링이 RT 태스크를 방해)
  • TSN 환경에서는 taprio + threaded NAPI 조합으로 시간 결정론적 네트워킹 구성

네트워크 스택 메모리 관리(Memory Management)

네트워크 스택은 초당 수백만 개의 패킷을 처리해야 하므로 메모리 할당/해제 성능이 매우 중요합니다. 커널은 전용 할당자, 사전 할당 풀, zero-copy 메커니즘 등 다양한 최적화를 적용합니다.

네트워크 메모리 할당 계층: page_pool, NAPI alloc, slab NIC 드라이버 (RX 경로) DMA 버퍼 할당 -> 패킷 수신 -> skb 구성 napi_alloc_skb() / build_skb() / page_pool_alloc() page_pool (5.x+) 페이지 재활용 (DMA unmap 절약) per-CPU 캐시 + 벌크 할당 NAPI alloc napi_alloc_skb napi_alloc_frag kmem_cache (slab) skbuff_head_cache skb_small_head_cache 페이지 할당자 (Buddy Allocator) — alloc_pages() GFP_ATOMIC (인터럽트 컨텍스트) / GFP_KERNEL (프로세스 컨텍스트) 소켓 메모리 제어: sk->sk_wmem_alloc, sk->sk_rmem_alloc, net.core.rmem_max, net.core.wmem_max

page_pool

page_pool은 네트워크 드라이버의 RX 버퍼 할당을 최적화하는 전용 할당자입니다. 페이지(Page)를 재활용(Recycling)하여 DMA map/unmap 오버헤드를 제거하고, per-CPU 캐시로 할당 속도를 극대화합니다.

/* include/net/page_pool/types.h — page_pool 핵심 구조 */

struct page_pool {
    struct page_pool_params p;
    struct pp_alloc_cache alloc;    /* per-CPU 캐시 */
    unsigned long          pages_state_hold_cnt;
    struct ptr_ring       ring;     /* 반환 링 버퍼 */
    atomic_t              pages_state_release_cnt;
    /* DMA 매핑 정보 (사전 매핑으로 반복 매핑 회피) */
    dma_addr_t           (*dma_map)(struct page_pool *, struct page *);
};

/* page_pool 할당 사이클:
 * 1. page_pool_alloc_pages()
 *    ├→ alloc.cache[]에서 꺼내기 (fastest, per-CPU)
 *    ├→ ring에서 꺼내기 (fast, 다른 CPU에서 반환된 페이지)
 *    └→ alloc_pages(GFP_ATOMIC)로 신규 할당 (slow)
 *
 * 2. 드라이버가 skb에 페이지 연결 (skb_add_rx_frag)
 *
 * 3. 패킷 처리 완료 → skb_free/consume
 *    └→ page_pool_return_page()
 *        ├→ 동일 CPU: alloc.cache[]에 직접 반환
 *        └→ 다른 CPU: ring에 반환
 *
 * 장점:
 * - DMA 매핑 유지: 반환된 페이지는 이미 DMA 매핑 → 재사용 시 dma_map 불필요
 * - 캐시 핫: per-CPU 캐시 → L1/L2 캐시 히트율 극대화
 * - 벌크 할당: page_pool_alloc_pages_bulk()로 한 번에 여러 페이지
 */

/* 드라이버에서 page_pool 사용 패턴 */
struct page_pool_params pp_params = {
    .flags   = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
    .order   = 0,           /* 4KB 페이지 */
    .pool_size = 256,       /* 링 크기 */
    .nid     = dev_to_node(&pdev->dev),
    .dev     = &pdev->dev,
    .dma_dir = DMA_FROM_DEVICE,
    .max_len = PAGE_SIZE,
};
pool = page_pool_create(&pp_params);

소켓 메모리 제어

sysctl 매개변수기본값설명튜닝 가이드
net.core.rmem_max 212992 소켓 수신 버퍼 최대 크기 고대역폭: 16MB+ (16777216)
net.core.wmem_max 212992 소켓 송신 버퍼 최대 크기 고대역폭: 16MB+ (16777216)
net.ipv4.tcp_rmem 4096 131072 6291456 TCP 수신 버퍼 (min/default/max) max를 BDP 이상으로 설정
net.ipv4.tcp_wmem 4096 16384 4194304 TCP 송신 버퍼 (min/default/max) max를 BDP 이상으로 설정
net.ipv4.tcp_mem 시스템 RAM 기반 자동 TCP 전체 메모리 한도 (페이지 단위) 메모리 풍부 시 상향
net.core.netdev_budget 300 softirq 사이클당 최대 패킷 수 10G+: 600~1200
net.core.optmem_max 20480 소켓 옵션 메모리 최대 cmsg 사용 시 증가
# BDP(Bandwidth-Delay Product) 계산과 버퍼 튜닝

# BDP = 대역폭(bps) x RTT(초)
# 예: 10Gbps, RTT 1ms → BDP = 10e9 * 0.001 / 8 = 1,250,000 bytes

# TCP 버퍼 설정 (BDP 기반)
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216

# TCP 메모리 압박 모니터링
cat /proc/net/sockstat
# TCP: inuse 1234 orphan 0 tw 567 alloc 1500 mem 890
#                                           ^^^ 페이지 단위

# 소켓별 메모리 사용량 확인
ss -tmpie | grep -A2 "ESTAB"
# → skmem:(r:0,rb:131072,t:0,tb:46080,f:0,w:0,o:0,bl:0,d:0)
#   r=읽기대기, rb=수신버퍼, t=전송대기, tb=송신버퍼

멀티큐 NIC와 IRQ affinity 실전

최신 NIC는 수십~수백 개의 하드웨어 큐를 지원하며, 각 큐는 독립적인 MSI-X 인터럽트 벡터를 가집니다. IRQ affinity를 올바르게 설정하면 CPU 간 캐시 바운싱을 줄이고 NUMA 지역성을 최대화할 수 있습니다.

멀티큐 NIC IRQ affinity: NUMA 인식 큐-CPU 매핑 NIC 8 RX/TX 큐 8 MSI-X 벡터 NUMA 0 연결 NUMA Node 0 (NIC 연결) CPU 0 CPU 1 CPU 2 ... IRQ 128 -> CPU 0 (큐 0) IRQ 129 -> CPU 1 (큐 1) IRQ 130 -> CPU 2 (큐 2) IRQ 131 -> CPU 3 (큐 3) NUMA Node 1 (원격) CPU 4 CPU 5 CPU 6 ... NUMA 원격 접근 시 추가 지연 IRQ를 여기에 매핑하면 성능 저하 RPS로 원격 노드 CPU 활용 가능 (L3 캐시 미스 감안) IRQ affinity 설정 체크리스트 1. NIC의 NUMA 노드 확인: cat /sys/class/net/eth0/device/numa_node 2. 해당 노드의 CPU에만 IRQ affinity 설정 3. irqbalance 데몬: NUMA 인식 자동 분배 (기본 권장) 4. 큐 수 = NIC NUMA 노드의 CPU 수 이하로 설정 5. XPS: TX 큐도 동일 NUMA 노드 CPU에 매핑

IRQ affinity 설정 실전

# 1. NIC의 NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# 0

# 2. 해당 NUMA 노드의 CPU 목록
cat /sys/devices/system/node/node0/cpulist
# 0-7 (8코어)

# 3. NIC 큐 수를 NUMA 노드 CPU 수에 맞춤
ethtool -L eth0 combined 8

# 4. 각 큐의 IRQ를 해당 CPU에 고정
# IRQ 번호 확인
grep eth0 /proc/interrupts
# 128:  eth0-TxRx-0
# 129:  eth0-TxRx-1
# ...

# CPU affinity 설정 (비트마스크)
echo 1 > /proc/irq/128/smp_affinity     # CPU 0
echo 2 > /proc/irq/129/smp_affinity     # CPU 1
echo 4 > /proc/irq/130/smp_affinity     # CPU 2
echo 8 > /proc/irq/131/smp_affinity     # CPU 3

# 또는 CPU 리스트로 설정 (더 직관적)
echo 0 > /proc/irq/128/smp_affinity_list  # CPU 0
echo 1 > /proc/irq/129/smp_affinity_list  # CPU 1

# 5. XPS 설정 (TX 큐 → CPU 매핑)
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus  # CPU 0
echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus  # CPU 1

# 6. irqbalance 제외 (수동 관리 시)
# /etc/sysconfig/irqbalance에 추가:
# IRQBALANCE_BANNED_INTERRUPTS="128 129 130 131 132 133 134 135"

# 7. 적응형 인터럽트 코얼레싱
ethtool -C eth0 adaptive-rx on adaptive-tx on
# 또는 고정값 설정
ethtool -C eth0 rx-usecs 50 rx-frames 64

멀티큐 모니터링

# 큐별 패킷 분포 확인
ethtool -S eth0 | grep rx_queue
# rx_queue_0_packets: 1234567
# rx_queue_1_packets: 1234890
# rx_queue_2_packets: 1234123
# ...

# 큐별 불균형 검출 (최대/최소 비율)
ethtool -S eth0 | grep rx_queue_.*_packets | \
    awk -F: '{print $2}' | sort -n | \
    awk 'NR==1{min=$1} END{print "max/min ratio:", $1/min}'

# IRQ별 CPU 분포 확인
watch -n1 "cat /proc/interrupts | grep eth0"

# softnet_stat: CPU별 네트워크 처리 통계
cat /proc/net/softnet_stat
# 열: processed, dropped, time_squeeze, ?, ?, ?, ?, ?, ?, flow_limit, backlog_len, index
# time_squeeze > 0: netdev_budget 부족 → 증가 고려

# NAPI 인스턴스 확인 (커널 6.1+)
cat /sys/class/net/eth0/napi_list

# RX ring 크기 조정
ethtool -g eth0           # 현재 링 크기
ethtool -G eth0 rx 4096  # RX 링 크기 증가
멀티큐 NIC 트러블슈팅 체크리스트:
  • 큐 불균형 — RSS 해시 키 변경 또는 대칭 키 사용
  • softnet_stat time_squeezenetdev_budget 증가
  • softnet_stat droppednetdev_max_backlog 증가 또는 RPS 비활성화
  • RX ring overflowethtool -G으로 링 크기 증가
  • NUMA 교차 접근 — IRQ affinity를 NIC의 NUMA 노드로 제한
  • CPU 핫스팟 — 특정 CPU에 인터럽트 집중 시 큐 수 증가 또는 RFS 활성화
  • 하이퍼스레딩 — 물리 코어당 하나의 큐 매핑 권장 (HT 쌍은 L1 공유로 경합)

네트워크 스택과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.