AF_XDP (XDP Sockets)

AF_XDP는 XDP와 결합하여 커널 우회(kernel bypass) 없이 userspace로 초고속 패킷 전달을 제공하는 소켓 패밀리입니다. Zero-copy 모드에서 수백만 pps의 처리 성능을 달성합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택네트워크 디바이스 드라이버 문서를 먼저 읽으세요. 고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다.

핵심 요약

  • UMEM — 사용자 공간 공유 패킷 메모리
  • RX/TX/FILL/COMP Ring — lockless 큐 기반 데이터 흐름
  • Zero-copy — 드라이버 지원 시 최고 성능 경로
  • XDP redirect — 패킷 선별/전달 엔트리 포인트
  • 큐 핀ning — CPU/NIC queue affinity가 핵심 튜닝 요소

단계별 이해

  1. UMEM 준비
    frame size와 ring depth를 워크로드 특성에 맞춰 결정합니다.
  2. XDP 연결
    프로그램에서 대상 패킷을 AF_XDP 소켓으로 redirect합니다.
  3. 모드 선택
    copy/zero-copy 성능 차이와 드라이버 제약을 검증합니다.
  4. 운영 튜닝
    IRQ affinity, NAPI budget, batching 전략을 조정합니다.
관련 문서: BPF/XDP (XDP 프로그램), 네트워크 스택 (패킷 처리), DPDK/SmartNIC (성능 비교), sk_buff (패킷 버퍼)

개요

AF_XDP는 XDP 프로그램과 협력하여 선택된 패킷을 userspace 애플리케이션으로 직접 전달합니다. DPDK처럼 커널을 완전히 우회하지 않고도 높은 성능을 얻을 수 있습니다.

NIC RX Queue XDP_REDIRECT AF_XDP Socket RX/FILL Ring Userspace Poll Loop UMEM 소비 TX/COMPLETION Ring 재전송/회수

AF_XDP 패킷 생애주기

AF_XDP 패킷 생애주기 (RX → 처리 → TX) NIC RX Wire → DMA → UMEM frame XDP 프로그램 필터/분류 XDP_REDIRECT RX Ring 커널 → User desc 게시 Userspace 처리 peek → 파싱 → 비즈니스 로직 → 응답 생성 TX Ring User → 커널 sendto() kick NIC TX DMA 전송 → Wire Completion Ring → Fill Ring (프레임 재순환) 전체 RTT (RX → 처리 → TX): Zero-copy ~2-5μs | Copy ~20-50μs 일반 소켓 경로 대비 10~100배 낮은 레이턴시

주요 특징

아키텍처

Userspace Application UMEM (User Memory) Pkt0 | Pkt1 | Pkt2 | ... | PktN Kernel/User Boundary AF_XDP Socket RX Ring (Consumer) TX Ring (Producer) XDP Program: if (selected) return XDP_REDIRECT; NIC Driver (Zero-Copy Mode) - RX Queue 0 | RX Queue 1 | RX Queue 2 Physical NIC

UMEM (User Memory)

UMEM은 패킷 데이터를 저장하는 userspace 메모리 영역입니다. 커널과 userspace가 공유합니다.

UMEM 구조

#include <linux/if_xdp.h>
#include <bpf/xsk.h>

struct xsk_umem_config {
    __u32 fill_size;         /* Fill ring 크기 */
    __u32 comp_size;         /* Completion ring 크기 */
    __u32 frame_size;        /* 각 프레임 크기 (2048 or 4096) */
    __u32 frame_headroom;    /* 프레임 헤드룸 (256) */
    __u32 flags;             /* XDP_UMEM_UNALIGNED_CHUNK_FLAG */
};

/* UMEM 할당 및 등록 */
void *umem_area;
size_t umem_size = NUM_FRAMES * FRAME_SIZE;

/* Hugepage 사용 권장 (성능 향상) */
umem_area = mmap(NULL, umem_size,
                 PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                 -1, 0);

struct xsk_umem_config umem_config = {
    .fill_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
    .comp_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
    .frame_size = FRAME_SIZE,
    .frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
    .flags = 0,
};

struct xsk_umem *umem;
int ret = xsk_umem__create(&umem, umem_area, umem_size,
                          &fill_ring, &comp_ring, &umem_config);

UMEM 프레임 레이아웃

UMEM 프레임 구조 (예: 2048 바이트) 0 256 2048 (byte) Headroom 256 bytes Packet Data 최대 1792 bytes — 실제 패킷 Unused (여유) XDP 메타데이터/헤더 추가용 rx_batch로 채움, sendmsg()로 전송 Total: 2048 bytes (0x800) — chunk_size 정렬 단위 UMEM은 이 프레임이 연속적으로 배열된 공유 메모리 영역

AF_XDP Rings

AF_XDP는 4개의 링(Ring)을 사용하여 패킷을 주고받습니다.

Ring 종류

Ring 방향 역할
RX Ring Kernel → User 수신 패킷 전달 (CONSUMER)
TX Ring User → Kernel 송신 패킷 제출 (PRODUCER)
Fill Ring User → Kernel 빈 프레임 제공 (RX용)
Completion Ring Kernel → User 송신 완료 프레임 반환

Ring 동작 흐름

/* RX 경로 */
1. User: Fill Ring에 빈 프레임 주소 추가
2. Kernel: 패킷 수신 시 Fill Ring에서 프레임 가져옴
3. Kernel: 패킷 복사 후 RX Ring에 프레임 주소 추가
4. User: RX Ring에서 패킷 처리

/* TX 경로 */
1. User: TX Ring에 패킷 프레임 주소 추가
2. Kernel: TX Ring에서 프레임 가져와 송신
3. Kernel: 송신 완료 후 Completion Ring에 프레임 주소 추가
4. User: Completion Ring에서 프레임 재사용

수신(RX) 데이터 흐름 상세

AF_XDP 수신(RX) 데이터 흐름 User / Kernel Boundary 1. Fill Queue 채우기 User: UMEM 빈 프레임 주소 제출 2. Fill → DMA Kernel: Fill에서 프레임 가져옴 3. NIC DMA 수신 패킷 → UMEM 프레임에 기록 4. RX Queue 게시 Kernel: RX Ring에 desc 추가 5. RX 패킷 소비 User: peek → 패킷 처리 프레임 재순환 User(Fill 채움) → Kernel(DMA 수신) → Kernel(RX 게시) → User(패킷 소비) → 프레임 재사용 순환

송신(TX) 데이터 흐름 상세

AF_XDP 송신(TX) 데이터 흐름 User / Kernel Boundary 1. TX Queue 채우기 User: 패킷 작성 + desc 제출 2. sendto() / kick User: 커널에 송신 알림 3. TX Ring 소비 Kernel: TX desc 가져옴 4. NIC DMA 송신 UMEM 프레임 → NIC 전송 5. Completion Queue Kernel: 완료 프레임 주소 반환 6. UMEM 프레임 회수 User: Comp peek → 재사용 프레임 재순환 User(TX 채움 + sendto) → Kernel(TX 소비 → DMA 전송) → Kernel(Completion) → User(프레임 회수) 순환

AF_XDP 소켓 생성

xsk_socket 생성 (libbpf)

#include <bpf/xsk.h>

struct xsk_socket_config {
    __u32 rx_size;           /* RX ring 크기 */
    __u32 tx_size;           /* TX ring 크기 */
    __u32 libbpf_flags;      /* XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD */
    __u32 xdp_flags;         /* XDP_FLAGS_* */
    __u16 bind_flags;        /* XDP_COPY, XDP_ZEROCOPY, XDP_USE_NEED_WAKEUP */
};

/* AF_XDP 소켓 생성 */
struct xsk_socket *xsk;
struct xsk_socket_config cfg = {
    .rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
    .tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
    .libbpf_flags = 0,
    .xdp_flags = XDP_FLAGS_UPDATE_IF_NOEXIST,
    .bind_flags = XDP_ZEROCOPY,  /* or XDP_COPY */
};

int ret = xsk_socket__create(&xsk, ifname, queue_id, umem,
                              &rx_ring, &tx_ring, &cfg);
if (ret) {
    fprintf(stderr, "Failed to create XSK socket: %d\\n", ret);
    return ret;
}

/* 소켓 파일 디스크립터 획득 */
int xsk_fd = xsk_socket__fd(xsk);

Bind Flags

플래그 설명
XDP_COPY Copy 모드 (모든 드라이버 지원, 낮은 성능)
XDP_ZEROCOPY Zero-copy 모드 (드라이버 지원 필요, 고성능)
XDP_USE_NEED_WAKEUP Wakeup 플래그 사용 (CPU 사용률 감소)
XDP_SHARED_UMEM 여러 소켓이 동일 UMEM 공유

패킷 수신

RX 루프

/* Fill Ring에 빈 프레임 추가 */
void xsk_populate_fill_ring(struct xsk_ring_prod *fill, __u64 *frame_addr)
{
    __u32 idx;
    if (xsk_ring_prod__reserve(fill, BATCH_SIZE, &idx) == BATCH_SIZE) {
        for (int i = 0; i < BATCH_SIZE; i++) {
            *xsk_ring_prod__fill_addr(fill, idx++) = frame_addr[i];
        }
        xsk_ring_prod__submit(fill, BATCH_SIZE);
    }
}

/* RX Ring에서 패킷 수신 */
void xsk_receive_packets(struct xsk_socket *xsk, struct xsk_ring_cons *rx)
{
    __u32 idx_rx = 0;
    unsigned int rcvd = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx_rx);

    for (unsigned int i = 0; i < rcvd; i++) {
        const struct xdp_desc *desc = xsk_ring_cons__rx_desc(rx, idx_rx++);

        __u64 addr = desc->addr;
        __u32 len = desc->len;
        void *pkt = xsk_umem__get_data(umem_area, addr);

        /* 패킷 처리 */
        process_packet(pkt, len);

        /* 프레임을 Fill Ring에 반환 (재사용) */
        xsk_populate_fill_ring(&fill_ring, &addr);
    }

    xsk_ring_cons__release(rx, rcvd);
}

poll()과 통합

#include <poll.h>

/* AF_XDP 소켓은 poll() 가능 */
struct pollfd fds = {
    .fd = xsk_socket__fd(xsk),
    .events = POLLIN,
};

while (1) {
    int ret = poll(&fds, 1, -1);
    if (ret > 0 && fds.revents & POLLIN) {
        xsk_receive_packets(xsk, &rx_ring);
    }
}

패킷 송신

TX 루프

/* 패킷 송신 */
void xsk_send_packet(struct xsk_socket *xsk, struct xsk_ring_prod *tx,
                      void *pkt_data, size_t pkt_len, __u64 frame_addr)
{
    __u32 idx;
    if (xsk_ring_prod__reserve(tx, 1, &idx) == 1) {
        struct xdp_desc *desc = xsk_ring_prod__tx_desc(tx, idx);

        /* 패킷 데이터 복사 */
        void *frame = xsk_umem__get_data(umem_area, frame_addr);
        memcpy(frame, pkt_data, pkt_len);

        /* Descriptor 설정 */
        desc->addr = frame_addr;
        desc->len = pkt_len;

        xsk_ring_prod__submit(tx, 1);

        /* Kernel에 송신 시작 알림 (XDP_USE_NEED_WAKEUP 사용 시) */
        if (xsk_ring_prod__needs_wakeup(tx))
            sendto(xsk_socket__fd(xsk), NULL, 0, MSG_DONTWAIT, NULL, 0);
    }
}

/* Completion Ring에서 송신 완료 프레임 회수 */
void xsk_complete_tx(struct xsk_ring_cons *comp)
{
    __u32 idx;
    unsigned int completed = xsk_ring_cons__peek(comp, BATCH_SIZE, &idx);

    if (completed > 0) {
        xsk_ring_cons__release(comp, completed);
        /* 회수된 프레임 재사용 */
    }
}

XDP 프로그램 연동

XDP_REDIRECT to AF_XDP

/* XDP 프로그램: 특정 패킷을 AF_XDP 소켓으로 리다이렉트 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
    __uint(max_entries, 64);
} xsks_map SEC(".maps");

SEC("xdp")
int xdp_sock_prog(struct xdp_md *ctx)
{
    int index = ctx->rx_queue_index;

    /* 패킷 필터링 로직 */
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

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

    /* UDP 포트 12345 패킷만 AF_XDP로 */
    if (eth->h_proto == htons(ETH_P_IP)) {
        struct iphdr *ip = (struct iphdr *)(eth + 1);
        if ((void *)(ip + 1) > data_end)
            return XDP_PASS;

        if (ip->protocol == IPPROTO_UDP) {
            struct udphdr *udp = (struct udphdr *)(ip + 1);
            if ((void *)(udp + 1) > data_end)
                return XDP_PASS;

            if (ntohs(udp->dest) == 12345) {
                /* AF_XDP 소켓으로 리다이렉트 */
                return bpf_redirect_map(&xsks_map, index, 0);
            }
        }
    }

    /* 나머지 패킷은 일반 네트워크 스택으로 */
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

XSKMAP 업데이트

/* Userspace: XSKMAP에 AF_XDP 소켓 등록 */
int queue_id = 0;
int xsk_fd = xsk_socket__fd(xsk);
int map_fd;  /* BPF_MAP_TYPE_XSKMAP의 fd */

bpf_map_update_elem(map_fd, &queue_id, &xsk_fd, 0);

Zero-Copy vs Copy 모드

모드 비교

특성 Copy 모드 Zero-Copy 모드
메모리 복사 커널 버퍼 → UMEM 복사 복사 없음 (직접 UMEM 사용)
드라이버 지원 모든 드라이버 i40e, ice, ixgbe, mlx5, bnxt, igc, veth, virtio_net, stmmac 등
성능 ~수백만 pps ~천만 pps 이상
CPU 사용률 높음 낮음
레이턴시 ~수십 μs ~수 μs

Zero-Copy 요구사항

/* Zero-copy 지원 드라이버 확인 */
$ ethtool -i eth0 | grep driver
driver: i40e

/* Zero-copy 모드 시도 */
struct xsk_socket_config cfg = {
    .bind_flags = XDP_ZEROCOPY,
};

int ret = xsk_socket__create(&xsk, "eth0", 0, umem, &rx, &tx, &cfg);
if (ret == -EOPNOTSUPP) {
    printf("Zero-copy not supported, falling back to copy mode\\n");
    cfg.bind_flags = XDP_COPY;
    ret = xsk_socket__create(&xsk, "eth0", 0, umem, &rx, &tx, &cfg);
}

Shared UMEM

여러 AF_XDP 소켓이 동일한 UMEM을 공유하여 메모리 효율을 높입니다. XDP_SHARED_UMEM 플래그를 사용하면 N개 큐에 대해 하나의 UMEM만 할당하면 되므로, 멀티큐 환경에서 메모리 사용량이 1/N로 줄어듭니다.

Shared UMEM: 하나의 UMEM을 여러 소켓이 공유 공유 UMEM (Single Allocation) Frame 0 | Frame 1 | Frame 2 | ... | Frame N-1 Socket 0 (Queue 0) Fill Ring + Comp Ring Socket 1 (Queue 1) Fill Ring + Comp Ring Socket 2 (Queue 2) Fill Ring + Comp Ring Shared UMEM 핵심 규칙 1. 첫 번째 소켓: UMEM 생성 (XDP_SHARED_UMEM 없이) 2. 이후 소켓: XDP_SHARED_UMEM 플래그로 동일 UMEM에 바인딩 3. 각 소켓은 자신만의 Fill/Completion Ring을 가짐 (RX/TX Ring은 소켓별 독립)

Shared UMEM 코드

/* 첫 번째 소켓: UMEM 생성 (기본 모드) */
struct xsk_socket *xsk1;
struct xsk_socket_config cfg = {
    .rx_size = 4096,
    .tx_size = 4096,
    .bind_flags = XDP_ZEROCOPY,  /* Shared UMEM 없이 */
};
xsk_socket__create(&xsk1, "eth0", 0, umem, &rx1, &tx1, &cfg);

/* 두 번째 소켓: 동일한 UMEM 공유 */
struct xsk_socket *xsk2;
cfg.bind_flags = XDP_ZEROCOPY | XDP_SHARED_UMEM;
xsk_socket__create(&xsk2, "eth0", 1, umem, &rx2, &tx2, &cfg);

/* 세 번째 소켓: 역시 동일 UMEM 공유 */
struct xsk_socket *xsk3;
xsk_socket__create(&xsk3, "eth0", 2, umem, &rx3, &tx3, &cfg);

/* 주의: 프레임 주소 관리를 프로그래머가 직접 해야 함
 * 같은 프레임을 두 소켓에서 동시에 사용하면 데이터 오염! */
Shared UMEM 주의사항:
  • 프레임 소유권 관리가 프로그래머 책임 — 동일 프레임을 두 소켓이 동시에 사용하면 안 됨
  • Fill Ring에 반환하는 프레임 주소가 현재 다른 소켓에서 사용 중이 아닌지 확인 필요
  • XDP 프로그램에서 bpf_redirect_map의 대상이 올바른 소켓인지 queue_index로 결정

성능 최적화

Busy Polling

/* XDP_USE_NEED_WAKEUP 비활성화로 busy polling */
cfg.bind_flags = XDP_ZEROCOPY;  /* XDP_USE_NEED_WAKEUP 생략 */

while (1) {
    /* poll() 없이 바로 수신 (CPU 100% 사용) */
    xsk_receive_packets(xsk, &rx_ring);
    xsk_complete_tx(&comp_ring);
}

Batching

/* Batch 크기 조정으로 성능 향상 */
#define BATCH_SIZE 64  /* 32, 64, 128 등 */

__u32 idx;
unsigned int rcvd = xsk_ring_cons__peek(rx, BATCH_SIZE, &idx);
/* 한 번에 최대 64개 패킷 처리 */

CPU Affinity

/* 특정 CPU에 바인딩 */
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);  /* CPU 2 */
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

/* NIC IRQ도 동일 CPU로 설정 */
# echo 4 > /proc/irq/123/smp_affinity  # CPU 2 (bitmask 0x4)

Hugepage 메모리

# Hugepage 설정 (2MB 페이지 1024개 = 2GB)
$ sudo sysctl -w vm.nr_hugepages=1024
$ cat /proc/meminfo | grep Huge
HugePages_Total:    1024
HugePages_Free:     1024
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
/* Hugepage UMEM 할당 — TLB miss 감소로 성능 향상 */
void *umem_area = mmap(NULL, umem_size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                       -1, 0);
if (umem_area == MAP_FAILED) {
    /* Hugepage 불가 시 일반 페이지로 fallback */
    umem_area = mmap(NULL, umem_size,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
}

/* mlock으로 페이지 스왑 방지 (선택적이나 권장) */
mlock(umem_area, umem_size);
성능 차이: Hugepage 사용 시 TLB(Translation Lookaside Buffer) miss가 크게 줄어 10~20% 성능 향상이 가능합니다. 특히 UMEM 크기가 수십 MB 이상일 때 효과가 뚜렷합니다.

SO_PREFER_BUSY_POLL (커널 5.11+)

/* 소켓 레벨 busy polling — NAPI 재스케줄 없이 직접 polling */
int xsk_fd = xsk_socket__fd(xsk);

/* busy poll 활성화 */
int optval = 1;
setsockopt(xsk_fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &optval, sizeof(optval));

/* busy poll 예산 (패킷 수) */
optval = 64;
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL_BUDGET, &optval, sizeof(optval));

/* busy poll 타임아웃 (마이크로초) */
optval = 20;
setsockopt(xsk_fd, SOL_SOCKET, SO_BUSY_POLL, &optval, sizeof(optval));

sysctl 튜닝

# NAPI 관련 튜닝
$ sudo sysctl -w net.core.busy_poll=50         # busy poll 타임아웃 (μs)
$ sudo sysctl -w net.core.busy_read=50         # busy read 타임아웃 (μs)
$ sudo sysctl -w net.core.netdev_budget=300    # NAPI budget
$ sudo sysctl -w net.core.netdev_budget_usecs=2000

# 메모리 관련
$ sudo sysctl -w net.core.optmem_max=65536
$ sudo sysctl -w vm.nr_hugepages=1024

# CPU 전력 관리 비활성화 (최저 레이턴시)
# 부트 파라미터에 추가:
# intel_idle.max_cstate=1 processor.max_cstate=1 idle=poll

성능 벤치마크

xdpsock 샘플 프로그램

# 커널 샘플 프로그램 빌드
$ cd linux/samples/bpf
$ make xdpsock

# RX 벤치마크 (Zero-copy, queue 0)
$ sudo ./xdpsock -i eth0 -q 0 -z -r

# TX 벤치마크
$ sudo ./xdpsock -i eth0 -q 0 -z -t

# L2FWD (Layer 2 Forwarding)
$ sudo ./xdpsock -i eth0 -q 0 -z -l

# 결과 예시
RX:      10,234,567 pps         5,239 Mb/s
TX:       9,876,543 pps         5,059 Mb/s

디버깅

통계 확인

/* AF_XDP 통계 */
struct xdp_statistics stats;
socklen_t len = sizeof(stats);
getsockopt(xsk_socket__fd(xsk), SOL_XDP, XDP_STATISTICS, &stats, &len);

printf("rx_dropped: %llu\\n", stats.rx_dropped);
printf("rx_invalid_descs: %llu\\n", stats.rx_invalid_descs);
printf("tx_invalid_descs: %llu\\n", stats.tx_invalid_descs);
printf("rx_ring_full: %llu\\n", stats.rx_ring_full);
printf("rx_fill_ring_empty_descs: %llu\\n", stats.rx_fill_ring_empty_descs);
printf("tx_ring_empty_descs: %llu\\n", stats.tx_ring_empty_descs);

bpftool로 XSKMAP 확인

# XSKMAP 내용 확인
$ sudo bpftool map dump id 123
key: 00 00 00 00  value: 0a 00 00 00  # queue 0 → socket fd 10

커널 추적 (tracing)

# ftrace로 AF_XDP 함수 추적
$ sudo trace-cmd record -p function_graph -g xsk_rcv -g xsk_sendmsg
$ sudo trace-cmd report | head -50

# bpftrace로 AF_XDP 드롭 원인 분석
$ sudo bpftrace -e '
kprobe:xsk_rcv {
    @calls = count();
}
kretprobe:xsk_rcv /retval != 0/ {
    @errors[retval] = count();
    printf("xsk_rcv error: %d\n", retval);
}
'

# XDP redirect 성공/실패 추적
$ sudo bpftrace -e '
tracepoint:xdp:xdp_redirect_map_err {
    @redirect_err[args->act, args->map_id] = count();
}
tracepoint:xdp:xdp_redirect_map {
    @redirect_ok = count();
}
'

# perf로 AF_XDP 핫스팟 분석
$ sudo perf record -g -p $(pgrep af_xdp_app) -- sleep 10
$ sudo perf report --stdio

ethtool XDP/XSK 통계

# 드라이버별 AF_XDP 통계 확인 (ice 예시)
$ ethtool -S eth0 | grep -E 'xdp|xsk'
     rx_xdp_aborted: 0
     rx_xdp_drop: 0
     rx_xdp_pass: 12345
     rx_xdp_tx: 0
     rx_xdp_invalid: 0
     rx_xdp_redirect: 9876543
     tx-0.tx_xsk_xmit: 5432100
     tx-0.tx_xsk_mpwqe: 0
     rx-0.rx_xsk_packets: 9876543
     rx-0.rx_xsk_bytes: 631458752
     rx-0.rx_xsk_drops: 0
     rx-0.rx_xsk_cqe_err: 0
     rx-0.rx_xsk_congst_umr: 0

커널 설정

# AF_XDP 필수 설정
CONFIG_XDP_SOCKETS=y              # AF_XDP 소켓 지원
CONFIG_XDP_SOCKETS_DIAG=y          # AF_XDP 진단 (ss --xdp)

# XDP 기본 지원
CONFIG_BPF=y                       # BPF 서브시스템
CONFIG_BPF_SYSCALL=y               # bpf() 시스템 콜
CONFIG_BPF_JIT=y                   # BPF JIT 컴파일러
CONFIG_HAVE_EBPF_JIT=y             # 아키텍처 JIT 지원
CONFIG_NET_XDP=y                   # XDP 코어

# 성능 최적화 관련
CONFIG_HUGETLBFS=y                 # Hugepage 지원
CONFIG_HUGETLB_PAGE=y
CONFIG_TRANSPARENT_HUGEPAGE=y

# 디버깅 (개발 시에만)
CONFIG_BPF_EVENTS=y                # BPF 이벤트 추적
CONFIG_DEBUG_INFO_BTF=y            # BTF (bpftool 사용 시 필요)

# NIC 드라이버 (Zero-copy 지원 드라이버 예시)
CONFIG_ICE=m                       # Intel E810 (100GbE)
CONFIG_I40E=m                      # Intel X710/XL710
CONFIG_IXGBE=m                     # Intel 82599/X520
CONFIG_MLX5_CORE=m                 # Mellanox ConnectX-5/6/7

설정 확인 스크립트

#!/bin/bash
# af_xdp_config_check.sh — AF_XDP 필수 커널 설정 확인

configs=(
    "CONFIG_XDP_SOCKETS"
    "CONFIG_XDP_SOCKETS_DIAG"
    "CONFIG_BPF"
    "CONFIG_BPF_SYSCALL"
    "CONFIG_BPF_JIT"
    "CONFIG_NET_XDP"
)

for cfg in "${configs[@]}"; do
    val=$(zgrep "$cfg=" /proc/config.gz 2>/dev/null || \
          grep "$cfg=" /boot/config-$(uname -r) 2>/dev/null)
    if [ -z "$val" ]; then
        echo "MISSING: $cfg"
    elif echo "$val" | grep -q "=y\|=m"; then
        echo "OK:      $val"
    else
        echo "DISABLED: $val"
    fi
done

# BPF JIT 활성화 확인
echo ""
echo "BPF JIT: $(cat /proc/sys/net/core/bpf_jit_enable)"
echo "Hugepages: $(cat /proc/meminfo | grep HugePages_Total)"

멀티큐 아키텍처와 RSS 연동

현대 NIC은 여러 RX/TX 큐를 제공하며, RSS(Receive Side Scaling)를 통해 패킷을 큐별로 분산합니다. AF_XDP는 큐 단위로 소켓을 바인딩하므로, 멀티큐 환경에서 자연스러운 병렬 처리가 가능합니다.

AF_XDP 멀티큐 + RSS 아키텍처 NIC (Physical / RSS Engine) RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 XDP Program: bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0) AF_XDP Socket 0 Queue 0 바인딩 CPU 0 affinity AF_XDP Socket 1 Queue 1 바인딩 CPU 1 affinity AF_XDP Socket 2 Queue 2 바인딩 CPU 2 affinity AF_XDP Socket 3 Queue 3 바인딩 CPU 3 affinity Shared UMEM (XDP_SHARED_UMEM) Frame Pool: Pkt0 | Pkt1 | ... | PktN — 모든 소켓이 동일 메모리 영역 사용 Userspace Application (Thread per Queue — pthread_setaffinity_np)

멀티큐 설정 절차

# 1. NIC 큐 수 확인 및 설정
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
  Combined:     64
Current hardware settings:
  Combined:     4

# 큐 수를 CPU 코어 수에 맞춤
$ sudo ethtool -L eth0 combined 4

# 2. 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
    ...

# 3. IRQ affinity를 큐별 CPU에 고정
$ sudo sh -c 'echo 1 > /proc/irq/50/smp_affinity'  # Queue 0 → CPU 0
$ sudo sh -c 'echo 2 > /proc/irq/51/smp_affinity'  # Queue 1 → CPU 1
$ sudo sh -c 'echo 4 > /proc/irq/52/smp_affinity'  # Queue 2 → CPU 2
$ sudo sh -c 'echo 8 > /proc/irq/53/smp_affinity'  # Queue 3 → CPU 3

# 또는 irqbalance 비활성화 후 스크립트로 설정
$ sudo systemctl stop irqbalance
$ sudo set_irq_affinity.sh eth0

멀티큐 AF_XDP 코드 구조

#include <pthread.h>
#include <bpf/xsk.h>

#define NUM_QUEUES 4

struct xsk_per_queue {
    struct xsk_socket *xsk;
    struct xsk_ring_cons rx;
    struct xsk_ring_prod tx;
    struct xsk_ring_prod fill;
    struct xsk_ring_cons comp;
    int queue_id;
};

static void *rx_thread(void *arg)
{
    struct xsk_per_queue *q = arg;

    /* 이 스레드를 queue_id와 동일한 CPU에 고정 */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(q->queue_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

    while (running) {
        __u32 idx = 0;
        unsigned int rcvd = xsk_ring_cons__peek(&q->rx, BATCH_SIZE, &idx);
        if (!rcvd) {
            if (xsk_ring_prod__needs_wakeup(&q->fill))
                recvmsg(xsk_socket__fd(q->xsk), NULL, MSG_DONTWAIT);
            continue;
        }
        /* 패킷 처리 ... */
        xsk_ring_cons__release(&q->rx, rcvd);
        refill_fill_ring(&q->fill, rcvd);
    }
    return NULL;
}

int main(void)
{
    struct xsk_umem *umem;
    struct xsk_per_queue queues[NUM_QUEUES];
    pthread_t threads[NUM_QUEUES];

    /* 공유 UMEM 생성 (한 번만) */
    xsk_umem__create(&umem, umem_area, umem_size,
                      &queues[0].fill, &queues[0].comp, &umem_cfg);

    /* 큐별 AF_XDP 소켓 생성 */
    for (int i = 0; i < NUM_QUEUES; i++) {
        queues[i].queue_id = i;
        struct xsk_socket_config cfg = {
            .rx_size = 4096,
            .tx_size = 4096,
            .bind_flags = XDP_ZEROCOPY | (i > 0 ? XDP_SHARED_UMEM : 0),
        };
        xsk_socket__create(&queues[i].xsk, "eth0", i, umem,
                            &queues[i].rx, &queues[i].tx, &cfg);

        /* XSKMAP에 등록 */
        int fd = xsk_socket__fd(queues[i].xsk);
        bpf_map_update_elem(xsks_map_fd, &i, &fd, 0);
    }

    /* 큐별 스레드 시작 */
    for (int i = 0; i < NUM_QUEUES; i++)
        pthread_create(&threads[i], NULL, rx_thread, &queues[i]);

    for (int i = 0; i < NUM_QUEUES; i++)
        pthread_join(threads[i], NULL);
}
성능 팁: 멀티큐 AF_XDP에서 최고 성능을 위해서는 NIC Queue ↔ IRQ ↔ CPU ↔ AF_XDP Socket Thread를 1:1:1:1로 고정해야 합니다. irqbalance를 비활성화하고 수동으로 IRQ affinity를 설정하세요.

Flow Steering을 통한 트래픽 분리

# ethtool ntuple을 이용해 특정 트래픽을 특정 큐로 유도
# UDP 포트 53 (DNS) 트래픽 → Queue 0
$ sudo ethtool -N eth0 flow-type udp4 dst-port 53 action 0

# UDP 포트 4789 (VXLAN) 트래픽 → Queue 1
$ sudo ethtool -N eth0 flow-type udp4 dst-port 4789 action 1

# 설정된 규칙 확인
$ ethtool -n eth0
4 RX rings available
Total 2 rules

# 규칙 삭제
$ sudo ethtool -N eth0 delete 0

NEED_WAKEUP 메커니즘

XDP_USE_NEED_WAKEUP 플래그는 AF_XDP의 CPU 효율을 대폭 개선하는 핵심 메커니즘입니다. NAPI 컨텍스트에서 커널이 직접 큐를 소비할 수 있을 때는 wakeup이 불필요하고, NAPI가 완료된 후에만 userspace가 커널을 깨워야 합니다.

NEED_WAKEUP 상태 전이 다이어그램 NAPI 활성 상태 need_wakeup = false 커널이 직접 Fill Ring에서 프레임을 가져감 → wakeup 불필요 NAPI 비활성 상태 need_wakeup = true 커널이 polling 중단 User가 poll()/recvmsg()로 깨워야 함 NAPI budget 소진 / 타임아웃 poll() / recvmsg() → 인터럽트 재활성화 User 행동 (NEED_WAKEUP 활성) 1. xsk_ring_prod__needs_wakeup(&fq) 검사 2. true → recvmsg() / poll() 호출 3. false → syscall 생략 (CPU 절약) Busy-poll 모드 (비교) NEED_WAKEUP 미사용 항상 spin-loop → CPU 100% 최저 레이턴시 but 최고 전력 소비

NEED_WAKEUP RX 코드 패턴

/* 권장: NEED_WAKEUP을 활용한 효율적 RX 루프 */
struct xsk_socket_config cfg = {
    .rx_size = 4096,
    .bind_flags = XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP,
};

while (running) {
    __u32 idx = 0;
    unsigned int rcvd = xsk_ring_cons__peek(&rx, BATCH_SIZE, &idx);

    if (!rcvd) {
        /* Fill Ring wakeup 필요 여부 확인 */
        if (xsk_ring_prod__needs_wakeup(&fill)) {
            /* NAPI가 비활성 → syscall로 깨움 */
            struct pollfd pfd = {
                .fd = xsk_socket__fd(xsk),
                .events = POLLIN,
            };
            poll(&pfd, 1, 1000);  /* 1초 타임아웃 */
        }
        continue;
    }

    /* 패킷 처리 */
    process_batch(&rx, idx, rcvd);
    xsk_ring_cons__release(&rx, rcvd);
    refill_fill_ring(&fill, rcvd);
}

NEED_WAKEUP TX 코드 패턴

/* TX에서의 NEED_WAKEUP 활용 */
xsk_ring_prod__submit(&tx, batch_size);

/* TX Ring wakeup 필요 여부 확인 */
if (xsk_ring_prod__needs_wakeup(&tx)) {
    /* 커널에 송신 시작 알림 */
    sendto(xsk_socket__fd(xsk), NULL, 0, MSG_DONTWAIT, NULL, 0);
}

/* NEED_WAKEUP 없이는 매번 sendto() 호출 필요 → syscall 오버헤드 */
성능 차이: XDP_USE_NEED_WAKEUP를 사용하면 NAPI가 활성 상태일 때 불필요한 poll()/recvmsg() syscall을 건너뛰어 10~30%의 성능 향상을 얻을 수 있습니다. 특히 낮은 트래픽에서 CPU 사용률이 크게 감소합니다.

XDP 메타데이터 전달

XDP 프로그램에서 AF_XDP 소켓으로 패킷과 함께 커스텀 메타데이터를 전달할 수 있습니다. 이를 통해 패킷 분류 결과, 타임스탬프, 해시 값 등을 userspace로 효율적으로 전달합니다.

메타데이터 레이아웃

UMEM 프레임 메타데이터 레이아웃 0 meta_off headroom data_end frame_size Unused XDP Metadata hash, tstamp, mark Packet Data L2 + L3 + L4 + Payload Unused desc->addr은 Packet Data 시작 위치를 가리킴 메타데이터는 desc->addr - (ctx->data - ctx->data_meta) 위치 frame_headroom 내에 메타데이터 공간 확보 필요

XDP 프로그램에서 메타데이터 작성

/* XDP 프로그램: 패킷에 커스텀 메타데이터 추가 */
struct xdp_meta {
    __u64 timestamp;    /* 패킷 수신 타임스탬프 */
    __u32 hash;         /* RSS 해시 값 */
    __u32 mark;         /* 분류 마크 */
};

SEC("xdp")
int xdp_with_meta(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    /* 메타데이터 공간 확보 (data 앞에 16바이트) */
    int ret = bpf_xdp_adjust_meta(ctx, -(int)sizeof(struct xdp_meta));
    if (ret < 0)
        return XDP_PASS;

    /* 포인터 재계산 (bpf_xdp_adjust_meta 후 필수) */
    data = (void *)(long)ctx->data;
    void *data_meta = (void *)(long)ctx->data_meta;

    if (data_meta + sizeof(struct xdp_meta) > data)
        return XDP_PASS;

    struct xdp_meta *meta = data_meta;
    meta->timestamp = bpf_ktime_get_ns();
    meta->hash = ctx->rx_queue_index;
    meta->mark = 0xCAFE;

    return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0);
}

Userspace에서 메타데이터 읽기

/* Userspace: RX 패킷에서 메타데이터 추출 */
struct xdp_meta {
    __u64 timestamp;
    __u32 hash;
    __u32 mark;
};

void process_with_meta(const struct xdp_desc *desc, void *umem_area)
{
    void *pkt = xsk_umem__get_data(umem_area, desc->addr);

    /* 메타데이터는 패킷 데이터 바로 앞에 위치 */
    struct xdp_meta *meta = pkt - sizeof(struct xdp_meta);

    printf("Timestamp: %llu ns, Hash: %u, Mark: 0x%x\n",
           meta->timestamp, meta->hash, meta->mark);

    /* 실제 패킷 데이터 처리 */
    process_packet(pkt, desc->len);
}
주의: 메타데이터를 사용하려면 UMEM의 frame_headroom이 메타데이터 크기보다 커야 합니다. 기본 headroom은 0이므로, 메타데이터 사용 시 반드시 umem_config.frame_headroom = 256 등으로 설정하세요. 또한 XDP 프로그램에서 bpf_xdp_adjust_meta()를 호출하지 않으면 메타데이터가 존재하지 않습니다.

Unaligned 청크 모드

리눅스 5.7+에서 도입된 XDP_UMEM_UNALIGNED_CHUNK_FLAG는 UMEM 프레임을 정렬 제약 없이 가변 크기로 사용할 수 있게 합니다. 이는 MTU보다 작은 패킷이 대부분인 워크로드에서 메모리 효율을 높입니다.

Aligned vs Unaligned 비교

특성 Aligned (기본) Unaligned
프레임 주소 frame_size 경계 정렬 필수 임의 주소 허용
주소 형식 addr = n * frame_size addr = base + offset (하위 16비트 = offset)
메모리 낭비 작은 패킷에서 낭비 큼 실제 사용량만큼만 할당
프레임 관리 단순 (인덱스 기반) 복잡 (free list 필요)
Zero-copy 지원 드라이버에 따라 다름
커널 버전 4.18+ 5.7+

Unaligned 모드 설정

/* Unaligned 청크 모드 UMEM 설정 */
struct xsk_umem_config umem_cfg = {
    .fill_size = 4096,
    .comp_size = 4096,
    .frame_size = 2048,             /* 여전히 최대 프레임 크기 지정 */
    .frame_headroom = 0,
    .flags = XDP_UMEM_UNALIGNED_CHUNK_FLAG,  /* 핵심 플래그 */
};

/* 주소 인코딩: 상위 비트 = 오프셋, 하위 비트 = 청크 내 위치 */
/* Unaligned 모드에서 desc->addr의 의미가 달라짐 */
#define XSK_UNALIGNED_BUF_OFFSET_SHIFT 48
#define XSK_UNALIGNED_BUF_ADDR_MASK    \
    ((1ULL << XSK_UNALIGNED_BUF_OFFSET_SHIFT) - 1)

/* 실제 주소 추출 */
static inline __u64 xsk_umem__extract_addr(__u64 addr)
{
    return addr & XSK_UNALIGNED_BUF_ADDR_MASK;
}

/* 오프셋 추출 */
static inline __u64 xsk_umem__extract_offset(__u64 addr)
{
    return addr >> XSK_UNALIGNED_BUF_OFFSET_SHIFT;
}

/* 조합된 주소 생성 */
static inline __u64 xsk_umem__add_offset_to_addr(__u64 addr)
{
    return xsk_umem__extract_addr(addr) + xsk_umem__extract_offset(addr);
}

커널 내부 자료구조

AF_XDP의 커널 측 구현은 net/xdp/ 디렉토리에 있으며, 핵심 자료구조들의 관계를 이해하면 성능 튜닝과 디버깅에 큰 도움이 됩니다.

AF_XDP 커널 내부 자료구조 관계도 struct xdp_sock (net/xdp/xsk.c) struct sock sk; struct xsk_queue *rx; struct xsk_queue *tx; struct xsk_buff_pool *pool; struct net_device *dev; u16 queue_id; /* zc, state, umem 등 */ struct xsk_buff_pool struct xsk_queue *fq; (Fill) struct xsk_queue *cq; (Comp) struct xdp_buff **free_heads; dma_addr_t *dma_pages; u32 chunk_size, headroom; u32 free_heads_cnt; /* addrs, umem, netdev 등 */ struct xsk_queue u32 ring_mask; u32 nentries; u32 cached_prod / cached_cons; struct xdp_ring *ring; /* mmap'd shared memory */ struct xdp_ring (UAPI) u32 producer; /* User 갱신 */ u32 consumer; /* Kernel 갱신 */ u32 flags; /* NEED_WAKEUP */ struct xdp_desc desc[]; /* lockless SPSC ring */ xdp_desc __u64 addr; __u32 len; __u32 options; net_device ndo_xsk_wakeup ndo_bpf struct xdp_buff void *data, *data_end, *data_meta; struct xsk_buff_pool *pool; pool rx/tx queue fq/cq queue ring desc[] dev xdp_buff per packet

xdp_sock 구조체 상세

/* include/net/xdp_sock.h — AF_XDP 소켓의 커널 표현 */
struct xdp_sock {
    struct sock sk;                    /* 일반 소켓 임베딩 */
    struct xsk_queue *rx;               /* RX Ring (kernel → user) */
    struct xsk_queue *tx;               /* TX Ring (user → kernel) */
    struct list_head map_list;          /* XSKMAP 등록 목록 */
    struct spinlock map_list_lock;

    struct xsk_buff_pool *pool;         /* UMEM 버퍼 풀 */
    u16 queue_id;                       /* 바인딩된 NIC 큐 번호 */

    enum {
        XSK_READY = 0,
        XSK_BOUND,
        XSK_UNBOUND,
    } state;

    struct xdp_umem *umem;              /* UMEM 메모리 영역 */
    struct net_device *dev;             /* 바인딩된 네트워크 디바이스 */

    bool zc;                            /* zero-copy 활성 여부 */
    u64 rx_dropped;                     /* 드롭 통계 */
    u64 rx_queue_full;                  /* RX ring full 통계 */

    struct list_head flush_node;        /* flush 대기열 */
};

xsk_buff_pool 구조체

/* net/xdp/xsk_buff_pool.c — 버퍼 풀 관리의 핵심 */
struct xsk_buff_pool {
    struct device *dev;                /* DMA 디바이스 */
    struct net_device *netdev;         /* 네트워크 디바이스 */
    struct list_head free_list;        /* DMA 매핑 해제 대기 */

    struct xdp_buff **heads;           /* 모든 xdp_buff 헤더 배열 */
    u64 *addrs;                        /* UMEM 주소 배열 */
    u32 heads_cnt;                     /* xdp_buff 수 */
    u32 free_heads_cnt;                /* 사용 가능한 xdp_buff 수 */

    struct xsk_queue *fq;              /* Fill Queue (user → kernel) */
    struct xsk_queue *cq;              /* Completion Queue (kernel → user) */

    dma_addr_t *dma_pages;             /* DMA 매핑된 페이지 배열 */
    struct xdp_buff_xsk *free_heads[]; /* 가용 버퍼 스택 */

    u32 chunk_size;                    /* 프레임 크기 */
    u32 chunk_mask;
    u32 frame_len;                     /* chunk_size - headroom */
    u8  cached_need_wakeup;            /* NEED_WAKEUP 캐시 */
    bool uses_need_wakeup;
    bool unaligned;                    /* unaligned chunk 모드 */
};

xsk_queue Ring 구현

/* net/xdp/xsk_queue.h — SPSC(Single Producer Single Consumer) Ring */
struct xsk_queue {
    u32 ring_mask;               /* nentries - 1 (2의 거듭제곱) */
    u32 nentries;                /* 엔트리 수 */
    u32 cached_prod;             /* 로컬 캐시된 producer 인덱스 */
    u32 cached_cons;             /* 로컬 캐시된 consumer 인덱스 */
    struct xdp_ring *ring;       /* mmap된 공유 메모리 */
    u64 invalid_descs;            /* 유효하지 않은 descriptor 수 */
    u64 queue_empty_descs;        /* 큐 빈 상태 카운트 */
};

/* UAPI: User/Kernel 공유 링 구조 */
struct xdp_ring {
    __u32 producer __attribute__((aligned(CACHELINE_SIZE)));
    __u32 pad1     __attribute__((aligned(CACHELINE_SIZE)));
    __u32 consumer __attribute__((aligned(CACHELINE_SIZE)));
    __u32 pad2     __attribute__((aligned(CACHELINE_SIZE)));
    __u32 flags;                  /* XDP_RING_NEED_WAKEUP */
    __u32 pad3     __attribute__((aligned(CACHELINE_SIZE)));
};
/*
 * producer/consumer 인덱스는 각각 별도의 cache line에 위치
 * → false sharing 방지 (성능 핵심)
 * → User가 producer를 쓰고, Kernel이 consumer를 쓰는 SPSC 패턴
 */
SPSC Ring: Producer/Consumer 인덱스 동작 Ring Buffer (nentries = 8, ring_mask = 7) desc[0] desc[1] desc[2] desc[3] desc[4] desc[5] desc[6] desc[7] consumer = 3 (다음 소비할 위치) producer = 5 (다음 생산할 위치) 소비 완료 (반환 가능) 데이터 보유 (소비 대기) 비어있음 사용 가능 슬롯 = producer - consumer = 5 - 3 = 2개 인덱스 접근: ring->desc[idx & ring_mask] → wrap-around 자동 처리

AF_XDP 시스템 콜 경로

시스템 콜 커널 함수 동작
socket(AF_XDP, ...) xsk_create() xdp_sock 할당, 초기화
setsockopt(SOL_XDP, XDP_UMEM_REG) xsk_setsockopt() UMEM 등록, 페이지 pin
setsockopt(SOL_XDP, XDP_UMEM_FILL_RING) xsk_setsockopt() Fill Ring 생성/mmap
setsockopt(SOL_XDP, XDP_UMEM_COMPLETION_RING) xsk_setsockopt() Completion Ring 생성/mmap
setsockopt(SOL_XDP, XDP_RX_RING) xsk_setsockopt() RX Ring 생성/mmap
setsockopt(SOL_XDP, XDP_TX_RING) xsk_setsockopt() TX Ring 생성/mmap
bind() xsk_bind() NIC 큐 바인딩, ZC 설정, pool 활성화
mmap() xsk_mmap() Ring/UMEM 메모리를 user 매핑
sendto() xsk_sendmsg() TX kick, ndo_xsk_wakeup 호출
recvmsg() xsk_recvmsg() RX wakeup, NAPI 스케줄
poll() xsk_poll() 이벤트 대기, NAPI 스케줄
getsockopt(XDP_STATISTICS) xsk_getsockopt() 드롭/에러 통계 조회

bind() 내부 흐름

/* net/xdp/xsk.c: xsk_bind() 핵심 흐름 (간략화) */
static int xsk_bind(struct socket *sock, struct sockaddr *addr, int addr_len)
{
    struct xdp_sock *xs = xdp_sk(sock->sk);
    struct sockaddr_xdp *sxdp = (struct sockaddr_xdp *)addr;

    /* 1. 네트워크 디바이스 찾기 */
    dev = dev_get_by_index(net, sxdp->sxdp_ifindex);

    /* 2. queue_id 유효성 검사 */
    if (sxdp->sxdp_queue_id >= dev->real_num_rx_queues)
        return -EINVAL;

    /* 3. Zero-copy 모드 판단 */
    if (sxdp->sxdp_flags & XDP_ZEROCOPY) {
        /* 드라이버가 ndo_bpf + XDP_SETUP_XSK_POOL 지원 확인 */
        err = xsk_check_common(xs, dev);
        if (!err)
            xs->zc = true;
    }

    /* 4. xsk_buff_pool 생성 및 DMA 매핑 */
    pool = xp_create_and_assign_umem(xs, xs->umem);
    xp_dma_map(pool, dev, sxdp->sxdp_queue_id);

    /* 5. 드라이버에 pool 등록 (ZC 시) */
    if (xs->zc) {
        bpf.command = XDP_SETUP_XSK_POOL;
        bpf.xsk.pool = pool;
        bpf.xsk.queue_id = sxdp->sxdp_queue_id;
        dev->netdev_ops->ndo_bpf(dev, &bpf);
    }

    /* 6. 상태 전이 */
    xs->state = XSK_BOUND;
    xs->dev = dev;
    xs->queue_id = sxdp->sxdp_queue_id;
}

Copy vs Zero-Copy 내부 데이터 경로

AF_XDP의 두 가지 모드는 패킷이 NIC에서 userspace까지 도달하는 경로가 근본적으로 다릅니다. 이 차이가 성능 격차의 원인입니다.

Copy 모드 vs Zero-Copy 모드 데이터 경로 Copy 모드 Zero-Copy 모드 1. NIC DMA → 드라이버의 page(sk_buff) 2. XDP 프로그램 실행 → XDP_REDIRECT 반환 3. memcpy: 드라이버 page → UMEM 프레임 xsk_copy_xdp() — 성능 병목! 4. RX Ring에 descriptor 게시 5. User: peek → 패킷 처리 1. NIC DMA → UMEM 프레임에 직접 기록 2. XDP 프로그램 실행 → XDP_REDIRECT 반환 3. memcpy 없음! 주소만 전달 xsk_buff_free() — 제로카피 핵심 4. RX Ring에 descriptor 게시 5. User: peek → 패킷 처리 ~2-5M pps (memcpy 비용 포함) ~10-24M pps (memcpy 제거)

Copy 모드 커널 경로

/* net/xdp/xsk.c: Copy 모드 수신 경로 (간략화) */
static int xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp)
{
    u32 len = xdp->data_end - xdp->data;
    u32 metalen = xdp->data - xdp->data_meta;

    /* 1. Fill Ring에서 빈 프레임 주소 가져오기 */
    u64 addr;
    if (!xskq_cons_peek_addr_unchecked(xs->pool->fq, &addr))
        return -ENOBUFS;  /* Fill Ring 비었으면 드롭! */

    /* 2. UMEM 프레임으로 패킷 데이터 복사 (병목) */
    void *buffer = xsk_buff_raw_get_data(xs->pool, addr);
    memcpy(buffer, xdp->data_meta, len + metalen);

    /* 3. RX Ring에 descriptor 추가 */
    struct xdp_desc desc = {
        .addr = addr + metalen,
        .len  = len,
    };
    xskq_prod_submit_addr(xs->rx, &desc);

    /* 4. Fill Ring에서 소비 완료 */
    xskq_cons_release(xs->pool->fq);
    return 0;
}

Zero-Copy 모드 커널 경로

/* Zero-copy: 드라이버가 직접 xsk_buff_pool에서 버퍼 할당 */

/* 1. 드라이버 NAPI poll에서: Fill Ring → DMA 버퍼 할당 */
struct xdp_buff *xsk_buff_alloc(struct xsk_buff_pool *pool)
{
    /* Fill Ring에서 프레임 주소를 가져와 xdp_buff 구성 */
    struct xdp_buff_xsk *xskb = pool->free_heads[--pool->free_heads_cnt];
    xskb->xdp.data = xskb->orig_addr + pool->headroom;
    xskb->xdp.data_end = xskb->xdp.data;
    return &xskb->xdp;
}

/* 2. NIC DMA가 UMEM 프레임에 직접 수신 */
/* 3. XDP 프로그램 실행 후 XDP_REDIRECT */

/* 4. xdp_do_redirect에서 AF_XDP 소켓으로 전달 */
int xsk_rcv_zc(struct xdp_sock *xs, struct xdp_buff *xdp)
{
    /* memcpy 없이 주소만 RX Ring에 게시 */
    struct xdp_buff_xsk *xskb = container_of(xdp, ...);
    struct xdp_desc desc = {
        .addr = xp_get_handle(xskb),
        .len  = xdp->data_end - xdp->data,
    };
    return xskq_prod_reserve_desc(xs->rx, &desc);
}

Zero-Copy 지원 드라이버 상세

드라이버 NIC ZC 도입 커널 비고
i40e Intel X710, XL710, XXV710 4.18 최초 ZC 지원, 가장 성숙
ixgbe Intel 82599, X520, X540 4.18 10GbE 레거시, 안정적
ice Intel E810 (100GbE) 5.5 100GbE, 최고 성능
mlx5 Mellanox ConnectX-5/6/7 5.3 25/40/100GbE, multi-buffer 지원
bnxt_en Broadcom NetXtreme-E 5.13 25/50/100GbE
igc Intel I225/I226 (2.5GbE) 5.14 데스크탑/임베디드
stmmac Synopsys DWC Ethernet 5.13 임베디드/ARM SoC
veth 가상 이더넷 쌍 5.9 컨테이너 네트워킹
virtio_net QEMU/KVM VirtIO 6.4 가상화 환경

실전 활용 사례

고성능 DNS 서버

DNS는 작은 UDP 패킷으로 구성되어 AF_XDP의 이점을 극대화할 수 있는 대표적인 워크로드입니다.

/* AF_XDP 기반 DNS 서버 핵심 루프 (개념 코드) */
while (running) {
    __u32 idx = 0;
    unsigned int rcvd = xsk_ring_cons__peek(&rx, BATCH_SIZE, &idx);

    for (unsigned int i = 0; i < rcvd; i++) {
        const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx, idx++);
        void *pkt = xsk_umem__get_data(umem_area, desc->addr);

        /* L2/L3/L4 파싱 */
        struct ethhdr *eth = pkt;
        struct iphdr *ip = pkt + sizeof(*eth);
        struct udphdr *udp = (void *)ip + ip->ihl * 4;
        void *dns_payload = (void *)udp + sizeof(*udp);

        /* DNS 쿼리 처리 */
        int resp_len = process_dns_query(dns_payload, dns_response);

        /* 응답을 같은 프레임에 구성 (in-place 수정) */
        swap_mac_addrs(eth);
        swap_ip_addrs(ip);
        swap_udp_ports(udp);
        memcpy(dns_payload, dns_response, resp_len);

        /* TX Ring으로 전송 */
        xsk_send_frame(&tx, desc->addr, total_len);
    }
    xsk_ring_cons__release(&rx, rcvd);
}
성능: AF_XDP 기반 DNS 서버는 일반 소켓 기반 대비 5~10배 높은 QPS(Queries per Second)를 달성할 수 있습니다. Meta의 Katran 로드밸런서, Cloudflare의 DNS 파이프라인 등이 유사한 아키텍처를 사용합니다.

고속 패킷 캡처

/* AF_XDP 기반 패킷 캡처: pcapng 형식 저장 */
#define RING_SIZE 16384
#define WRITE_BATCH 256

struct capture_ctx {
    FILE *pcap_file;
    __u64 captured;
    __u64 dropped;
};

void capture_loop(struct xsk_socket *xsk, struct capture_ctx *ctx)
{
    __u32 idx = 0;
    unsigned int rcvd = xsk_ring_cons__peek(&rx, WRITE_BATCH, &idx);

    for (unsigned int i = 0; i < rcvd; i++) {
        const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx, idx++);
        void *pkt = xsk_umem__get_data(umem_area, desc->addr);
        __u64 ts = get_timestamp_ns();

        /* pcapng Enhanced Packet Block 작성 */
        write_pcapng_epb(ctx->pcap_file, pkt, desc->len, ts);
        ctx->captured++;
    }
    xsk_ring_cons__release(&rx, rcvd);
    refill_fill_ring(&fill, rcvd);
}

L2 포워딩 / 브릿지

/* AF_XDP L2 포워딩: 두 인터페이스 간 패킷 전달 */
struct xsk_socket *xsk_rx;  /* eth0 수신 */
struct xsk_socket *xsk_tx;  /* eth1 송신 */

void l2fwd_loop(void)
{
    while (running) {
        __u32 rx_idx = 0, tx_idx = 0;
        unsigned int rcvd = xsk_ring_cons__peek(&rx_ring, BATCH_SIZE, &rx_idx);

        if (!rcvd)
            continue;

        /* TX Ring에 공간 확보 */
        if (xsk_ring_prod__reserve(&tx_ring, rcvd, &tx_idx) != rcvd) {
            /* Completion Ring 회수 후 재시도 */
            complete_tx(&comp_ring);
            xsk_ring_prod__reserve(&tx_ring, rcvd, &tx_idx);
        }

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *rx_desc = xsk_ring_cons__rx_desc(&rx_ring, rx_idx++);
            struct xdp_desc *tx_desc = xsk_ring_prod__tx_desc(&tx_ring, tx_idx++);

            /* 프레임을 TX로 직접 전달 (같은 UMEM 사용 시 zero-copy) */
            tx_desc->addr = rx_desc->addr;
            tx_desc->len = rx_desc->len;
        }

        xsk_ring_cons__release(&rx_ring, rcvd);
        xsk_ring_prod__submit(&tx_ring, rcvd);

        if (xsk_ring_prod__needs_wakeup(&tx_ring))
            sendto(xsk_socket__fd(xsk_tx), NULL, 0, MSG_DONTWAIT, NULL, 0);
    }
}

IDS/IPS 패킷 검사

XDP + AF_XDP 하이브리드 패턴: XDP 프로그램에서 빠른 1차 필터링(IP 블랙리스트, 레이트 리밋)을 수행하고, 정밀 검사가 필요한 패킷만 AF_XDP로 전달하면 CPU 효율이 극대화됩니다. Suricata IDS가 이 패턴을 AF_XDP 백엔드로 지원합니다(7.0+).
/* XDP: 의심 패킷만 AF_XDP로 전달하는 1차 필터 */
SEC("xdp")
int ids_filter(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) return XDP_PASS;

    if (eth->h_proto == htons(ETH_P_IP)) {
        struct iphdr *ip = (struct iphdr *)(eth + 1);
        if ((void *)(ip + 1) > data_end) return XDP_PASS;

        /* 알려진 악성 IP → 즉시 DROP (XDP에서 처리) */
        if (bpf_map_lookup_elem(&blocklist, &ip->saddr))
            return XDP_DROP;

        /* TCP SYN, 비정상 플래그 → AF_XDP로 정밀 검사 */
        if (ip->protocol == IPPROTO_TCP) {
            struct tcphdr *tcp = (struct tcphdr *)(((void *)ip) + ip->ihl * 4);
            if ((void *)(tcp + 1) > data_end) return XDP_PASS;

            if (tcp->syn || tcp->fin || tcp->rst)
                return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, 0);
        }
    }
    return XDP_PASS;  /* 나머지 → 일반 스택 */
}

veth / 컨테이너 환경에서의 AF_XDP

리눅스 5.9+에서 veth 드라이버가 AF_XDP zero-copy를 지원하면서, 컨테이너 네트워킹에서도 고성능 패킷 처리가 가능해졌습니다.

컨테이너 환경 AF_XDP (veth ZC) Host Namespace Container Namespace Physical NIC (eth0) + XDP veth_host (veth pair 호스트 측) AF_XDP Socket (호스트) veth_host 바인딩 + ZC 호스트 애플리케이션 / 프록시 veth_container (veth pair 컨테이너 측) AF_XDP Socket (컨테이너) veth_container 바인딩 + ZC 컨테이너 애플리케이션 veth pair CAP_NET_RAW + CAP_BPF 필요 또는 unprivileged BPF 허용

veth AF_XDP 설정

# 1. veth pair 생성
$ sudo ip link add veth_host type veth peer name veth_cont

# 2. 한 쪽을 컨테이너 네임스페이스로 이동
$ sudo ip link set veth_cont netns container_ns

# 3. 양쪽 인터페이스 활성화
$ sudo ip link set veth_host up
$ sudo ip netns exec container_ns ip link set veth_cont up

# 4. XDP native 모드 확인 (veth는 native XDP 지원)
$ sudo ip link set dev veth_host xdp obj xdp_prog.o sec xdp

# 5. AF_XDP 소켓을 veth_host에 바인딩 (호스트 측)
# → 코드에서 xsk_socket__create(..., "veth_host", 0, ...) 사용
제약사항: veth AF_XDP는 물리 NIC 대비 성능이 낮습니다 (veth 자체의 오버헤드). 그러나 일반 소켓 대비 3~5배 빠르며, 컨테이너 사이드카 프록시(Envoy, Cilium 등)에서 유용합니다.

libxdp API

libxdp는 libbpf 위에 구축된 XDP 전용 라이브러리로, 멀티 프로그램 디스패치와 AF_XDP 설정을 단순화합니다.

libxdp 멀티 프로그램

#include <xdp/libxdp.h>

/* 여러 XDP 프로그램을 체인으로 연결 */
struct xdp_program *prog1, *prog2;

/* 프로그램 로드 */
prog1 = xdp_program__open_file("stats.o", "xdp", NULL);
prog2 = xdp_program__open_file("redirect.o", "xdp", NULL);

/* 우선순위 설정 */
xdp_program__set_run_order(prog1, 10);
xdp_program__set_run_order(prog2, 20);

/* 인터페이스에 attach (multiprog 자동 관리) */
xdp_program__attach(prog1, ifindex, XDP_MODE_NATIVE, 0);
xdp_program__attach(prog2, ifindex, XDP_MODE_NATIVE, 0);

/* libxdp이 freplace BPF 프로그램으로 디스패처를 자동 생성 */

libxdp AF_XDP 헬퍼

# xdp-tools 설치 (libxdp 포함)
$ git clone https://github.com/xdp-project/xdp-tools.git
$ cd xdp-tools && ./configure && make
$ sudo make install

# xdp-loader: XDP 프로그램 관리
$ sudo xdp-loader load -m native eth0 xdp_prog.o
$ sudo xdp-loader status eth0
$ sudo xdp-loader unload eth0 --all

# xdp-filter: 내장 패킷 필터
$ sudo xdp-filter load eth0
$ sudo xdp-filter port 12345    # 포트 12345 차단
$ sudo xdp-filter ip 10.0.0.1   # IP 차단

완전한 예제: AF_XDP RX/TX 애플리케이션

시작부터 끝까지 동작하는 최소한의 완전한 AF_XDP 애플리케이션 구조입니다.

전체 초기화 및 실행 흐름

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <sys/mman.h>
#include <bpf/bpf.h>
#include <bpf/xsk.h>

#define NUM_FRAMES    4096
#define FRAME_SIZE    XSK_UMEM__DEFAULT_FRAME_SIZE  /* 4096 */
#define BATCH_SIZE    64
#define INVALID_UMEM_FRAME UINT64_MAX

static bool running = true;

struct xsk_app {
    struct xsk_umem *umem;
    struct xsk_socket *xsk;
    struct xsk_ring_cons rx;
    struct xsk_ring_prod tx;
    struct xsk_ring_prod fill;
    struct xsk_ring_cons comp;
    void *umem_area;

    /* 프레임 풀 (간단한 스택 기반) */
    __u64 frame_stack[NUM_FRAMES];
    __u32 frame_stack_top;

    /* 통계 */
    __u64 rx_packets;
    __u64 tx_packets;
};

/* --- 프레임 풀 관리 --- */
static __u64 frame_alloc(struct xsk_app *app)
{
    if (app->frame_stack_top == 0)
        return INVALID_UMEM_FRAME;
    return app->frame_stack[--app->frame_stack_top];
}

static void frame_free(struct xsk_app *app, __u64 addr)
{
    app->frame_stack[app->frame_stack_top++] = addr;
}

/* --- UMEM 초기화 --- */
static int setup_umem(struct xsk_app *app)
{
    size_t size = NUM_FRAMES * FRAME_SIZE;

    app->umem_area = mmap(NULL, size, PROT_READ | PROT_WRITE,
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (app->umem_area == MAP_FAILED)
        return -1;

    struct xsk_umem_config cfg = {
        .fill_size = NUM_FRAMES,
        .comp_size = NUM_FRAMES,
        .frame_size = FRAME_SIZE,
        .frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
        .flags = 0,
    };

    int ret = xsk_umem__create(&app->umem, app->umem_area, size,
                              &app->fill, &app->comp, &cfg);
    if (ret)
        return ret;

    /* 프레임 스택 초기화 */
    for (__u32 i = 0; i < NUM_FRAMES; i++)
        app->frame_stack[i] = i * FRAME_SIZE;
    app->frame_stack_top = NUM_FRAMES;

    return 0;
}

/* --- AF_XDP 소켓 초기화 --- */
static int setup_socket(struct xsk_app *app, const char *ifname, int queue_id)
{
    struct xsk_socket_config cfg = {
        .rx_size = NUM_FRAMES,
        .tx_size = NUM_FRAMES,
        .libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
        .xdp_flags = 0,
        .bind_flags = XDP_USE_NEED_WAKEUP | XDP_COPY,
    };

    int ret = xsk_socket__create(&app->xsk, ifname, queue_id,
                                  app->umem, &app->rx, &app->tx, &cfg);
    if (ret)
        return ret;

    /* Fill Ring에 초기 프레임 제공 */
    __u32 idx;
    ret = xsk_ring_prod__reserve(&app->fill, NUM_FRAMES / 2, &idx);
    for (__u32 i = 0; i < NUM_FRAMES / 2; i++)
        *xsk_ring_prod__fill_addr(&app->fill, idx++) = frame_alloc(app);
    xsk_ring_prod__submit(&app->fill, NUM_FRAMES / 2);

    return 0;
}

/* --- 메인 루프 --- */
static void run(struct xsk_app *app)
{
    struct pollfd pfd = {
        .fd = xsk_socket__fd(app->xsk),
        .events = POLLIN,
    };

    while (running) {
        /* NEED_WAKEUP 활용 */
        if (xsk_ring_prod__needs_wakeup(&app->fill))
            poll(&pfd, 1, 100);

        /* Completion Ring 회수 */
        __u32 comp_idx;
        unsigned int completed = xsk_ring_cons__peek(&app->comp, BATCH_SIZE, &comp_idx);
        for (unsigned int i = 0; i < completed; i++) {
            frame_free(app, *xsk_ring_cons__comp_addr(&app->comp, comp_idx++));
        }
        if (completed)
            xsk_ring_cons__release(&app->comp, completed);

        /* RX 처리 */
        __u32 rx_idx = 0;
        unsigned int rcvd = xsk_ring_cons__peek(&app->rx, BATCH_SIZE, &rx_idx);

        for (unsigned int i = 0; i < rcvd; i++) {
            const struct xdp_desc *desc =
                xsk_ring_cons__rx_desc(&app->rx, rx_idx++);

            void *pkt = xsk_umem__get_data(app->umem_area, desc->addr);
            printf("RX: %u bytes\n", desc->len);

            /* 프레임을 Fill Ring에 반환 */
            frame_free(app, desc->addr);
            app->rx_packets++;
        }

        if (rcvd) {
            xsk_ring_cons__release(&app->rx, rcvd);

            /* Fill Ring 보충 */
            __u32 fill_idx;
            unsigned int reserved = xsk_ring_prod__reserve(&app->fill, rcvd, &fill_idx);
            for (unsigned int i = 0; i < reserved; i++)
                *xsk_ring_prod__fill_addr(&app->fill, fill_idx++) = frame_alloc(app);
            xsk_ring_prod__submit(&app->fill, reserved);
        }
    }
}

static void sigint_handler(int sig) { running = false; }

int main(int argc, char **argv)
{
    if (argc < 2) { fprintf(stderr, "Usage: %s <ifname>\n", argv[0]); return 1; }

    signal(SIGINT, sigint_handler);

    struct xsk_app app = {};

    if (setup_umem(&app) < 0)     { perror("UMEM"); return 1; }
    if (setup_socket(&app, argv[1], 0) < 0) { perror("socket"); return 1; }

    printf("AF_XDP ready on %s queue 0. Ctrl+C to stop.\n", argv[1]);
    run(&app);

    printf("RX: %llu packets, TX: %llu packets\n", app.rx_packets, app.tx_packets);
    xsk_socket__delete(app.xsk);
    xsk_umem__delete(app.umem);
    munmap(app.umem_area, NUM_FRAMES * FRAME_SIZE);
    return 0;
}

빌드 방법

# 의존성 설치 (Ubuntu/Debian)
$ sudo apt install libbpf-dev libxdp-dev clang llvm pkg-config

# XDP BPF 프로그램 컴파일
$ clang -O2 -g -target bpf -c xdp_prog.c -o xdp_prog.o

# Userspace 애플리케이션 컴파일
$ gcc -O2 -o af_xdp_app af_xdp_app.c \
    $(pkg-config --cflags --libs libbpf) -lxdp -lpthread

# 실행 (root 권한 필요)
$ sudo ./af_xdp_app eth0

흔한 실수와 트러블슈팅

자주 발생하는 문제

증상 원인 해결
ENOBUFS 에러 Fill Ring이 비어있음 Fill Ring에 프레임을 충분히 보충
EOPNOTSUPP (bind) 드라이버가 ZC 미지원 XDP_COPY로 fallback
EINVAL (bind) 잘못된 queue_id ethtool -l로 큐 수 확인
EBUSY (bind) 큐에 다른 XSK가 이미 바인딩됨 다른 queue_id 사용 또는 기존 소켓 해제
rx_dropped 증가 RX Ring이 가득 차 있음 RX ring size 증가, 처리 속도 개선
rx_fill_ring_empty_descs 증가 Fill Ring 보충이 너무 느림 Fill Ring 크기 증가, batch 보충
tx_invalid_descs 증가 유효하지 않은 UMEM 주소 addr가 UMEM 범위 내인지 확인
패킷이 전혀 수신 안 됨 XDP 프로그램 미로드 또는 XSKMAP 미등록 bpftool prog list, bpftool map dump 확인
성능이 기대보다 낮음 IRQ/CPU affinity 불일치 NIC queue, IRQ, 스레드 CPU 1:1 매핑
NEED_WAKEUP 후 hang poll() timeout이 너무 큼 적절한 timeout 설정 (100~1000ms)

디버깅 명령어 모음

# 1. XDP 프로그램 상태 확인
$ sudo bpftool prog list
$ sudo bpftool prog show id <ID>

# 2. XSKMAP 확인
$ sudo bpftool map list
$ sudo bpftool map dump id <MAP_ID>

# 3. AF_XDP 소켓 상태 (ss 유틸리티)
$ sudo ss -ax | grep xdp
$ sudo ss --xdp -a

# 4. NIC 큐 통계
$ ethtool -S eth0 | grep -E 'xdp|xsk'
     rx_xdp_redirect: 12345678
     rx_xdp_drop: 0
     rx_xdp_tx: 0
     rx_xsk_packets: 12345678

# 5. 커널 로그 확인
$ dmesg | grep -i xdp
$ dmesg | grep -i xsk

# 6. perf로 AF_XDP 핫스팟 프로파일링
$ sudo perf top -g -p $(pgrep af_xdp_app)

# 7. bpftrace로 AF_XDP 경로 추적
$ sudo bpftrace -e 'kprobe:xsk_rcv { @[kstack] = count(); }'

# 8. XDP 통계 (ip link)
$ ip -d link show eth0 | grep xdp
    prog/xdp id 42 tag abc123...

# 9. NAPI 상태 확인
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
$ cat /proc/softirqs | grep NET_RX

성능 최적화 체크리스트

성능이 기대보다 낮을 때 순서대로 확인:
  1. Zero-copy 모드 활성화 여부ethtool -S eth0 | grep xsk에 ZC 통계가 나오는지 확인
  2. IRQ affinitycat /proc/interrupts로 NIC IRQ가 의도한 CPU에 고정되었는지 확인
  3. NUMA 노드 — NIC과 동일한 NUMA 노드의 CPU/메모리 사용. cat /sys/class/net/eth0/device/numa_node
  4. Hugepage — UMEM에 hugepage 사용. MAP_HUGETLB 플래그 또는 hugetlbfs
  5. Batch size — 32~128 범위에서 최적값 탐색 (워크로드에 따라 다름)
  6. Ring size — 최소 4096 권장. rx_fill_ring_empty_descs가 0이어야 정상
  7. C-states 비활성화idle=poll 또는 processor.max_cstate=1 부트 파라미터
  8. Turbo Boost 고정 — 일관된 클럭 주파수 유지

NUMA 고려사항

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

# NUMA 노드 0의 CPU 목록
$ numactl --hardware | grep "node 0 cpus"
node 0 cpus: 0 1 2 3 4 5

# NUMA-aware로 AF_XDP 앱 실행
$ sudo numactl --cpunodebind=0 --membind=0 ./af_xdp_app eth0
/* NUMA-aware UMEM 할당 */
#include <numa.h>
#include <numaif.h>

int numa_node = 0;  /* NIC의 NUMA 노드 */

/* 1. UMEM 메모리를 특정 NUMA 노드에 할당 */
void *umem_area = mmap(NULL, umem_size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                       -1, 0);

/* NUMA 바인딩 */
unsigned long nodemask = 1UL << numa_node;
mbind(umem_area, umem_size, MPOL_BIND, &nodemask,
      sizeof(nodemask) * 8, MPOL_MF_MOVE | MPOL_MF_STRICT);

/* 2. 스레드도 동일 NUMA 노드에 고정 */
numa_run_on_node(numa_node);

커널 버전별 AF_XDP 변천사

커널 버전 변경사항
4.18 (2018) AF_XDP 최초 도입. Copy 모드, i40e/ixgbe Zero-copy
5.0 AF_XDP 통계(XDP_STATISTICS), XDP_USE_NEED_WAKEUP 지원
5.3 mlx5 ZC 지원, XDP_SHARED_UMEM (여러 소켓 UMEM 공유)
5.4 xsk_buff_pool 도입 — 기존 UMEM 관리 대폭 리팩토링
5.5 ice 드라이버 ZC 지원 (Intel E810 100GbE)
5.7 XDP_UMEM_UNALIGNED_CHUNK_FLAG — Unaligned 청크 모드
5.9 veth ZC 지원 — 컨테이너 네트워킹 가능
5.11 AF_XDP busy-poll (SO_PREFER_BUSY_POLL) 지원
5.13 bnxt_en, stmmac ZC 지원. Multi-buffer XDP 기초
5.14 igc ZC 지원. XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD
5.18 AF_XDP multi-buffer (Jumbo frame 지원). tx_metadata_len
6.0 libxdp AF_XDP API 안정화. netlink 기반 XDP 관리
6.4 virtio_net ZC 지원. AF_XDP TX metadata
6.6 TX 타임스탬프, TX 체크섬 오프로드 메타데이터
6.8+ AF_XDP netdev generic queue API. XDP hints 확장

AF_XDP vs 다른 기술

패킷 처리 기술 성능 비교 (64B 패킷, 단일 큐) Mpps (백만 pps) 25 20 15 10 5 0 ~0.1M AF_PACKET ~1M AF_PKT (MMAP) ~3M AF_XDP (Copy) ~14M AF_XDP (Zero-copy) ~24M DPDK (PMD) ~8M io_uring (zerocopy) ※ 수치는 단일 코어, 64B 패킷, 10GbE NIC 기준 근사값 (환경에 따라 달라짐)
특성 AF_PACKET AF_XDP (Copy) AF_XDP (ZC) DPDK io_uring
성능 (64B pps) ~100K ~1-3M ~10-14M ~20-24M ~5-8M
커널 통합 완전 완전 완전 없음 (우회) 완전
일반 소켓 공존 가능 가능 가능 불가 가능
드라이버 지원 모든 NIC XDP 지원 NIC 일부 NIC PMD 필요 모든 NIC
CPU 전용화 불필요 선택 권장 필수 선택
레이턴시 ~100μs ~20μs ~2-5μs ~1-2μs ~10-20μs
개발 복잡도 낮음 중간 중간 높음 중간
보안/격리 커널 보호 커널 보호 커널 보호 약함 커널 보호
최소 커널 버전 2.6 4.18 4.18 N/A 5.19+
선택 가이드:
  • AF_PACKET — 단순 캡처/모니터링, tcpdump/Wireshark 수준
  • AF_XDP — 커널 통합이 필요한 고성능 처리 (DNS, 로드밸런서, IDS)
  • DPDK — 최대 성능이 최우선, NIC을 전용으로 사용 가능한 환경
  • io_uring — 기존 소켓 API와 호환하면서 성능 개선이 필요한 경우

참고자료

공식 문서

튜토리얼 및 프로젝트

주요 참고 글

커널 소스 경로

파일 역할
net/xdp/xsk.c AF_XDP 소켓 구현 (bind, sendmsg, poll, recvmsg)
net/xdp/xsk_buff_pool.c xsk_buff_pool — DMA 매핑, 버퍼 할당/해제
net/xdp/xsk_queue.h Fill/RX/TX/Completion SPSC Ring 구현
net/xdp/xsk_diag.c AF_XDP 소켓 진단 (ss --xdp)
include/net/xdp_sock.h xdp_sock, xdp_umem 내부 구조체
include/net/xdp_sock_drv.h 드라이버용 ZC API (xsk_buff_alloc 등)
include/uapi/linux/if_xdp.h UAPI — xdp_desc, xdp_statistics, sockaddr_xdp, 플래그
include/net/xdp.h xdp_buff, xdp_frame, xdp_rxq_info 구조체
tools/lib/bpf/xsk.c libbpf AF_XDP userspace API 구현
tools/lib/bpf/xsk.h xsk_socket, xsk_umem, xsk_ring API 헤더
samples/bpf/xdpsock_user.c AF_XDP RX/TX/L2FWD 벤치마크 샘플
tools/testing/selftests/bpf/xdp* AF_XDP 커널 셀프테스트
drivers/net/ethernet/intel/ice/ice_xsk.c ice 드라이버 ZC 구현 (참조용)

주요 발표/영상

다음 학습:
필수 관련 문서: 참고 문서: