DPDK
DPDK(Data Plane Development Kit) 고성능 패킷(Packet) 처리 구조를 심층 분석합니다. EAL 초기화와 hugepage/NUMA 메모리 모델, IOVA(VA/PA), PMD poll-mode 루프, rte_mbuf/ring/graph/mempool 데이터 경로, VFIO/UIO 및 bifurcated PMD, AF_XDP와 representor/rte_flow/hairpin, dmadev, OVS-DPDK·VPP·testpmd 운영 시나리오, trace·metrics·power 관리와 하드닝·트러블슈팅까지 실무 중심으로 다룹니다.
핵심 요약
- EAL (Environment Abstraction Layer) — DPDK의 초기화 계층으로, CPU 코어 바인딩, 휴즈페이지(Hugepage) 메모리 할당, PCI 디바이스 탐색 등 플랫폼 추상화를 담당합니다.
- PMD (Poll Mode Driver) — 인터럽트 없이 폴링(Polling) 방식으로 NIC 큐를 직접 읽어 패킷을 수신합니다. 커널 드라이버를 완전히 우회하는 DPDK의 핵심 메커니즘입니다.
- rte_mbuf — DPDK의 패킷 버퍼 구조체로, 커널의
sk_buff를 대체합니다. 메모리 풀에서 사전 할당되어 할당/해제 오버헤드를 최소화합니다. - Ring 라이브러리 — 락-프리(Lock-free) FIFO 큐로, 코어 간 패킷 전달에 사용됩니다. SPSC/MPMC 등 다양한 생산자-소비자 모드를 지원합니다.
- Mempool —
rte_mbuf객체를 사전 할당하는 메모리 풀입니다. NUMA 노드별 할당과 코어별 로컬 캐시로 메모리 접근 지연을 줄입니다. - VFIO/UIO — NIC를 커널 드라이버에서 분리하여 유저 공간 PMD에 바인딩하는 디바이스 바인딩 메커니즘입니다.
- rte_flow — NIC 하드웨어 수준에서 플로우 분류(Flow Classification)와 오프로드 규칙을 정의하는 API입니다.
- OVS-DPDK — Open vSwitch의 DPDK 백엔드로, 가상화 환경에서 vhost-user를 통해 VM 간 고속 패킷 스위칭을 수행합니다.
단계별 이해
- EAL 환경 구성 — 휴즈페이지와 CPU 코어를 준비합니다.
DPDK는 커널 메모리 할당자를 사용하지 않으므로, 휴즈페이지(2MB/1GB)를 먼저 예약하고
rte_eal_init()으로 코어·메모리·PCI 디바이스를 초기화합니다. - NIC 바인딩과 PMD 이해 — 커널 드라이버를 분리하고 유저 공간에서 NIC를 직접 제어합니다.
dpdk-devbind.py로 NIC를 VFIO 또는 UIO에 바인딩한 뒤, PMD가 폴링 루프에서rte_eth_rx_burst()/rte_eth_tx_burst()로 패킷을 주고받는 구조를 학습합니다. - mbuf·Mempool·Ring 파이프라인 구성 — 패킷 버퍼 할당과 코어 간 전달 경로를 설계합니다.
Mempool에서 mbuf를 할당받아 수신하고, Ring 큐로 워커(Worker) 코어에 전달한 뒤 송신하는 기본 파이프라인을 구현합니다.
- rte_flow로 하드웨어 오프로드 적용 — NIC 수준에서 플로우 분류와 RSS(Receive Side Scaling) 규칙을 설정합니다.
소프트웨어 분류 전에 NIC가 먼저 패킷을 분류하도록
rte_flow규칙을 작성하여 CPU 부하를 줄입니다. - 성능 측정과 병목 분석 — PPS, 지연, 캐시 미스를 계측하여 최적화합니다.
dpdk-testpmd로 기본 성능을 측정하고,dpdk-proc-info와 perf로 코어별 패킷 처리량과 LLC 캐시 미스를 추적합니다.
DPDK는 Intel이 주도하여 개발한 고성능 패킷 처리 프레임워크로, 커널 네트워크 스택을 완전히 바이패스하여 유저 공간에서 NIC를 직접 제어합니다. 표준 커널 드라이버가 인터럽트(Interrupt) 기반으로 패킷을 처리하는 반면, DPDK는 폴링(polling) 기반 PMD(Poll Mode Driver)를 사용하여 컨텍스트 스위칭(Context Switching)과 인터럽트 오버헤드(Overhead)를 제거합니다. 10~400GbE 환경에서 수십~수백 Mpps(백만 패킷/초)의 처리량을 단일 서버에서 달성할 수 있으며, 통신사(NFV), 클라우드(가상 스위칭), 금융(초저지연 트레이딩), CDN, 보안 장비 등에서 핵심 기술로 사용됩니다.
VFIO, UIO, hugepages, IOMMU 등
여러 서브시스템에 의존하며, 최근에는 커널의 AF_XDP 소켓(Socket)을 PMD 백엔드로 사용하는
하이브리드 접근도 지원합니다. 커널 개발자가 DPDK의 동작 원리를 이해하면
드라이버 최적화, VFIO/IOMMU 튜닝, AF_XDP 개발 등에서 큰 도움이 됩니다.
DPDK 아키텍처 개요
| 계층 | 구성 요소 | 역할 |
|---|---|---|
| Application | l2fwd, l3fwd, OVS-DPDK, VPP, Pktgen | 패킷 처리 로직 구현 |
| Core Libraries | mbuf, Ring, Mempool, Hash, LPM, ACL | 고성능 데이터 구조 및 알고리즘 |
| EAL | Environment Abstraction Layer | hugepage, CPU 코어, PCI 디바이스, 타이머(Timer), 로깅 추상화 |
| PMD | Poll Mode Driver (ixgbe, i40e, mlx5, virtio, af_xdp...) | NIC별 RX/TX 드라이버 (유저 공간) |
| Kernel | VFIO, UIO, hugepages, IOMMU | 디바이스 접근 및 메모리 관리(Memory Management) 지원 |
| Hardware | NIC, DMA, PCIe BAR | 물리적 패킷 송수신, DMA 전송 |
커널 네트워크 스택 vs DPDK 비교
| 항목 | 커널 네트워크 스택 | DPDK |
|---|---|---|
| 실행 공간 | 커널 공간(Kernel Space) | 유저 공간 |
| 패킷 수신 모델 | 인터럽트 → NAPI 폴링 (하이브리드) | 100% 폴링 (busy-wait) |
| 패킷 버퍼 | sk_buff (동적 할당) | rte_mbuf (사전 할당 Mempool) |
| 프로토콜 스택 | 완전한 L2~L7 (TCP/IP, Netfilter, conntrack...) | 없음 (앱이 직접 구현 또는 별도 라이브러리) |
| CPU 사용 | 필요 시만 사용 (이벤트 기반) | 전용 코어 상시 100% 사용 (busy-poll) |
| 지연 시간 | 수~수십 μs | 수백 ns ~ 수 μs |
| 처리량 (64B pkt) | ~1-5 Mpps/core | ~14.88 Mpps/core (10GbE wire rate) |
| 메모리 관리 | SLAB/SLUB + per-CPU 캐시(Cache) | Hugepage 기반 Mempool |
| 보안/격리(Isolation) | 네임스페이스(Namespace), Netfilter, SELinux 등 | IOMMU/VFIO로 DMA 격리만 |
| 도구/디버깅(Debugging) | tcpdump, ss, ip, nftables, eBPF | dpdk-proc-info, dpdk-pdump, DPDK telemetry |
| 적용 분야 | 범용 서버/데스크탑/IoT | NFV, SDN 스위칭, 패킷 브로커, DPI, 로드밸런서 |
항목별 상세 비교
위 표의 각 항목이 왜 차이를 만드는지 구체적으로 살펴봅니다.
- 패킷 수신 모델 — 커널은 NIC 인터럽트 → softirq → NAPI 폴링의 하이브리드 모델입니다. 인터럽트 진입/탈출에 약 1μs, softirq 스케줄링에 추가 지연이 발생합니다. DPDK는 전용 코어가
rte_eth_rx_burst()를 상시 호출하는 busy-poll 방식이므로 인터럽트 지연이 0입니다. - 패킷 버퍼 —
sk_buff는 범용 구조체로 200바이트 이상의 메타데이터를 포함하며 동적 할당/해제 비용이 큽니다.rte_mbuf는 128바이트(2캐시라인) 고정 구조체로 사전 할당된 Mempool에서 꺼내 쓰므로 할당 비용이 거의 없습니다. - 프로토콜 스택 — 커널 스택은 L2→L3→L4→Netfilter→conntrack→socket 계층을 모두 통과합니다. DPDK 애플리케이션은 필요한 프로토콜만 직접 파싱하므로 불필요한 계층을 건너뜁니다. 다만 TCP/IP가 필요하면 VPP나 F-Stack 같은 유저 공간 스택을 별도로 통합해야 합니다.
- CPU 사용 패턴 — 커널은 이벤트 기반이므로 유휴 시 CPU를 반납합니다. DPDK는 할당된 코어를 100% 점유하므로, 패킷이 없어도 CPU를 소비합니다. 8코어 서버에서 DPDK에 4코어를 할당하면 나머지 서비스는 4코어만 사용할 수 있습니다.
- 보안/격리 — 커널 스택은 네임스페이스, cgroup, Netfilter, SELinux/AppArmor로 다층 격리를 제공합니다. DPDK는 IOMMU/VFIO 수준의 DMA 격리만 있으므로, 멀티테넌트(Multi-tenant) 환경에서는 애플리케이션 수준의 격리를 별도로 구현해야 합니다.
- 처리량 차이의 원인 — 64바이트 패킷 기준으로 커널 스택은 코어당 1~5 Mpps인 반면, DPDK PMD는 14.88 Mpps(10GbE 와이어레이트)에 도달합니다. 이 3~15배 차이는 인터럽트 제거, 복사 제거, 배치 처리(burst), 캐시라인 정렬 최적화가 누적된 결과입니다.
iptables, tc, conntrack, TCP/IP 프로토콜 처리 등
커널이 제공하는 모든 기능을 사용할 수 없습니다. 이런 기능이 필요하면 앱에서 직접 구현하거나
VPP/FD.io 같은 유저 공간 네트워크 스택을 사용해야 합니다. 또한 전용 CPU 코어를 상시 폴링에 사용하므로
코어 수가 제한된 환경에서는 비효율적입니다.
EAL (Environment Abstraction Layer)
EAL은 DPDK의 초기화 계층으로, 애플리케이션이 하드웨어와 OS 세부 사항에 독립적으로 동작하도록 추상화합니다.
rte_eal_init() 호출 시 수행되는 핵심 작업:
/* DPDK 애플리케이션 기본 구조 */
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>
int main(int argc, char *argv[])
{
/* EAL 초기화: hugepage 매핑, 코어 설정, PCI 스캔 */
int ret = rte_eal_init(argc, argv);
if (ret < 0)
rte_exit(EXIT_FAILURE, "EAL init failed\\n");
/* Mempool 생성 (패킷 버퍼 풀) */
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
"MBUF_POOL",
8192, /* 풀 크기 (mbuf 개수) */
256, /* per-core 캐시 크기 */
0, /* priv_size */
RTE_MBUF_DEFAULT_BUF_SIZE, /* 데이터룸 크기 */
rte_socket_id() /* NUMA 소켓 */
);
/* 포트 설정 및 시작 */
struct rte_eth_conf port_conf = { 0 };
rte_eth_dev_configure(port_id, nb_rx_q, nb_tx_q, &port_conf);
rte_eth_rx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
NULL, mbuf_pool);
rte_eth_tx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
NULL);
rte_eth_dev_start(port_id);
/* 메인 패킷 처리 루프 (busy-poll) */
while (!force_quit) {
struct rte_mbuf *bufs[32];
uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, 32);
if (nb_rx == 0)
continue;
/* 패킷 처리 ... */
process_packets(bufs, nb_rx);
uint16_t nb_tx = rte_eth_tx_burst(port_id, 0, bufs, nb_rx);
for (uint16_t i = nb_tx; i < nb_rx; i++)
rte_pktmbuf_free(bufs[i]); /* 전송 실패 mbuf 반환 */
}
rte_eal_cleanup();
return 0;
}
-l 0-3— 사용할 논리 코어 목록. DPDK 워커 스레드(Thread)가 이 코어에 1:1 매핑(Mapping)-n 4— 메모리 채널 수 (NUMA interleaving 최적화)--socket-mem 1024,1024— NUMA 노드별 hugepage 할당량 (MB)-a 0000:03:00.0— 허용할 PCI 디바이스 (allowlist)--file-prefix dpdk1— 여러 DPDK 프로세스(Process) 공존 시 hugepage 파일 구분--proc-type secondary— 멀티프로세스 모드 (primary/secondary)--no-huge— hugepage 없이 실행 (개발/테스트용, 성능 저하)
rte_eal_init() 내부 구현 분석
rte_eal_init()은 DPDK의 진입점으로, 내부적으로 20개 이상의 서브시스템을 순서대로 초기화합니다. 핵심 단계를 소스 수준에서 분석합니다.
/* lib/eal/linux/eal.c — rte_eal_init() 핵심 흐름 (간략화) */
int
rte_eal_init(int argc, char **argv)
{
/* ① 커맨드라인 파싱: -l, -n, --socket-mem, -a 등 */
eal_parse_args(argc, argv);
/* ② 프로세스 타입 결정: primary vs secondary */
rte_config.process_type = eal_proc_type_detect();
/* ③ 플러그인 로딩: lib/ 아래 PMD .so 동적 로드 */
eal_plugins_init();
/* ④ Hugepage 메모리 매핑 — 가장 중요한 단계 */
rte_eal_hugepage_init();
/* 내부:
* - /sys/kernel/mm/hugepages 스캔 → 2MB/1GB 크기별 페이지 수 확인
* - /dev/hugepages/에 파일 생성 → mmap(MAP_HUGETLB | MAP_POPULATE)
* - /proc/self/pagemap 읽어 물리 주소(PA) 획득 → IOVA PA 모드에 사용
* - NUMA 노드별 메모리 분배: --socket-mem 또는 자동 감지
*/
/* ⑤ 메모리 서브시스템 초기화 */
rte_eal_memzone_init(); /* 이름 기반 메모리 영역 관리 */
rte_eal_malloc_heap_init(); /* 힙 관리자: hugepage 위 동적 할당 */
/* ⑥ IOVA 모드 결정: VA vs PA */
iova_mode = rte_bus_get_iommu_class();
/* IOMMU 있음 → VA 모드 (유저 가상 주소 = IOVA)
* IOMMU 없음 → PA 모드 (물리 주소 = IOVA, root 필요) */
/* ⑦ lcore 스레드 생성 */
RTE_LCORE_FOREACH_WORKER(i) {
pthread_create(&lcore_config[i].thread_id,
NULL, eal_thread_loop, (void *)(uintptr_t)i);
/* 각 스레드를 지정된 CPU에 affinity 설정 */
pthread_setaffinity_np(lcore_config[i].thread_id,
sizeof(cpu_set), &lcore_config[i].cpuset);
}
/* ⑧ 버스 스캔: PCI, VDEV 등 모든 디바이스 열거 */
rte_bus_scan();
/* PCI 버스: /sys/bus/pci/devices/ 순회
* 각 디바이스의 vendor:device ID를 등록된 PMD와 매칭
* VFIO/UIO 바인딩 상태 확인 */
/* ⑨ 버스 프로빙: 매칭된 PMD로 디바이스 초기화 */
rte_bus_probe();
/* PMD의 probe() 콜백 호출
* PCI BAR mmap, 하드웨어 초기화, 포트 등록 */
/* ⑩ 서비스 코어, 타이머, 로깅 등 런타임 초기화 */
rte_service_init();
rte_timer_subsystem_init();
return fctret; /* 파싱된 EAL 인자 수 반환 */
}
코드 분석: rte_eal_init() 내부
- ④ rte_eal_hugepage_init(): EAL의 가장 핵심 단계입니다.
/sys/kernel/mm/hugepages/에서 사용 가능한 hugepage를 확인한 후,/dev/hugepages/에 파일을 만들고mmap(MAP_HUGETLB | MAP_POPULATE)로 매핑합니다.MAP_POPULATE가 핵심인데, 이 플래그가 없으면 첫 접근 시 페이지 폴트가 발생하여 데이터 경로에서 지연 스파이크를 일으킵니다. PA 모드에서는 추가로/proc/self/pagemap을 읽어 가상 주소 → 물리 주소 변환 테이블을 구축합니다. - ⑥ IOVA 모드:
rte_bus_get_iommu_class()는 VFIO가 사용 가능하면 VA 모드, UIO나 no-IOMMU면 PA 모드를 선택합니다. VA 모드에서는 유저 가상 주소를 그대로 IOVA로 사용하므로/proc/self/pagemap조회가 불필요하고, IOMMU가 가상→물리 변환을 처리합니다. - ⑦ lcore 스레드:
-l 0-3으로 지정된 각 CPU에 1개씩 pthread를 생성하고,pthread_setaffinity_np()로 해당 CPU에 고정합니다. 생성된 스레드는eal_thread_loop()에서rte_eal_mp_wait_lcore()가 호출될 때까지 대기하다가,rte_eal_remote_launch()로 워커 함수를 할당받으면 실행을 시작합니다. - ⑧⑨ 버스 스캔/프로빙:
rte_bus_scan()은/sys/bus/pci/devices/를 순회하며 모든 PCI 디바이스를 열거합니다.rte_bus_probe()는 각 디바이스의 vendor:device ID를 등록된 PMD의 ID 테이블과 비교하여 매칭되면 PMD의probe()콜백을 호출합니다. 이때 PCI BAR이 mmap되고, NIC 하드웨어가 초기화되며, ethdev 포트가 등록됩니다.
Hugepage 매핑 상세 과정
/* lib/eal/linux/eal_hugepage_info.c — hugepage 정보 수집 */
static int
hugepage_info_init(void)
{
/* /sys/kernel/mm/hugepages/ 아래 디렉토리 순회 */
/* hugepages-2048kB, hugepages-1048576kB 등 */
for (dirent = readdir(dir); dirent; dirent = readdir(dir)) {
/* 크기별 free/total hugepage 수 읽기 */
get_hugepage_dir(hpi->hugepage_sz, hpi->hugedir,
sizeof(hpi->hugedir));
/* 마운트포인트 확인: /dev/hugepages 또는 /mnt/huge 등 */
hpi->num_pages[socket] = get_num_hugepages(
hpi->hugedir, hpi->hugepage_sz, socket);
}
return 0;
}
/* lib/eal/linux/eal_memory.c — 실제 hugepage mmap */
static void *
alloc_hugepage(struct hugepage_info *hpi, int socket)
{
/* hugepage 파일 생성 */
fd = open(filepath, O_CREAT | O_RDWR, 0600);
ftruncate(fd, hpi->hugepage_sz);
/* mmap: MAP_HUGETLB로 hugepage 매핑
* MAP_POPULATE: 즉시 물리 페이지 할당 (fault 방지)
* MAP_SHARED: 멀티프로세스 모드에서 secondary와 공유 */
addr = mmap(NULL, hpi->hugepage_sz,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE | MAP_HUGETLB,
fd, 0);
/* mbind()로 NUMA 노드에 바인딩 */
mbind(addr, hpi->hugepage_sz, MPOL_BIND,
&nodemask, max_node, MPOL_MF_STRICT);
/* PA 모드: /proc/self/pagemap에서 물리 주소 획득 */
if (rte_eal_iova_mode() == RTE_IOVA_PA)
physaddr = rte_mem_virt2phy(addr);
return addr;
}
코드 분석: Hugepage 매핑
- hugepage_info_init(): sysfs를 읽어 시스템에서 사용 가능한 hugepage 크기와 개수를 파악합니다. DPDK는 가능한 가장 큰 hugepage(1GB > 2MB)를 우선 사용합니다. 1GB hugepage 1개 = 2MB hugepage 512개와 동일한 메모리를 TLB 엔트리 1개로 커버합니다.
- MAP_POPULATE: 이 플래그 없이 mmap하면 처음 접근 시 minor page fault가 발생합니다. DPDK 데이터 경로에서 page fault는 수십 μs의 지연 스파이크를 일으키므로, 초기화 시점에 모든 페이지를 미리 할당합니다.
- MAP_SHARED: 멀티프로세스 모드에서 primary와 secondary 프로세스가 동일한 hugepage 파일을 mmap하여 메모리를 공유합니다. mempool, ring 등의 데이터 구조가 이 공유 메모리 위에 배치됩니다.
- mbind(MPOL_BIND): 특정 NUMA 노드에서만 물리 메모리를 할당하도록 강제합니다. NIC이 NUMA 노드 0에 연결되어 있다면, 해당 NIC의 RX/TX 큐에 사용할 mempool도 노드 0에 배치해야 크로스-NUMA 지연(40~100ns)을 방지할 수 있습니다.
- rte_mem_virt2phy():
/proc/self/pagemap을 읽어 가상 주소를 물리 주소로 변환합니다. PA 모드에서 NIC DMA가 물리 주소로 접근하므로 이 변환이 필수입니다. VA 모드에서는 IOMMU가 변환을 처리하므로 이 단계가 생략됩니다.
PCI 버스 프로빙과 PMD 매칭
/* drivers/bus/pci/linux/pci.c — PCI 디바이스 스캔 */
static int
pci_scan_one(const char *dirname, const struct rte_pci_addr *addr)
{
struct rte_pci_device *dev;
dev = malloc(sizeof(*dev));
dev->addr = *addr;
/* sysfs에서 디바이스 정보 읽기 */
snprintf(filename, sizeof(filename),
"%s/vendor", dirname);
pci_parse_sysfs_value(filename, &dev->id.vendor_id);
snprintf(filename, sizeof(filename),
"%s/device", dirname);
pci_parse_sysfs_value(filename, &dev->id.device_id);
/* NUMA 노드 확인 */
snprintf(filename, sizeof(filename),
"%s/numa_node", dirname);
pci_parse_sysfs_value(filename, &dev->device.numa_node);
/* VFIO/UIO 바인딩 드라이버 확인 */
snprintf(filename, sizeof(filename),
"%s/driver", dirname);
dev->kdrv = pci_get_kernel_driver_by_path(filename);
/* RTE_PCI_KDRV_VFIO, RTE_PCI_KDRV_UIO_GENERIC 등 */
/* PCI BAR 리소스 매핑 정보 */
pci_parse_sysfs_resource(dirname, dev);
rte_pci_insert_device(dev);
return 0;
}
/* drivers/bus/pci/pci_common.c — PMD 매칭과 프로빙 */
static int
pci_probe_all_drivers(struct rte_pci_device *dev)
{
/* 등록된 모든 PMD 드라이버를 순회 */
TAILQ_FOREACH(dr, &rte_pci_bus.driver_list, next) {
/* vendor:device ID 매칭 */
if (rte_pci_match(dr, dev)) {
/* PMD의 probe() 콜백 호출 */
ret = dr->probe(dr, dev);
/* probe() 내부:
* 1. PCI BAR mmap (VFIO/UIO 경유)
* 2. NIC 하드웨어 리셋 + 초기화
* 3. rte_eth_dev 등록 (포트 할당)
* 4. dev_ops 함수 포인터 테이블 설정
*/
return ret;
}
}
return -ENODEV;
}
코드 분석: PCI 프로빙
- pci_scan_one():
/sys/bus/pci/devices/0000:XX:YY.Z/아래의 vendor, device, numa_node, driver 등 sysfs 파일을 읽어 PCI 디바이스 정보를 수집합니다. 이 정보는 이후 PMD 매칭에 사용됩니다. - kdrv 필드: NIC이 현재 어떤 커널 드라이버에 바인딩되어 있는지 확인합니다.
vfio-pci면 VFIO 경로,uio_pci_generic이면 UIO 경로로 BAR 매핑을 수행합니다. 커널 NIC 드라이버(ixgbe, i40e 등)에 바인딩된 상태면 DPDK가 사용할 수 없습니다. - rte_pci_match(): PMD가 등록한
pci_id_table[]의 vendor:device:subsystem ID와 스캔된 디바이스의 ID를 비교합니다. 예: ixgbe PMD는 Intel 82599(8086:10FB), i40e PMD는 Intel X710(8086:1572)에 매칭됩니다. - probe() 콜백: 매칭된 PMD의
probe()가 호출되면, (1) VFIO를 통해 PCI BAR을 mmap하고, (2) NIC 하드웨어를 리셋/초기화하고, (3)rte_eth_dev구조체를 등록하여 ethdev 포트를 생성합니다. 이후 애플리케이션은rte_eth_dev_configure()로 큐를 설정합니다.
PMD (Poll Mode Driver)
PMD는 DPDK의 핵심으로, 커널 드라이버 없이 유저 공간에서 NIC를 직접 제어합니다.
NIC의 PCIe BAR를 mmap()하여 하드웨어 레지스터(Register)에 직접 접근하고,
DMA 디스크립터 링(descriptor ring)을 유저 공간 메모리에 배치합니다.
전통적인 네트워크 스택은 NIC가 패킷을 수신하면 커널이 Interrupt를 발생시켜 처리합니다. 하지만 고속 패킷 처리에서 Interrupt 기반 모델은:
- Interrupt 오버헤드: 초당 수백만 개의 Interrupt가 발생하여 CPU 부하 증가
- 컨텍스트 스위치: 커널-유저 공간 전환 비용 발생
- 복사 오버헤드: 패킷이 커널 버퍼 → 유저 버퍼로 복사
PMD는 이 문제를 해결하기 위해 폴링(Polling) 방식으로 동작합니다. 애플리케이션이 직접 NIC를 폴링하여 패킷을 수신하므로 오버헤드가 없습니다.
RX (수신) 동작 원리
PMD의 RX 동작은 생산자-소비자(Producer-Consumer) 패턴을 따릅니다:
- 초기 상태: 빈 Descriptor
PMD는 초기화 시에 mbuf 메모리 풀(mempool)에서 Descriptor마다 빈 mbuf를 할당하여 Descriptor Ring에 채워둡니다. 이 mbuf는 패킷 데이터를 저장할 수 있는 버퍼입니다. - NIC의 DMA 동작
NIC가 네트워크 케이블에서 패킷을 수신하면, Descriptor에 있는 mbuf 주소로 DMA를 통해 패킷 데이터를 직접 메모리에 기록합니다. 이때 Descriptor의 DD(Descriptor Done) 비트를 1로 설정합니다. - PMD의 폴링
애플리케이션이rte_eth_rx_burst()를 호출하면, PMD는 Descriptor Ring을 순회하며 각 Descriptor의 DD 비트를 확인합니다. - 패킷 처리
DD=1인 Descriptor(패킷이 도착한)를 찾으면:- Descriptor에서 패킷 메타데이터(길이, RSS hash, VLAN 등)를 추출
- mbuf에 패킷 데이터가 있으므로, 이를 mbuf 포인터 배열에 추가
- Descriptor 재사용
사용된 Descriptor에 새로운 mbuf를 할당하여 채워 넣고, Tail Register를 갱신하여 NIC에게 "이 Descriptor를 사용 가능"함을 알려줍니다.
이 과정이 매번 반복되며, PMD는 Interrupt 없이도 패킷을 지속적으로 수신할 수 있습니다.
/* PMD의 RX burst 내부 (간략화) — ixgbe_recv_pkts() 기반 */
static uint16_t
pmd_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
{
struct pmd_rx_queue *rxq = rx_queue;
volatile union rx_desc *rxdp; /* 하드웨어 descriptor 포인터 */
uint16_t nb_rx = 0;
while (nb_rx < nb_pkts) {
rxdp = &rxq->rx_ring[rxq->rx_tail];
/* DD 비트 확인: NIC가 DMA를 완료했는가? */
if (!(rxdp->wb.status & RX_DESC_DD))
break; /* 더 이상 수신된 패킷 없음 */
/* mbuf 회수 및 메타데이터 설정 */
struct rte_mbuf *mb = rxq->sw_ring[rxq->rx_tail];
mb->pkt_len = rxdp->wb.pkt_len;
mb->data_len = rxdp->wb.pkt_len;
mb->hash.rss = rxdp->wb.rss_hash;
mb->vlan_tci = rxdp->wb.vlan_tag;
rx_pkts[nb_rx++] = mb;
/* 새 mbuf를 할당하여 descriptor에 채움 (NIC에 반환) */
struct rte_mbuf *nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
rxdp->read.pkt_addr = rte_mbuf_data_iova(nmb);
rxq->sw_ring[rxq->rx_tail] = nmb;
rxq->rx_tail = (rxq->rx_tail + 1) & rxq->rx_tail_mask;
}
/* tail 레지스터 갱신 → NIC에 새 descriptor 사용 가능 알림 */
if (nb_rx)
rte_write32(rxq->rx_tail, rxq->tail_ptr);
return nb_rx;
}
/* PMD의 TX burst 내부 (간략화) */
static uint16_t
pmd_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
{
struct pmd_tx_queue *txq = tx_queue;
uint16_t nb_tx = 0;
/* 이전에 전송 완료된 descriptor 정리 (mbuf 반환) */
tx_free_bufs(txq);
while (nb_tx < nb_pkts) {
volatile union tx_desc *txdp = &txq->tx_ring[txq->tx_tail];
struct rte_mbuf *mb = tx_pkts[nb_tx];
/* TX descriptor에 mbuf의 DMA 주소와 길이 설정 */
txdp->read.buffer_addr = rte_mbuf_data_iova(mb);
txdp->read.cmd_type_len = mb->data_len | TX_DESC_EOP | TX_DESC_RS;
txq->sw_ring[txq->tx_tail] = mb;
txq->tx_tail = (txq->tx_tail + 1) & txq->tx_tail_mask;
nb_tx++;
}
/* tail 레지스터 갱신 → NIC가 전송 시작 */
rte_wmb(); /* 메모리 배리어: descriptor 쓰기 완료 보장 */
rte_write32(txq->tx_tail, txq->tail_ptr);
return nb_tx;
}
코드 분석: PMD RX/TX 구현 핵심
- pmd_recv_pkts() — RX 경로:
- rx_ring[rx_tail]: NIC의 하드웨어 descriptor ring은 hugepage 위의 DMA 가능 메모리에 배치됩니다.
volatile키워드는 NIC가 DMA로 descriptor를 수정하므로 컴파일러 최적화를 방지합니다. - DD 비트 검사:
rxdp->wb.status & RX_DESC_DD는 NIC가 DMA 완료 후 설정하는 "Descriptor Done" 비트입니다. 이 비트가 0이면 아직 패킷이 도착하지 않았으므로 루프를 종료합니다. 이것이 "폴링"의 실체입니다. - sw_ring: 하드웨어 descriptor에는 DMA 물리 주소만 있으므로, 대응하는
rte_mbuf가상 주소 포인터를 소프트웨어 링(sw_ring[])에 별도로 유지합니다. - rte_mbuf_raw_alloc(): 사용된 mbuf를 대체하기 위해 mempool에서 새 mbuf를 즉시 할당합니다. 이 "swap" 패턴이 제로카피(Zero-copy)의 핵심입니다 — 패킷 데이터를 복사하지 않고, mbuf 포인터만 교체합니다.
- rte_write32(rx_tail): NIC의 MMIO Tail Register에 새 tail 위치를 기록합니다. NIC는 이 레지스터를 폴링하여 사용 가능한 descriptor가 추가되었음을 인지하고, 해당 descriptor의 mbuf 주소로 다음 패킷을 DMA합니다.
- rx_ring[rx_tail]: NIC의 하드웨어 descriptor ring은 hugepage 위의 DMA 가능 메모리에 배치됩니다.
- pmd_xmit_pkts() — TX 경로:
- tx_free_bufs(): 이전 호출에서 NIC가 전송 완료한 descriptor를 확인(DD 비트)하고, 해당 mbuf를 mempool에 반환합니다. TX에서 mbuf 반환 시점이 RX와 다른 핵심 차이점입니다.
- TX_DESC_EOP | TX_DESC_RS: EOP(End Of Packet)은 이 descriptor가 패킷의 마지막 세그먼트임을, RS(Report Status)는 NIC가 전송 완료 후 DD 비트를 설정하라는 요청입니다. RS를 매 descriptor마다 설정하면 오버헤드가 커지므로, 보통 N개(예: 32)마다 한 번씩 설정합니다.
- rte_wmb(): Write Memory Barrier입니다. descriptor 메모리 쓰기가 CPU 파이프라인을 벗어나 실제 메모리에 반영된 후에 Tail Register를 갱신하도록 보장합니다. 이 배리어가 없으면 NIC이 아직 쓰이지 않은 descriptor를 읽어 잘못된 주소로 DMA할 수 있습니다.
TX (송신) 동작 원리
TX 동작은 RX의 역순으로 진행됩니다:
- 애플리케이션의 패킷 준비
애플리케이션이 전송할 패킷이 담긴 mbuf 배열을 준비합니다. - Descriptor 채우기
PMD는 각 mbuf의 DMA 주소와 길이를 TX Descriptor에 기록합니다. 명령어 필드에 패킷 끝(EOP), Report Status(RS) 플래그를 설정합니다. - Tail Register 갱신
모든 Descriptor를 채운 후, 메모리 배리어(Memory Barrier)를 수행하고 Tail Register에 새 위치를 기록합니다. 이를 통해 NIC에게 "이제 전송해도 좋다"고 알려줍니다. - NIC의 DMA 및 전송
NIC가 Descriptor를 읽어 mbuf에서 데이터를 DMA로 읽어 네트워크 케이블로 전송합니다. - Descriptor 회수
전송이 완료되면 NIC가 Descriptor의 DD 비트를 설정하고, PMD가 다음 번 호출 시 사용된 mbuf를 회수하여 Mempool에 반환합니다.
rte_wmb()는 Descriptor 쓰기가 실제 메모리에 완료된 후 Tail Register를 갱신하도록 보장합니다.
이를 통해 NIC가 Descriptor를 읽을 때 데이터가 아직 메모리에 쓰여지지 않은 추측적 읽기를 방지합니다.| PMD 종류 | 백엔드 | 대표 드라이버 | 특징 |
|---|---|---|---|
| Physical PMD | VFIO / UIO | ixgbe, i40e, ice, mlx5, bnxt, ena | 실제 NIC 직접 제어, 최고 성능 |
| Virtual PMD | virtio | virtio-net, vmxnet3, avf | VM 내부에서 가상 NIC 접근 |
| AF_XDP PMD | 커널 AF_XDP 소켓 | af_xdp | 커널 기능 유지하면서 고성능, VFIO 불필요 |
| SW PMD | libpcap / TAP | pcap, net_tap | 개발/테스트용, 커널 NIC에 연결 |
| Crypto PMD | QAT / AESNI / SW | qat, aesni_mb, openssl | 암호화(Encryption) 가속기 접근 |
| Compress PMD | QAT / zlib | qat_comp, zlib | 압축 가속기 |
| Event PMD | 하드웨어 이벤트 스케줄러(Scheduler) | dlb2, sw_event | 이벤트 기반 패킷 분배 |
Rx/Tx 오프로드 플래그
DPDK PMD는 NIC 하드웨어의 오프로드 기능을 ol_flags와 tx_offload 필드로 노출합니다.
올바른 오프로드 설정은 CPU 부하를 NIC으로 이전하여 처리량을 크게 향상시킵니다.
| Rx 오프로드 | 플래그 | 동작 |
|---|---|---|
| IP Checksum 검증 | RTE_ETH_RX_OFFLOAD_IPV4_CKSUM | NIC가 IPv4 헤더 체크섬(Checksum)을 검증, 결과를 ol_flags에 기록 |
| TCP/UDP Checksum 검증 | RTE_ETH_RX_OFFLOAD_TCP_CKSUM | NIC가 L4 체크섬 검증 |
| VLAN 스트리핑 | RTE_ETH_RX_OFFLOAD_VLAN_STRIP | NIC가 VLAN 태그를 제거하고 vlan_tci에 저장 |
| RSS | RTE_ETH_RX_OFFLOAD_RSS_HASH | NIC가 패킷 해시(Hash) 계산, hash.rss에 저장 |
| Scatter (SG) | RTE_ETH_RX_OFFLOAD_SCATTER | Jumbo Frame을 다중 mbuf에 분산 수신 |
| LRO | RTE_ETH_RX_OFFLOAD_TCP_LRO | NIC가 TCP 세그먼트를 병합 (GRO의 하드웨어 버전) |
| Timestamp | RTE_ETH_RX_OFFLOAD_TIMESTAMP | NIC가 수신 타임스탬프 기록 (PTP/IEEE 1588) |
| Tx 오프로드 | 플래그 | 동작 |
|---|---|---|
| IP Checksum 계산 | RTE_ETH_TX_OFFLOAD_IPV4_CKSUM | NIC가 송신 시 IPv4 체크섬 계산 (앱은 0으로 설정) |
| TCP/UDP Checksum | RTE_ETH_TX_OFFLOAD_TCP_CKSUM | NIC가 L4 체크섬 계산 |
| TSO (TCP Segmentation) | RTE_ETH_TX_OFFLOAD_TCP_TSO | NIC가 대형 TCP 세그먼트를 MSS 단위로 분할 |
| VLAN 삽입 | RTE_ETH_TX_OFFLOAD_VLAN_INSERT | NIC가 VLAN 태그 삽입 |
| Multi-segment | RTE_ETH_TX_OFFLOAD_MULTI_SEGS | Scatter-Gather 송신 (다중 mbuf 체인) |
| VXLAN/GRE TSO | RTE_ETH_TX_OFFLOAD_VXLAN_TNL_TSO | 터널(Tunnel) 내부 TCP에 대한 TSO |
/* Tx 오프로드 설정 예시: TSO + Checksum */
struct rte_eth_conf port_conf = {
.txmode = {
.offloads = RTE_ETH_TX_OFFLOAD_IPV4_CKSUM
| RTE_ETH_TX_OFFLOAD_TCP_CKSUM
| RTE_ETH_TX_OFFLOAD_TCP_TSO,
},
};
/* 패킷 송신 시 mbuf 오프로드 필드 설정 */
pkt->ol_flags = RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CKSUM
| RTE_MBUF_F_TX_TCP_CKSUM | RTE_MBUF_F_TX_TCP_SEG;
pkt->l2_len = sizeof(struct rte_ether_hdr); /* 14 */
pkt->l3_len = sizeof(struct rte_ipv4_hdr); /* 20 */
pkt->l4_len = sizeof(struct rte_tcp_hdr); /* 20 (옵션 없음) */
pkt->tso_segsz = 1460; /* MSS */
/* Checksum: pseudo-header checksum을 앱이 계산, 나머지는 NIC */
rte_ipv4_phdr_cksum(ip_hdr, pkt->ol_flags);
/* NIC capability 조회 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);
printf("RX offloads: 0x%lx\\n", dev_info.rx_offload_capa);
printf("TX offloads: 0x%lx\\n", dev_info.tx_offload_capa);
l2_len/l3_len이 틀리면 NIC가 잘못된 위치에 체크섬을 계산합니다.
testpmd의 csum fwd 모드로 오프로드 조합을 먼저 검증하세요.
벡터화 PMD와 스칼라 PMD
DPDK PMD는 동일 NIC에 대해 여러 코드 경로를 제공합니다. 벡터화(vectorized) PMD는 SSE/AVX2/AVX-512/NEON SIMD 명령어를 사용하여 한 번에 여러 descriptor를 처리하므로 스칼라 경로 대비 1.5~3배의 처리량을 달성합니다.
| PMD 경로 | 특징 | 활성화 조건 |
|---|---|---|
| 벡터 (AVX-512) | 16개 descriptor 동시 처리, 최고 성능 | AVX-512 CPU + 단순 오프로드 + 단일 세그먼트 |
| 벡터 (AVX2) | 8개 descriptor 동시 처리 | AVX2 CPU + 제한된 오프로드 |
| 벡터 (SSE/NEON) | 4개 descriptor 동시 처리 | 기본 x86-64 / ARM64 |
| 스칼라 (simple) | 1개씩 처리, 단순 오프로드만 | 벡터 비활성 시 fallback |
| 스칼라 (full) | 1개씩 처리, 모든 오프로드 지원 | TSO, Scatter, Tunnel 등 복잡한 오프로드 |
RTE_ETH_RX_OFFLOAD_SCATTER활성화 (Jumbo Frame)- 복잡한 Tx 오프로드 (TSO, 터널 체크섬)
- VLAN 스트리핑/삽입 (일부 PMD)
rte_eth_dev_info_get()의rx_burst_mode/tx_burst_mode로 현재 경로 확인 가능testpmd에서show port 0 rx_offload capabilities로 벡터 호환 오프로드 확인
벡터 PMD 구현 분석 (AVX2 RX 경로)
벡터 PMD가 어떻게 SIMD로 다수 descriptor를 동시 처리하는지 i40e AVX2 RX 경로를 기반으로 분석합니다.
/* drivers/net/i40e/i40e_rxtx_vec_avx2.c — AVX2 벡터 RX (간략화) */
static inline uint16_t
_recv_raw_pkts_vec_avx2(struct i40e_rx_queue *rxq,
struct rte_mbuf **rx_pkts,
uint16_t nb_pkts)
{
volatile union i40e_rx_desc *rxdp = rxq->rx_ring + rxq->rx_tail;
struct rte_mbuf **sw_ring = rxq->sw_ring + rxq->rx_tail;
uint16_t nb_rx = 0;
/* 8개 descriptor를 한 번에 256비트 AVX2 레지스터로 로드 */
while (nb_rx < nb_pkts) {
/* ① 8개 descriptor의 status 필드를 AVX2로 일괄 로드 */
__m256i descs_lo = _mm256_loadu_si256(
(__m256i *)(rxdp)); /* desc[0]~desc[3] */
__m256i descs_hi = _mm256_loadu_si256(
(__m256i *)(rxdp + 4)); /* desc[4]~desc[7] */
/* ② DD 비트 추출: 8개 descriptor 동시 검사 */
__m256i staterr = _mm256_and_si256(
_mm256_unpackhi_epi32(descs_lo, descs_hi),
dd_mask);
uint8_t dd_bits = (uint8_t)_mm256_movemask_epi8(
_mm256_cmpeq_epi32(staterr, dd_mask));
/* DD=0인 첫 번째 descriptor에서 중단 */
int num_valid = __builtin_ctz(~dd_bits) / 4;
if (num_valid == 0) break;
/* ③ 패킷 길이를 AVX2로 일괄 추출 → mbuf.pkt_len 설정 */
__m256i pktlen = _mm256_srli_epi32(
_mm256_and_si256(descs_lo, len_mask), 10);
/* shuffle로 pkt_len 필드 위치에 정렬 */
/* ④ mbuf 포인터를 rx_pkts[]에 일괄 저장 */
_mm256_storeu_si256((__m256i *)&rx_pkts[nb_rx],
_mm256_loadu_si256((__m256i *)&sw_ring[0]));
/* ⑤ 새 mbuf 할당 + descriptor 리필 (배치) */
i40e_rxq_rearm(rxq);
nb_rx += num_valid;
rxdp += 8;
sw_ring += 8;
}
return nb_rx;
}
코드 분석: AVX2 벡터 RX
- ① 256비트 일괄 로드:
_mm256_loadu_si256()는 256비트(32바이트)를 한 번에 로드합니다. i40e descriptor가 32바이트이므로, 두 번의 로드로 8개 descriptor(256바이트)를 처리합니다. 스칼라 경로에서 8번의 메모리 읽기가 필요한 작업을 2번으로 줄입니다. - ② DD 비트 일괄 검사:
_mm256_cmpeq_epi32+_mm256_movemask_epi8조합으로 8개 descriptor의 DD 비트를 한 번에 비교하여 비트마스크로 변환합니다.__builtin_ctz()로 연속된 유효 descriptor 수를 O(1)에 계산합니다. 스칼라 경로에서 8번의 조건 분기가 필요한 작업을 분기 없이 처리합니다. - ③ 패킷 길이 일괄 추출: descriptor의 pkt_len 필드를 비트 시프트와 마스크로 한 번에 추출하여 mbuf 구조체의 해당 위치에 기록합니다. SIMD shuffle 명령어로 바이트 위치를 재배열하는 기법이 핵심입니다.
- ④ mbuf 포인터 일괄 저장: sw_ring에서 8개 mbuf 포인터를
_mm256_loadu_si256()로 한 번에 읽어 rx_pkts[] 배열에 저장합니다. 8개의 포인터 복사가 단일 SIMD 명령어로 완료됩니다. - ⑤ descriptor 리필:
i40e_rxq_rearm()은 사용된 descriptor에 새 mbuf를 배치로 할당합니다. mempool에서 8개를 한 번에 가져와(rte_mempool_get_bulk) descriptor에 IOVA 주소를 기록합니다. - 성능 효과: 스칼라 경로 대비 (1) 메모리 로드 횟수 8배 감소, (2) 조건 분기 제거로 분기 예측 실패 없음, (3) 포인터 이동이 SIMD 레지스터 단위로 수행. 결과적으로 코어당 처리량이 1.5~2.5배 향상됩니다.
인터럽트 모드 (하이브리드 폴링)
DPDK는 100% busy-poll만 지원하는 것이 아닙니다. Rx 인터럽트 모드를 사용하면
빈 폴링 시 epoll_wait()로 블로킹하여 CPU를 절약하고,
패킷이 도착하면 VFIO eventfd 인터럽트로 깨어나 폴링을 재개합니다.
/* 인터럽트 모드: 빈 폴링 시 epoll 대기 */
while (!force_quit) {
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, 32);
if (nb_rx == 0) {
/* 패킷 없음 → 인터럽트 활성화 후 대기 */
rte_eth_dev_rx_intr_enable(port_id, queue_id);
struct rte_epoll_event ev;
rte_epoll_wait(epfd, &ev, 1, timeout_ms);
rte_eth_dev_rx_intr_disable(port_id, queue_id);
continue;
}
/* 패킷 있음 → 일반 폴링 처리 */
process_packets(pkts, nb_rx);
}
| 모드 | CPU 사용 | 지연 | 적합한 경우 |
|---|---|---|---|
| Pure polling | 100% (항상) | 최소 (~수백 ns) | 고 트래픽, 지연 민감 (NFV, 금융) |
| Interrupt + polling | 부하 비례 | 첫 패킷 ~수 μs (인터럽트 복귀) | 저 트래픽, 전력 민감, 코어 부족 |
| Empty poll power | 적응형 | 설정에 따라 가변 | 가변 부하, 전력 관리 필요 |
l3fwd-power는 인터럽트 모드와 empty poll 전력 관리를 함께 시연합니다.
이 예제를 분석하면 busy-poll과 인터럽트 하이브리드의 실제 구현 패턴을 이해할 수 있습니다.
rte_mbuf 구조체(Struct)
rte_mbuf는 DPDK의 패킷 버퍼로, 커널의 sk_buff에 대응하지만 설계 철학이 다릅니다.
sk_buff는 동적으로 할당되며 메타데이터가 풍부한 반면, rte_mbuf는 Mempool에서
사전 할당되어 고정 크기의 경량 구조체입니다.
/* rte_mbuf 핵심 필드 (lib/mbuf/rte_mbuf_core.h) */
struct rte_mbuf {
/* 캐시라인 0 (핫 필드) */
void *buf_addr; /* 가상 주소: headroom + 데이터 시작 */
rte_iova_t buf_iova; /* IO 가상 주소 (DMA용, IOVA) */
union {
struct {
uint32_t pkt_len; /* 전체 패킷 길이 (체인 포함) */
uint16_t data_len; /* 이 세그먼트의 데이터 길이 */
uint16_t vlan_tci; /* VLAN Tag */
};
};
uint64_t ol_flags; /* Offload 플래그 (checksum, TSO, VLAN...) */
union {
uint32_t rss; /* RSS 해시값 */
struct {
uint16_t hash;
uint16_t id;
} fdir; /* Flow Director 필터 ID */
} hash;
uint16_t data_off; /* buf_addr부터 데이터 시작까지 오프셋 (headroom) */
uint16_t refcnt; /* 참조 카운트 (공유 mbuf) */
uint16_t nb_segs; /* 체인된 세그먼트 수 */
uint16_t port; /* 입력 포트 ID */
/* 캐시라인 1 (TX offload) */
uint64_t tx_offload; /* L2/L3/L4 len, TSO segsz 등 (비트필드) */
struct rte_mempool *pool; /* 소속 Mempool (반환 시 사용) */
struct rte_mbuf *next; /* 다음 세그먼트 (체인) */
/* 메모리 배치: rte_mbuf 구조체 + headroom + packet data + tailroom */
/* 자세한 배치는 아래 SVG 참고 */
};
| 비교 항목 | sk_buff (커널) | rte_mbuf (DPDK) |
|---|---|---|
| 크기 | ~240 바이트 (가변) | 2 캐시라인 (128 바이트) 고정 |
| 할당 | __alloc_skb() → SLAB | Mempool에서 사전 할당 (rte_pktmbuf_alloc) |
| 해제 | kfree_skb() / consume_skb() | rte_pktmbuf_free() → Mempool 반환 |
| DMA 주소 | dma_map_single() 필요 | buf_iova 필드에 사전 매핑 |
| 메타데이터 | 풍부 (sk, dst, nf_bridge, tc...) | 최소한 (앱이 필요 시 priv_size로 확장) |
| 체인 | frag_list, skb_shinfo | next 포인터로 세그먼트 체인 |
| 복제 | skb_clone() (데이터 공유) | rte_pktmbuf_clone() (refcnt 증가) |
mbuf 조작 함수 상세 분석
DPDK의 mbuf 조작 함수들은 성능을 위해 대부분 static inline으로 구현됩니다. 핵심 함수의 내부 동작을 분석합니다.
/* lib/mbuf/rte_mbuf.h — mbuf 할당과 해제 핵심 구현 */
/* ━━━ 할당: rte_pktmbuf_alloc() ━━━ */
static inline struct rte_mbuf *
rte_pktmbuf_alloc(struct rte_mempool *mp)
{
struct rte_mbuf *m;
/* mempool에서 mbuf 1개를 꺼냄 */
if (rte_mempool_get(mp, (void **)&m) < 0)
return NULL;
/* mbuf 필드 초기화 (패킷 수신 준비) */
rte_pktmbuf_reset(m);
/* rte_pktmbuf_reset() 내부:
* m->next = NULL;
* m->pkt_len = 0;
* m->tx_offload = 0;
* m->vlan_tci = 0;
* m->nb_segs = 1;
* m->port = RTE_MBUF_PORT_INVALID;
* m->ol_flags = 0;
* m->data_off = RTE_PKTMBUF_HEADROOM; // 128바이트
* m->data_len = 0;
*/
return m;
}
/* ━━━ 해제: rte_pktmbuf_free() ━━━ */
static inline void
rte_pktmbuf_free(struct rte_mbuf *m)
{
struct rte_mbuf *m_next;
while (m != NULL) {
m_next = m->next;
/* 참조 카운트 감소: 0이면 실제 해제 */
if (__rte_mbuf_refcnt_update(m, -1) == 0) {
m->next = NULL;
rte_mbuf_raw_free(m);
/* → rte_mempool_put(m->pool, m)
* per-core 캐시에 반환 */
}
m = m_next;
}
}
/* ━━━ 데이터 접근: rte_pktmbuf_mtod() ━━━ */
#define rte_pktmbuf_mtod_offset(m, t, o) \
((t)((char *)(m)->buf_addr + (m)->data_off + (o)))
#define rte_pktmbuf_mtod(m, t) \
rte_pktmbuf_mtod_offset(m, t, 0)
/* 사용 예: 이더넷 헤더 접근 */
struct rte_ether_hdr *eth = rte_pktmbuf_mtod(m,
struct rte_ether_hdr *);
/* ━━━ 헤더 추가: rte_pktmbuf_prepend() ━━━ */
static inline char *
rte_pktmbuf_prepend(struct rte_mbuf *m, uint16_t len)
{
/* headroom 공간이 충분한지 확인 */
if (rte_pktmbuf_headroom(m) < len)
return NULL;
/* data_off를 앞으로 이동 → 헤더 공간 확보 */
m->data_off -= len;
m->data_len += len;
m->pkt_len += len;
return (char *)m->buf_addr + m->data_off;
}
/* ━━━ 데이터 추가: rte_pktmbuf_append() ━━━ */
static inline char *
rte_pktmbuf_append(struct rte_mbuf *m, uint16_t len)
{
void *tail;
struct rte_mbuf *m_last = rte_pktmbuf_lastseg(m);
/* tailroom 공간이 충분한지 확인 */
if (rte_pktmbuf_tailroom(m_last) < len)
return NULL;
/* 현재 데이터 끝 위치 계산 */
tail = (char *)m_last->buf_addr + m_last->data_off
+ m_last->data_len;
m_last->data_len += len;
m->pkt_len += len;
return (char *)tail;
}
/* ━━━ 복제: rte_pktmbuf_clone() ━━━ */
static inline struct rte_mbuf *
rte_pktmbuf_clone(struct rte_mbuf *md,
struct rte_mempool *mp)
{
struct rte_mbuf *mc;
/* 새 mbuf 할당 (간접 mbuf) */
mc = rte_pktmbuf_alloc(mp);
/* 원본의 buf_addr과 data_off를 공유 → 데이터 복사 없음 */
rte_pktmbuf_attach(mc, md);
/* attach 내부:
* mc->buf_addr = md->buf_addr;
* mc->buf_iova = md->buf_iova;
* mc->data_off = md->data_off;
* mc->data_len = md->data_len;
* rte_mbuf_refcnt_update(md, 1); // 원본 참조 카운트 증가
* mc->ol_flags |= IND_ATTACHED_MBUF;
*/
mc->pkt_len = md->pkt_len;
mc->nb_segs = md->nb_segs;
return mc;
}
코드 분석: mbuf 조작 함수
- rte_pktmbuf_alloc():
rte_mempool_get()은 먼저 per-core 로컬 캐시에서 mbuf를 꺼냅니다(락 없음, O(1)). 캐시가 비면 공용 Ring에서 배치로 가져옵니다.rte_pktmbuf_reset()은 메타데이터를 초기화하고data_off를 128바이트(headroom)로 설정합니다. headroom은 이후rte_pktmbuf_prepend()로 터널 헤더나 VLAN 태그를 추가할 공간입니다. - rte_pktmbuf_free(): 체인된 모든 세그먼트를 순회하며 각각의
refcnt를 감소시킵니다.refcnt가 0이 되면rte_mempool_put()으로 per-core 캐시에 반환합니다.rte_pktmbuf_clone()으로 복제된 mbuf는 원본의 refcnt가 2 이상이므로, clone을 free해도 원본 데이터가 유지됩니다. - rte_pktmbuf_mtod(): 매크로로 구현되어 함수 호출 오버헤드가 없습니다.
buf_addr + data_off가 패킷 데이터의 시작 주소입니다.data_off는prepend/adj연산에 의해 변경될 수 있습니다. - rte_pktmbuf_prepend(): headroom을 소비하여 패킷 앞에 헤더를 추가합니다. 데이터 복사 없이
data_off포인터만 이동하므로 O(1)입니다. 터널 캡슐화(VXLAN, GRE)에서 외부 헤더 추가에 사용됩니다. headroom이 부족하면 NULL을 반환하므로, 여러 단계의 캡슐화가 필요하면 headroom을 넉넉히(256바이트 이상) 설정해야 합니다. - rte_pktmbuf_append(): tailroom을 소비하여 패킷 뒤에 데이터를 추가합니다. 패딩, 트레일러, FCS 삽입 등에 사용됩니다. 멀티 세그먼트 mbuf에서는 마지막 세그먼트의 tailroom을 사용합니다.
- rte_pktmbuf_clone(): 데이터를 복사하지 않고 새 mbuf가 원본의 버퍼를 공유하는 "간접 mbuf"를 생성합니다. 원본의
refcnt를 1 증가시켜 원본이 먼저 해제되지 않도록 보호합니다. 패킷 미러링, 멀티캐스트(여러 포트로 동일 패킷 전송)에 유용합니다.
배치 할당/해제 (Bulk Operations)
/* 배치 할당: 한 번에 여러 mbuf를 mempool에서 꺼냄 */
struct rte_mbuf *mbufs[32];
int ret = rte_pktmbuf_alloc_bulk(pool, mbufs, 32);
/* 내부: rte_mempool_get_bulk()로 32개를 한 번에 가져옴
* → per-core 캐시에서 32개 pop (CAS 불필요)
* → 캐시 부족 시 공용 Ring에서 bulk 리필 (CAS 1회)
* → 개별 alloc 32번 호출 대비 ~3배 빠름 */
/* 배치 해제: 한 번에 여러 mbuf를 mempool에 반환 */
unsigned int i;
for (i = 0; i < nb_rx; i++)
pkts[i] = bufs[i]; /* 미전송 패킷 수집 */
rte_pktmbuf_free_bulk(pkts, nb_rx);
/* 내부: rte_mempool_put_bulk()로 한 번에 반환
* → per-core 캐시에 한 번에 push
* → 캐시 오버플로우 시 공용 Ring에 bulk 반환 */
rte_pktmbuf_alloc()/rte_pktmbuf_free()를 루프에서 호출하면 매번 캐시 상태를 확인합니다. _bulk() 변형은 단일 캐시 접근으로 여러 객체를 처리하므로 3~5배 빠릅니다. PMD 내부에서 descriptor 리필 시 항상 bulk 할당을 사용하는 이유입니다.
멀티 세그먼트 mbuf (Scatter-Gather)
단일 mbuf의 데이터 영역(기본 2048B)보다 큰 패킷(Jumbo Frame, 최대 9KB)은
여러 mbuf를 next 포인터로 체이닝하여 표현합니다.
이를 Scatter-Gather라고 하며, NIC의 SG DMA 기능과 연동됩니다.
/* 멀티 세그먼트 mbuf 순회 */
struct rte_mbuf *seg = pkt;
uint32_t total_len = 0;
while (seg != NULL) {
uint8_t *data = rte_pktmbuf_mtod(seg, uint8_t *);
process_segment(data, seg->data_len);
total_len += seg->data_len;
seg = seg->next;
}
/* total_len == pkt->pkt_len 이어야 정상 */
/* 멀티 세그먼트를 단일 세그먼트로 선형화 (필요 시) */
if (rte_pktmbuf_linearize(pkt) != 0)
rte_pktmbuf_free(pkt); /* 선형화 실패 (headroom 부족) */
PMD Scatter RX 구현 분석
NIC가 수신한 패킷이 단일 descriptor 버퍼(2048B)보다 크면, PMD는 여러 descriptor에 걸쳐 수신하고 mbuf를 체이닝합니다.
/* drivers/net/ixgbe/ixgbe_rxtx.c — Scatter RX 경로 (간략화) */
static uint16_t
ixgbe_recv_pkts_lro_bulk_alloc(void *rx_queue,
struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
{
struct ixgbe_rx_queue *rxq = rx_queue;
struct rte_mbuf *first_seg = NULL;
struct rte_mbuf *last_seg = NULL;
uint16_t nb_rx = 0;
while (nb_rx < nb_pkts) {
volatile union ixgbe_adv_rx_desc *rxdp;
rxdp = &rxq->rx_ring[rxq->rx_tail];
if (!(rxdp->wb.upper.status_error & IXGBE_RXDADV_STAT_DD))
break;
struct rte_mbuf *rxm = rxq->sw_ring[rxq->rx_tail];
rxm->data_len = rxdp->wb.upper.length;
/* ① 첫 번째 세그먼트인가? */
if (first_seg == NULL) {
first_seg = rxm;
first_seg->pkt_len = 0;
first_seg->nb_segs = 0;
}
/* ② 체인에 현재 세그먼트 연결 */
first_seg->pkt_len += rxm->data_len;
first_seg->nb_segs++;
if (last_seg != NULL)
last_seg->next = rxm;
last_seg = rxm;
rxm->next = NULL;
/* ③ EOP(End Of Packet) 비트: 패킷 완료? */
if (rxdp->wb.upper.status_error & IXGBE_RXDADV_STAT_EOP) {
/* 패킷 완전 수신 — 메타데이터 설정 */
first_seg->hash.rss = rxdp->wb.lower.hi_dword.rss;
first_seg->ol_flags = rx_desc_status_to_pkt_flags(
rxdp->wb.upper.status_error);
first_seg->port = rxq->port_id;
rx_pkts[nb_rx++] = first_seg;
first_seg = NULL;
last_seg = NULL;
}
/* 새 mbuf 할당하여 descriptor 리필 */
struct rte_mbuf *nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
rxdp->read.pkt_addr = rte_mbuf_data_iova_default(nmb);
rxq->sw_ring[rxq->rx_tail] = nmb;
rxq->rx_tail = (rxq->rx_tail + 1) & rxq->rx_tail_mask;
}
return nb_rx;
}
코드 분석: PMD Scatter RX
- first_seg / last_seg 추적: 패킷이 여러 descriptor에 걸칠 수 있으므로, 첫 번째 세그먼트와 마지막 세그먼트를 별도로 추적합니다. 새 descriptor를 처리할 때마다
last_seg->next = rxm으로 체인을 연장합니다. - EOP 비트: NIC는 패킷의 마지막 descriptor에 EOP(End Of Packet) 상태 비트를 설정합니다. EOP가 없는 descriptor는 패킷의 중간 세그먼트이므로, 다음 descriptor를 계속 체이닝합니다. EOP를 만나면
first_seg를rx_pkts[]에 저장하고 초기화합니다. - pkt_len vs data_len:
first_seg->pkt_len은 전체 패킷 길이(모든 세그먼트 합), 각 세그먼트의data_len은 해당 세그먼트만의 데이터 길이입니다. 첫 번째 세그먼트만pkt_len이 유효하고 후속 세그먼트의pkt_len은 0입니다. - 성능 영향: Scatter RX는 descriptor마다 조건 분기(EOP 체크)와 포인터 연결이 추가되므로 벡터 PMD를 사용할 수 없습니다. 9000B Jumbo Frame은 최소 5개 descriptor(9000/2048=4.4)를 소비하므로, 동일 PPS에서 descriptor 소비율이 ~5배 증가합니다.
rte_pktmbuf_chain()과 rte_pktmbuf_linearize() 구현
/* lib/mbuf/rte_mbuf.h — 두 mbuf를 체이닝 */
static inline int
rte_pktmbuf_chain(struct rte_mbuf *head,
struct rte_mbuf *tail)
{
struct rte_mbuf *cur_tail;
/* head의 마지막 세그먼트 찾기 */
cur_tail = rte_pktmbuf_lastseg(head);
/* tail이 이미 간접 mbuf이거나 refcnt > 1이면 실패 */
if (tail->ol_flags & IND_ATTACHED_MBUF ||
rte_mbuf_refcnt_read(tail) != 1)
return -EOVERFLOW;
/* 체인 연결 */
cur_tail->next = tail;
head->nb_segs += tail->nb_segs;
head->pkt_len += tail->pkt_len;
/* tail은 더 이상 독립 패킷이 아님 */
tail->pkt_len = tail->data_len;
return 0;
}
/* lib/mbuf/rte_mbuf.c — 멀티 세그먼트를 단일 버퍼로 선형화 */
int
rte_pktmbuf_linearize(struct rte_mbuf *mbuf)
{
size_t seg_len, copy_len;
struct rte_mbuf *m, *m_next;
char *buffer;
/* 이미 단일 세그먼트면 아무것도 안 함 */
if (mbuf->nb_segs == 1)
return 0;
/* 첫 번째 mbuf의 tailroom에 나머지 데이터가 들어가는지 확인 */
copy_len = mbuf->pkt_len - mbuf->data_len;
if (rte_pktmbuf_tailroom(mbuf) < copy_len)
return -1; /* 공간 부족 */
/* 후속 세그먼트 데이터를 첫 번째 mbuf 뒤에 복사 */
buffer = rte_pktmbuf_mtod_offset(mbuf, char *,
mbuf->data_len);
m = mbuf->next;
while (m != NULL) {
m_next = m->next;
seg_len = m->data_len;
rte_memcpy(buffer,
rte_pktmbuf_mtod(m, char *),
seg_len);
buffer += seg_len;
/* 후속 세그먼트를 mempool에 반환 */
rte_pktmbuf_free_seg(m);
m = m_next;
}
/* 단일 세그먼트로 갱신 */
mbuf->data_len = mbuf->pkt_len;
mbuf->nb_segs = 1;
mbuf->next = NULL;
return 0;
}
코드 분석: chain()과 linearize()
- rte_pktmbuf_chain(): 두 개의 독립 mbuf(또는 체인)를 하나의 패킷으로 연결합니다. 터널 캡슐화에서 외부 헤더(새 mbuf)와 내부 패킷(기존 mbuf)을 합칠 때 유용합니다.
head->pkt_len에 tail의 길이를 누적하고,head->nb_segs에 tail의 세그먼트 수를 더합니다. 간접 mbuf(refcnt > 1)는 체인할 수 없습니다. - rte_pktmbuf_linearize(): 멀티 세그먼트 mbuf를 단일 연속 버퍼로 변환합니다. 후속 세그먼트의 데이터를 첫 번째 mbuf의 tailroom에
rte_memcpy()로 복사하고, 후속 세그먼트를 mempool에 반환합니다. tailroom이 부족하면 실패합니다. - linearize 대안: 9000B 패킷을 linearize하려면 첫 번째 mbuf에 9000B tailroom이 필요하지만, 기본 mbuf 데이터 영역은 2048B입니다. 따라서 Jumbo Frame에 linearize를 적용하려면 mempool 생성 시
data_room_size를 9KB+headroom으로 늘려야 합니다. 이는 메모리 낭비가 크므로, 실무에서는 세그먼트를 직접 순회하는 것이 일반적입니다. - rte_pktmbuf_free_seg(): 체인 전체가 아닌 단일 세그먼트만 해제합니다.
rte_pktmbuf_free()와 달리next포인터를 따라가지 않으므로, linearize에서 후속 세그먼트를 개별 해제할 때 사용합니다.
rte_pktmbuf_linearize()보다는 세그먼트를 직접 순회하는 것이 더 효율적입니다.
Ring 라이브러리
DPDK rte_ring은 고정 크기, FIFO, lock-free 큐로,
코어 간 패킷 전달, 멀티프로세스 IPC, 태스크(Task) 분배 등에 사용됩니다.
CAS(Compare-And-Swap) 연산 기반으로 락 없이 다수 생산자/소비자를 지원합니다.
/* Ring 기본 사용 */
struct rte_ring *ring = rte_ring_create(
"MY_RING",
1024, /* 크기 (2의 거듭제곱이어야 함) */
rte_socket_id(), /* NUMA 소켓 */
RING_F_SP_ENQ | RING_F_SC_DEQ /* 단일 생산자/소비자 (더 빠름) */
);
/* 생산자: 패킷을 Ring에 삽입 */
struct rte_mbuf *pkts[32];
unsigned nb = rte_eth_rx_burst(port, 0, pkts, 32);
unsigned sent = rte_ring_enqueue_burst(ring, (void **)pkts, nb, NULL);
/* 소비자: Ring에서 패킷 추출 (다른 코어) */
struct rte_mbuf *dequeued[32];
unsigned got = rte_ring_dequeue_burst(ring, (void **)dequeued, 32, NULL);
RING_F_SP_ENQ | RING_F_SC_DEQ— 1:1 코어 매핑 시 최적 (파이프라인(Pipeline) 모델)RING_F_MP_HTS_ENQ | RING_F_MC_HTS_DEQ— head/tail sync 방식. 기존 MP/MC보다 공정하고 순서 보장(Ordering) 강화- Ring 크기는 반드시 2의 거듭제곱이어야 합니다 (마스크 연산 최적화)
MP enqueue CAS 알고리즘 구현 분석
Multi-Producer(MP) enqueue는 여러 코어가 동시에 Ring에 삽입할 수 있도록 lock-free CAS 알고리즘을 사용합니다. 이 구현은 DPDK 성능의 핵심 기반입니다.
/* lib/ring/rte_ring_c11_pvt.h — MP enqueue 핵심 (간략화) */
static inline unsigned int
__rte_ring_move_prod_head(struct rte_ring *r,
unsigned int is_sp, unsigned int n,
enum rte_ring_queue_behavior behavior,
uint32_t *old_head, uint32_t *new_head,
uint32_t *free_entries)
{
uint32_t cons_tail;
int success;
*old_head = __atomic_load_n(&r->prod.head,
__ATOMIC_RELAXED);
do {
/* ① 현재 소비자 tail 읽기 (데이터 의존성으로 acquire) */
cons_tail = __atomic_load_n(&r->cons.tail,
__ATOMIC_ACQUIRE);
/* ② 빈 슬롯 수 계산 */
*free_entries = r->capacity -
(*old_head - cons_tail);
/* ③ 삽입할 수 있는 개수 결정 */
if (n > *free_entries)
n = (behavior == RTE_RING_QUEUE_FIXED) ?
0 : *free_entries;
if (n == 0) return 0;
*new_head = *old_head + n;
if (is_sp) {
/* SP 모드: CAS 불필요, 직접 갱신 */
r->prod.head = *new_head;
success = 1;
} else {
/* ④ MP 모드: CAS로 head를 원자적으로 이동 */
success = __atomic_compare_exchange_n(
&r->prod.head,
old_head, *new_head,
0, /* weak CAS: 실패 시 재시도 */
__ATOMIC_RELAXED,
__ATOMIC_RELAXED);
}
} while (!success);
/* CAS 성공: [old_head, new_head) 구간이 이 코어에 예약됨 */
return n;
}
/* 예약 후 데이터 복사 + tail 갱신 */
static inline unsigned int
__rte_ring_do_enqueue_elem(struct rte_ring *r,
const void *obj_table, unsigned int esize,
unsigned int n, ...)
{
uint32_t old_head, new_head;
/* ① head 이동 (슬롯 예약) */
n = __rte_ring_move_prod_head(r, is_sp, n,
behavior, &old_head, &new_head, &free);
/* ② 예약된 슬롯에 데이터 복사 */
__rte_ring_enqueue_elems(r, old_head, obj_table,
esize, n);
/* ③ tail을 순차적으로 갱신
* 다른 코어가 먼저 CAS에 성공했지만 아직 데이터 복사 중이면,
* 이 코어는 spin-wait하며 기다림 → 순서 보장 */
__rte_ring_update_tail(&r->prod, old_head,
new_head, is_sp, 1);
/* update_tail 내부:
* while (rte_atomic_load(&prod.tail) != old_head)
* rte_pause(); // 이전 코어의 복사 완료 대기
* rte_atomic_store(&prod.tail, new_head, release);
*/
return n;
}
코드 분석: Ring MP enqueue CAS 알고리즘
- 3단계 프로토콜: MP enqueue는 (1) CAS로 head를 이동하여 슬롯 예약 → (2) 예약된 슬롯에 데이터 복사 → (3) tail을 순차적으로 갱신하는 3단계입니다. 이 분리가 lock-free의 핵심입니다.
- CAS(Compare-And-Swap):
__atomic_compare_exchange_n()은prod.head가 예상값(old_head)과 같으면new_head로 교체하고, 다르면(다른 코어가 먼저 수정) 실패하여 do-while 루프에서 재시도합니다. 코어 수가 적을수록 CAS 경합이 적어 성능이 좋습니다. - SP 모드 최적화: 단일 생산자에서는 CAS가 불필요합니다. 직접
prod.head를 갱신하므로 메모리 배리어 비용만 남습니다. 파이프라인 모델에서 코어 간 1:1 Ring에는 SP/SC를 사용하는 것이 2~3배 빠릅니다. - tail 순차 갱신: CAS는 head만 이동하므로, 코어 A가 슬롯 [0,4)를 예약하고 코어 B가 [4,8)을 예약한 상황에서 B가 먼저 데이터 복사를 완료할 수 있습니다. 하지만 tail은 0→4→8 순서로만 갱신되어야 소비자가 올바른 데이터를 읽습니다. 따라서 B는 A의 tail 갱신이 완료될 때까지
rte_pause()로 spin-wait합니다. - rte_pause(): x86에서는
PAUSE명령어로 구현됩니다. spin-wait 루프에서 파이프라인 플러시를 줄이고, Hyper-Threading 환경에서 sibling 코어에 실행 자원을 양보합니다. - 마스크 연산: Ring 크기가 2^n이므로
old_head & mask로 인덱스를 계산합니다. 나머지 연산(%)보다 비트 AND(&)가 수십 배 빠릅니다. 이것이 Ring 크기가 반드시 2의 거듭제곱이어야 하는 이유입니다. - capacity vs size: 실제 사용 가능 슬롯 수(
capacity)는size - 1입니다. 한 슬롯을 비워두어 "가득 참"과 "빈 상태"를 구분합니다 (head == tail이면 빈 상태).
Mempool과 메모리 관리
rte_mempool은 고정 크기 객체의 풀로, DPDK에서 mbuf 할당/해제의 핵심입니다.
Hugepage 위에 구축되며, per-core 캐시로 코어 간 경합(Contention)을 최소화합니다.
/* Mempool 생성과 NUMA 인식 */
struct rte_mempool *pool;
/* 기본: 패킷 mbuf 풀 */
pool = rte_pktmbuf_pool_create(
"PKT_POOL",
65535, /* 총 mbuf 개수 (2^n - 1 권장) */
512, /* per-core 캐시 크기 (0이면 캐시 비활성) */
0, /* priv_size: mbuf당 추가 메타데이터 크기 */
RTE_MBUF_DEFAULT_BUF_SIZE, /* 2048 + 128(headroom) = 2176 */
rte_socket_id() /* NUMA 노드 */
);
/* 고급: 외부 메모리로 Mempool 생성 (huge 1GB) */
struct rte_pktmbuf_extmem ext_mem = {
.buf_ptr = mmap_addr, /* 사전 매핑된 hugepage 주소 */
.buf_iova = iova_addr, /* IOMMU 매핑된 DMA 주소 */
.buf_len = 1ULL << 30, /* 1GB */
.elt_size = 2176, /* mbuf + data 크기 */
};
pool = rte_pktmbuf_pool_create_extbuf("EXT_POOL", 65535, 512,
0, 2176, rte_socket_id(),
&ext_mem, 1);
Mempool 내부 구현: per-core 캐시와 할당 경로
/* lib/mempool/rte_mempool.h — per-core 캐시 구조 */
struct rte_mempool_cache {
uint32_t size; /* 캐시 최대 크기 (생성 시 지정) */
uint32_t flushthresh; /* = size × 1.5, 초과 시 Ring으로 반환 */
uint32_t len; /* 현재 캐시에 있는 객체 수 */
void *objs[0]; /* 객체 포인터 배열 (LIFO 스택) */
};
/* lib/mempool/rte_mempool.h — 할당 핵심: rte_mempool_get() */
static inline int
rte_mempool_get_bulk(struct rte_mempool *mp,
void **obj_table, unsigned int n)
{
struct rte_mempool_cache *cache;
/* ① 현재 코어의 로컬 캐시 가져오기 */
cache = rte_mempool_default_cache(mp,
rte_lcore_id());
if (cache != NULL && cache->len >= n) {
/* ② 캐시에 충분한 객체 → 캐시에서 직접 pop
* 락 없음, CAS 없음, O(1) */
cache->len -= n;
rte_memcpy(obj_table,
&cache->objs[cache->len],
sizeof(void *) * n);
return 0;
}
/* ③ 캐시 부족 → 공용 Ring에서 배치 리필 */
unsigned int req = n + (cache ? cache->size : 0);
int ret = rte_mempool_ops_dequeue_bulk(mp,
cache ? cache->objs : obj_table, req);
if (ret < 0)
return ret; /* Ring도 비어있음 */
if (cache) {
cache->len = cache->size; /* 캐시 리필 완료 */
/* 나머지 n개를 obj_table에 복사 */
rte_memcpy(obj_table,
&cache->objs[cache->len],
sizeof(void *) * n);
cache->len -= n;
}
return 0;
}
/* lib/mempool/rte_mempool.h — 반환 핵심: rte_mempool_put() */
static inline void
rte_mempool_put_bulk(struct rte_mempool *mp,
void * const *obj_table,
unsigned int n)
{
struct rte_mempool_cache *cache;
cache = rte_mempool_default_cache(mp,
rte_lcore_id());
if (cache != NULL) {
/* 캐시에 push (LIFO 스택) */
rte_memcpy(&cache->objs[cache->len],
obj_table,
sizeof(void *) * n);
cache->len += n;
/* flushthresh 초과 시 넘치는 분량을 Ring에 반환 */
if (cache->len >= cache->flushthresh) {
rte_mempool_ops_enqueue_bulk(mp,
&cache->objs[cache->size],
cache->len - cache->size);
cache->len = cache->size;
}
return;
}
/* 캐시 없음: 공용 Ring에 직접 반환 */
rte_mempool_ops_enqueue_bulk(mp, obj_table, n);
}
코드 분석: Mempool per-core 캐시
- 2레벨 구조: Mempool은 "per-core 로컬 캐시 → 공용 Ring" 2레벨로 구성됩니다. 데이터 경로의 99% 이상의 할당/반환은 로컬 캐시에서 완료되어 코어 간 경합이 없습니다.
- LIFO 스택: 로컬 캐시는
objs[]배열 +len카운터의 LIFO 스택입니다. 가장 최근에 반환된 mbuf를 다시 할당하므로 L1/L2 캐시에 hot한 상태로 재사용될 확률이 높습니다. - flushthresh:
size × 1.5로 설정됩니다. put 시 캐시가 이 임계치를 넘으면size까지만 유지하고 나머지를 공용 Ring에 반환합니다. 이 히스테리시스가 없으면 "캐시 가득 → Ring 반환 → 바로 캐시 빔 → Ring 로드" 핑퐁이 발생합니다. - rte_mempool_ops_dequeue_bulk(): 공용 Ring에서 객체를 꺼내는 연산입니다. 내부적으로
rte_ring_mc_dequeue_bulk()를 호출하며, 이때만 CAS 연산이 발생합니다. 캐시 리필 시cache->size개를 한 번에 가져오므로 CAS는 수백~수천 패킷당 1회만 발생합니다. - rte_lcore_id(): TLS(Thread-Local Storage)에서 현재 lcore ID를 O(1)에 반환합니다. 이 값으로 per-core 캐시 배열에 인덱싱합니다. 따라서 DPDK 스레드가 아닌 외부 스레드에서 mempool에 접근하면 캐시를 사용할 수 없어 성능이 저하됩니다.
rte_pktmbuf_pool_create() 내부 동작
/* lib/mbuf/rte_mbuf.c — Mempool 생성 핵심 (간략화) */
struct rte_mempool *
rte_pktmbuf_pool_create(const char *name,
unsigned int n, unsigned int cache_size,
uint16_t priv_size, uint16_t data_room_size,
int socket_id)
{
struct rte_mempool *mp;
unsigned int elt_size;
/* ① 요소 크기 계산 */
elt_size = sizeof(struct rte_mbuf) /* 128B */
+ priv_size /* 사용자 메타데이터 */
+ data_room_size; /* headroom + 데이터 */
/* 기본: 128 + 0 + 2176 = 2304B per mbuf */
/* ② Mempool 구조체 생성 */
mp = rte_mempool_create(name, n, elt_size,
cache_size, 0,
NULL, NULL,
rte_pktmbuf_init, NULL,
socket_id, 0);
/* rte_mempool_create() 내부:
* a) memzone 할당: hugepage에서 n × elt_size 연속 메모리 확보
* b) Ring 생성: 공용 풀로 사용할 rte_ring 할당
* c) per-core 캐시 배열 할당: lcore 수 × sizeof(cache)
* d) 각 요소에 대해 rte_pktmbuf_init() 콜백 호출
*/
return mp;
}
/* lib/mbuf/rte_mbuf.c — mbuf 초기화 콜백 */
void
rte_pktmbuf_init(struct rte_mempool *mp,
void *opaque_arg,
void *_m, unsigned int i)
{
struct rte_mbuf *m = _m;
uint32_t mbuf_size = rte_pktmbuf_priv_size(mp)
+ mp->elt_size;
memset(m, 0, mbuf_size);
/* buf_addr: mbuf 구조체 바로 뒤 */
m->buf_addr = (char *)m + sizeof(struct rte_mbuf)
+ rte_pktmbuf_priv_size(mp);
/* buf_iova: DMA용 IO 가상 주소 */
m->buf_iova = rte_mempool_virt2iova(m)
+ sizeof(struct rte_mbuf)
+ rte_pktmbuf_priv_size(mp);
m->buf_len = mp->elt_size
- sizeof(struct rte_mbuf)
- rte_pktmbuf_priv_size(mp);
m->data_off = RTE_PKTMBUF_HEADROOM; /* 128B */
m->refcnt = 1;
m->nb_segs = 1;
m->pool = mp;
}
코드 분석: Mempool 생성과 mbuf 초기화
- elt_size 계산: 각 mbuf 요소는
rte_mbuf 구조체(128B) + priv_size + data_room_size(2176B)로 구성됩니다.RTE_MBUF_DEFAULT_BUF_SIZE는RTE_PKTMBUF_HEADROOM(128) + RTE_ETHER_MAX_LEN(1518) + margin= 2176바이트입니다. 모든 요소가 동일 크기이므로 메모리 단편화가 없습니다. - memzone 할당:
rte_mempool_create()내부에서 hugepage 위에n × elt_size바이트의 연속 메모리를 확보합니다. 이 메모리가 물리적으로 연속(1GB hugepage 내부)하면 NIC DMA가 scatter 없이 접근할 수 있어 효율적입니다. - rte_pktmbuf_init() 콜백: 각 mbuf에 대해 한 번씩 호출되어
buf_addr,buf_iova,buf_len등을 설정합니다.buf_iova는 생성 시점에 계산되어 고정되므로, RX 경로에서 매번 가상→물리 주소 변환을 할 필요가 없습니다. 이 "사전 매핑(pre-mapping)" 최적화가 DPDK의 제로카피 DMA를 가능하게 합니다. - n 값 권장:
n = 2^k - 1(예: 65535)을 권장합니다. 내부 Ring 크기가2^k이고 1슬롯을 가드로 사용하므로,2^k - 1개의 요소를 저장하면 공간 낭비 없이 Ring을 최대한 활용합니다. - cache_size: 0이면 per-core 캐시 비활성(매번 공용 Ring 접근). 256~512가 일반적입니다. burst_size × 1.5 이상을 권장합니다(예: burst=32이면 cache≥48). 너무 크면 mempool의 유효 객체 수가
n - (lcore_count × cache_size)로 줄어들어 부족해질 수 있습니다.
- TLB 효율 — 64K개 mbuf × 2KB = 128MB. 4KB 페이지(Page)면 32,768 TLB 엔트리 필요하지만, 2MB hugepage면 64개, 1GB hugepage면 1개
- 연속 물리 메모리(Physical Memory) — NIC DMA가 물리적으로 연속된 영역을 필요로 함. Hugepage는 2MB/1GB 단위로 물리 연속 보장
- IOVA 변환 단순화 — 큰 페이지 단위로 IOMMU 매핑하므로 IOTLB 미스 최소화
- 페이지 폴트(Page Fault) 제거 — mmap 시 MAP_POPULATE로 즉시 물리 페이지 할당. 런타임 페이지 폴트 없음
rte_hash — Cuckoo 해시 테이블
rte_hash는 DPDK의 고성능 해시 테이블로, 플로우 테이블, 세션 캐시, ARP 테이블 등 데이터 경로의 키-값 조회에 사용됩니다. 내부적으로 Cuckoo 해싱(Cuckoo Hashing)을 사용하여 최악의 경우에도 O(1) 조회를 보장합니다.
rte_hash 생성과 조회 구현
/* rte_hash 생성 */
struct rte_hash_parameters params = {
.name = "flow_table",
.entries = 65536, /* 최대 엔트리 수 */
.key_len = sizeof(struct flow_key), /* 키 크기 (바이트) */
.hash_func = rte_hash_crc, /* CRC32 해시 함수 */
.hash_func_init_val = 0,
.socket_id = rte_socket_id(),
.extra_flag = RTE_HASH_EXTRA_FLAGS_RW_CONCURRENCY_LF
| RTE_HASH_EXTRA_FLAGS_MULTI_WRITER_ADD,
};
struct rte_hash *h = rte_hash_create(¶ms);
/* 키-값 삽입 */
struct flow_key key = { .src_ip = ..., .dst_ip = ..., ... };
int32_t pos = rte_hash_add_key(h, &key);
/* pos >= 0이면 성공, pos가 key_store 인덱스 */
flow_data[pos] = my_flow_data; /* 사용자 데이터 매핑 */
/* 키-값 삽입 (데이터 포인터 포함) */
rte_hash_add_key_data(h, &key, (void *)flow_data_ptr);
/* 조회 */
int32_t ret = rte_hash_lookup(h, &key);
if (ret >= 0)
result = flow_data[ret]; /* 인덱스로 데이터 접근 */
/* 데이터 포인터로 직접 조회 */
void *data;
ret = rte_hash_lookup_data(h, &key, &data);
/* 배치 조회: 여러 키를 한 번에 조회 (prefetch 최적화) */
const void *keys[32];
int32_t positions[32];
int hits = rte_hash_lookup_bulk(h, keys, 32, positions);
/* 삭제 */
rte_hash_del_key(h, &key);
rte_hash 내부 구현: Cuckoo 해싱 알고리즘
/* lib/hash/rte_cuckoo_hash.c — rte_hash_add_key() 내부 (간략화) */
int32_t
__rte_hash_add_key_with_hash(const struct rte_hash *h,
const void *key, hash_sig_t sig)
{
uint32_t prim_bucket_idx, sec_bucket_idx;
uint16_t short_sig; /* signature: 해시의 상위 16비트 */
/* ① 해시값에서 primary/secondary 버킷 인덱스 계산 */
prim_bucket_idx = sig & h->bucket_bitmask;
short_sig = get_short_sig(sig);
sec_bucket_idx = rte_hash_secondary_hash(sig)
& h->bucket_bitmask;
/* ② primary 버킷에서 빈 슬롯 탐색 */
struct rte_hash_bucket *prim_bkt =
&h->buckets[prim_bucket_idx];
for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
if (prim_bkt->sig_current[i] == NULL_SIGNATURE) {
/* 빈 슬롯 발견 → 키 저장 */
prim_bkt->sig_current[i] = short_sig;
prim_bkt->key_idx[i] = alloc_key_slot(h);
rte_memcpy(get_key_slot(h, prim_bkt->key_idx[i]),
key, h->key_len);
return prim_bkt->key_idx[i];
}
}
/* ③ primary 가득 참 → secondary 버킷 시도 */
struct rte_hash_bucket *sec_bkt =
&h->buckets[sec_bucket_idx];
for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
if (sec_bkt->sig_current[i] == NULL_SIGNATURE) {
sec_bkt->sig_current[i] = short_sig;
sec_bkt->key_idx[i] = alloc_key_slot(h);
rte_memcpy(get_key_slot(h, sec_bkt->key_idx[i]),
key, h->key_len);
return sec_bkt->key_idx[i];
}
}
/* ④ 양쪽 모두 가득 → Cuckoo eviction (BFS 탐색) */
return rte_hash_cuckoo_insert_mw(h, prim_bkt, sec_bkt,
key, sig, short_sig);
}
/* lib/hash/rte_cuckoo_hash.c — rte_hash_lookup() 내부 (간략화) */
int32_t
__rte_hash_lookup_with_hash(const struct rte_hash *h,
const void *key, hash_sig_t sig)
{
uint32_t prim_idx = sig & h->bucket_bitmask;
uint16_t short_sig = get_short_sig(sig);
struct rte_hash_bucket *bkt;
/* ① primary 버킷에서 signature 비교 (캐시라인 1개) */
bkt = &h->buckets[prim_idx];
for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
if (bkt->sig_current[i] == short_sig) {
/* signature 일치 → 키 전체 비교 */
void *key_slot = get_key_slot(h, bkt->key_idx[i]);
if (rte_hash_cmp_eq(key, key_slot, h) == 0)
return bkt->key_idx[i]; /* 찾음 */
}
}
/* ② secondary 버킷에서 재시도 */
uint32_t sec_idx = rte_hash_secondary_hash(sig)
& h->bucket_bitmask;
bkt = &h->buckets[sec_idx];
for (int i = 0; i < RTE_HASH_BUCKET_ENTRIES; i++) {
if (bkt->sig_current[i] == short_sig) {
void *key_slot = get_key_slot(h, bkt->key_idx[i]);
if (rte_hash_cmp_eq(key, key_slot, h) == 0)
return bkt->key_idx[i];
}
}
return -ENOENT; /* 키 없음 */
}
코드 분석: rte_hash Cuckoo 해싱
- Cuckoo 해싱 원리: 각 키에 두 개의 후보 버킷(primary, secondary)이 있습니다. 삽입 시 primary에 먼저 시도하고, 가득 차면 secondary를 시도합니다. 양쪽 모두 가득 차면 기존 엔트리를 "쫓아내어(evict)" 그 엔트리의 대안 버킷으로 이동시킵니다. 이 연쇄 이동을 BFS로 탐색합니다.
- 버킷 구조 (8-way set-associative): 각 버킷에 8개 슬롯이 있으며, 64바이트 캐시라인에 정렬됩니다. signature(16비트)와 key_idx(32비트)만 버킷에 저장하고 실제 키 데이터는 별도 배열(
key_store)에 둡니다. 이 구조로 조회 시 캐시라인 1개만 로드하여 8개 signature를 한 번에 비교할 수 있습니다. - signature 비교: 해시의 상위 16비트를 signature로 사용합니다. signature 비교로 대부분의 비매칭을 걸러내고, signature가 일치한 경우에만 실제 키 전체를 비교합니다. 이 2단계 필터로 키 비교 횟수를 최소화합니다.
- rte_hash_secondary_hash(): secondary 버킷 인덱스는
primary_hash XOR short_sig으로 계산합니다. XOR 관계이므로 secondary에서 primary를 역으로 계산할 수 있어, eviction 시 원래 버킷을 찾을 수 있습니다. - 조회 보장: 키가 존재하면 반드시 primary 또는 secondary 버킷 2개만 확인하면 됩니다. 최악의 경우에도 O(1)이며, 캐시라인 접근은 최대 2회(+ 키 비교 1회)입니다.
- RW_CONCURRENCY_LF: lock-free 읽기/쓰기 동시성을 활성화합니다. 읽기 스레드는 잠금 없이 조회하고, 쓰기 스레드는 CAS로 버킷 슬롯을 갱신합니다. 삭제 시에는 QSBR(RCU)로 안전한 메모리 회수가 필요합니다.
- rte_hash_lookup_bulk(): 여러 키를 배치로 조회합니다. 내부적으로 첫 번째 키의 버킷을 prefetch하면서 이전 키의 비교를 수행하는 소프트웨어 파이프라이닝을 적용합니다. 단일 조회 대비 ~2배 처리량을 달성합니다.
배치 조회 파이프라이닝 기법
/* rte_hash_lookup_bulk() 내부의 소프트웨어 파이프라이닝 (간략화) */
/* 1단계: 모든 키의 해시 계산 + primary 버킷 prefetch */
for (uint32_t i = 0; i < num_keys; i++) {
hash_vals[i] = rte_hash_hash(h, keys[i]);
prim_idx[i] = hash_vals[i] & h->bucket_bitmask;
rte_prefetch0(&h->buckets[prim_idx[i]]);
}
/* 2단계: primary 버킷 signature 비교 + secondary prefetch */
for (uint32_t i = 0; i < num_keys; i++) {
struct rte_hash_bucket *bkt = &h->buckets[prim_idx[i]];
int found = search_bucket(bkt, short_sigs[i]);
if (!found) {
sec_idx[i] = rte_hash_secondary_hash(hash_vals[i])
& h->bucket_bitmask;
rte_prefetch0(&h->buckets[sec_idx[i]]);
}
}
/* 3단계: secondary 비교 + 키 전체 비교 */
for (uint32_t i = 0; i < num_keys; i++) {
if (positions[i] < 0) {
/* primary 미스 → secondary에서 재시도 */
search_bucket(&h->buckets[sec_idx[i]], ...);
}
/* signature 일치 엔트리가 있으면 키 전체 비교 */
rte_prefetch0(get_key_slot(h, key_idx));
}
/* 4단계: 키 데이터 비교 (이미 prefetch 완료) */
for (uint32_t i = 0; i < num_keys; i++) {
if (rte_hash_cmp_eq(keys[i], key_slot, h) == 0)
positions[i] = key_idx;
}
entries는 예상 최대 엔트리 수의 1.5~2배로 설정합니다. 점유율(load factor)이 75%를 넘으면 Cuckoo eviction 빈도가 급증하여 삽입 성능이 저하됩니다. 65536 엔트리에 최대 40000개 플로우를 저장하면 점유율 ~61%로 안정적입니다.
RCU QSBR — 안전한 메모리 회수
QSBR(Quiescent State Based Reclamation)은 DPDK의 RCU(Read-Copy-Update) 구현입니다. lock-free 해시 테이블에서 삭제된 엔트리의 메모리를 안전하게 회수하는 데 사용됩니다. 삭제 시점에 다른 코어가 해당 메모리를 읽고 있을 수 있으므로, 모든 읽기 코어가 quiescent state를 보고한 뒤에 메모리를 해제합니다.
/* QSBR + rte_hash 통합 사용 패턴 */
#include <rte_hash.h>
#include <rte_rcu_qsbr.h>
/* ━━━ 초기화 ━━━ */
size_t sz = rte_rcu_qsbr_get_memsize(num_threads);
struct rte_rcu_qsbr *qsv = rte_zmalloc("qsbr", sz,
RTE_CACHE_LINE_SIZE);
rte_rcu_qsbr_init(qsv, num_threads);
/* rte_hash에 QSBR 연결 (삭제 시 자동 defer) */
struct rte_hash_rcu_config rcu_cfg = {
.v = qsv,
.mode = RTE_HASH_QSBR_MODE_DQ, /* Deferred Queuing */
.dq_size = 1024,
.trigger_reclaim_limit = 0,
.max_reclaim_size = 128,
};
rte_hash_rcu_qsbr_add(hash_table, &rcu_cfg);
/* ━━━ Reader 스레드 (데이터 경로) ━━━ */
rte_rcu_qsbr_thread_register(qsv, lcore_id);
rte_rcu_qsbr_thread_online(qsv, lcore_id);
while (!force_quit) {
/* 패킷 수신 + 해시 조회 (lock-free) */
uint16_t nb_rx = rte_eth_rx_burst(port, queue, pkts, 32);
for (int i = 0; i < nb_rx; i++) {
int32_t pos = rte_hash_lookup(hash_table, &key);
if (pos >= 0)
process_flow(flow_data[pos], pkts[i]);
}
/* 폴링 루프 끝에서 quiescent state 보고 */
rte_rcu_qsbr_quiescent(qsv, lcore_id);
}
/* ━━━ Writer 스레드 (제어 경로) ━━━ */
/* 삭제: rte_hash가 내부적으로 QSBR defer를 사용 */
rte_hash_del_key(hash_table, &key);
/* → 즉시 키 슬롯 제거, 메모리는 grace period 후 자동 회수
* rte_hash_rcu_qsbr_add()로 연결했으므로 별도 코드 불필요 */
코드 분석: QSBR(RCU) 메모리 회수
- Quiescent State: reader가
rte_rcu_qsbr_quiescent()를 호출하면 "현재 공유 데이터에 대한 참조를 보유하고 있지 않습니다"는 신호입니다. DPDK 데이터 경로에서는 폴링 루프의 끝(모든 패킷 처리 완료 후)에 호출합니다. - Grace Period: writer가 키를 삭제한 시점부터, 삭제 시점에 활성 상태였던 모든 reader가 quiescent state를 보고할 때까지의 기간입니다. grace period가 완료되면 삭제된 엔트리의 메모리를 안전하게 해제할 수 있습니다.
- RTE_HASH_QSBR_MODE_DQ: Deferred Queuing 모드입니다. 삭제된 키를 즉시 해제하지 않고 내부 큐에 쌓아두었다가, grace period가 완료되면 배치로 해제합니다. 이 모드에서는
rte_hash_del_key()가 자동으로 defer 처리를 수행하므로 사용자 코드가 단순해집니다. - rte_rcu_qsbr_thread_online/offline: reader가 활성 상태인지 비활성 상태인지를 표시합니다. offline인 스레드는 grace period 계산에서 제외됩니다. 스레드가 블로킹 작업(sleep, I/O)을 수행할 때는 offline으로 전환하여 grace period가 무한히 길어지는 것을 방지해야 합니다.
- 왜 RCU가 필요한가: lock-free 해시 테이블에서 writer가 키를 삭제하더라도, 삭제 시점에 다른 코어의 reader가 해당 엔트리의 키 데이터를 읽고 있을 수 있습니다. RCU 없이 즉시 free하면 use-after-free가 발생합니다. RCU는 모든 reader가 기존 참조를 포기할 때까지 기다려 안전하게 메모리를 해제합니다.
- 성능 영향: reader 경로의
rte_rcu_qsbr_quiescent()는 단순한 카운터 증가(~1ns)이므로 데이터 경로 성능에 거의 영향이 없습니다. 이것이 rwlock 대비 QSBR의 핵심 장점입니다. rwlock은 reader도 캐시라인 쓰기(lock 획득)가 필요하여 코어 수에 비례하여 성능이 저하됩니다.
Memzone — 이름 기반 메모리 영역 관리
rte_memzone은 hugepage 위에 이름으로 참조 가능한 연속 메모리 영역을 예약하는 저수준 API입니다. Mempool, Ring, 해시 테이블 등 DPDK의 모든 데이터 구조가 내부적으로 memzone을 통해 메모리를 할당합니다. 멀티프로세스 모드에서 primary와 secondary가 동일한 이름으로 memzone을 조회하여 메모리를 공유합니다.
rte_memzone_reserve() 구현 분석
/* lib/eal/common/eal_common_memzone.c — memzone 예약 (간략화) */
const struct rte_memzone *
rte_memzone_reserve_aligned(const char *name,
size_t len, int socket_id,
unsigned int flags, unsigned int align)
{
struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
struct rte_memzone *mz;
void *addr;
/* ① 이름 중복 검사 */
rte_rwlock_write_lock(&mcfg->mlock);
if (memzone_lookup_thread_unsafe(name) != NULL) {
rte_rwlock_write_unlock(&mcfg->mlock);
return NULL; /* 이미 존재 */
}
/* ② 빈 memzone 슬롯 할당 */
mz = get_free_memzone_slot(mcfg);
if (mz == NULL) {
rte_rwlock_write_unlock(&mcfg->mlock);
return NULL; /* RTE_MAX_MEMZONE 초과 */
}
/* ③ malloc_heap에서 hugepage 메모리 할당 */
addr = malloc_heap_alloc(name, len, socket_id, flags, align);
if (addr == NULL) {
rte_rwlock_write_unlock(&mcfg->mlock);
return NULL; /* 메모리 부족 */
}
/* ④ memzone 메타데이터 기록 */
strlcpy(mz->name, name, sizeof(mz->name));
mz->addr = addr;
mz->iova = rte_malloc_virt2iova(addr);
mz->len = len;
mz->hugepage_sz = mcfg->memseg[0].hugepage_sz;
mz->socket_id = socket_id;
mz->flags = flags;
rte_rwlock_write_unlock(&mcfg->mlock);
return mz;
}
/* memzone 조회 (멀티프로세스에서 secondary가 사용) */
const struct rte_memzone *
rte_memzone_lookup(const char *name)
{
struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
const struct rte_memzone *mz;
rte_rwlock_read_lock(&mcfg->mlock);
mz = memzone_lookup_thread_unsafe(name);
rte_rwlock_read_unlock(&mcfg->mlock);
return mz;
}
코드 분석: memzone 예약과 조회
- rte_memzone_reserve(): hugepage 메모리를 이름 기반으로 예약합니다. 내부적으로
malloc_heap_alloc()을 호출하여 EAL이 mmap한 hugepage 영역에서 요청 크기만큼 할당합니다. 할당된 메모리의 가상 주소(VA)와 IOVA가 모두 memzone 메타데이터에 기록됩니다. - 이름 기반 식별: memzone 이름은 최대 32자이며, 전역 메타데이터 테이블에서 유일해야 합니다.
rte_mempool_create("PKT_POOL", ...)를 호출하면 내부적으로rte_memzone_reserve("RG_PKT_POOL", ...)로 메모리를 할당합니다. - 멀티프로세스 공유: primary 프로세스가
rte_memzone_reserve()로 메모리를 할당하면, secondary 프로세스는rte_memzone_lookup()으로 동일한 이름의 memzone을 찾아 같은 hugepage 메모리에 접근합니다. 두 프로세스가 같은 hugepage 파일을 mmap하므로 물리적으로 동일한 메모리입니다. - RTE_MAX_MEMZONE: 최대 memzone 수는 기본 2560개입니다. 포트 수 × 큐 수 × (RX descriptor ring + TX descriptor ring) + mempool + ring + hash 등을 합산하면 대규모 환경에서 한계에 도달할 수 있습니다. 컴파일 시
RTE_MAX_MEMZONE을 늘려 조정합니다. - 정렬(align):
rte_memzone_reserve_aligned()의 align 파라미터로 캐시라인(64B), 페이지(4KB), hugepage(2MB) 등 정렬을 지정할 수 있습니다. NIC의 descriptor ring은 보통 128B 또는 4KB 정렬이 필요합니다. - IOVA 연속성:
RTE_MEMZONE_IOVA_CONTIG플래그를 지정하면 IOVA 주소가 연속인 메모리만 할당합니다. NIC DMA가 scatter-gather를 지원하지 않는 경우 필요합니다. 1GB hugepage 내부는 항상 IOVA 연속이므로 이 플래그의 성공률이 높습니다.
/* memzone 실전 사용 예시: 공유 통계 메모리 */
/* Primary 프로세스: memzone 예약 */
const struct rte_memzone *mz = rte_memzone_reserve_aligned(
"APP_STATS",
sizeof(struct app_stats) * MAX_PORTS,
rte_socket_id(),
RTE_MEMZONE_SIZE_HINT_ONLY, /* 크기 부족 시 다른 NUMA도 허용 */
RTE_CACHE_LINE_SIZE); /* 64B 정렬 */
struct app_stats *stats = mz->addr;
memset(stats, 0, mz->len);
/* Secondary 프로세스: 동일 이름으로 조회 */
const struct rte_memzone *mz = rte_memzone_lookup("APP_STATS");
struct app_stats *stats = mz->addr;
/* stats는 primary와 동일한 물리 메모리를 가리킴 */
/* 모든 memzone 목록 출력 (디버깅) */
rte_memzone_dump(stdout);
rte_malloc()은 이름 없는 동적 할당이고, rte_memzone_reserve()는 이름 기반 고정 할당입니다. memzone은 멀티프로세스 공유가 가능하고 IOVA 연속성을 보장할 수 있지만, 개수 제한(RTE_MAX_MEMZONE)이 있고 해제 후 재사용이 제한적입니다. 일반적으로 mempool/ring/hash 같은 장기 데이터 구조는 memzone, 임시 버퍼는 rte_malloc을 사용합니다.
DPDK 커널 인터페이스: VFIO vs UIO
DPDK가 유저 공간에서 NIC를 제어하려면 커널의 도움이 필요합니다. PCIe BAR 메모리를 유저 공간에 매핑하고, DMA를 위한 물리/IO 주소 변환(Address Translation)을 제공하는 두 가지 메커니즘:
| 항목 | VFIO (vfio-pci) | UIO (igb_uio / uio_pci_generic) |
|---|---|---|
| IOMMU | 필수 (DMA 격리) | 불필요 (DMA 격리 없음) |
| 보안 | 안전 (IOMMU가 DMA 범위 제한) | 위험 (디바이스가 모든 물리 메모리에 접근 가능) |
| 인터럽트 | MSI/MSI-X eventfd (전체 지원) | 제한적 (INTx만 또는 커널 패치(Patch) 필요) |
| 비특권 실행 | 가능 (/dev/vfio 권한 설정) | root 필수 |
| IOMMU 그룹 | 그룹 내 모든 디바이스를 vfio에 바인딩해야 함 | 개별 디바이스만 바인딩 |
| VM 패스스루 | KVM/QEMU와 통합 지원 | 미지원 |
| 커널 모듈 | vfio-pci (in-tree) | igb_uio (DPDK 동봉), uio_pci_generic (in-tree) |
| 권장 여부 | 권장 (기본) | 레거시 (IOMMU 없는 환경에서만) |
# ━━━ VFIO 기반 DPDK 디바이스 바인딩 ━━━
# 1. IOMMU 활성화 확인
dmesg | grep -i iommu
# "DMAR: IOMMU enabled" 또는 "AMD-Vi: IOMMU performance counters supported"
# 2. vfio-pci 모듈 로드
modprobe vfio-pci
# 3. NIC 상태 확인
dpdk-devbind --status
# Network devices using kernel driver
# 0000:03:00.0 'Ethernet Controller X710' if=ens3f0 drv=i40e
# 0000:03:00.1 'Ethernet Controller X710' if=ens3f1 drv=i40e
# 4. 커널 드라이버에서 언바인딩 → VFIO에 바인딩
ip link set ens3f0 down
dpdk-devbind --bind=vfio-pci 0000:03:00.0
# 5. 바인딩 확인
dpdk-devbind --status
# Network devices using DPDK-compatible driver
# 0000:03:00.0 'Ethernet Controller X710' drv=vfio-pci
# ━━━ Hugepage 설정 ━━━
# 2MB hugepage 1024개 = 2GB
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# NUMA 노드별 설정 (듀얼 소켓)
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# 1GB hugepage (부팅 커맨드라인에서만 예약 가능)
# GRUB: default_hugepagesz=1G hugepagesz=1G hugepages=4
# hugetlbfs 마운트
mount -t hugetlbfs nodev /dev/hugepages
# ━━━ UIO 기반 (레거시, IOMMU 없는 환경) ━━━
modprobe uio_pci_generic
dpdk-devbind --bind=uio_pci_generic 0000:03:00.0
# ⚠ DMA 격리 없음 — 프로덕션에서 비권장
/* 커널 VFIO-PCI 드라이버가 DPDK에 제공하는 인터페이스 */
/* 1. Container FD — IOMMU 도메인 (DMA 매핑 관리) */
container_fd = open("/dev/vfio/vfio", O_RDWR);
ioctl(container_fd, VFIO_SET_IOMMU, VFIO_TYPE1v2_IOMMU);
/* 2. Group FD — IOMMU 그룹 */
group_fd = open("/dev/vfio/15", O_RDWR);
ioctl(group_fd, VFIO_GROUP_SET_CONTAINER, &container_fd);
/* 3. Device FD — 특정 PCI 디바이스 */
device_fd = ioctl(group_fd, VFIO_GROUP_GET_DEVICE_FD, "0000:03:00.0");
/* 4. BAR 매핑 — NIC 레지스터에 직접 접근 */
struct vfio_region_info reg = { .argsz = sizeof(reg), .index = 0 };
ioctl(device_fd, VFIO_DEVICE_GET_REGION_INFO, ®);
bar0 = mmap(NULL, reg.size, PROT_READ | PROT_WRITE, MAP_SHARED,
device_fd, reg.offset);
/* 이제 bar0[offset]으로 NIC 레지스터 직접 read/write 가능 */
/* 5. DMA 매핑 — hugepage를 IOMMU에 등록 */
struct vfio_iommu_type1_dma_map dma = {
.argsz = sizeof(dma),
.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
.vaddr = (uint64_t)hugepage_vaddr,
.iova = (uint64_t)hugepage_vaddr, /* VA=IOVA 모드 */
.size = hugepage_size,
};
ioctl(container_fd, VFIO_IOMMU_MAP_DMA, &dma);
/* NIC DMA가 이 hugepage 영역에 접근 가능 */
코드 분석: VFIO API 시퀀스
- Container FD:
/dev/vfio/vfio를 열면 VFIO 컨테이너를 생성합니다. 컨테이너는 IOMMU 도메인을 대표하며, 같은 컨테이너에 속한 디바이스들은 동일한 DMA 주소 공간을 공유합니다.VFIO_SET_IOMMU로 Type1 IOMMU 백엔드를 설정하면 Intel VT-d / AMD-Vi가 활성화됩니다. - Group FD:
/dev/vfio/<group_id>는 IOMMU 그룹을 나타냅니다. 같은 PCIe bus 세그먼트에 있는 디바이스들이 하나의 그룹에 속하며, 그룹 내 모든 디바이스가 vfio-pci에 바인딩되어야 컨테이너에 추가할 수 있습니다.VFIO_GROUP_SET_CONTAINER로 그룹을 컨테이너에 연결합니다. - Device FD:
VFIO_GROUP_GET_DEVICE_FD로 특정 PCI 디바이스의 파일 디스크립터를 획득합니다. 이 FD를 통해 BAR 영역 mmap, 인터럽트 설정, 디바이스 리셋 등 모든 디바이스 제어가 가능합니다. DPDK EAL은 이 FD를 PMD에 전달하여 NIC 하드웨어를 초기화합니다. - BAR 매핑 (4단계):
VFIO_DEVICE_GET_REGION_INFO로 BAR 크기와 오프셋을 조회한 후,mmap()으로 유저 공간에 매핑합니다. 이후 PMD는 이 매핑된 주소에 직접 읽기/쓰기하여 NIC의 Descriptor Ring Head/Tail 레지스터, 통계 레지스터, 설정 레지스터에 접근합니다. - DMA 매핑 (5단계):
VFIO_IOMMU_MAP_DMA는 hugepage의 가상 주소를 IOMMU에 등록하여 NIC DMA가 접근할 수 있게 합니다. VA 모드에서는vaddr == iova로 설정하여 유저 공간 가상 주소를 그대로 DMA 주소로 사용합니다. IOMMU 하드웨어가 이 IOVA를 물리 주소로 변환합니다.
IOVA 모드: VA vs PA
현재 DPDK EAL은 Linux에서 버스(Bus)가 요구하는 모드, 물리 주소(Physical Address) 접근 가능 여부, IOMMU 그룹 존재 여부를 함께 보고 IOVA 모드를 고릅니다. 공식 Programmer's Guide는 대부분의 경우 RTE_IOVA_VA를 기본 선호 모드로 설명합니다. VFIO + IOMMU가 있으면 VA와 IOVA를 같게 둘 수 있어 초기화와 DMA 매핑이 단순해지기 때문입니다.
| 항목 | RTE_IOVA_VA | RTE_IOVA_PA |
|---|---|---|
| 기본 선호도 | 대부분의 Linux + VFIO 환경에서 권장 | 버스/드라이버가 PA를 요구할 때만 사용 |
| 물리 주소 접근 | 불필요 | 필요할 수 있음 |
| 권한 요구 | 낮음 | /proc/self/pagemap 접근 권한이 문제 되기 쉬움 |
| mempool/메모리 부팅 시간 | 유리 | IOVA 연속 메모리 탐색 비용이 커질 수 있음 |
| 대표 사용처 | VFIO, 멀티프로세스, 일반적인 최신 NIC | 레거시 UIO, 일부 특수 PMD/플랫폼 |
--iova-mode=va 또는 --iova-mode=pa를 명시해 EAL 결정 로직을 고정하는 편이 좋습니다.
특히 cannot open /proc/self/pagemap 계열 오류는 "DPDK 전체 문제"가 아니라 PA 모드 전제와 권한 문제인 경우가 많습니다.
비특권 실행과 EAL 메모리 옵션
최신 Linux GSG는 비특권 DPDK 실행을 공식적으로 다룹니다. 핵심은 hugepage 예약은 root가 먼저 수행하고, 그 뒤에는 hugetlbfs 디렉터리와 /dev/vfio/* 권한을 사용자나 전용 그룹에 넘겨주는 방식입니다. 멀티프로세스가 필요 없다면 --in-memory를 사용해 hugetlbfs 파일 접근 자체를 생략할 수도 있습니다.
# 1. root가 hugepage 예약
sudo dpdk-hugepages.py -p 1G --setup 2G
# 2. 사용자 전용 hugepage 마운트 생성
export HUGEDIR=$HOME/huge-1G
mkdir -p "$HUGEDIR"
sudo dpdk-hugepages.py --mount --directory "$HUGEDIR" \
--user "$(id -u)" --group "$(id -g)"
# 3. VFIO 디바이스를 일반 사용자에게 넘김
sudo dpdk-devbind --bind=vfio-pci --uid "$(id -u)" --gid "$(id -g)" \
0000:03:00.0
# 4. 필요 리소스 한도 조정 (/etc/security/limits.d/dpdk.conf)
* soft memlock unlimited
* hard memlock unlimited
* soft nofile 1048576
* hard nofile 1048576
# 5. 비특권 testpmd 실행
dpdk-testpmd -l 2-5 -n 4 \
--huge-dir "$HUGEDIR" \
--iova-mode=va \
-a 0000:03:00.0 \
-- -i --rxq=2 --txq=2 --forward-mode=io
| EAL 옵션 | 의미 | 주의점 |
|---|---|---|
--in-memory | hugetlbfs 파일 대신 익명 매핑 사용 | 멀티프로세스에는 부적합 |
--single-file-segments | 페이지별 파일 대신 memseg list당 파일 수 축소 | vhost-user처럼 FD 수가 민감한 환경에 유리 |
--legacy-mem | 시작 시 모든 hugepage를 미리 잡아 연속 IOVA 확보 | 메모리 사용량이 커지고 동적 해제가 어려움 |
--match-allocations | 할당과 해제를 더 엄격히 대응시킴 | --legacy-mem, --no-huge와 같이 쓰지 않음 |
--huge-unlink | hugetlbfs에 남는 backing file 오염을 줄임 | 멀티프로세스를 사실상 포기하는 쪽에 가깝습니다 |
cap_dac_read_search, cap_ipc_lock, cap_sys_admin가 필요할 수 있다고 설명합니다.
따라서 가능하면 vfio-pci + RTE_IOVA_VA 조합으로 문제를 단순화하는 것이 낫습니다.
bifurcated PMD와 Linux control plane
모든 DPDK 포트가 VFIO/UIO로 디바이스 전체를 탈취하는 것은 아닙니다. 최신 DPDK 문서는 일부 NIC가 bifurcated driver 모델을 제공한다고 설명합니다. 이 모델에서는 커널 드라이버가 netdev와 control plane을 유지하고, DPDK PMD는 데이터 경로를 분담합니다. 대표적인 예가 mlx5 계열입니다.
| 모델 | 커널 netdev | 장점 | 주의점 |
|---|---|---|---|
| Full device ownership | 없음 또는 비활성 | 가장 단순한 성능 모델, 디바이스를 애플리케이션이 독점 | 커널 도구와 공존이 어렵고 VFIO/IOMMU 구성이 필수 |
| Bifurcated PMD | 유지 | Linux control plane, representor, switchdev/TC와 조합 가능 | 드라이버별 제약이 크고 디버깅이 복합적 |
패킷 처리 파이프라인 모델
DPDK 애플리케이션은 두 가지 기본 패킷 처리 모델을 사용합니다:
| 코어 | 단계 | 역할 |
|---|---|---|
| Core 0 | RX burst | 수신 패킷을 가져와 다음 단계로 전달 |
| Core 1 | DPI/파싱 | L3~L7 분류 및 메타데이터 생성 |
| Core 2 | NAT/암호화 | 정책 적용/변환 처리 |
| Core 3 | TX burst | 처리 완료 패킷 송신 |
단계 간 전달은 rte_ring으로 수행합니다. 장점은 단계별 독립 확장, 단점은 Ring 통과 지연과 NUMA 크로싱 비용입니다.
/* Run-to-Completion 모델 구현 예시 (RSS 기반 L2 포워딩) */
/* 포트 설정: 코어 수만큼 RX/TX 큐 생성 */
struct rte_eth_conf port_conf = {
.rxmode = {
.mq_mode = RTE_ETH_MQ_RX_RSS, /* RSS 활성화 */
},
.rx_adv_conf = {
.rss_conf = {
.rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP,
},
},
};
rte_eth_dev_configure(port_id, nb_cores, nb_cores, &port_conf);
/* 각 코어에 RX/TX 큐 1개씩 할당 */
for (int q = 0; q < nb_cores; q++) {
rte_eth_rx_queue_setup(port_id, q, 1024, socket_id, NULL, mbuf_pool);
rte_eth_tx_queue_setup(port_id, q, 1024, socket_id, NULL);
}
/* 워커 함수: 각 코어가 독립적으로 실행 */
static int
worker_main(void *arg)
{
unsigned core_id = rte_lcore_id();
unsigned queue_id = core_to_queue[core_id];
while (!force_quit) {
struct rte_mbuf *pkts[32];
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, 32);
for (uint16_t i = 0; i < nb_rx; i++) {
struct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkts[i],
struct rte_ether_hdr *);
/* MAC 주소 스왑 (L2 포워딩) */
struct rte_ether_addr tmp = eth->dst_addr;
eth->dst_addr = eth->src_addr;
eth->src_addr = tmp;
}
uint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, pkts, nb_rx);
for (uint16_t i = nb_tx; i < nb_rx; i++)
rte_pktmbuf_free(pkts[i]);
}
return 0;
}
/* 모든 워커 코어에서 실행 시작 */
rte_eal_mp_remote_launch(worker_main, NULL, CALL_MAIN);
코드 분석: Run-to-Completion 파이프라인 구현
- RTE_ETH_MQ_RX_RSS: NIC 하드웨어의 RSS(Receive Side Scaling)를 활성화합니다. NIC는 패킷의 5-tuple(src/dst IP, src/dst port, protocol) 해시를 계산하여 여러 RX 큐에 분배합니다.
rss_hf필드로 해시에 포함할 헤더를 선택합니다. - rte_eth_dev_configure() 내부:
- PMD의
dev_configure()콜백을 호출하여 NIC 하드웨어에 큐 수, RSS 설정, 오프로드 플래그를 프로그래밍합니다. - RX/TX 큐 수가 NIC의
max_rx_queues/max_tx_queues를 초과하면 에러를 반환합니다. - 요청된 오프로드 플래그를 NIC의
rx_offload_capa/tx_offload_capa와 대조하여 지원 여부를 검증합니다.
- PMD의
- rte_eth_rx_queue_setup() 내부:
- Descriptor Ring 메모리를 hugepage에서 DMA 가능 영역으로 할당합니다 (
rte_eth_dma_zone_reserve()). - sw_ring(mbuf 포인터 배열)을 할당하고, mempool에서 mbuf를 꺼내 descriptor에 채웁니다.
socket_id파라미터로 NIC이 연결된 NUMA 노드에 메모리를 배치합니다.
- Descriptor Ring 메모리를 hugepage에서 DMA 가능 영역으로 할당합니다 (
- rte_eal_mp_remote_launch(): 모든 lcore에서
worker_main()을 동시에 실행합니다.CALL_MAIN플래그는 main lcore(EAL 초기화를 수행한 코어)도 워커에 포함시킵니다. 각 코어의 pthread는 이미 EAL 초기화 시 생성되어 대기 중이므로, 이 함수는 단순히 함수 포인터를 설정하고 배리어를 해제합니다. - core_to_queue 매핑: 각 코어가 전용 RX/TX 큐를 사용하므로 코어 간 경합이 없습니다. RSS가 5-tuple 해시로 패킷을 분배하므로 동일 플로우의 패킷은 항상 같은 코어에서 처리되어 순서가 보장됩니다.
Pipeline 모델: 코어 간 Ring 전달 구현
/* Pipeline 모델 — 단계별 전용 코어 할당 */
/* 코어 간 전달용 Ring 생성 */
struct rte_ring *rx_to_worker = rte_ring_create(
"RX_WORKER", 1024,
rte_socket_id(),
RING_F_SP_ENQ | RING_F_SC_DEQ); /* 1:1이므로 SP/SC */
struct rte_ring *worker_to_tx = rte_ring_create(
"WORKER_TX", 1024,
rte_socket_id(),
RING_F_SP_ENQ | RING_F_SC_DEQ);
/* RX 코어: NIC → Ring */
static int rx_core(void *arg) {
while (!force_quit) {
struct rte_mbuf *pkts[32];
uint16_t nb = rte_eth_rx_burst(port, 0, pkts, 32);
if (nb)
rte_ring_enqueue_burst(rx_to_worker,
(void **)pkts, nb, NULL);
}
return 0;
}
/* Worker 코어: Ring → 처리 → Ring */
static int worker_core(void *arg) {
while (!force_quit) {
struct rte_mbuf *pkts[32];
unsigned nb = rte_ring_dequeue_burst(rx_to_worker,
(void **)pkts, 32, NULL);
for (unsigned i = 0; i < nb; i++)
process_packet(pkts[i]); /* DPI, NAT, ACL 등 */
if (nb)
rte_ring_enqueue_burst(worker_to_tx,
(void **)pkts, nb, NULL);
}
return 0;
}
/* TX 코어: Ring → NIC */
static int tx_core(void *arg) {
while (!force_quit) {
struct rte_mbuf *pkts[32];
unsigned nb = rte_ring_dequeue_burst(worker_to_tx,
(void **)pkts, 32, NULL);
if (nb) {
uint16_t sent = rte_eth_tx_burst(port, 0,
pkts, nb);
for (uint16_t i = sent; i < nb; i++)
rte_pktmbuf_free(pkts[i]);
}
}
return 0;
}
코드 분석: Pipeline 모델 구현
- SP/SC Ring: RX→Worker, Worker→TX가 각각 1:1이므로
RING_F_SP_ENQ | RING_F_SC_DEQ로 CAS를 생략합니다. SP/SC는 MP/MC 대비 2~3배 빠릅니다. - rte_ring_enqueue_burst(): Ring이 가득 차면 삽입할 수 없는 패킷은 버립니다. 반환값이 enqueue된 수이므로, 미삽입 패킷의 mbuf를 free하지 않으면 mempool 누수가 발생합니다. 위 예시에서 RX 코어는 이를 생략했는데, 실무에서는 반드시 처리해야 합니다.
- NUMA 고려: Ring을 NIC과 같은 NUMA 노드에 배치해야 합니다. 크로스-NUMA Ring 접근은 40~100ns 추가 지연이 발생합니다. Worker 코어도 동일 NUMA 노드에 할당하는 것이 이상적이지만, 코어가 부족하면 Ring만이라도 NIC 노드에 두는 것이 효과적입니다.
- Pipeline vs RTC 선택: 처리 단계별 CPU 비용이 크게 다를 때(예: RX 10%, DPI 70%, TX 20%) Pipeline이 유리합니다. Worker 코어를 여러 개 두고 RX→Worker Ring을 MP/SC로 설정하면 부하를 분산할 수 있습니다. 처리가 균일하면 RTC가 단순하고 Ring 통과 지연이 없어 더 좋습니다.
Graph 라이브러리 — 노드 기반 패킷 실행기
rte_graph는 DPDK의 패킷 처리 로직을 "노드 그래프"로 표현하는 런타임입니다. 예전의 수동 while (rx_burst → process → tx_burst) 루프를 대체한다기보다, 그 루프를 재사용 가능한 노드 단위로 정규화해 주는 계층이라고 보는 편이 정확합니다. 최신 Programmer's Guide는 Graph가 run-to-completion(RTC)와 dispatch 두 실행 모델을 제공하고, 통계/멀티프로세스/그래프 export까지 지원한다고 설명합니다.
/* Graph 생성 흐름의 핵심만 남긴 예시 */
struct rte_graph_param graph_conf = {
.socket_id = rte_socket_id(),
.nb_node_patterns = 1,
.node_patterns = (const char *[]){ "*" },
};
rte_graph_t graph_id = rte_graph_create("graph0", &graph_conf);
struct rte_graph *graph = rte_graph_lookup("graph0");
/* 메인 루프는 rte_graph_walk() 한 줄로 단순화 */
while (!force_quit)
rte_graph_walk(graph);
| 항목 | 수동 RTC 루프 | Graph | Eventdev |
|---|---|---|---|
| 구현 단위 | 직접 작성한 while 루프 | 노드와 에지 | 이벤트 큐와 스케줄러 |
| 장점 | 가장 단순, 오버헤드 최소 | 구조화, 재사용, export/통계 | 유연한 워커 스케줄링, 플로우 순서 제어 |
| 적합한 경우 | 짧은 포워딩 경로 | 서비스 체인, 공통 노드 재사용 | 부하 편차가 큰 멀티스테이지 파이프라인 |
| 주의점 | 코드가 커지면 유지보수 어려움 | 노드 설계와 디버깅 규율 필요 | 가장 복잡, 디바이스 지원 차이 큼 |
rte_graph_walk() 내부 구현 분석
/* lib/graph/rte_graph_worker.h — rte_graph_walk() 핵심 (간략화) */
static inline void
rte_graph_walk(struct rte_graph *graph)
{
struct rte_node *node;
rte_graph_off_t head;
/* ① 보류 큐(pending list)에서 실행할 노드를 순회 */
head = graph->head;
while (head != RTE_GRAPH_OFF_INVALID) {
node = RTE_PTR_ADD(graph, head);
head = node->next; /* 다음 보류 노드로 이동 */
/* ② 노드의 입력 objs 배열에 패킷이 있으면 process() 호출 */
if (node->idx > 0) {
node->process(graph, node,
node->objs, /* rte_mbuf** 배열 */
node->idx); /* 패킷 수 */
node->idx = 0; /* 처리 완료, 카운터 리셋 */
}
}
/* ③ 보류 큐 초기화 — 다음 walk()를 위해 비움 */
graph->head = RTE_GRAPH_OFF_INVALID;
graph->tail = RTE_GRAPH_OFF_INVALID;
}
/* 노드 process() 콜백 예시: ethdev_rx 소스 노드 */
static uint16_t
ethdev_rx_node_process(struct rte_graph *graph,
struct rte_node *node,
void **objs, uint16_t cnt)
{
struct ethdev_rx_node_ctx *ctx = node->ctx;
uint16_t next_index = 0; /* 기본 다음 노드 */
/* NIC에서 패킷 수신 */
uint16_t nb_rx = rte_eth_rx_burst(
ctx->port_id, ctx->queue_id,
(struct rte_mbuf **)objs,
RTE_GRAPH_BURST_SIZE);
if (nb_rx == 0)
return 0;
/* 수신된 패킷을 다음 노드(예: ip4_lookup)로 전달 */
rte_node_next_stream_move(graph, node,
next_index);
return nb_rx;
}
/* 노드 process() 예시: ip4_lookup 처리 노드 */
static uint16_t
ip4_lookup_node_process(struct rte_graph *graph,
struct rte_node *node,
void **objs, uint16_t cnt)
{
struct rte_mbuf *mbuf;
uint16_t next;
for (uint16_t i = 0; i < cnt; i++) {
mbuf = (struct rte_mbuf *)objs[i];
/* FIB 조회 → 다음 홉 결정 → 다음 노드 선택 */
next = lpm_lookup(mbuf);
/* 패킷을 해당 다음 노드의 스트림에 추가 */
rte_node_enqueue_x1(graph, node, next, objs[i]);
}
return cnt;
}
코드 분석: rte_graph_walk() 내부
- 보류 큐(Pending List): Graph는 "실행 대기 중인 노드"를 연결 리스트로 관리합니다. 소스 노드(ethdev_rx)가 패킷을 수신하면 자신과 다음 노드를 보류 큐에 추가합니다.
rte_graph_walk()는 이 큐를 순회하며 각 노드의process()를 호출합니다. - node->objs[]: 각 노드는 입력 패킷(rte_mbuf 포인터)을 저장하는 배열을 가집니다. 이전 노드가
rte_node_enqueue_x1()으로 패킷을 넣으면node->idx가 증가하고,process()에서 이 배열을 받아 처리합니다. - rte_node_next_stream_move(): 현재 노드의 objs를 다음 노드의 objs로 "이동"합니다. 실제로는 포인터만 교환하므로 mbuf 복사가 없습니다. 소스 노드(ethdev_rx)처럼 모든 패킷이 같은 다음 노드로 가는 경우에 최적입니다.
- rte_node_enqueue_x1(): 패킷별로 다른 다음 노드를 선택해야 할 때 사용합니다. ip4_lookup처럼 FIB 조회 결과에 따라 rewrite, drop, local 등 여러 다음 노드로 분기할 때 필요합니다.
- 성능 핵심: Graph의 성능은 "같은 다음 노드로 가는 패킷을 얼마나 배치(batch)할 수 있는가"에 달렸습니다. 256개 패킷이 모두 같은 다음 노드로 가면
rte_node_next_stream_move()한 번이지만, 패킷마다 다르면 256번의rte_node_enqueue_x1()이 호출됩니다. 실무에서는rte_node_enqueue_x4()로 4개씩 배치 삽입합니다.
AF_XDP PMD — 커널 통합 고성능 경로
AF_XDP는 DPDK의 완전한 커널 바이패스와 커널 네트워크 스택 사이의 중간 지점입니다.
커널의 XDP 훅에서 패킷을 유저 공간으로 제로카피 전달하므로, VFIO 바인딩 없이 커널 드라이버를 유지하면서
고성능 패킷 처리가 가능합니다.
| 비교 항목 | DPDK (VFIO PMD) | DPDK (AF_XDP PMD) | 커널 XDP |
|---|---|---|---|
| 커널 바이패스 | 완전 바이패스 | 부분 바이패스 (XDP 훅까지 커널) | 커널 내 처리 |
| NIC 드라이버 | DPDK PMD (유저 공간) | 커널 NIC 드라이버 유지 | 커널 NIC 드라이버 |
| 커널 기능 | 사용 불가 (tc, iptables, tcpdump...) | 부분 사용 가능 (같은 NIC의 다른 큐) | 모두 사용 가능 |
| 성능 | 최고 (~14.88 Mpps/core @10G) | 높음 (~10 Mpps/core @10G) | 높음 (~24 Mpps @XDP_TX) |
| IOMMU | 필요 (VFIO) | 불필요 | 불필요 |
| 특권 | CAP_SYS_RAWIO | CAP_NET_RAW / CAP_BPF | CAP_BPF |
| NIC 공유 | 불가 (DPDK 전용) | 가능 (큐별 분리) | 가능 |
| 설정 복잡도 | VFIO 바인딩, hugepage | XDP 프로그램 로드, UMEM 설정 | XDP 프로그램만 |
# ━━━ AF_XDP PMD로 DPDK 실행 ━━━
# NIC은 커널 드라이버에 그대로 유지 (VFIO 바인딩 불필요!)
ip link show ens3f0
# ens3f0: ... state UP ... driver: i40e
# AF_XDP PMD로 DPDK 앱 실행
dpdk-l2fwd -l 0-3 -n 4 \
--vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
-- -p 0x1
# 제로카피 모드 (NIC 드라이버가 지원하는 경우)
# i40e, ice, mlx5 등이 AF_XDP 제로카피 지원
dpdk-l2fwd -l 0-3 -n 4 \
--vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
-- -p 0x1
# 제로카피는 NIC 드라이버가 자동 감지하여 활성화
rte_flow, representor, embedded switch
현대 NIC와 DPU는 단순한 RX/TX 큐 이상의 기능을 제공합니다. PF/VF/SF 앞단의 embedded switch(eSwitch)를 하드웨어가 직접 처리하고, DPDK는 representor 포트와 rte_flow transfer 규칙으로 이 내부 스위치를 제어합니다. 이 개념을 이해해야 OVS hardware offload, SR-IOV, SmartNIC, switchdev 구성이 한 그림으로 연결됩니다.
| 개념 | 의미 | 운영 포인트 |
|---|---|---|
| PF | Physical Function. NIC의 기본 함수 | uplink 제어, VF/SF 관리, representor의 기준점 |
| VF | SR-IOV 가상 함수 | VM/컨테이너(Container)에 직결되기 쉽고 representor와 1:1 대응 |
| SF | SubFunction | 일부 최신 NIC/DPU가 제공하는 더 세밀한 함수 단위 |
| Representor | eSwitch 내부 포트를 호스트에서 표현한 가상 포트 | control plane, 통계, 미러링, 정책 투입 지점 |
transfer 규칙 | wire ↔ VF/SF ↔ representor 사이 하드웨어 스위치 도메인 규칙 | 일반 ingress/egress 큐 규칙과 구분해서 생각해야 함 |
| Bifurcated PMD | 커널 netdev와 DPDK가 기능을 분담 | 대표적으로 mlx5 계열에서 중요 |
# representor 포트를 함께 열어 eSwitch를 제어하는 예시
dpdk-testpmd -l 2-7 -n 4 \
-a 0000:03:00.0,representor=vf[0-3] \
-- -i
# rte_flow 관점에서는 attr.transfer = 1 이면
# 큐가 아니라 eSwitch 도메인에 규칙을 넣겠다는 뜻입니다.
/* eSwitch transfer 규칙의 핵심만 남긴 예시 */
struct rte_flow_attr attr = {
.ingress = 1,
.transfer = 1, /* 큐가 아닌 eSwitch 도메인 */
};
/* 패턴/액션은 NIC별 지원 범위가 다르므로
* 반드시 rte_flow_validate()와 rte_eth_dev_info_get()로 먼저 확인 */
rte_flow_validate(port_id, &attr, pattern, actions, &error);
rte_flow 패턴·액션 카탈로그
rte_flow는 NIC 하드웨어에 패킷 분류/액션 규칙을 설치하는 통합 API입니다.
패턴(pattern)으로 매칭 조건을 정의하고, 액션(action)으로 매칭된 패킷의 처리를 지정합니다.
NIC별 지원 범위가 다르므로 반드시 rte_flow_validate()로 검증해야 합니다.
| 패턴 항목 | 매칭 대상 | 사용 예 |
|---|---|---|
RTE_FLOW_ITEM_TYPE_ETH | 이더넷 헤더 (src/dst MAC, EtherType) | 특정 MAC 주소 필터링 |
RTE_FLOW_ITEM_TYPE_VLAN | VLAN 태그 (VID, PCP, DEI) | VLAN별 큐 분배 |
RTE_FLOW_ITEM_TYPE_IPV4 | IPv4 헤더 (src/dst IP, protocol, TTL) | 서브넷별 라우팅 |
RTE_FLOW_ITEM_TYPE_IPV6 | IPv6 헤더 (src/dst IP, next_hdr, flow_label) | IPv6 플로우 레이블 분류 |
RTE_FLOW_ITEM_TYPE_TCP | TCP 헤더 (src/dst port, flags) | 특정 포트 트래픽 분리 |
RTE_FLOW_ITEM_TYPE_UDP | UDP 헤더 (src/dst port) | DNS/VXLAN 트래픽 식별 |
RTE_FLOW_ITEM_TYPE_VXLAN | VXLAN 헤더 (VNI) | 오버레이(Overlay) 네트워크 분류 |
RTE_FLOW_ITEM_TYPE_GRE | GRE 헤더 (protocol, key) | 터널 트래픽 분류 |
RTE_FLOW_ITEM_TYPE_GENEVE | Geneve 헤더 (VNI, options) | OVN/Geneve 오버레이 |
RTE_FLOW_ITEM_TYPE_MARK | 이전 규칙이 설정한 마크 | 다단계 필터링 |
RTE_FLOW_ITEM_TYPE_META | 메타데이터 (드라이버 정의) | eSwitch/representor 간 상태 전달 |
RTE_FLOW_ITEM_TYPE_CONNTRACK | CT 상태 (established, new, invalid) | 상태 기반 방화벽(Firewall) |
RTE_FLOW_ITEM_TYPE_PORT_REPRESENTOR | representor 포트 | eSwitch 규칙에서 소스/대상 지정 |
| 액션 | 동작 | 사용 예 |
|---|---|---|
RTE_FLOW_ACTION_TYPE_QUEUE | 특정 RX 큐로 전달 | 플로우별 코어 고정 |
RTE_FLOW_ACTION_TYPE_RSS | RSS로 큐 그룹에 분배 | 서비스별 RSS 분리 |
RTE_FLOW_ACTION_TYPE_DROP | 패킷 드롭 | DDoS 필터링, ACL |
RTE_FLOW_ACTION_TYPE_MARK | mbuf에 마크 값 설정 | 소프트웨어 후처리 힌트 |
RTE_FLOW_ACTION_TYPE_COUNT | 매칭 패킷/바이트 카운터 | 통계, 과금 |
RTE_FLOW_ACTION_TYPE_SET_MAC_SRC/DST | MAC 주소 변경 | L2 NAT |
RTE_FLOW_ACTION_TYPE_SET_IPV4_SRC/DST | IP 주소 변경 | NAT, 로드밸런싱 |
RTE_FLOW_ACTION_TYPE_SET_TP_SRC/DST | TCP/UDP 포트 변경 | NAPT |
RTE_FLOW_ACTION_TYPE_VXLAN_ENCAP/DECAP | VXLAN 캡슐화(Encapsulation)/해제 | 오버레이 네트워크 |
RTE_FLOW_ACTION_TYPE_PORT_ID | 다른 포트(representor)로 전달 | eSwitch 내부 포워딩 |
RTE_FLOW_ACTION_TYPE_JUMP | 다른 flow 테이블로 이동 | 다단계 파이프라인 |
RTE_FLOW_ACTION_TYPE_METER | 트래픽 미터링 (srTCM/trTCM) | QoS, 과금 |
RTE_FLOW_ACTION_TYPE_AGE | 타임아웃 기반 규칙 만료 | 동적 플로우 관리 |
RTE_FLOW_ACTION_TYPE_CONNTRACK | CT 상태 추적/업데이트 | 하드웨어 상태 방화벽 |
RTE_FLOW_ACTION_TYPE_SAMPLE | 패킷 샘플링 (미러링) | 모니터링, 디버깅 |
/* rte_flow 규칙 생성 — IPv4 TCP 포트 80을 Queue 2로 전달 */
struct rte_flow_attr attr = { .ingress = 1 };
/* 패턴: ETH / IPv4 / TCP dst_port=80 */
struct rte_flow_item_eth eth_spec = { 0 };
struct rte_flow_item_ipv4 ipv4_spec = { 0 };
struct rte_flow_item_tcp tcp_spec = {
.hdr.dst_port = rte_cpu_to_be_16(80),
};
struct rte_flow_item_tcp tcp_mask = {
.hdr.dst_port = 0xFFFF,
};
struct rte_flow_item pattern[] = {
{ .type = RTE_FLOW_ITEM_TYPE_ETH, .spec = ð_spec },
{ .type = RTE_FLOW_ITEM_TYPE_IPV4, .spec = &ipv4_spec },
{ .type = RTE_FLOW_ITEM_TYPE_TCP, .spec = &tcp_spec, .mask = &tcp_mask },
{ .type = RTE_FLOW_ITEM_TYPE_END },
};
/* 액션: Queue 2 + 카운터 */
struct rte_flow_action_queue queue = { .index = 2 };
struct rte_flow_action_count count = { 0 };
struct rte_flow_action actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_COUNT, .conf = &count },
{ .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &queue },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};
/* 검증 후 생성 */
struct rte_flow_error error;
if (rte_flow_validate(port_id, &attr, pattern, actions, &error) == 0) {
struct rte_flow *flow = rte_flow_create(port_id, &attr,
pattern, actions, &error);
} else {
printf("Flow validate failed: %s\\n", error.message);
}
Connection Tracking (CT) 오프로드
DPDK 22.x 이후 rte_flow는 하드웨어 기반 Connection Tracking을 지원합니다.
커널의 conntrack처럼 TCP/UDP 연결 상태(NEW, ESTABLISHED, RELATED, INVALID)를 NIC eSwitch 하드웨어가 추적하고,
rte_flow 규칙에서 상태를 기반으로 패킷을 분류/드롭합니다. OVS hardware offload의 stateful firewall 지원에 핵심입니다.
| CT 상태 | 의미 | 전형적 액션 |
|---|---|---|
| NEW | 새 연결의 첫 패킷 (SYN) | CT 테이블에 등록, 정책 검사 후 허용/드롭 |
| ESTABLISHED | 양방향 패킷이 관찰된 연결 | 빠른 경로로 직접 전달 (하드웨어 오프로드) |
| RELATED | 기존 연결과 관련된 패킷 (FTP DATA 등) | 허용 |
| INVALID | 추적할 수 없는 패킷 | 드롭 |
/* CT 오프로드: ESTABLISHED 연결은 하드웨어에서 직접 전달 */
struct rte_flow_item_conntrack ct_item = {
.flags = RTE_FLOW_CONNTRACK_FLAG_ESTABLISHED,
};
struct rte_flow_item ct_pattern[] = {
{ .type = RTE_FLOW_ITEM_TYPE_ETH },
{ .type = RTE_FLOW_ITEM_TYPE_IPV4 },
{ .type = RTE_FLOW_ITEM_TYPE_TCP },
{ .type = RTE_FLOW_ITEM_TYPE_CONNTRACK, .spec = &ct_item },
{ .type = RTE_FLOW_ITEM_TYPE_END },
};
/* ESTABLISHED → representor로 직접 전달 (CPU 바이패스) */
struct rte_flow_action ct_actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_PORT_ID, .conf = &repr_port },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};
tc-ct 오프로드를 통해 커널 conntrack 하드웨어 오프로드를 지원하고,
DPDK 측에서는 rte_flow CT 액션이 같은 NIC eSwitch 하드웨어를 사용합니다. 두 경로 모두 SmartNIC(mlx5, bnxt 등)의
eSwitch CT 테이블을 활용하므로, NIC 펌웨어(Firmware)의 CT 테이블 크기가 실질적 한계입니다.
Flow isolated mode
isolated mode는 "명시적으로 설치한 rte_flow 규칙만 포트에 들어오게 하겠습니다"는 선언입니다. 이 모드를 켜면 PMD의 기본 RSS/기본 큐 처리 경로를 차단하고, 포트 전체를 flow rule 기반 제어면으로 바꾸게 됩니다. 하드웨어 오프로드 어플라이언스나 엄격한 서비스 체인에서 특히 유용합니다.
testpmd> flow isolate 0 true
testpmd> flow create 0 ingress pattern eth / ipv4 / tcp / end \
actions queue index 0 / end
testpmd> flow list 0
| 장점 | 주의점 |
|---|---|
| 기본 포트 동작을 제거해 "무엇이 패킷을 받는지"를 명시적으로 통제 | 드라이버별로 되돌림 가능 여부와 지원 범위가 다릅니다. |
| 오프로드 디버깅 시 소프트웨어 fallback을 숨겨 문제를 더 빨리 드러냄 | rule 누락 시 패킷이 조용히 사라질 수 있으므로 초기 bring-up 때는 신중해야 합니다. |
| 보안 관점에서 default queue를 닫고 allowlist 규칙만 남기기 쉬움 | 테스트 도구 없는 운영 전환은 위험합니다. |
Hairpin queue와 intra-device 포워딩
hairpin queue는 패킷을 호스트 메모리까지 올리지 않고 NIC 내부에서 다른 RX/TX 큐나 포트로 되돌리는 기능입니다. OVS hardware offload, representor 기반 서비스 체인, 고속 TAP 없는 VM 브리징, 장치 내부 loopback 테스트에서 중요합니다. 최신 testpmd 문서도 hairpin 관련 명령과 포트 프록시 조회 기능을 별도로 다룹니다.
/* Hairpin capability 조회 후 큐 구성하는 패턴 */
struct rte_eth_hairpin_cap cap;
rte_eth_dev_hairpin_capability_get(port_id, &cap);
struct rte_eth_hairpin_conf hp = {
.peer_count = 1,
.manual_bind = 1,
.tx_explicit = 1,
};
rte_eth_rx_hairpin_queue_setup(port_id, rxq_id, 1024, &hp);
rte_eth_tx_hairpin_queue_setup(port_id, txq_id, 1024, &hp);
| 적합한 경우 | 이유 |
|---|---|
| Representor 기반 서비스 체인 | VM/VF/SF 사이 트래픽을 NIC 내부에서 우회시켜 CPU 부하를 줄입니다. |
| Port-to-port 브리지(Bridge) | 동일 디바이스 안의 포트를 초저지연으로 연결할 수 있습니다. |
| 하드웨어 오프로드 검증 | flow rule이 실제 eSwitch 경로를 타는지 빠르게 확인할 수 있습니다. |
Virtio/Vhost-user 아키텍처
DPDK 환경에서 VM과의 패킷 교환은 vhost-user 프로토콜을 통해 이루어집니다. QEMU 게스트의 virtio-net 프론트엔드와 호스트의 DPDK vhost 백엔드가 공유 메모리(hugepage) 위의 virtqueue를 통해 제로카피로 패킷을 전달합니다. 이 구조를 이해해야 OVS-DPDK, Snabbswitch, VPP 등 가상 스위칭의 성능 특성을 설명할 수 있습니다.
| 항목 | vhost-user | vhost-net (커널) | SR-IOV 패스스루 |
|---|---|---|---|
| 호스트 백엔드 | DPDK 유저 공간 | 커널 vhost 모듈 | VF 직접 할당 |
| 스위칭 | OVS-DPDK / VPP | 커널 브릿지 / OVS | NIC eSwitch |
| 성능 (64B) | ~8 Mpps/core | ~2 Mpps/core | ~14 Mpps (wire) |
| 유연성 | 높음 (소프트웨어 스위칭) | 중간 | 낮음 (고정 기능) |
| 라이브 마이그레이션 | 지원 (dirty page tracking) | 지원 | 제한적 |
| 멀티큐 | MQ virtio 지원 | 지원 | VF 큐 수에 의존 |
| 제어 채널 | UNIX 소켓 | ioctl | 없음 (직접 VF) |
# ━━━ QEMU vhost-user 설정 ━━━
# 1. vhost-user 소켓 경로 (OVS-DPDK가 생성)
# /tmp/vhost-user0
# 2. QEMU 실행 (vhost-user 백엔드 연결)
qemu-system-x86_64 \
-m 4G -smp 4 \
-object memory-backend-file,id=mem,size=4G,mem-path=/dev/hugepages,share=on \
-numa node,memdev=mem \
-chardev socket,id=char0,path=/tmp/vhost-user0,server=off \
-netdev vhost-user,id=net0,chardev=char0,vhostforce=on,queues=4 \
-device virtio-net-pci,netdev=net0,mq=on,vectors=10 \
...
# 핵심 옵션 설명:
# share=on — hugepage를 호스트와 게스트가 공유 (필수)
# vhostforce=on — vhost-user 강제 사용
# queues=4 — 멀티큐 virtio (vCPU 수에 맞춤)
# vectors=10 — MSI-X 벡터 수 (2*queues + 2)
rte_vhost_enqueue/dequeue 구현 분석
/* lib/vhost/virtio_net.c — 호스트→게스트 패킷 전달 (간략화) */
uint16_t
rte_vhost_enqueue_burst(int vid, uint16_t queue_id,
struct rte_mbuf **pkts, uint16_t count)
{
struct virtio_net *dev = get_device(vid);
struct vhost_virtqueue *vq = dev->virtqueue[queue_id];
uint16_t avail_idx, free_entries;
/* ① 게스트의 avail ring에서 빈 descriptor 확인 */
avail_idx = __atomic_load_n(&vq->avail->idx,
__ATOMIC_ACQUIRE);
free_entries = avail_idx - vq->last_avail_idx;
count = RTE_MIN(count, free_entries);
for (uint16_t i = 0; i < count; i++) {
/* ② avail ring에서 descriptor index 가져오기 */
uint16_t desc_idx = vq->avail->ring[
(vq->last_avail_idx + i) & (vq->size - 1)];
struct vring_desc *desc = &vq->desc[desc_idx];
/* ③ 게스트 메모리에 패킷 데이터 복사
* (공유 hugepage이므로 호스트에서 직접 접근 가능) */
void *dst = (void *)(uintptr_t)
gpa_to_hva(dev, desc->addr);
rte_memcpy(dst + virtio_hdr_size,
rte_pktmbuf_mtod(pkts[i], void *),
pkts[i]->pkt_len);
/* ④ virtio 헤더 설정 (checksum 오프로드 힌트 등) */
struct virtio_net_hdr *hdr = dst;
hdr->num_buffers = 1;
hdr->flags = 0;
/* ⑤ used ring에 완료 보고 */
vq->used->ring[used_idx & (vq->size - 1)] =
(struct vring_used_elem){
.id = desc_idx,
.len = pkts[i]->pkt_len + virtio_hdr_size,
};
}
/* ⑥ used ring index 갱신 → 게스트에게 새 패킷 도착 알림 */
__atomic_store_n(&vq->used->idx,
vq->last_used_idx + count, __ATOMIC_RELEASE);
/* ⑦ 게스트에 인터럽트(eventfd kick) 전송 (필요 시) */
if (!(vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT))
eventfd_write(vq->callfd, (eventfd_t)1);
return count;
}
코드 분석: vhost-user 패킷 전달
- avail ring / used ring: virtio의 split virtqueue는 3개 영역으로 구성됩니다. descriptor table(버퍼 주소/길이), avail ring(게스트→호스트: "이 descriptor가 비어있음"), used ring(호스트→게스트: "이 descriptor에 데이터를 넣었음"). DPDK vhost는 호스트 측에서 이 링을 직접 조작합니다.
- gpa_to_hva(): 게스트 물리 주소(GPA)를 호스트 가상 주소(HVA)로 변환합니다. QEMU가
memory-backend-file,share=on으로 게스트 RAM을 hugepage에 매핑했으므로, 호스트의 DPDK 프로세스도 같은 hugepage에 접근할 수 있습니다. 이 주소 변환 테이블은 vhost-user UNIX 소켓의 제어 메시지로 교환됩니다. - rte_memcpy(): 패킷 데이터를 게스트의 virtio 버퍼에 복사합니다. vhost-user는 "제로카피"라고 불리지만, 실제로는 DPDK mbuf → 게스트 virtio 버퍼 간 1회 복사가 발생합니다. 진정한 제로카피(복사 0회)는 packed virtqueue + 직접 mbuf 공유 모드에서만 가능하며, 게스트의 virtio 드라이버가 이를 지원해야 합니다.
- eventfd kick: 게스트가
VRING_AVAIL_F_NO_INTERRUPT플래그를 설정하지 않았다면, 호스트가eventfd_write()로 게스트에 인터럽트를 전달합니다. 게스트의 virtio 드라이버가 이 인터럽트를 받아 used ring을 확인합니다. 고성능 환경에서는 게스트도 폴링 모드로 동작하여 인터럽트를 비활성화합니다. - dequeue(게스트→호스트):
rte_vhost_dequeue_burst()는 반대 방향으로 동작합니다. 게스트가 avail ring에 전송할 패킷의 descriptor를 게시하면, 호스트 PMD가 게스트 메모리에서 데이터를 읽어 DPDK mbuf에 복사합니다.
OVS-DPDK (Open vSwitch + DPDK)
OVS-DPDK는 Open vSwitch의 데이터플레인을 DPDK로 교체하여 가상 스위칭 성능을 극대화한 구성입니다. 클라우드/NFV 환경에서 VM/컨테이너 간 네트워크 트래픽을 유저 공간에서 처리하여, 기존 커널 OVS 대비 5~10배의 처리량을 달성합니다.
DPDK 25.11.0 지원을 명시합니다.
OVS-DPDK는 커널 모듈보다 OVS 사용자 공간(User Space) 바이너리와 DPDK ABI 조합에 더 민감하므로, 배포판 기본 패키지와 자체 빌드를 섞기 전에 지원 조합을 먼저 고정해야 합니다.
# ━━━ OVS-DPDK 설정 예시 ━━━
# 1. OVS에 DPDK 초기화 파라미터 설정
ovs-vsctl --no-wait set Open_vSwitch . \
other_config:dpdk-init=true \
other_config:dpdk-socket-mem="1024,1024" \
other_config:dpdk-lcore-mask=0x3 \
other_config:dpdk-hugepage-dir="/dev/hugepages"
# 2. OVS 데몬 재시작
systemctl restart openvswitch-switch
# 3. DPDK 브릿지 생성
ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev
# 4. DPDK 물리 포트 추가 (VFIO 바인딩된 NIC)
ovs-vsctl add-port br0 dpdk-p0 -- set Interface dpdk-p0 \
type=dpdk options:dpdk-devargs=0000:03:00.0
# 5. vhost-user 포트 추가 (VM 연결용)
ovs-vsctl add-port br0 vhost-user0 -- set Interface vhost-user0 \
type=dpdkvhostuserclient \
options:vhost-server-path="/tmp/vhost-user0"
# 6. PMD 스레드 CPU 할당 (NUMA 인식)
ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x3C
# 7. 플로우 설정 (OpenFlow)
ovs-ofctl add-flow br0 "in_port=dpdk-p0,actions=output:vhost-user0"
ovs-ofctl add-flow br0 "in_port=vhost-user0,actions=output:dpdk-p0"
# ━━━ 성능 확인 ━━━
ovs-appctl dpif-netdev/pmd-stats-show # PMD 스레드 통계
ovs-appctl dpif-netdev/pmd-rxq-show # RX 큐 매핑
ovs-appctl dpctl/dump-flows # 데이터패스 플로우
ovs-appctl coverage/show # 내부 이벤트 카운터
OVS-DPDK 데이터패스 내부 구현 분석
OVS-DPDK의 패킷 처리 핵심은 PMD 스레드가 실행하는 dp_netdev_process_rxq_port() 함수입니다. 이 함수가 NIC/vhost-user 큐에서 패킷을 수신하고, 플로우 캐시를 조회하여 액션을 실행합니다.
/* lib/dpif-netdev.c — OVS-DPDK PMD 스레드 메인 루프 (간략화) */
static void
dp_netdev_process_rxq_port(struct dp_netdev_pmd_thread *pmd,
struct dp_netdev_rxq *rxq)
{
struct dp_packet_batch batch;
int error;
/* ① NIC/vhost-user에서 패킷 배치 수신 */
error = netdev_rxq_recv(rxq->rx, &batch, NULL);
/* netdev_rxq_recv()는 내부적으로
* rte_eth_rx_burst() 또는 rte_vhost_dequeue_burst()를 호출 */
if (error || dp_packet_batch_is_empty(&batch))
return;
/* ② 패킷을 플로우 캐시에서 조회하여 분류 */
dp_netdev_input(pmd, &batch, rxq->port->port_no);
}
/* lib/dpif-netdev.c — 플로우 조회 3단계 캐시 */
static void
dp_netdev_input(struct dp_netdev_pmd_thread *pmd,
struct dp_packet_batch *packets,
odp_port_t port_no)
{
/* ① EMC (Exact Match Cache) — O(1) 해시 조회
* 최빈 플로우를 해시 테이블에 캐시
* 히트 시 즉시 액션 실행 → 최고 성능 경로 */
emc_processing(pmd, packets, keys, &missed_batch);
if (dp_packet_batch_is_empty(&missed_batch))
return; /* 모든 패킷 EMC 히트 → 완료 */
/* ② dpcls (Datapath Classifier) — 튜플 기반 분류
* EMC 미스 패킷을 5-tuple 마스크 기반으로 조회
* EMC보다 느리지만 와일드카드 매칭 지원 */
dpcls_lookup(pmd->cls, keys, &missed_batch,
&upcall_batch);
if (!dp_packet_batch_is_empty(&upcall_batch)) {
/* ③ upcall — ofproto로 미스 보고
* dpcls에도 없는 새 플로우 → OpenFlow 테이블 조회
* 매칭된 액션을 데이터패스에 캐시 설치 */
dp_netdev_upcall(pmd, &upcall_batch);
}
/* 분류된 모든 패킷에 대해 액션 실행 */
dp_netdev_execute_actions(pmd, packets, actions);
/* 액션 예: output(포트 전송), mod_vlan, set_field,
* ct(conntrack), drop, controller 등 */
}
코드 분석: OVS-DPDK 데이터패스
- 3단계 플로우 캐시: OVS-DPDK의 성능 핵심은 EMC → dpcls → upcall 3단계 조회입니다. EMC는 최빈 플로우를 해시 O(1)로 매칭하여 ~14 Mpps/core를 달성합니다. EMC 히트율이 90% 이상이면 커널 OVS 대비 10배 이상 성능이 나옵니다.
- EMC(Exact Match Cache): 8192개 엔트리의 해시 테이블로, 5-tuple(src/dst IP, src/dst port, protocol) 해시로 인덱싱합니다. 히트 시 캐시된
dp_netdev_flow구조체의 액션을 즉시 실행합니다. 테이블 크기는other_config:n-dpdk-rxqs로 조정할 수 있습니다. - dpcls(Datapath Classifier): 와일드카드 매칭이 가능한 튜플 기반 분류기입니다. EMC가 exact match만 지원하는 반면, dpcls는 서브넷 마스크 등 부분 매칭을 처리합니다. 내부적으로 튜플 공간(Tuple Space Search) 알고리즘을 사용합니다.
- upcall: EMC와 dpcls 모두 미스인 새 플로우의 첫 패킷은 ofproto(OpenFlow 파이프라인)로 "올려보냅니다(upcall)". ofproto가 OpenFlow 테이블을 순회하여 매칭 액션을 결정하면, 이를 데이터패스에 캐시로 설치합니다. upcall은 느리므로(~1000 pps), 정상 운영에서는 대부분의 패킷이 EMC/dpcls에서 처리되어야 합니다.
- dp_netdev_execute_actions(): 분류 결과에 따라 패킷에 액션을 실행합니다.
output액션은 해당 포트(vhost-user, DPDK physical)의netdev_send()를 호출하여rte_eth_tx_burst()또는rte_vhost_enqueue_burst()로 패킷을 전송합니다. - PMD 스레드:
pmd-cpu-mask로 지정된 코어에서 busy-poll로 실행됩니다. 각 PMD 스레드는 할당된 RX 큐(물리 NIC + vhost-user 포트)를 라운드 로빈으로 폴링합니다.ovs-appctl dpif-netdev/pmd-rxq-show로 큐-스레드 매핑을 확인할 수 있습니다.
DPDK 성능 튜닝
DPDK 성능 튜닝은 시스템 레벨(커널 부트 파라미터, BIOS)에서 시작하여 DPDK 레벨(burst 크기, descriptor ring, mempool), 마지막으로 애플리케이션 레벨(파이프라인 구조, 캐시 정렬)까지 3단계로 진행합니다. 각 항목이 성능에 미치는 영향과 설정 근거를 상세히 설명합니다.
| 튜닝 항목 | 설정 | 효과 |
|---|---|---|
| CPU 격리 | isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 |
DPDK 코어에서 커널 스케줄러/RCU/타이머 인터럽트 제거. jitter 최소화 |
| 1GB Hugepage | hugepagesz=1G hugepages=8 |
2MB 대비 TLB 미스 99% 감소. IOTLB 효율도 향상 |
| NUMA 정렬 | NIC과 같은 NUMA 노드의 코어/메모리 사용 | 크로스-NUMA 접근 시 40~100ns 추가 지연 방지 |
| RX/TX burst 크기 | 32~64 (2의 거듭제곱) | 벡터화 PMD 경로 활성화. prefetch 효율 극대화 |
| Descriptor ring 크기 | 2048~4096 | 마이크로버스트 흡수. 너무 크면 캐시 효율 저하 |
| Mempool 캐시 | per-core 캐시 256~512 | 공용 Ring 접근 빈도 감소. 캐시 > burst_size × 1.5 권장 |
| 하이퍼스레딩 | 비활성화 또는 sibling 회피 | HT sibling이 같은 L1/L2 캐시를 공유하여 캐시 오염 |
| 전력 관리 | intel_pstate=disable processor.max_cstate=1 |
C-state 전환 지연 제거 (C6→C0 복귀에 ~100μs) |
| IRQ 밸런싱 | irqbalance 중지, DPDK 코어에서 IRQ 배제 |
폴링 코어에 인터럽트 간섭 제거 |
| PCIe MMIO 최적화 | Write-Combining 활성화 (RTE_ETH_TX_OFFLOAD_WC) |
TX tail 레지스터 쓰기를 배치하여 PCIe 트랜잭션(Transaction) 감소 |
# ━━━ 시스템 레벨 DPDK 성능 튜닝 ━━━
# CPU 격리 (GRUB 커맨드라인)
# GRUB_CMDLINE_LINUX="isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 \
# intel_pstate=disable processor.max_cstate=1 intel_idle.max_cstate=0 \
# default_hugepagesz=1G hugepagesz=1G hugepages=8 \
# iommu=pt intel_iommu=on"
# NUMA 토폴로지 확인 — NIC이 어느 NUMA 노드에 있는지
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
# 0 → NUMA 노드 0의 코어와 메모리를 사용해야 함
# 해당 NUMA 노드의 코어 확인
lscpu | grep "NUMA node0"
# NUMA node0 CPU(s): 0-7,16-23
# irqbalance 비활성화 + DPDK 코어에서 IRQ 배제
systemctl stop irqbalance
# /proc/irq/default_smp_affinity에서 DPDK 코어 제외
echo 000F > /proc/irq/default_smp_affinity # 코어 0-3만 IRQ 허용
# CPU 주파수 고정 (Turbo Boost는 유지, P-state 전환 최소화)
cpupower -c 4-15 frequency-set -g performance
# 코어별 C-state 확인
turbostat --quiet --show Core,CPU,Busy%,Bzy_MHz,IRQ,C1%,C6% sleep 1
burst 크기와 벡터 PMD의 관계
DPDK PMD는 burst 크기에 따라 서로 다른 코드 경로를 선택합니다. burst 크기가 충분히 크고 2의 거듭제곱이면 벡터화(Vectorized) PMD가 활성화되어 SIMD 명령어(SSE4/AVX2/AVX-512)로 여러 descriptor를 한 번에 처리합니다.
| burst 크기 | PMD 경로 | 성능 특성 |
|---|---|---|
| 1~4 | 스칼라(Scalar) 경로 | descriptor 하나씩 처리. 기능은 모두 지원하지만 느림 |
| 8~16 | 스칼라 + prefetch | prefetch로 캐시 미스 감소, 중간 성능 |
| 32~64 (권장) | 벡터(Vector) 경로 | SIMD로 4~8개 descriptor 동시 처리. 최대 성능 |
| 128 이상 | 벡터 경로 (과다) | L1/L2 캐시 오염 위험. 성능 하락 가능 |
dpdk-testpmd의 show port info all에서 "Rx offloads"를 확인하여 벡터 경로가 활성화되었는지 판단할 수 있습니다.
성능 측정과 병목 분석
# ━━━ testpmd로 기본 PPS 측정 ━━━
dpdk-testpmd -l 4-7 -n 4 -- -i --nb-cores=2 --rxq=2 --txq=2
# testpmd 내부 명령
testpmd> set fwd io # 단순 포워딩 (최대 PPS 측정용)
testpmd> start
testpmd> show port stats all # RX/TX PPS, 바이트, 에러 확인
# ━━━ perf로 코어별 병목 분석 ━━━
# LLC(Last Level Cache) 미스 확인 — 캐시 효율 측정
perf stat -e LLC-load-misses,LLC-store-misses \
-e instructions,cycles \
-C 4-7 sleep 5
# 함수별 CPU 사용 프로파일링
perf top -C 4 -g --no-children
# ━━━ dpdk-proc-info로 큐별 통계 ━━━
dpdk-proc-info -- --stats
dpdk-proc-info -- --xstats # 확장 통계 (드롭, 에러 상세)
QoS/Traffic Management (rte_sched)
rte_sched는 DPDK의 계층적 QoS 스케줄러로, 통신사 NFV와 엔터프라이즈 네트워크에서
가입자/서비스별 대역폭(Bandwidth) 보장과 트래픽 셰이핑을 유저 공간에서 구현합니다.
커널의 tc/HTB/TBF에 대응하지만, DPDK 데이터 경로 내에서
패킷 단위로 동작하므로 수십 Mpps 수준의 처리량에서도 정밀한 QoS를 유지할 수 있습니다.
| 계층 | 스케줄링 알고리즘 | 설정 예 |
|---|---|---|
| Port | Token Bucket (전체 출력 rate) | 10 Gbps line rate |
| Subport | Token Bucket + TC간 WFQ | 고객 A: 2 Gbps 보장 |
| Pipe | Token Bucket (가입자 셰이핑) | 가입자당 100 Mbps |
| TC | Strict Priority (높은 TC 우선) | TC0=BE, TC1=AF, TC2=EF(VoIP) |
| Queue | WRR (가중치 라운드 로빈(Round Robin)) | Q0: weight=4, Q1: weight=2 |
/* rte_sched QoS 스케줄러 설정 (간략화) */
struct rte_sched_subport_params subport_params = {
.tb_rate = 1250000000, /* 10 Gbps in bytes/s */
.tb_size = 1000000, /* 토큰 버킷 크기 */
.tc_rate = {
[0] = 500000000, /* TC0: 4 Gbps (Best Effort) */
[1] = 375000000, /* TC1: 3 Gbps (Assured Forwarding) */
[2] = 250000000, /* TC2: 2 Gbps (Expedited Forwarding) */
[3] = 125000000, /* TC3: 1 Gbps */
},
.tc_period = 10, /* TC 갱신 주기 (ms) */
};
struct rte_sched_pipe_params pipe_profiles[] = {
{ /* 프로필 0: 일반 가입자 100 Mbps */
.tb_rate = 12500000, /* 100 Mbps */
.tb_size = 100000,
.tc_rate = { 6250000, 3125000, 3125000, 0 },
.tc_period = 40,
.wrr_weights = { 1, 1, 1, 1 },
},
};
/* 스케줄러 생성 및 패킷 큐잉/디큐잉 */
struct rte_sched_port *sched = rte_sched_port_config(&port_params);
rte_sched_subport_config(sched, 0, &subport_params, 0);
rte_sched_pipe_config(sched, 0, pipe_id, 0);
/* 데이터 경로에서: classify → enqueue → dequeue */
rte_sched_port_pkt_write(sched, pkts, nb_pkts, subport, pipe, tc, queue);
uint16_t nb_enq = rte_sched_port_enqueue(sched, pkts, nb_pkts);
uint16_t nb_deq = rte_sched_port_dequeue(sched, out_pkts, 32);
rte_eth_tx_burst(port_id, 0, out_pkts, nb_deq);
코드 분석: rte_sched 데이터 경로
- rte_sched_port_pkt_write(): 패킷의 mbuf에 QoS 분류 정보(subport, pipe, TC, queue)를 기록합니다. 내부적으로
mbuf->hash.sched필드에 4개 값을 packed 형태로 저장합니다. 이 함수를 호출하기 전에 애플리케이션이 패킷을 분류(5-tuple → 가입자 → pipe 매핑)해야 합니다. - rte_sched_port_enqueue() 내부:
- 패킷의 sched 메타데이터를 읽어 해당 subport/pipe/TC/queue를 결정합니다.
- WRED(Weighted Random Early Detection)로 드롭 여부를 판단합니다. 큐 점유율이 min_th 이하면 무조건 수용, max_th 이상이면 무조건 드롭, 사이에서는 확률적으로 드롭합니다.
- 드롭되지 않은 패킷을 해당 큐(rte_ring)에 삽입합니다.
- rte_sched_port_dequeue() 내부 — Token Bucket 알고리즘:
- 현재 시간(
rte_rdtsc())을 기준으로 마지막 갱신 이후 경과 시간에 비례하여 Port, Subport, Pipe의 토큰을 충전합니다. - 각 Pipe에 대해 TC를 Strict Priority 순서로 순회합니다. 높은 우선순위 TC(EF)가 토큰이 있으면 먼저 dequeue합니다.
- 같은 TC 내의 여러 Queue는 WRR(Weighted Round Robin)로 순회합니다. weight가 높은 큐가 더 많은 패킷을 dequeue합니다.
- dequeue된 패킷의 바이트 수만큼 Pipe/Subport/Port의 토큰을 소비합니다. 토큰이 부족하면 해당 계층의 dequeue가 중단됩니다.
- 현재 시간(
- Token Bucket 파라미터:
tb_rate는 초당 충전되는 토큰 바이트 수(= 대역폭),tb_size는 버킷 최대 크기(= 버스트 허용량)입니다. 예:tb_rate=12500000(100Mbps),tb_size=100000(100KB 버스트 허용). - tc_period: TC 토큰 충전 주기입니다. 짧을수록 정밀하지만 CPU 오버헤드가 증가합니다. 10~40ms가 일반적입니다.
WRED 드롭 정책 구현
/* lib/sched/rte_sched.c — WRED 드롭 판단 (간략화) */
static inline int
rte_sched_port_enqueue_qwa(struct rte_sched_port *port,
uint32_t qindex, struct rte_mbuf *pkt)
{
struct rte_sched_queue *q = port->queue + qindex;
uint32_t qlen = q->qw - q->qr; /* 현재 큐 길이 */
/* WRED: 색상별 드롭 확률 계산 */
enum rte_color color = pkt->color; /* GREEN/YELLOW/RED */
struct rte_red *red = &q->red_params[color];
/* EWMA(지수 가중 이동 평균)로 평균 큐 길이 갱신 */
red->avg = ((1 - red->wq_log2) * red->avg)
+ (red->wq_log2 * qlen);
if (red->avg < red->min_th) {
/* 안전 영역: 무조건 수용 */
return 0; /* enqueue 허용 */
} else if (red->avg >= red->max_th) {
/* 위험 영역: 무조건 드롭 */
rte_pktmbuf_free(pkt);
return -1;
} else {
/* 중간 영역: 확률적 드롭
* 드롭 확률 = (avg - min_th) / (max_th - min_th) × maxp */
uint32_t drop_prob = rte_red_calc_drop_prob(red);
if (rte_rand() % 100 < drop_prob) {
rte_pktmbuf_free(pkt);
return -1;
}
return 0;
}
}
코드 분석: WRED 드롭 정책
- 패킷 색상(Color): DSCP나 외부 미터(meter)가 패킷에 GREEN/YELLOW/RED 색상을 부여합니다. 색상별로 다른 min_th/max_th를 설정하여 RED 패킷을 먼저 드롭하고 GREEN 패킷을 보호합니다.
- EWMA 평균: 순간 큐 길이 대신 지수 가중 이동 평균을 사용하여 마이크로버스트에 과민 반응하지 않습니다.
wq_log2가 클수록 평균이 느리게 변합니다. - 확률적 드롭: 중간 영역에서 큐 길이에 비례하여 드롭 확률이 증가합니다. TCP의 혼잡 제어가 패킷 드롭을 감지하여 전송률을 줄이므로, 큐가 가득 차기 전에 미리 신호를 보내는 효과가 있습니다.
- Tail Drop과의 차이: Tail Drop(큐 가득 차면 모든 신규 패킷 드롭)은 "전역 동기화(Global Synchronization)" 문제를 일으킵니다. 여러 TCP 플로우가 동시에 드롭되어 동시에 재전송하면 처리량이 급감합니다. WRED는 확률적으로 분산 드롭하여 이를 방지합니다.
tc/HTB는 커널 공간에서 sk_buff를 다루고 Qdisc 프레임워크를 사용합니다.
DPDK rte_sched는 유저 공간에서 rte_mbuf를 직접 큐잉하므로 컨텍스트 스위칭 없이 동작하지만,
커널의 nftables/conntrack 기반 QoS 정책과는 통합되지 않습니다.
Eventdev — 이벤트 기반 패킷 스케줄링
rte_eventdev는 DPDK의 이벤트 기반 프로그래밍 모델로,
하드웨어 이벤트 스케줄러(Intel DLB2 등)나 소프트웨어 구현을 통해
패킷을 워커 코어에 동적으로 분배합니다. Run-to-Completion과 Pipeline 모델의 장점을 결합합니다.
큐 타입별 시맨틱 상세
| 큐 타입 | 플로우 순서 | 동시성 | 락 필요 | 사용 사례 |
|---|---|---|---|---|
| Atomic | 동일 flow_id → 동일 워커 보장 | 서로 다른 플로우만 병렬 | 불필요 (플로우 단위 직렬화) | conntrack, 세션 상태, NAT 테이블 갱신 |
| Ordered | 출력 시 원래 순서로 재정렬 | 모든 워커 병렬 처리 가능 | 상태 접근 시 필요 | IPSec 암호화/복호화 (순서 유지 필수) |
| Parallel | 보장 없음 | 최대 (제약 없음) | 상태 접근 시 필요 | 무상태 패킷 검사, DPI, 로깅 |
flow_id(보통 5-tuple 해시)를 가진 이벤트는 이전 이벤트의 처리가 완료(rte_event_enqueue_burst() 또는 릴리스)될 때까지 다른 워커에 분배되지 않습니다. 이 보장 덕분에 플로우별 상태(세션 테이블, 카운터)에 락 없이 접근할 수 있습니다.
Eventdev 워커 루프 예시
/* eventdev 워커 코어 — 이벤트 수신 → 처리 → 전달 */
static int
event_worker_loop(void *arg)
{
uint8_t dev_id = *(uint8_t *)arg;
struct rte_event events[BURST_SIZE];
while (!force_quit) {
/* 이벤트 스케줄러에서 이벤트 수신 */
uint16_t nb_rx = rte_event_dequeue_burst(
dev_id, /* eventdev ID */
port_id, /* 워커 포트 ID */
events, BURST_SIZE,
0 /* timeout: 0이면 non-blocking */
);
for (uint16_t i = 0; i < nb_rx; i++) {
struct rte_mbuf *m = events[i].mbuf;
/* 패킷 처리 로직 (L3 포워딩, DPI 등) */
process_packet(m);
/* 처리 결과를 TX Adapter 큐로 전달 */
events[i].queue_id = TX_QUEUE_ID;
events[i].op = RTE_EVENT_OP_FORWARD;
}
if (nb_rx > 0)
rte_event_enqueue_burst(dev_id, port_id,
events, nb_rx);
}
return 0;
}
코드 분석
- 5행:
rte_event구조체는 mbuf 포인터, flow_id, queue_id, 스케줄링 타입, 우선순위를 포함합니다. ethdev의rte_mbuf를 이벤트로 감싸서 스케줄러에 넣는 구조입니다. - 8행:
rte_event_dequeue_burst()는 ethdev의rte_eth_rx_burst()에 해당하는 이벤트 수신 함수입니다. 스케줄러가 큐 타입(Atomic/Ordered/Parallel) 정책에 따라 이벤트를 분배합니다. - 21행:
RTE_EVENT_OP_FORWARD는 이벤트를 다른 큐로 전달(forward)하겠다는 의미입니다.RTE_EVENT_OP_RELEASE로 설정하면 이벤트를 소비(drop)합니다. - 24행:
rte_event_enqueue_burst()로 처리된 이벤트를 TX Adapter 큐에 넣으면, TX Adapter가 자동으로rte_eth_tx_burst()를 호출하여 패킷을 송신합니다.
Eventdev 초기화 코드
/* eventdev + RX/TX Adapter 초기화 골격 */
struct rte_event_dev_config dev_conf = {
.nb_event_queues = 4,
.nb_event_ports = 6, /* 워커 포트 수 */
.nb_events_limit = 4096,
.nb_event_queue_flows = 1024,
.nb_event_port_dequeue_depth = 128,
.nb_event_port_enqueue_depth = 128,
};
rte_event_dev_configure(dev_id, &dev_conf);
/* Atomic 큐 설정 — 플로우별 직렬화 보장 */
struct rte_event_queue_conf q_conf = {
.schedule_type = RTE_SCHED_TYPE_ATOMIC,
.nb_atomic_flows = 1024,
.nb_atomic_order_sequences = 1024,
};
rte_event_queue_setup(dev_id, queue_id, &q_conf);
/* RX Adapter: ethdev 포트 → eventdev 큐 연결 */
rte_event_eth_rx_adapter_create(adapter_id, dev_id, &port_conf);
rte_event_eth_rx_adapter_queue_add(adapter_id,
eth_port_id, -1, /* -1 = 모든 RX 큐 */
&queue_conf);
rte_event_eth_rx_adapter_start(adapter_id);
/* TX Adapter: eventdev → ethdev 포트 연결 */
rte_event_eth_tx_adapter_create(tx_adapter_id, dev_id, &port_conf);
rte_event_eth_tx_adapter_queue_add(tx_adapter_id,
eth_port_id, -1);
rte_event_eth_tx_adapter_start(tx_adapter_id);
Eventdev vs 수동 Pipeline vs Run-to-Completion
| 항목 | Run-to-Completion | 수동 Pipeline (Ring) | Eventdev |
|---|---|---|---|
| 코어 할당 | RX+처리+TX 동일 코어 | 단계별 전용 코어 | 스케줄러가 동적 분배 |
| 부하 분산 | RSS 고정 | Ring 용량으로 간접 조절 | 스케줄러가 자동 분배 |
| 플로우 순서 | RSS로 보장 | Ring FIFO로 보장 | Atomic/Ordered 큐로 보장 |
| 코어 추가/제거 | RSS 재설정 필요 | 새 Ring + 코어 할당 필요 | 포트 추가만으로 완료 |
| HW 가속 | 없음 | 없음 | Intel DLB2, Marvell SSO 등 |
| 복잡도 | 낮음 | 중간 | 높음 (초기 설정) |
| 적합 워크로드 | 균일 트래픽, 단순 처리 | 단계별 처리 비용 차이 클 때 | 가변 트래픽, 다단계 파이프라인 |
sw)는 CPU 코어 1개를 스케줄러로 소비합니다.
하드웨어 eventdev(Intel DLB2 등)는 전용 ASIC이 스케줄링하므로 CPU 오버헤드가 0이고 지연이 ~1μs 수준입니다.
워커 코어가 8개 이상인 환경에서 가변 트래픽을 처리한다면 HW eventdev의 ROI가 명확합니다.
Cryptodev — 암호화 가속 프레임워크
rte_cryptodev는 DPDK의 암호화 추상화 계층으로, 하드웨어 가속기(Intel QAT, Marvell OCTEON, ARM CE)와
소프트웨어 구현(AESNI, OpenSSL, ARMv8 CE)을 동일한 API로 접근합니다.
IPSec VPN, TLS 프록시, MACsec, 디스크 암호화 가속 등에서 DPDK 데이터 경로와 통합되어
패킷 단위 암호 연산을 인라인 또는 lookaside 방식으로 처리합니다.
/* Cryptodev 세션 생성 및 암호화 요청 (간략화) */
/* 1. Crypto 디바이스 설정 */
struct rte_cryptodev_config conf = {
.nb_queue_pairs = 1,
.socket_id = rte_socket_id(),
};
rte_cryptodev_configure(cdev_id, &conf);
/* 2. 세션 풀 생성 */
struct rte_mempool *sess_pool = rte_cryptodev_sym_session_pool_create(
"SESS_POOL", 256, 0, 0, 0, rte_socket_id());
/* 3. 대칭 암호화 세션: AES-128-CBC + HMAC-SHA256 */
struct rte_crypto_sym_xform cipher_xform = {
.type = RTE_CRYPTO_SYM_XFORM_CIPHER,
.cipher = {
.op = RTE_CRYPTO_CIPHER_OP_ENCRYPT,
.algo = RTE_CRYPTO_CIPHER_AES_CBC,
.key = { .data = key, .length = 16 },
.iv = { .offset = IV_OFFSET, .length = 16 },
},
.next = &auth_xform, /* 인증 체이닝 */
};
void *session = rte_cryptodev_sym_session_create(cdev_id,
&cipher_xform, sess_pool);
/* 4. 암호화 요청 (enqueue/dequeue) */
struct rte_crypto_op *ops[32];
rte_crypto_op_bulk_alloc(op_pool, RTE_CRYPTO_OP_TYPE_SYMMETRIC,
ops, 32);
for (int i = 0; i < 32; i++) {
ops[i]->sym->m_src = pkts[i]; /* mbuf 연결 */
rte_crypto_op_attach_sym_session(ops[i], session);
}
uint16_t enq = rte_cryptodev_enqueue_burst(cdev_id, 0, ops, 32);
/* ... 다른 작업 수행 ... */
uint16_t deq = rte_cryptodev_dequeue_burst(cdev_id, 0, ops, 32);
| Crypto PMD | 백엔드 | 특징 |
|---|---|---|
| QAT | Intel QuickAssist | 하드웨어 가속, 대칭/비대칭/해시, 압축도 지원 |
| AESNI-MB | Intel AES-NI 명령어 | 소프트웨어, x86 AES-NI/AVX2/AVX-512 활용 |
| OpenSSL | libcrypto | 소프트웨어, 가장 넓은 알고리즘 지원 |
| ARMv8 CE | ARM Crypto Extension | ARM 서버 네이티브 가속 |
| Scheduler | 멀티 PMD 번들 | 여러 crypto PMD를 묶어 부하 분산(Load Balancing) |
| Null | 무연산 | 성능 베이스라인 측정용 |
| 모델 | 작동 방식 | 적합한 경우 |
|---|---|---|
| Lookaside Protocol | 전체 프로토콜(ESP 등)을 crypto 장치가 처리 | IPSec 게이트웨이, 하드웨어가 프로토콜 인식 |
| Lookaside None | 앱이 프로토콜 처리, crypto 장치는 알고리즘만 | 커스텀 프로토콜, 세밀한 제어 필요 시 |
| Inline Crypto | NIC RX/TX 경로에서 하드웨어 암복호화 | 와이어 스피드 IPSec, MACsec |
| Inline Protocol | NIC이 전체 IPSec/MACsec 프로토콜 처리 | 최저 지연 IPSec 어플라이언스 |
examples/ipsec-secgw는 DPDK 기반 IPSec 게이트웨이 레퍼런스 구현으로,
Lookaside/Inline 모델을 모두 지원합니다. VPP의 ipsec 노드와 함께 프로덕션 IPSec 구현의 두 가지 주요 선택지입니다.
DPDK 멀티프로세스 모드
DPDK는 primary-secondary 프로세스 모델을 지원합니다. Primary 프로세스가 EAL 초기화, 포트 설정, Mempool 생성을 수행하고, Secondary 프로세스가 공유 메모리(hugepage)를 통해 동일한 자원에 접근합니다.
# 멀티프로세스 실행 예시
# Primary 프로세스 시작
dpdk-l2fwd -l 0-3 -n 4 --proc-type primary -- -p 0x3
# Secondary 프로세스 (패킷 덤프)
dpdk-pdump -l 4 --proc-type secondary -- \
--pdump 'port=0,queue=*,rx-dev=/tmp/rx.pcap'
# Secondary 프로세스 (통계 모니터링)
dpdk-proc-info --proc-type secondary -- --stats
컨테이너·Kubernetes 통합
DPDK를 컨테이너화하면 배포·확장·롤백(Rollback)이 용이하지만, hugepage 마운트(Mount), VFIO 디바이스 노출, CPU 격리, NUMA 인식 스케줄링 등 추가 설정이 필요합니다. Kubernetes 환경에서는 SR-IOV Device Plugin과 Multus CNI가 DPDK 워크로드에 VF를 동적 할당하는 표준 패턴입니다.
# Kubernetes DPDK Pod 스펙 예시
apiVersion: v1
kind: Pod
metadata:
name: dpdk-testpmd
annotations:
k8s.v1.cni.cncf.io/networks: sriov-dpdk-net
spec:
containers:
- name: testpmd
image: dpdk-app:24.11
command: ["dpdk-testpmd"]
args:
- "-l"
- "2-5"
- "-n"
- "4"
- "--iova-mode=va"
- "--in-memory"
- "--"
- "-i"
- "--rxq=2"
- "--txq=2"
resources:
requests:
cpu: "4"
memory: "4Gi"
hugepages-1Gi: "2Gi"
intel.com/sriov_dpdk: "1"
limits:
cpu: "4"
memory: "4Gi"
hugepages-1Gi: "2Gi"
intel.com/sriov_dpdk: "1"
securityContext:
capabilities:
add: ["IPC_LOCK", "SYS_RAWIO"]
volumeMounts:
- name: hugepages
mountPath: /dev/hugepages
volumes:
- name: hugepages
emptyDir:
medium: HugePages-1Gi
| 컨테이너 환경 항목 | 설정 | 이유 |
|---|---|---|
| Hugepage | emptyDir.medium: HugePages-1Gi | DPDK는 hugetlbfs 필수, Pod 내 /dev/hugepages에 마운트 |
| CPU Manager | cpuManagerPolicy: static | Guaranteed QoS Pod에 전용 코어 할당 (busy-poll 필수) |
| Topology Manager | single-numa-node | NIC VF, CPU, 메모리를 같은 NUMA 노드에 배치 |
| SR-IOV Device Plugin | ConfigMap으로 VF 풀 정의 | VF를 VFIO 바인딩하고 리소스로 어드버타이징 |
| Multus CNI | NetworkAttachmentDefinition | 기본 CNI(Calico 등) + DPDK VF 다중 네트워크 |
| securityContext | IPC_LOCK, SYS_RAWIO | hugepage mlock과 VFIO 접근에 필요 (privileged 불필요) |
| Docker 직접 실행 | --device /dev/vfio/N -v /dev/hugepages | K8s 없는 Docker 환경 최소 설정 |
# ━━━ Docker 직접 실행 예시 ━━━
# SR-IOV VF 생성
echo 4 > /sys/class/net/ens3f0/device/sriov_numvfs
# VF를 vfio-pci에 바인딩
dpdk-devbind --bind=vfio-pci 0000:03:02.0
# VFIO 그룹 번호 확인
ls -la /sys/bus/pci/devices/0000:03:02.0/iommu_group
# → /sys/kernel/iommu_groups/42
# Docker 실행 (privileged 없이!)
docker run --rm -it \
--device /dev/vfio/42 \
--device /dev/vfio/vfio \
-v /dev/hugepages:/dev/hugepages \
--ulimit memlock=-1:-1 \
--cpuset-cpus=4-7 \
dpdk-app:24.11 \
dpdk-testpmd -l 4-7 -n 4 -a 0000:03:02.0 -- -i
--privileged플래그는 가능한 사용하지 말고, 필요한 capability만 부여- 멀티프로세스 모드(
--proc-type secondary)는 같은 hugepage 네임스페이스를 공유해야 하므로 컨테이너 간 사용이 복잡 --in-memory옵션은 hugetlbfs 파일을 만들지 않아 단일 프로세스 컨테이너에 적합- NUMA 토폴로지(Topology)가 호스트와 일치하는지 반드시 확인 (VM 위 컨테이너는 가짜 NUMA일 수 있음)
DPDK 디버깅과 모니터링
| 도구 | 용도 | 사용법 |
|---|---|---|
dpdk-proc-info |
포트 통계, 큐 통계, 메모리 사용량 | dpdk-proc-info -- --stats --xstats |
dpdk-pdump |
패킷 캡처 (pcap 형식) | dpdk-pdump -- --pdump 'port=0,...' |
dpdk-dumpcap |
Wireshark 친화적 캡처 (pcapng) | dpdk-dumpcap -i <iface> -w trace.pcapng |
dpdk-telemetry.py |
JSON 기반 텔레메트리 API | UNIX 소켓으로 런타임 통계 조회 |
dpdk-devbind |
PCI 디바이스 바인딩 관리 | dpdk-devbind --status |
dpdk-testpmd |
포트 기능 테스트, 성능 벤치마크 | dpdk-testpmd -- -i --forward-mode=io |
rte_eth_stats_get() |
프로그래밍 방식 통계 수집 | RX/TX 패킷/바이트/에러 카운터 |
# ━━━ DPDK 디버깅/모니터링 명령 모음 ━━━
# 포트 통계 (기본 + 확장)
dpdk-proc-info -- --stats
dpdk-proc-info -- --xstats # NIC별 상세 카운터 (rx_good_bytes, tx_errors, ...)
# 실시간 패킷 캡처 (secondary 프로세스)
dpdk-pdump -l 8 --proc-type secondary -- \
--pdump 'port=0,queue=*,rx-dev=/tmp/capture.pcap'
# 캡처 파일을 tcpdump/Wireshark로 분석
tcpdump -r /tmp/capture.pcap
# dumpcap 스타일 캡처 (pcapng)
dpdk-dumpcap --list-interfaces
dpdk-dumpcap -i "0000:03:00.0" -c 1000 -w /tmp/trace.pcapng
# 텔레메트리 API (DPDK 20.05+)
# UNIX 소켓 경로: /var/run/dpdk/rte/dpdk_telemetry.vX
dpdk-telemetry.py
# 대화형 쿼리:
# --> /ethdev/stats,0
# --> /ethdev/xstats,0
# --> /eal/params
# --> /mempool/list
# testpmd로 성능 측정
dpdk-testpmd -l 0-3 -n 4 -a 0000:03:00.0 -- \
-i \
--forward-mode=io \
--rxq=4 --txq=4 \
--nb-cores=3 \
--burst=32
# testpmd 내부 명령:
# testpmd> start
# testpmd> show port stats all
# testpmd> show fwd stats all
# testpmd> show port xstats 0
# testpmd> stop
# Mempool 상태 확인
dpdk-proc-info -- --mempool=MBUF_POOL
# EAL 로그 레벨 (런타임 조정)
# --log-level=lib.eal:8 (DEBUG)
# --log-level=pmd.net.mlx5:7 (INFO)
/* 프로그래밍 방식 통계 수집 */
struct rte_eth_stats stats;
rte_eth_stats_get(port_id, &stats);
printf("Port %u: RX %lu pkts (%lu bytes) TX %lu pkts (%lu bytes)\\n",
port_id,
stats.ipackets, stats.ibytes,
stats.opackets, stats.obytes);
printf(" RX errors: %lu TX errors: %lu RX no-mbuf: %lu\\n",
stats.ierrors, stats.oerrors, stats.rx_nombuf);
/* 확장 통계 (xstats): NIC별 상세 카운터 */
int len = rte_eth_xstats_get(port_id, NULL, 0);
struct rte_eth_xstat *xstats = malloc(len * sizeof(*xstats));
struct rte_eth_xstat_name *names = malloc(len * sizeof(*names));
rte_eth_xstats_get(port_id, xstats, len);
rte_eth_xstats_get_names(port_id, names, len);
for (int i = 0; i < len; i++)
printf(" %s: %lu\\n", names[i].name, xstats[i].value);
추가 런타임 서비스: DMA, 전력, 메트릭, 추적
DPDK는 ethdev와 mempool만으로 끝나지 않습니다. 최신 Programmer's Guide는 DMA 엔진 추상화(dmadev), 유휴 폴링 완화용 전력 관리, 메트릭 레지스트리, 저오버헤드 trace 라이브러리를 별도 서브시스템으로 제공합니다. 패킷 처리 코드가 어느 정도 안정되면, 실제 운영 품질은 이 주변 서비스에서 갈리는 경우가 많습니다.
dmadev — 복사/메모리 이동 오프로딩
rte_dmadev는 패킷 그 자체를 처리하는 장치가 아니라 메모리 이동 작업을 오프로딩하는 계층입니다. 공식 문서는 submission/completion ring 기반 API와 ring-less API를 함께 설명합니다. 패킷 payload 복사, 배치 memcpy, 압축/암호화 전 사전 정렬, 소프트웨어 파이프라인의 copy stage 분리에 유용합니다.
/* dmadev 장치 탐색 및 설정 */
int16_t dev_id = rte_dma_next_dev(0);
if (dev_id < 0)
rte_exit(EXIT_FAILURE, "No DMA device found\n");
struct rte_dma_conf dev_conf = { .nb_vchans = 1 };
rte_dma_configure(dev_id, &dev_conf);
/* vchan(가상 채널) 설정 — 방향, 큐 깊이 */
struct rte_dma_vchan_conf vchan_conf = {
.direction = RTE_DMA_DIR_MEM_TO_MEM,
.nb_desc = 2048, /* submission ring 크기 */
};
rte_dma_vchan_setup(dev_id, 0, &vchan_conf);
rte_dma_start(dev_id);
/* ━━━ 데이터 경로: 비동기 복사 + 완료 수거 ━━━ */
uint16_t vchan = 0;
uint16_t job = rte_dma_copy(dev_id, vchan,
src_iova, dst_iova, copy_len,
RTE_DMA_OP_FLAG_SUBMIT);
/* RTE_DMA_OP_FLAG_SUBMIT: copy + submit 한 번에 수행 */
/* 다른 패킷 처리 작업을 수행... */
/* 완료 수거 — 배치로 여러 작업의 완료 확인 */
uint16_t last_idx;
bool has_error;
uint16_t done = rte_dma_completed(dev_id, vchan,
32, &last_idx, &has_error);
if (has_error) {
/* 실패한 작업의 인덱스 확인 */
rte_dma_completed_status(dev_id, vchan,
32, &last_idx, status);
}
코드 분석
- 1~14행:
rte_dma_next_dev()로 사용 가능한 DMA 장치를 탐색합니다. Intel IOAT/CBDMA/IDXD, ARM DPAA2 QDMA 등이 지원됩니다.nb_vchans로 하나의 물리 DMA 엔진을 여러 가상 채널로 나눌 수 있습니다. - 18~21행:
RTE_DMA_OP_FLAG_SUBMIT플래그를 사용하면rte_dma_copy()와rte_dma_submit()을 하나로 합쳐 함수 호출 오버헤드를 줄입니다. 여러 작업을 배치로 모아 제출할 때는 플래그 없이rte_dma_copy()만 반복 후 마지막에rte_dma_submit()을 한 번 호출합니다. - 26~32행:
rte_dma_completed()는 완료된 작업 수를 반환합니다.has_error가 true이면rte_dma_completed_status()로 개별 작업의 실패 원인을 확인할 수 있습니다.
| 패턴 | 언제 쓰나 | 주의점 |
|---|---|---|
| ring-based API | 배치 큐잉, 명확한 completion 수거가 필요할 때 | 큐 깊이와 completion polling 지연을 같이 튜닝해야 합니다. |
| ring-less API | 드라이버가 더 직접적인 제출 경로를 제공할 때 | 장치별 지원 범위 차이가 큽니다. |
| vchan | 하나의 DMA 장치를 논리 채널로 분리할 때 | ethdev queue와 일대일 대응을 강제하지 말고 작업 특성에 맞춰 배치해야 합니다. |
| scatter-gather | 비연속 메모리 영역을 한 번에 복사할 때 | rte_dma_copy_sg()는 일부 드라이버만 지원하므로 rte_dma_info_get()으로 사전 확인이 필요합니다. |
memcpy()가 DMA 엔진보다 빠릅니다(제출/완료 오버헤드). 64KB 이상의 대량 복사 또는 패킷 처리 코어의 메모리 대역폭이 병목인 경우에 dmadev가 유리합니다. Intel DSA(IDXD) 벤치마크 기준 256KB 복사에서 CPU 대비 ~30% 대역폭 절약 효과가 있습니다.
유휴 폴링과 전력 관리
DPDK는 본질적으로 busy-poll 프레임워크이지만, 최신 power management 문서는 empty poll 기반 절전, monitor/pause instruction, CPU frequency scaling 연계를 별도로 제공합니다. 즉 "DPDK는 항상 100% CPU를 태운다"는 단순화는 반만 맞습니다. 낮은 부하 구간에서 지연 허용치가 있다면, empty poll 횟수를 기준으로 절전과 지연을 절충할 수 있습니다.
| 전략 | 장점 | 대가 | 적합 환경 |
|---|---|---|---|
| 순수 busy-poll | 지연 최저, 가장 단순 | 전력 사용량 최고 | HFT, 금융 거래소, 패킷 브로커 |
| empty poll + pause | 낮은 부하에서 전력 절감 | tail latency가 약간 흔들릴 수 있음 | CDN, DNS 리졸버, 로드밸런서 |
| frequency scaling 연동 | 장시간 저부하 서비스에서 유리 | 복귀 지연이 workload 민감 | 에지 NFV, 야간 운영 |
| monitor/mwait 기반 | 메모리 쓰기 감지 시 즉시 복귀 | x86 특화, 일부 CPU만 지원 | 지연 민감 + 전력 절감 동시 필요 |
/* empty poll 기반 전력 관리 적용 예시 */
#include <rte_power_empty_poll.h>
/* 초기화: 임계치와 주파수 단계 설정 */
rte_power_empty_poll_stat_init(&ep_params,
freq_info, RTE_MAX_LCORE);
/* 폴링 루프 내부 */
uint16_t nb_rx = rte_eth_rx_burst(port, queue, bufs, BURST);
if (nb_rx == 0) {
/* 빈 폴링 횟수 누적 → 임계치 초과 시 자동으로 주파수 낮춤 */
rte_power_empty_poll_stat_update(lcore_id);
} else {
/* 패킷 수신 → 즉시 최대 주파수 복귀 */
rte_power_poll_stat_update(lcore_id, nb_rx);
}
Metrics와 Trace
Metrics 라이브러리는 이름이 등록된 메트릭 집합에 값을 채워 넣는 모델이고, Trace 라이브러리는 저오버헤드 순환 버퍼에 이벤트를 남긴 뒤 파일로 내보내는 모델입니다. 관측 대상을 "지속적 수치"와 "순간 사건"으로 분리하면 설계가 단순해집니다.
/* ━━━ Metrics 라이브러리: 커스텀 메트릭 등록과 갱신 ━━━ */
const char * const names[] = {
"rx_missed_errors",
"flow_table_hits",
"flow_table_misses",
};
int key = rte_metrics_reg_names(names, 3);
/* 주기적 갱신 (예: 1초 간격) */
uint64_t values[3] = {
rx_missed,
flow_hits,
flow_misses,
};
rte_metrics_update_values(RTE_METRICS_GLOBAL,
key, values, 3);
/* ━━━ Trace 라이브러리: 커스텀 트레이스 포인트 정의 ━━━ */
/* 헤더 파일에서 트레이스 포인트 선언 */
RTE_TRACE_POINT(
app_trace_pkt_drop,
RTE_TRACE_POINT_ARGS(uint16_t port_id,
uint32_t reason),
rte_trace_point_emit_u16(port_id);
rte_trace_point_emit_u32(reason);
)
/* 데이터 경로에서 호출 */
app_trace_pkt_drop(port_id, DROP_REASON_TTL_EXPIRED);
코드 분석
- 2~6행:
rte_metrics_reg_names()는 여러 메트릭 이름을 한 번에 등록합니다. 반환값은 첫 번째 키(key)이며, 나머지는 연속된 정수입니다. 포트별 xstats와 겹치지 않는 "서비스 관점 지표"(플로우 테이블 적중률 등)를 별도로 관리하면 운영 가시성이 높아집니다. - 19~27행:
RTE_TRACE_POINT매크로는 CTF(Common Trace Format) 호환 트레이스 포인트를 정의합니다. 데이터 경로에서 호출해도 오버헤드가 ~20ns 수준이므로, 드롭 사유나 상태 전이 같은 희귀 이벤트를 기록하는 데 적합합니다. - 30행: 트레이스 덤프는
--trace=.*EAL 옵션이나rte_trace_save()API로 파일에 기록하며,babeltrace2로 분석합니다.
# ━━━ Telemetry 소켓으로 런타임 모니터링 ━━━
# Telemetry 소켓 경로 확인
ls /var/run/dpdk/rte/dpdk_telemetry.*
# dpdk-telemetry.py로 대화형 조회
dpdk-telemetry.py
--> /ethdev/xstats,0
--> /ethdev/info,0
--> /mempool/list
--> /mempool/info,pktmbuf_pool_0
# Trace 수집 및 분석
# EAL 옵션으로 트레이스 활성화
./dpdk-app --trace="app_trace_*" --trace-dir=/tmp/dpdk-trace
# babeltrace2로 CTF 트레이스 분석
babeltrace2 /tmp/dpdk-trace/rte-*
| 도구 | 용도 | 오버헤드 | 팁 |
|---|---|---|---|
| Metrics | 지속적 수치, 외부 모니터링 수집 | 갱신 시 ~10ns (atomic write) | 포트 xstats와 중복되지 않게 "서비스 관점 지표"를 따로 두면 좋습니다. |
| Trace | 희귀 사건, 상태 전이, 에러 경로 분석 | 이벤트당 ~20ns | overwrite/discard 정책을 정하고, 장애 재현 시점에만 활성화하는 편이 안전합니다. |
| Telemetry | 런타임 조회 인터페이스 | 조회 시만 (비동기) | Metrics/Trace/ethdev/mempool 상태를 한 소켓으로 묶어 조회할 수 있습니다. |
| Prometheus 연동 | 시계열 대시보드 | 수집 주기에 비례 | Telemetry → JSON → Prometheus exporter → Grafana 파이프라인이 일반적입니다. |
실전 예제: 고성능 L2 포워딩 애플리케이션
DPDK의 가장 기본적인 데이터 경로인 L2(MAC 기반) 포워딩을 실전 수준으로 구현합니다. 단순 예제와 달리 NUMA 배치, 멀티큐 활용, graceful shutdown, xstats 모니터링까지 포함한 프로덕션 패턴을 다룹니다.
/* l2fwd 프로덕션 패턴 — 핵심 루프 */
#define MAX_PKT_BURST 32
#define BURST_TX_DRAIN_US 100
static volatile bool force_quit = false;
static void
l2fwd_main_loop(void)
{
struct rte_mbuf *pkts_burst[MAX_PKT_BURST];
struct rte_eth_dev_tx_buffer *buffer;
uint16_t port_id, dst_port;
unsigned lcore_id = rte_lcore_id();
uint64_t prev_tsc = 0, cur_tsc, diff_tsc;
const uint64_t drain_tsc =
((uint64_t)BURST_TX_DRAIN_US * rte_get_tsc_hz()) / 1000000;
/* 이 lcore가 담당하는 포트/큐 정보 조회 */
struct lcore_queue_conf *qconf = &lcore_queue_conf[lcore_id];
if (qconf->n_rx_port == 0) return;
while (!force_quit) {
cur_tsc = rte_rdtsc();
diff_tsc = cur_tsc - prev_tsc;
/* TX drain: 미완성 burst를 주기적으로 flush */
if (unlikely(diff_tsc > drain_tsc)) {
for (unsigned i = 0; i < qconf->n_rx_port; i++) {
port_id = qconf->rx_port_list[i];
dst_port = l2fwd_dst_ports[port_id];
buffer = tx_buffer[dst_port];
rte_eth_tx_buffer_flush(dst_port, 0, buffer);
}
prev_tsc = cur_tsc;
}
/* RX burst → MAC 교환 → TX buffer */
for (unsigned i = 0; i < qconf->n_rx_port; i++) {
port_id = qconf->rx_port_list[i];
dst_port = l2fwd_dst_ports[port_id];
buffer = tx_buffer[dst_port];
uint16_t nb_rx = rte_eth_rx_burst(port_id, 0,
pkts_burst, MAX_PKT_BURST);
if (nb_rx == 0) continue;
for (uint16_t j = 0; j < nb_rx; j++) {
struct rte_mbuf *m = pkts_burst[j];
rte_prefetch0(rte_pktmbuf_mtod(m, void *));
l2fwd_simple_forward(m, dst_port, buffer);
}
}
}
}
static void
l2fwd_simple_forward(struct rte_mbuf *m,
uint16_t dst_port,
struct rte_eth_dev_tx_buffer *buffer)
{
struct rte_ether_hdr *eth =
rte_pktmbuf_mtod(m, struct rte_ether_hdr *);
/* src와 dst MAC 교환 */
struct rte_ether_addr tmp;
rte_ether_addr_copy(ð->src_addr, &tmp);
rte_ether_addr_copy(ð->dst_addr, ð->src_addr);
rte_ether_addr_copy(&tmp, ð->dst_addr);
rte_eth_tx_buffer(dst_port, 0, buffer, m);
}
포트 초기화와 mempool 설정
/* mempool 생성 — NUMA 노드별 */
struct rte_mempool *pktmbuf_pool[RTE_MAX_NUMA_NODES];
for (int i = 0; i < rte_socket_count(); i++) {
int sid = rte_socket_id_by_idx(i);
char name[32];
snprintf(name, sizeof(name), "mbuf_pool_%d", sid);
pktmbuf_pool[sid] = rte_pktmbuf_pool_create(
name,
8192, /* n: 포트당 4096 + 여유 */
256, /* cache_size: lcore 로컬 캐시 */
0, /* priv_size */
RTE_MBUF_DEFAULT_BUF_SIZE,
sid);
if (!pktmbuf_pool[sid])
rte_exit(EXIT_FAILURE, "NUMA %d mempool 생성 실패\n", sid);
}
/* 포트 구성 */
struct rte_eth_conf port_conf = {
.rxmode = { .mq_mode = RTE_ETH_MQ_RX_RSS },
.rx_adv_conf.rss_conf = {
.rss_key = NULL, /* 기본 Toeplitz 키 사용 */
.rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP,
},
.txmode = { .mq_mode = RTE_ETH_MQ_TX_NONE },
};
RTE_ETH_FOREACH_DEV(port_id) {
int socket_id = rte_eth_dev_socket_id(port_id);
rte_eth_dev_configure(port_id, nb_rxq, nb_txq, &port_conf);
for (uint16_t q = 0; q < nb_rxq; q++)
rte_eth_rx_queue_setup(port_id, q, 1024,
socket_id, NULL, pktmbuf_pool[socket_id]);
for (uint16_t q = 0; q < nb_txq; q++)
rte_eth_tx_queue_setup(port_id, q, 1024,
socket_id, NULL);
rte_eth_dev_start(port_id);
rte_eth_promiscuous_mode_enable(port_id);
}
rte_prefetch0()는 다음 mbuf 데이터를 L1 캐시에 미리 로드하여 cache miss를 줄입니다.tx_buffer는 불완전한 burst를 모아 한 번에 전송하므로 PCIe 트랜잭션 수를 줄입니다.- mempool을 NUMA 노드별로 분리하면 크로스소켓 메모리 접근 지연을 피할 수 있습니다.
- RSS를 활성화하면 NIC이 플로우 해시 기반으로 패킷을 여러 RX 큐에 분산합니다.
실전 예제: rte_flow 하드웨어 오프로드
rte_flow API를 사용하면 NIC 하드웨어에 직접 플로우 규칙을 설치하여 소프트웨어 처리 없이 패킷을 분류·드롭·리디렉트할 수 있습니다. 여기서는 실전에서 자주 사용되는 패턴 세 가지 — 특정 플로우를 지정 큐로 분배, 불필요한 트래픽 하드웨어 드롭, hairpin을 이용한 NIC 내부 루프백 — 을 다룹니다.
패턴 1: 특정 TCP 포트 트래픽을 지정 큐로 분배
/* TCP dst port 80 → RX Queue 1로 분배 */
struct rte_flow_attr attr = {
.ingress = 1,
.priority = 0,
};
/* 패턴: ETH / IPv4 / TCP dst_port=80 */
struct rte_flow_item_eth eth_spec = { 0 };
struct rte_flow_item_ipv4 ipv4_spec = { 0 };
struct rte_flow_item_tcp tcp_spec = {
.hdr.dst_port = rte_cpu_to_be_16(80),
};
struct rte_flow_item_tcp tcp_mask = {
.hdr.dst_port = 0xFFFF,
};
struct rte_flow_item pattern[] = {
{ .type = RTE_FLOW_ITEM_TYPE_ETH, .spec = ð_spec },
{ .type = RTE_FLOW_ITEM_TYPE_IPV4, .spec = &ipv4_spec },
{ .type = RTE_FLOW_ITEM_TYPE_TCP, .spec = &tcp_spec,
.mask = &tcp_mask },
{ .type = RTE_FLOW_ITEM_TYPE_END },
};
/* 액션: Queue 1로 분배 + 카운터 */
struct rte_flow_action_queue queue_action = { .index = 1 };
struct rte_flow_action_count count_action = { 0 };
struct rte_flow_action actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_COUNT, .conf = &count_action },
{ .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &queue_action },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};
struct rte_flow_error error;
struct rte_flow *flow = rte_flow_create(port_id, &attr,
pattern, actions, &error);
if (!flow)
rte_exit(EXIT_FAILURE, "Flow 생성 실패: %s\n", error.message);
패턴 2: 하드웨어 수준 트래픽 드롭
/* 특정 src IP로부터의 모든 트래픽을 하드웨어에서 드롭 */
struct rte_flow_item_ipv4 ipv4_spec = {
.hdr.src_addr = rte_cpu_to_be_32(RTE_IPV4(10,0,0,100)),
};
struct rte_flow_item_ipv4 ipv4_mask = {
.hdr.src_addr = 0xFFFFFFFF,
};
struct rte_flow_item pattern[] = {
{ .type = RTE_FLOW_ITEM_TYPE_ETH },
{ .type = RTE_FLOW_ITEM_TYPE_IPV4,
.spec = &ipv4_spec, .mask = &ipv4_mask },
{ .type = RTE_FLOW_ITEM_TYPE_END },
};
struct rte_flow_action actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_DROP },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};
/* validate → create 2단계 패턴 사용 */
if (rte_flow_validate(port_id, &attr, pattern, actions, &error))
fprintf(stderr, "Flow 유효성 검사 실패: %s\n", error.message);
else
flow = rte_flow_create(port_id, &attr, pattern, actions, &error);
패턴 3: Hairpin — NIC 내부 RX→TX 루프백
/* Hairpin 큐 설정: RX Queue 2 → TX Queue 2 (NIC 내부) */
struct rte_eth_hairpin_conf hairpin_conf = {
.peer_count = 1,
.peers[0] = { .port = port_id, .queue = 2 },
.manual_bind = 0,
.force_memory = 1,
};
rte_eth_rx_hairpin_queue_setup(port_id, 2, 0, &hairpin_conf);
rte_eth_tx_hairpin_queue_setup(port_id, 2, 0, &hairpin_conf);
/* rte_flow로 매칭 트래픽을 hairpin 큐로 보냄 */
struct rte_flow_action_queue hairpin_queue = { .index = 2 };
struct rte_flow_action actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &hairpin_queue },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};
rte_flow_validate()를 항상 먼저 호출하여 하드웨어 지원 여부를 확인하세요. Flow aging(RTE_FLOW_ACTION_TYPE_AGE)을 활용하면 오래된 규칙을 자동으로 만료시킬 수 있습니다.
실전 예제: pktgen-dpdk 패킷 생성기 활용
pktgen-dpdk는 DPDK 기반 고성능 패킷 생성·수신 도구로, line rate 트래픽 생성과 정밀한 성능 측정을 동시에 수행합니다. 10/25/100 GbE 환경에서 실제 PPS, 지연, 드롭률을 측정하는 실전 절차를 다룹니다.
# pktgen-dpdk 빌드 (meson 기반)
git clone https://github.com/pktgen/Pktgen-DPDK.git
cd Pktgen-DPDK
meson build -Denable_lua=false
ninja -C build
# 실행: 4 lcore, 2 포트
sudo ./build/app/pktgen -l 0-3 -n 4 --socket-mem 1024,0 -- \
-P -m "[1:2].0" -m "[3].1"
# -P: 프로미스큐어스 모드
# -m "[lcore_list].port": lcore와 포트 매핑
성능 측정 시나리오
# pktgen 콘솔에서 실행하는 명령
# 시나리오 1: 64바이트 최소 프레임 — 최대 PPS 측정
set 0 size 64
set 0 rate 100
set 0 count 0 # 무한 전송
start 0
# 시나리오 2: IMIX (Internet Mix) — 실제 트래픽 패턴 시뮬레이션
# 64B:40%, 594B:33%, 1518B:27% (일반적인 인터넷 트래픽 비율)
set 0 size 64
set 0 rate 40
start 0
# 별도 포트/스트림에서 594B, 1518B 생성
# 시나리오 3: 지연 측정 (latency)
enable 0 latency
set 0 rate 50 # 50% rate로 지연 측정
start 0
page latency # 지연 통계 페이지 전환
# 결과 확인
page main # 메인 통계 (PPS, Mbps, 오류)
page stats # 상세 포트 통계
# 결과 CSV 저장
save results.csv
| 프레임 크기 | 10 GbE 이론 PPS | 25 GbE 이론 PPS | 100 GbE 이론 PPS | 측정 포인트 |
|---|---|---|---|---|
| 64 B | 14.88 Mpps | 37.20 Mpps | 148.81 Mpps | CPU core당 PPS, 멀티큐 확장성 |
| 128 B | 8.45 Mpps | 21.13 Mpps | 84.46 Mpps | 소규모 페이로드(Payload) 패킷 처리 효율 |
| 512 B | 2.35 Mpps | 5.87 Mpps | 23.50 Mpps | 메모리 대역폭 병목(Bottleneck) 확인 |
| 1518 B | 0.81 Mpps | 2.03 Mpps | 8.13 Mpps | MTU 크기 대역폭 포화 테스트 |
| IMIX | ~3.5 Mpps | ~8.7 Mpps | ~34.8 Mpps | 실제 워크로드 시뮬레이션 |
- 최소 30초 이상 지속 전송하여 안정 상태(steady state) 수치를 확인합니다.
page stats에서oErrors가 0이 아니면 TX 큐 오버플로우를 의심합니다.- 지연 측정 시
enable latency는 패킷에 타임스탬프를 삽입하므로 PPS가 약간 감소합니다. - 비교 측정 시 반드시 동일 NUMA 노드, 동일 hugepage 구성에서 반복합니다.
흔한 실수와 안티패턴
DPDK는 커널 네트워크 스택 대비 하드웨어에 훨씬 가까운 수준에서 동작하므로, 한 가지 설정 오류가 성능을 10배 이상 저하시키는 경우가 빈번합니다. 아래는 실무에서 반복적으로 발생하는 실수와 그 진단·해결 방법입니다.
1. Hugepage 부족 / NUMA 불일치
| 증상 | 원인 | 해결 |
|---|---|---|
| EAL 초기화 실패 또는 mempool 생성 오류 | hugepage 총량 부족 | dpdk-hugepages.py --setup 4G로 충분한 hugepage 확보 |
| 성능이 기대치의 50~70% | NIC이 NUMA 0인데 hugepage가 NUMA 1에만 할당 | --socket-mem 2048,2048로 노드별 균등 할당 |
rte_pktmbuf_pool_create() 실패 |
다른 DPDK 프로세스가 hugepage를 점유 | ls /dev/hugepages/로 잔여 파일 확인 후 정리 |
# NUMA 노드별 hugepage 상태 확인
cat /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages
# NIC이 어느 NUMA 노드에 있는지 확인
cat /sys/bus/pci/devices/0000:3b:00.0/numa_node
# 노드별 hugepage 할당 (커널 부팅 파라미터)
# GRUB: hugepagesz=1G hugepages=4 default_hugepagesz=1G
# 또는 런타임:
echo 2 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
echo 2 > /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages
2. IOMMU/VFIO 설정 누락
| 증상 | 원인 | 해결 |
|---|---|---|
vfio-pci 바인딩 실패 |
IOMMU가 비활성 상태 | GRUB에 intel_iommu=on iommu=pt 추가 후 리부팅 |
| DMA 에러, 패킷 유실 | igb_uio 사용 시 DMA 격리 없음 |
vfio-pci로 전환 (프로덕션 필수) |
| IOMMU 그룹 충돌 | 같은 그룹의 다른 디바이스가 커널 드라이버에 바인딩 | 그룹 내 모든 디바이스를 vfio-pci에 바인딩 또는 ACS 활성화 |
- 프로덕션: 반드시
vfio-pci— DMA 격리, 비특권 실행, Secure Boot 호환 - 레거시/실험:
igb_uio— IOMMU 없는 환경에서만 (root 필수, 보안 취약) - 가상 머신:
vfio-pci+no-iommu모드 (격리 없음 인지 필요)
3. lcore 수와 RX/TX 큐 매핑 불일치
/* 흔한 실수: lcore 4개인데 RX 큐가 2개뿐 */
lcore 0 → RXQ 0 (정상)
lcore 1 → RXQ 1 (정상)
lcore 2 → ??? (유휴 — CPU 100% 점유하며 아무 일 안 함)
lcore 3 → ??? (유휴 — CPU 100% 점유하며 아무 일 안 함)
/* 올바른 매핑: lcore 수 = RX 큐 수 */
rte_eth_dev_configure(port_id, 4, 4, &port_conf); /* 4 RXQ, 4 TXQ */
/* 또는 lcore 수를 NIC 큐 수에 맞춤 */
dpdk-app -l 0-1 ... /* 2 lcore for 2 queues */
/* NIC 최대 큐 수 확인 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);
printf("max_rx_queues=%u max_tx_queues=%u\n",
dev_info.max_rx_queues, dev_info.max_tx_queues);
4. rte_mbuf 메모리 풀 크기 과소/과대 설정
| 문제 | 증상 | 권장 |
|---|---|---|
| 과소 설정 | rte_eth_rx_burst() 반환값이 0으로 고정, rx_mbuf_alloc_failed xstat 증가 |
최소 N = nb_ports × nb_rxq × rx_desc + nb_ports × nb_txq × tx_desc + nb_lcores × cache_size + 512 |
| 과대 설정 | hugepage 고갈, 다른 DPDK 앱/SPDK 실행 불가 | 실제 in-flight 패킷 수 기반으로 산정 (2~3배 여유면 충분) |
/* mempool 크기 산정 공식 */
#define NB_RXD 1024
#define NB_TXD 1024
#define MEMPOOL_CACHE_SIZE 256
unsigned nb_mbufs = RTE_MAX(
nb_ports * (nb_rxq * NB_RXD + nb_txq * NB_TXD +
MAX_PKT_BURST + nb_lcores * MEMPOOL_CACHE_SIZE),
8192U);
/* 실전 권장: 포트 2개, 큐 4개씩이면 */
/* 2 * (4*1024 + 4*1024 + 32 + 4*256) = 18,496 → 2^15 = 32768로 올림 */
5. RSS 해시 키 설정 오류
/* 흔한 실수: rss_hf에 NIC이 지원하지 않는 플래그 포함 */
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(port_id, &dev_info);
/* 반드시 NIC 지원 범위와 AND 연산 */
port_conf.rx_adv_conf.rss_conf.rss_hf &=
dev_info.flow_type_rss_offloads;
/* 흔한 실수 2: 커스텀 해시 키 길이가 NIC과 불일치 */
/* Intel NIC: 40바이트, Mellanox: 40바이트, 일부: 52바이트 */
printf("RSS key len = %u\n", dev_info.hash_key_size);
/* 대칭(Symmetric) RSS가 필요한 경우 — 양방향 동일 큐 */
/* Toeplitz 대칭 키 사용 또는 XOR 기반 해시 함수 선택 */
static uint8_t symmetric_key[40] = {
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A, 0x6D, 0x5A,
};
port_conf.rx_adv_conf.rss_conf.rss_key = symmetric_key;
port_conf.rx_adv_conf.rss_conf.rss_key_len = 40;
6. 애플리케이션 종료 시 리소스 미해제
/* 시그널 핸들러 */
static void signal_handler(int signum)
{
if (signum == SIGINT || signum == SIGTERM)
force_quit = true;
}
/* 올바른 종료 시퀀스 */
static void cleanup(void)
{
uint16_t port_id;
/* 1. 모든 lcore의 메인 루프 종료 대기 */
rte_eal_mp_wait_lcore();
/* 2. 포트 정지 및 해제 */
RTE_ETH_FOREACH_DEV(port_id) {
rte_eth_dev_stop(port_id);
rte_flow_flush(port_id, NULL); /* flow rule 정리 */
rte_eth_dev_close(port_id);
}
/* 3. mempool은 rte_eal_cleanup()이 처리 */
rte_eal_cleanup();
}
/* main()에서: */
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
/* ... 메인 루프 ... */
cleanup();
rte_flow_flush()미호출 → NIC에 고아 flow rule 잔존, 재시작(Reboot) 시 예기치 않은 패킷 분류rte_eth_dev_stop()미호출 → NIC이 계속 DMA 수행, hugepage 해제 불가rte_eal_cleanup()미호출 →/dev/hugepages/에 파일 잔존, 다음 실행 시 hugepage 부족- DPDK 22.11 이후
rte_eth_dev_close()에dev_stop()이 포함되지 않음 — 반드시 별도 호출
이벤트 드리븐 vs Poll 모드 비교
DPDK의 기본 모델인 Poll 모드(PMD)는 lcore가 무한 루프로 NIC을 폴링하여 최소 지연을 달성하지만, CPU를 항상 100% 점유합니다. 이벤트 드리븐 모드(rte_event)는 하드웨어 또는 소프트웨어 이벤트 스케줄러를 통해 "일이 있을 때만" 워커를 깨워 전력 효율과 확장성을 높입니다.
| 비교 항목 | Poll Mode (PMD) | Event-Driven (rte_event) | 하이브리드 |
|---|---|---|---|
| CPU 사용 | 100% 항상 | 이벤트 시에만 | 고부하 구간 poll, 저부하 구간 event |
| 지연 | 수백 ns | ~1-5 μs (스케줄링 포함) | 구간별 상이 |
| 확장성 | 큐 수 = lcore 수 고정 | 워커 동적 할당 가능 | 핫 경로만 poll 고정 |
| 플로우 순서 | RSS 기반 (해시 충돌 시 순서 역전 가능) | Atomic/Ordered 큐로 보장 | 경로별 선택 |
| 전력 효율 | 매우 낮음 | 양호 | 중간 |
| 구현 복잡도 | 낮음 | 높음 | 매우 높음 |
| 하드웨어 요구 | 없음 | DLB2/OCTEON SSO 권장 | 선택적 |
/* 이벤트 드리븐 모드 기본 루프 */
static void
event_worker_loop(void *arg)
{
uint8_t event_dev_id = *(uint8_t *)arg;
struct rte_event events[MAX_PKT_BURST];
while (!force_quit) {
uint16_t nb_events = rte_event_dequeue_burst(
event_dev_id,
0, /* port_id */
events,
MAX_PKT_BURST,
0); /* timeout_ticks: 0 = non-blocking */
for (uint16_t i = 0; i < nb_events; i++) {
struct rte_mbuf *m = events[i].mbuf;
/* 패킷 처리 로직 */
process_packet(m);
/* TX Adapter로 전달 */
events[i].op = RTE_EVENT_OP_FORWARD;
events[i].queue_id = TX_STAGE_QUEUE;
}
if (nb_events > 0)
rte_event_enqueue_burst(event_dev_id, 0,
events, nb_events);
}
}
l2fwd-event 예제가 이 패턴의 참고 구현입니다.
Graph Library: 패킷 처리 파이프라인
rte_graph는 기존 수동 RTC 루프를 재사용 가능한 노드 그래프로 구조화하는 프레임워크입니다. 여기서는 기본 개념을 넘어 커스텀 노드 작성, 노드 간 데이터 전달, 성능 통계 수집, 그리고 dispatch 모드 활용까지 실전 패턴을 다룹니다.
/* 커스텀 분류 노드 예시 */
static uint16_t
classifier_node_process(struct rte_graph *graph,
struct rte_node *node,
void **objs, uint16_t nb_objs)
{
uint16_t n_ip4 = 0, n_drop = 0;
void **to_ip4, **to_drop;
/* next edge 0 = ip4_lookup, edge 1 = drop */
to_ip4 = rte_node_next_stream_get(graph, node, 0, nb_objs);
to_drop = rte_node_next_stream_get(graph, node, 1, nb_objs);
for (uint16_t i = 0; i < nb_objs; i++) {
struct rte_mbuf *m = (struct rte_mbuf *)objs[i];
struct rte_ether_hdr *eth =
rte_pktmbuf_mtod(m, struct rte_ether_hdr *);
if (eth->ether_type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4))
to_ip4[n_ip4++] = m;
else
to_drop[n_drop++] = m;
}
rte_node_next_stream_put(graph, node, 0, n_ip4);
rte_node_next_stream_put(graph, node, 1, n_drop);
return nb_objs;
}
static struct rte_node_register classifier_node = {
.name = "classifier",
.process = classifier_node_process,
.nb_edges = 2,
.next_nodes = { [0] = "ip4_lookup",
[1] = "pkt_drop" },
};
RTE_NODE_REGISTER(classifier_node);
Graph 성능 통계와 디버깅
/* 그래프 통계 수집 활성화 */
struct rte_graph_cluster_stats_param stats_param = {
.socket_id = SOCKET_ID_ANY,
.fn = NULL, /* 기본 콘솔 출력 */
.nb_graph_patterns = 1,
.graph_patterns = (const char *[]){ "*" },
};
struct rte_graph_cluster_stats *stats =
rte_graph_cluster_stats_create(&stats_param);
/* 주기적으로 호출하여 노드별 통계 출력 */
rte_graph_cluster_stats_get(stats, 0);
/* 출력 예시:
* +------+---+------+----------+----------+---------+
* | Node |St | Calls| Objs | Realloc | Cycles |
* +------+---+------+----------+----------+---------+
* | rx | 0 | 1.2M | 38.5M | 0 | 120 |
* | cls | 0 | 1.2M | 38.5M | 12 | 180 |
* | tx | 0 | 1.1M | 37.0M | 0 | 95 |
* | drop | 0 | 0.1M | 1.5M | 0 | 40 |
* +------+---+------+----------+----------+---------+
*/
/* DOT 그래프 export (Graphviz 시각화용) */
rte_graph_export("graph0", fopen("pipeline.dot", "w"));
DPDK + GPU 연동 (gpudev)
DPDK 21.11에서 도입된 rte_gpudev 라이브러리는 GPU 메모리와 DPDK mbuf 간 zero-copy 데이터 교환을 가능하게 합니다. DPI(Deep Packet Inspection), AI/ML 기반 트래픽 분석, 암호화/압축 가속 등 GPU의 대규모 병렬 연산 능력을 네트워크 데이터 경로에 직접 통합할 수 있습니다.
/* DPDK gpudev 기본 사용 패턴 */
#include <rte_gpudev.h>
/* 1. GPU 디바이스 탐색 */
int16_t gpu_id = rte_gpu_find_next(RTE_GPU_ID_NONE);
struct rte_gpu_info gpu_info;
rte_gpu_info_get(gpu_id, &gpu_info);
printf("GPU: %s, NUMA: %d\n", gpu_info.name, gpu_info.numa_node);
/* 2. GPU 메모리 할당 */
size_t gpu_buf_size = 1024 * 1024; /* 1 MB */
void *gpu_buf = rte_gpu_mem_alloc(gpu_id, gpu_buf_size, 0);
/* 3. CPU에서 GPU 메모리 접근 가능하게 매핑 */
void *cpu_ptr;
rte_gpu_mem_cpu_map(gpu_id, gpu_buf_size, gpu_buf, &cpu_ptr);
/* 4. Communication List 생성 (비동기 배치 통신) */
struct rte_gpu_comm_list *comm_list =
rte_gpu_comm_create_list(gpu_id, 64); /* 64 슬롯 */
/* 5. 패킷 데이터를 comm_list에 등록 */
rte_gpu_comm_populate_list_pkts(comm_list, 0, pkts, nb_rx);
/* 6. GPU 커널 실행 (CUDA 예시) */
/* GPU 측에서 comm_list를 읽어 패킷 처리 */
/* 7. 정리 */
rte_gpu_comm_destroy_list(comm_list, 64);
rte_gpu_mem_cpu_unmap(gpu_id, cpu_ptr);
rte_gpu_mem_free(gpu_id, gpu_buf);
| 전송 경로 | 방법 | 지연 | 적합 용도 |
|---|---|---|---|
| NIC → CPU → GPU | mbuf → rte_gpu_mem_cpu_map → GPU 복사 | ~10 μs | 소량 패킷, CPU 전처리 필요 |
| NIC → GPU (GDR) | GPUDirect RDMA, NIC DMA → GPU VRAM 직접 | ~2 μs | 대량 패킷, 최소 지연 요구 |
| GPU → CPU → NIC | GPU 결과 → CPU 읽기 → TX burst | ~10 μs | GPU 분석 후 포워딩/드롭 결정 |
- NUMA 정렬 필수: NIC, GPU, DPDK lcore가 같은 NUMA 노드에 있어야 PCIe 대역폭을 온전히 활용합니다.
- 배치 크기: GPU 커널은 수천 개 패킷을 한 번에 처리할 때 효율적입니다. 소규모 burst(<32)에서는 커널 론칭 오버헤드가 지배적입니다.
- NVIDIA:
cuda.pc를 meson 빌드에 포함 (-Dgpu_drivers=cuda). CUDA 11.4 이상 권장. - gpudev는 실험적 API: DPDK 24.x 기준
__rte_experimental태그. ABI 안정성이 보장되지 않습니다.
DPDK 23.x / 24.x 주요 변경사항
DPDK는 연 3회(3월, 7월, 11월) 메이저 릴리스를 발행하며, 각 LTS(Long Term Support)는 2년간 유지됩니다. 최근 릴리스에서 데이터 경로 성능, API 안정성, 하드웨어 지원 범위에 중요한 변경이 있었습니다.
DPDK 23.x 시리즈 (23.03, 23.07, 23.11)
| 변경사항 | 버전 | 영향 |
|---|---|---|
| rte_flow 비동기 API 안정화 | 23.03 | rte_flow_async_create()가 실험적에서 안정 API로 전환. 수만 개 flow rule을 non-blocking으로 삽입/삭제 가능 |
| Graph dispatch 모드 추가 | 23.03 | 기존 RTC(run-to-completion)에 더해 dispatcher 기반 멀티코어 실행 모델 도입 |
| rte_mempool 성능 개선 | 23.07 | per-lcore 캐시 리필 로직 최적화, 소규모 burst에서 15~20% 처리량 향상 |
| Eventdev DLB2 v2.5 지원 | 23.07 | Intel DLB2 하드웨어 스케줄러 드라이버 업데이트, COS(Class of Service) 지원 |
| AF_XDP 멀티큐 개선 | 23.11 | AF_XDP PMD에서 여러 RX/TX 큐를 효율적으로 사용. 커널 6.6+ 요구 |
| Telemetry v2 | 23.11 | JSON 기반 텔레메트리 인터페이스 확장, Prometheus exporter와 직접 연동 가능 |
| Marvell OCTEON 10 PMD | 23.11 | ARM64 기반 DPU용 네트워크 드라이버 추가 |
DPDK 24.x 시리즈 (24.03, 24.07, 24.11)
| 변경사항 | 버전 | 영향 |
|---|---|---|
| C11 필수 전환 | 24.03 | 빌드 시 C11 표준 필수. _Atomic 타입 사용 확대, 구형 컴파일러(GCC < 4.9) 미지원 |
| rte_eth_dev 구조 정리 | 24.03 | 비추천 API 대량 제거. rte_eth_dev_info_get() 반환 구조체 변경 — 기존 코드 마이그레이션 필요 |
| MSIX 인터럽트 프레임워크 리팩터링 | 24.07 | 인터럽트 핸들러(Handler) 등록 API 변경. rte_intr_callback_register() 시그니처 수정 |
| gpudev CUDA 12 지원 | 24.07 | CUDA 12.x 호환, rte_gpu_comm_* API 개선, 비동기 커널 론칭 최적화 |
| NVIDIA BlueField-3 PMD | 24.07 | BF3 DPU 네이티브 PMD 추가. ConnectX-7 기반 200 GbE 지원 |
| Flow Quota / Meter 정책 확장 | 24.11 | rte_flow에 quota 기반 rate-limiting 액션 추가, meter 정책 동적 변경 지원 |
| Power 라이브러리 개편 | 24.11 | rte_power가 AMD EPYC cppc, Intel HWP를 직접 제어. 유휴 lcore 절전 전략 세분화 |
| 24.11 LTS | 24.11 | 2년 LTS. 22.11 LTS → 24.11 LTS 마이그레이션 시 ABI 호환성 주의 |
마이그레이션 주요 주의사항
/* 22.11 LTS → 24.11 LTS 마이그레이션 체크리스트 */
1. 빌드 환경
- GCC 10+ 또는 Clang 12+ (C11 필수)
- meson 0.53+ → 0.56+ 업그레이드 권장
- pkg-config 파일명 변경 확인: libdpdk.pc
2. API 변경 (컴파일 오류 유발)
- rte_eth_dev_count() → rte_eth_dev_count_avail()
- rte_eth_dev_attach() → rte_dev_probe() + rte_eth_dev_get_port_by_name()
- rte_memcpy() inline 제거 → 표준 memcpy() 사용 권장
- DEV_RX_OFFLOAD_* → RTE_ETH_RX_OFFLOAD_* 접두사 변경
3. ABI 변경 (재컴파일 필요)
- rte_eth_dev_info 구조체 크기 변경
- rte_flow_item 내부 필드 재배치
- rte_mbuf dynamic field 오프셋 변경 가능
4. 드라이버 지원
- net/bnxt: 펌웨어 226.x+ 필수
- net/mlx5: MLNX_OFED 5.9+ 또는 rdma-core 44+ 필요
- net/ice: DDP 패키지 버전 확인
rte_flow_validate()로 모든 규칙을 재검증해야 합니다.
testpmd 기반 재현 실습
testpmd는 DPDK를 배울 때 가장 중요한 도구입니다. 포트 초기화, 큐 배치, 오프로드 플래그, xstats, flow rule, representor, hairpin을 하나씩 검증할 수 있기 때문입니다. 실무에서는 "내 애플리케이션이 이상하다"라고 보기 전에 먼저 testpmd로 하드웨어와 EAL 조합이 정상인지 확인해야 합니다.
랩 환경 준비
# 1. hugepage 준비
sudo dpdk-hugepages.py -p 1G --setup 2G
dpdk-hugepages.py --show
# 2. 장치 바인딩 상태 확인
dpdk-devbind --status
# 3. VFIO 바인딩
sudo modprobe vfio-pci
sudo ip link set ens3f0 down
sudo dpdk-devbind --bind=vfio-pci 0000:03:00.0
# 4. interactive testpmd 시작
dpdk-testpmd -l 2-5 -n 4 \
--iova-mode=va \
-a 0000:03:00.0 \
-- -i \
--nb-cores=2 \
--rxq=2 --txq=2 \
--burst=32 \
--port-topology=chained
세션 중 확인 명령
testpmd> show port info all
testpmd> show port stats all
testpmd> show rxq info 0 0
testpmd> show txq info 0 0
testpmd> set fwd macswap
testpmd> set burst 64
testpmd> start
testpmd> stop
testpmd> show fwd stats all
testpmd> show port xstats 0
testpmd> clear port stats all
testpmd> dump mempool
testpmd> quit
패킷 캡처와 관찰 포인트
| 관찰 대상 | 확인 도구 | 정상 신호 |
|---|---|---|
| 링크/속도/오프로드 | show port info all | 링크 업, MTU, RSS/offload capability가 기대치와 일치 |
| 큐 배치 | show rxq info, show txq info | NUMA 노드와 큐 수가 워커 수에 맞음 |
| xstats | show port xstats 0 | rx_discards, rx_missed_errors, rx_nombuf가 낮음 |
| 패킷 캡처 | dpdk-dumpcap 또는 dpdk-pdump | 테스트 프레임이 기대한 queue/port로 보임 |
dpdk-dumpcap 제약: 공식 도구 문서는 dpdk-dumpcap이 primary 애플리케이션과 같은 DPDK 버전을 써야 하고,
primary 쪽에서 패킷 캡처 프레임워크가 준비되어 있어야 한다고 설명합니다. 바로 캡처가 되지 않으면 NIC 문제보다 버전/프레임워크 전제부터 확인하세요.
io 모드로 하드웨어 RX/TX만 먼저 확인하고, 그다음 macswap, flow, representor, hairpin처럼 상태 공간을 넓혀 가는 편이 디버깅 비용이 낮습니다.
VPP (FD.io) — 유저 공간 네트워크 스택
VPP(Vector Packet Processing)는 Cisco가 개발하고 Linux Foundation의 FD.io 프로젝트로 운영되는 고성능 유저 공간 네트워크 스택입니다. DPDK PMD 위에서 동작하며 L2~L4 스위칭/라우팅, NAT, IPSec, ACL 등 커널 네트워크 스택의 기능을 유저 공간에서 구현합니다. "벡터 처리(Vector Processing)"라는 고유한 패킷 처리 모델이 VPP의 핵심 성능 비결입니다.
벡터 처리 모델
전통적인 패킷 처리(OVS, 커널 등)는 한 패킷을 모든 처리 단계(L2→L3→ACL→NAT→TX)를 거치게 한 뒤 다음 패킷으로 넘어갑니다. 이 방식은 단계마다 다른 코드를 실행하므로 I-cache(명령어 캐시)가 매번 교체됩니다. VPP는 반대로 같은 처리 노드를 256개 패킷에 연속 적용한 뒤 다음 노드로 넘어가는 벡터 모델을 사용합니다.
CLIB_PREFETCH()로 미리 가져오므로 D-cache 미스도 줄어듭니다. 이 두 가지 효과가 합쳐져 코어당 ~15 Mpps를 달성합니다.
VPP 노드 그래프 구조
VPP의 패킷 처리 파이프라인은 노드 그래프(Node Graph)로 구성됩니다. 각 노드는 특정 처리 단계(L2 입력, IPv4 조회, ACL 적용 등)를 담당하며, 패킷의 다음 노드(next-node)를 결정하여 전달합니다.
| 노드 이름 | 역할 | 다음 노드 예시 |
|---|---|---|
dpdk-input | DPDK PMD에서 패킷 수신 | ethernet-input |
ethernet-input | 이더넷 헤더 파싱, VLAN 처리 | ip4-input, ip6-input, arp-input |
ip4-input | IPv4 헤더 검증, TTL 체크 | ip4-lookup, ip4-local |
ip4-lookup | FIB 조회, 다음 홉 결정 | ip4-rewrite, ip4-arp |
ip4-rewrite | MAC 재작성, TTL 감소 | interface-output |
acl-plugin-in-ip4-fa | ACL 규칙 매칭 | 다음 feature 노드 또는 drop |
nat44-in2out | NAT44 내부→외부 변환 | ip4-lookup |
interface-output | 출력 인터페이스로 패킷 전달 | dpdk-tx |
# ━━━ VPP CLI 기본 조작 ━━━
# VPP 상태 확인
vppctl show version
vppctl show interface
# DPDK 인터페이스 할당 후 활성화
vppctl create interface dpdk GigabitEthernet3/0/0
vppctl set interface state GigabitEthernet3/0/0 up
vppctl set interface ip address GigabitEthernet3/0/0 10.0.0.1/24
# NAT44 설정 예시
vppctl nat44 add interface address GigabitEthernet3/0/1
vppctl set interface nat44 in GigabitEthernet3/0/0 out GigabitEthernet3/0/1
# 노드 그래프 성능 분석
vppctl show runtime # 노드별 벡터 크기, 클럭, 호출 횟수
vppctl show node counters # 노드별 처리 패킷/드롭 카운터
vppctl clear runtime # 카운터 초기화
# 패킷 트레이싱 (디버깅)
vppctl trace add dpdk-input 100 # dpdk-input 노드에서 100개 패킷 캡처
vppctl show trace # 캡처된 패킷의 노드별 처리 경로 출력
OVS-DPDK vs VPP 상세 비교
| 비교 항목 | OVS-DPDK | VPP/FD.io |
|---|---|---|
| 주요 용도 | L2 가상 스위칭 (OpenFlow) | L2~L4 라우팅/NAT/IPSec |
| 패킷 처리 | 플로우 테이블 매칭 | 벡터 그래프 (노드 체인) |
| 처리 모델 | 플로우 캐시 + upcall | 벡터화: 같은 노드를 256패킷 배치로 처리 |
| I-cache 효율 | 보통 (플로우별 분기) | 높음 (동일 코드를 벡터 크기만큼 반복) |
| 기능 | L2 스위칭 특화 | L2~L4, NAT44/NAT64, SRv6, IPSec, MPLS, VXLAN 등 |
| 설정 | OpenFlow / OVN | CLI / API (VPP API, NETCONF/YANG) |
| 성능 (64B) | ~10 Mpps/core | ~15 Mpps/core (벡터화 효과) |
| 확장 메커니즘 | OpenFlow 규칙 + OVN | 플러그인 노드 그래프 (C API, 핫 플러그) |
| 오케스트레이션 | OpenStack Neutron, OVN 통합 성숙 | Honeycomb/hc2vpp (NETCONF/YANG), NSM |
| TCP/IP 스택 | 없음 (L2 전용) | HostStack (내장 TCP/TLS), VCL |
| 멀티코어 | PMD 코어 + 핸들러 코어 분리 | 워커별 독립 노드 그래프 실행 |
| 생태계 | 매우 넓음 (OpenStack, K8s, VMware 등) | NFV/SD-WAN 중심 (Cisco, Calico/VPP) |
DPDK 관련 커널 소스 구조
DPDK 자체는 유저 공간 라이브러리이지만, 커널 측에서 DPDK 동작을 지원하는 핵심 구성 요소가 있습니다. DPDK가 NIC를 직접 제어하려면 커널이 디바이스를 유저 공간에 노출하고, DMA를 격리하고, 대용량 페이지를 제공해야 합니다. 여기서는 각 커널 서브시스템이 DPDK 동작에 어떻게 기여하는지 소스 수준에서 분석합니다.
| 커널 경로 | 역할 | DPDK 관련성 |
|---|---|---|
drivers/vfio/ | VFIO 프레임워크 | PCIe 디바이스를 유저 공간에 안전하게 노출 |
drivers/uio/ | UIO 프레임워크 | 레거시 디바이스 접근 (IOMMU 없는 환경) |
net/xdp/ | AF_XDP 소켓 | 커널 기반 제로카피 패킷 전달 |
mm/hugetlb.c | Hugepage 관리 | DPDK 메모리 관리의 기반 |
drivers/iommu/ | IOMMU (VT-d/AMD-Vi) | VFIO DMA 격리, IOVA 매핑 |
kernel/irq/ | IRQ 관리 | MSI/MSI-X eventfd (VFIO 인터럽트) |
drivers/net/ | NIC 커널 드라이버 | AF_XDP PMD가 커널 드라이버의 XDP 지원에 의존 |
VFIO 커널 서브시스템 상세
VFIO(Virtual Function I/O)는 DPDK가 NIC를 유저 공간에서 안전하게 제어하는 핵심 메커니즘입니다. IOMMU를 이용하여 DMA 격리를 보장하면서 디바이스의 PCI BAR, 인터럽트, DMA 매핑을 유저 공간에 노출합니다.
/* drivers/vfio/pci/vfio_pci_core.c — PCI BAR 영역을 유저 공간에 노출 */
static int
vfio_pci_core_mmap(struct vfio_device *core_vdev,
struct vm_area_struct *vma)
{
struct vfio_pci_core_device *vdev =
container_of(core_vdev, struct vfio_pci_core_device, vdev);
unsigned int index;
index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT);
/* BAR 인덱스 유효성 검증 */
if (index >= VFIO_PCI_NUM_REGIONS ||
!(vdev->bar_mmap_supported & (1 << index)))
return -EINVAL;
/* Write-Combining 또는 Uncacheable 매핑 설정 */
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* PCI BAR 물리 주소를 유저 공간 VMA에 매핑 */
return remap_pfn_range(vma, vma->vm_start,
vdev->bar_mmap[index].pfn,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}
코드 분석: VFIO PCI BAR 매핑
- vfio_pci_core_mmap(): DPDK가
mmap()시스템 콜로 NIC의 PCI BAR 영역을 유저 공간에 매핑할 때 호출됩니다. PMD는 이 매핑을 통해 NIC의 하드웨어 레지스터(Descriptor Ring Head/Tail, 통계 등)에 직접 접근합니다. - vm_pgoff → index: VFIO는 BAR 번호를 VMA offset 상위 비트로 인코딩합니다. BAR0~BAR5까지 6개 영역을 각각 별도로 매핑할 수 있습니다.
- pgprot_noncached(): PCI BAR는 메모리 매핑 I/O(MMIO)이므로 CPU 캐시를 우회하는 uncacheable 매핑을 사용합니다. TX tail 레지스터 쓰기 시 Write-Combining이 활성화되면 PCIe 트랜잭션을 합쳐 성능이 향상됩니다.
- remap_pfn_range(): 물리 PCI BAR 주소를 유저 공간 가상 주소에 매핑하는 커널 함수입니다. 이후 DPDK PMD는 이 주소에 직접 읽기/쓰기하여 NIC를 제어합니다.
/* drivers/vfio/vfio_iommu_type1.c — DMA 매핑 (IOVA → 물리 주소) */
static int
vfio_dma_do_map(struct vfio_iommu *iommu,
struct vfio_iommu_type1_dma_map *map)
{
dma_addr_t iova = map->iova;
unsigned long vaddr = map->vaddr;
size_t size = map->size;
int prot = 0;
if (map->flags & VFIO_DMA_MAP_FLAG_READ)
prot |= IOMMU_READ;
if (map->flags & VFIO_DMA_MAP_FLAG_WRITE)
prot |= IOMMU_WRITE;
/* 유저 공간 hugepage의 물리 주소를 pin하고
* IOMMU 페이지 테이블에 IOVA → PA 매핑 설치 */
return vfio_pin_map_dma(iommu, iova, vaddr, size, prot);
}
코드 분석: VFIO DMA 매핑
- vfio_dma_do_map(): DPDK EAL이
ioctl(VFIO_IOMMU_MAP_DMA)를 호출하면 실행됩니다. DPDK의 hugepage 메모리를 NIC가 DMA로 접근할 수 있도록 IOMMU 페이지 테이블에 매핑을 설치합니다. - IOVA(I/O Virtual Address): NIC가 DMA 시 사용하는 주소입니다. DPDK의 IOVA VA 모드에서는 유저 공간 가상 주소와 동일한 값을 IOVA로 사용하고, PA 모드에서는 물리 주소를 사용합니다.
- vfio_pin_map_dma(): (1) 유저 공간 가상 주소에 해당하는 물리 페이지를 pin(고정)하여 스왑아웃을 방지하고, (2) IOMMU 하드웨어에 IOVA→물리 주소 매핑을 설치합니다. 이 매핑이 있어야 NIC의 DMA가 올바른 메모리에 도달합니다.
- prot 플래그: IOMMU_READ는 NIC가 메모리를 읽을 수 있음(TX 버퍼), IOMMU_WRITE는 NIC가 메모리에 쓸 수 있음(RX 버퍼)을 의미합니다. IOMMU는 이 권한을 하드웨어 수준에서 강제하여 DMA 격리를 보장합니다.
UIO 커널 서브시스템
UIO(Userspace I/O)는 VFIO 이전의 레거시 디바이스 접근 방식입니다. IOMMU 보호 없이 디바이스를 유저 공간에 노출하므로 보안상 VFIO를 우선 사용해야 합니다.
/* drivers/uio/uio_pci_generic.c — UIO PCI 디바이스 등록 */
static struct pci_driver uio_pci_driver = {
.name = "uio_pci_generic",
.id_table = NULL, /* 모든 PCI 디바이스에 바인딩 가능 */
.probe = uio_pci_generic_probe,
.remove = uio_pci_generic_remove,
};
static int
uio_pci_generic_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct uio_pci_generic_dev *gdev;
/* 디바이스가 INTx 인터럽트를 지원하는지 확인 */
if (!pdev->irq)
return -ENODEV;
gdev = devm_kzalloc(&pdev->dev, sizeof(*gdev), GFP_KERNEL);
gdev->info.name = "uio_pci_generic";
gdev->info.version = DRIVER_VERSION;
gdev->info.irqcontrol = uio_pci_generic_irqcontrol;
gdev->info.irq = pdev->irq;
/* PCI BAR 영역을 UIO 리소스로 등록 →
* 유저 공간에서 /dev/uioX를 mmap()하면 BAR에 접근 가능 */
return devm_uio_register_device(&pdev->dev, &gdev->info);
}
코드 분석: UIO vs VFIO 핵심 차이
- id_table = NULL: UIO PCI generic 드라이버는 특정 벤더/디바이스 ID 없이 모든 PCI 디바이스에 바인딩할 수 있습니다.
dpdk-devbind.py로 NIC를 uio_pci_generic에 바인딩하면 이 probe 함수가 호출됩니다. - IOMMU 부재: UIO는 IOMMU 통합이 없으므로, NIC이 DMA로 시스템 메모리 전체에 접근할 수 있습니다. 악의적/결함 NIC이 다른 프로세스의 메모리를 읽거나 덮어쓸 수 있어 프로덕션 환경에서는 VFIO가 필수입니다.
- 인터럽트: UIO는 INTx 레벨 인터럽트만 지원합니다. VFIO는 MSI/MSI-X를 eventfd로 연결하여 효율적인 인터럽트 처리가 가능합니다. DPDK는 PMD 폴링이 주 경로이지만, 링크 상태 변경 등 관리 인터럽트에는 eventfd가 필요합니다.
AF_XDP 커널 소켓 구현
AF_XDP는 커널 드라이버를 유지하면서 XDP 경로로 패킷을 유저 공간에 제로카피 전달하는 메커니즘입니다. DPDK AF_XDP PMD는 이 커널 소켓을 활용합니다.
/* net/xdp/xsk.c — AF_XDP 소켓의 패킷 수신 핵심 경로 */
int xsk_rcv(struct xdp_sock *xs,
struct xdp_buff *xdp)
{
u32 len = xdp->data_end - xdp->data;
u64 addr;
int err;
/* UMEM fill ring에서 빈 프레임 주소를 꺼냄 */
err = xsk_rcv_peek(xs, &addr);
if (err)
return err;
if (xs->umem->flags & XDP_UMEM_FLAG_UNALIGNED_CHUNKS) {
/* 제로카피 모드: NIC DMA가 UMEM에 직접 기록 */
xsk_rcv_zc(xs, xdp, addr, len);
} else {
/* 복사 모드: 커널 XDP 버퍼 → UMEM 프레임으로 memcpy */
memcpy(xsk_buff_raw_get_data(xs->umem, addr),
xdp->data, len);
}
/* RX ring에 완료된 프레임 정보를 넣어 유저 공간에 알림 */
xsk_rcv_commit(xs, addr, len);
return 0;
}
코드 분석: AF_XDP 수신 경로
- xsk_rcv(): NIC 드라이버의 XDP 프로그램이
XDP_REDIRECT로 패킷을 AF_XDP 소켓에 전달하면 호출됩니다. DPDK AF_XDP PMD의rte_eth_rx_burst()는 이 경로의 유저 공간 측 대응 함수입니다. - UMEM: 유저 공간과 커널이 공유하는 메모리 영역으로, DPDK의 mempool/hugepage와 유사한 역할입니다. Fill Ring(유저→커널: 빈 프레임 제공)과 Completion Ring(커널→유저: 완료 알림)으로 구성됩니다.
- 제로카피 vs 복사: 제로카피 모드에서는 NIC DMA가 UMEM에 직접 기록하므로 memcpy가 없습니다. 복사 모드에서는 커널 XDP 버퍼에서 UMEM으로 한 번 복사합니다. 제로카피는 NIC 드라이버가 AF_XDP 제로카피를 지원해야 합니다(i40e, ice, mlx5 등).
- DPDK와의 관계: DPDK AF_XDP PMD는 이 소켓을 통해 패킷을 수신하므로, 커널 NIC 드라이버가 유지됩니다.
ethtool,tc, 커널 라우팅 테이블 등을 함께 사용할 수 있어 순수 VFIO 바인딩보다 운영 유연성이 높습니다.
Hugepage와 DPDK 메모리 모델
/* mm/hugetlb.c — Hugepage 예약과 할당 핵심 */
long hugetlb_reserve_pages(struct inode *inode,
long from, long to,
struct vm_area_struct *vma,
vm_flags_t vm_flags)
{
long chg, add = -1;
struct hstate *h = hstate_inode(inode);
/* 요청된 범위만큼 hugepage 예약 수 증가 */
chg = region_chg(&inode->i_mapping->private_list,
from, to, NULL);
/* NUMA 정책에 따라 특정 노드에서 hugepage 확보 */
if (hugetlb_acct_memory(h, chg))
return -ENOMEM;
/* 예약 성공: 이후 fault 시 실제 물리 페이지 할당 */
region_add(&inode->i_mapping->private_list, from, to,
NULL, NULL);
return 0;
}
코드 분석: Hugepage 예약
- hugetlb_reserve_pages(): DPDK EAL이
/dev/hugepages에 파일을 생성하고mmap()하면, 커널은 이 함수로 hugepage를 예약합니다. DPDK는 이 hugepage 위에 mempool, ring, mbuf 등 모든 데이터 구조를 배치합니다. - hstate: hugepage 크기(2MB 또는 1GB)별 상태를 관리하는 구조체입니다. DPDK는 1GB hugepage를 권장합니다. 2MB hugepage는 TLB 엔트리를 더 많이 소비하고, 대규모 mempool에서 IOTLB 미스가 발생할 수 있습니다.
- NUMA 노드 할당:
hugetlb_acct_memory()는 NUMA 정책(mbind, set_mempolicy)에 따라 특정 노드에서 hugepage를 확보합니다. DPDK는 NIC가 연결된 NUMA 노드와 동일한 노드에서 hugepage를 할당하여 크로스-NUMA 접근 지연(40~100ns)을 방지합니다. - 핀닝(Pinning): hugepage는 스왑아웃되지 않으므로 DPDK 메모리가 항상 물리 RAM에 상주합니다. NIC의 DMA가 물리 주소로 직접 접근하므로, 페이지가 스왑아웃되면 데이터 손실이 발생합니다.
커널 설정 항목별 해설
# DPDK 관련 커널 설정 (CONFIG_*)
# ━━━ VFIO (권장: IOMMU 기반 안전한 디바이스 패스스루) ━━━
CONFIG_VFIO=m # VFIO 코어 프레임워크
CONFIG_VFIO_PCI=m # PCI 디바이스용 VFIO 드라이버
CONFIG_VFIO_IOMMU_TYPE1=m # Type1 IOMMU 백엔드 (DMA 격리)
CONFIG_VFIO_NOIOMMU=y # no-IOMMU 모드 (테스트용, 보안 위험)
# ━━━ UIO (레거시: IOMMU 없는 환경에서만 사용) ━━━
CONFIG_UIO=m # UIO 코어 프레임워크
CONFIG_UIO_PCI_GENERIC=m # 범용 PCI UIO 드라이버
# ━━━ IOMMU (VFIO의 DMA 격리를 제공하는 핵심) ━━━
CONFIG_IOMMU_SUPPORT=y # IOMMU 프레임워크 활성화
CONFIG_INTEL_IOMMU=y # Intel VT-d (Xeon/Core 서버)
CONFIG_AMD_IOMMU=y # AMD-Vi (EPYC/Ryzen 서버)
CONFIG_IOMMU_DEFAULT_DMA_LAZY=y # IOVA 지연 해제 (TLB 무효화 배치 처리로 성능 향상)
# ━━━ Hugepage (DPDK 메모리 관리의 기반) ━━━
CONFIG_HUGETLBFS=y # /dev/hugepages 파일시스템 지원
CONFIG_HUGETLB_PAGE=y # 2MB/1GB hugepage 지원
CONFIG_TRANSPARENT_HUGEPAGE=y # THP — DPDK는 명시적 hugetlbfs 선호
# (THP는 compaction 오버헤드 + 예측 불가)
# ━━━ AF_XDP (커널 드라이버 유지 + XDP 경로 고속 처리) ━━━
CONFIG_XDP_SOCKETS=y # AF_XDP 소켓 지원
CONFIG_BPF_SYSCALL=y # BPF 시스템 콜 (XDP 프로그램 로딩)
CONFIG_NET_CLS_BPF=m # tc BPF classifier (선택)
# ━━━ NUMA (멀티소켓 서버 필수) ━━━
CONFIG_NUMA=y # NUMA 토폴로지 인식
CONFIG_NUMA_BALANCING=y # 자동 NUMA 밸런싱 (커널 용도)
# DPDK는 자체 NUMA 바인딩 사용
# ━━━ 부트 커맨드라인 (GRUB) ━━━
# intel_iommu=on iommu=pt ← VFIO 사용 시 필수
# iommu=pt: pass-through 모드 (커널 DMA는 IOMMU 미적용,
# VFIO가 관리하는 디바이스만 IOMMU 적용 → 성능 향상)
# hugepagesz=1G hugepages=8 ← 부팅 시 1GB hugepage 8개 예약
# default_hugepagesz=1G ← 기본 hugepage 크기를 1GB로 설정
CONFIG_VFIO_NOIOMMU=y는 IOMMU 없는 환경에서 VFIO API를 사용할 수 있게 하지만, DMA 격리가 없으므로 NIC이 시스템 메모리 전체에 접근할 수 있습니다. 프로덕션 환경에서는 반드시 IOMMU가 활성화된 상태에서 vfio-pci를 사용해야 합니다.
DPDK 빌드 시스템(Build System)과 버전 변천
DPDK는 22.11 LTS 이후 Meson + Ninja를 유일한 공식 빌드 시스템으로 사용합니다. 이전의 make 기반 빌드는 21.11에서 완전히 제거되었습니다. ABI 안정성 정책은 LTS 릴리스 간에 유지되며, 연 1회 LTS와 분기별 안정 릴리스로 운영됩니다.
# ━━━ DPDK Meson 빌드 (권장) ━━━
# 의존성 설치 (Debian/Ubuntu)
apt install build-essential meson ninja-build python3-pyelftools \
libnuma-dev libpcap-dev pkg-config
# 소스 다운로드 및 빌드
tar xf dpdk-24.11.tar.xz && cd dpdk-24.11
meson setup build
# 또는 옵션 지정:
meson setup build \
-Dplatform=native \
-Dexamples=l2fwd,l3fwd,testpmd \
-Dmax_ethports=32 \
-Ddisable_drivers=net/bnxt,net/enic \
-Ddefault_library=shared
ninja -C build
ninja -C build install
ldconfig
# pkg-config로 DPDK 연동 확인
pkg-config --cflags --libs libdpdk
# ━━━ 애플리케이션 빌드 (Meson 연동) ━━━
# meson.build 예시:
# project('my_app', 'c')
# dpdk_dep = dependency('libdpdk')
# executable('my_app', 'main.c', dependencies: dpdk_dep)
| Meson 옵션 | 기본값 | 설명 |
|---|---|---|
-Dplatform | native | native(현재 CPU 최적화), generic(이식성), x86-64-v3(AVX2) 등 |
-Dmax_ethports | 32 | 최대 이더넷 포트 수 (구조체 크기에 영향) |
-Dmax_lcores | 128 | 최대 논리 코어 수 |
-Ddefault_library | shared | shared/static — 정적 링크는 단일 바이너리 배포에 유리 |
-Ddisable_drivers | 없음 | 불필요 PMD 제외 → 빌드 시간·바이너리 크기 감소 |
-Denable_kmods | false | igb_uio 등 커널 모듈 빌드 (deprecated) |
-Dtests | true | 유닛 테스트 빌드 (CI 환경) |
코어 빌드 옵션 상세
Meson 빌드 시스템에서 -D옵션=값 형태로 지정하는 핵심 빌드 옵션의 전체 목록입니다. meson configure build 명령으로 현재 설정을 확인할 수 있습니다.
| 옵션 | 기본값 | 허용 값 | 설명 |
|---|---|---|---|
-Dplatform | native | native, generic, x86-64-v2~v4 | CPU 최적화 수준. native는 빌드 호스트 CPU에 맞춰 최적화 |
-Dmachine | auto | GCC -march 값 | platform이 native일 때 세부 아키텍처 지정 (예: icelake-server) |
-Dcpu_instruction_set | auto | auto, generic, native | ARM 전용: NEON/SVE/SVE2 명령어 세트 선택 |
-Dbuildtype | release | release, debug, debugoptimized, plain | Meson 내장 옵션. debug는 -O0 -g, release는 -O3 |
-Doptimization | 3 | 0~3, s | buildtype 설정을 재정의하는 세부 최적화 수준 |
-Ddebug | true | true/false | 디버그 심볼 포함 여부 (-g 플래그) |
-Dwerror | false | true/false | 경고를 에러로 처리 (-Werror). CI에서 권장 |
-Ddeveloper_mode | auto | auto, enabled, disabled | Git 트리에서 자동 활성화. 추가 경고·werror·디버그 체크 포함 |
-Dcheck_includes | false | true/false | 각 공개 헤더의 독립 컴파일 가능성 검증 |
-Db_lto | false | true/false | LTO (Link-Time Optimization). 바이너리 크기 감소·성능 향상 |
-Db_pgo | off | off, generate, use | PGO (Profile-Guided Optimization). 2단계 빌드 필요 |
-Dprefix | /usr/local | 경로 | 설치 경로 접두사 |
-Dlibdir | lib | 경로 | 라이브러리 설치 디렉터리 (일부 배포판은 lib64) |
platform이 native 또는 generic이면 DPDK가 자동으로 적절한 -march 값을 선택합니다. machine을 명시하면 platform의 자동 감지를 재정의합니다. 크로스 컴파일(Cross Compilation) 시에는 machine이 반드시 필요합니다.
드라이버·라이브러리 선택 옵션
DPDK는 200개 이상의 PMD(Poll Mode Driver)를 포함하며, 불필요한 드라이버를 제외하면 빌드 시간과 바이너리 크기를 크게 줄일 수 있습니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
-Ddisable_drivers | 없음 | 제외할 드라이버 (쉼표 구분). 와일드카드 지원: net/i* |
-Denable_drivers | 없음 (전체) | 포함할 드라이버만 지정 (화이트리스트 방식) |
-Ddisable_libs | 없음 | 제외할 라이브러리 (쉼표 구분). 의존성 있는 드라이버도 함께 제외 |
-Denable_kmods | false | igb_uio 등 커널 모듈 빌드. DPDK 20.11+에서 deprecated |
-Dkernel_dir | 자동 감지 | 커널 헤더 경로 (kmod 빌드 또는 KNI에 필요) |
-Ddisable_apps | 없음 | 제외할 애플리케이션 (예: test, proc-info) |
-Denable_apps | 없음 (전체) | 포함할 애플리케이션만 지정 |
# ━━━ 최소 드라이버 빌드 (mlx5 + virtio만) ━━━
meson setup build \
-Denable_drivers=net/mlx5,net/virtio,common/mlx5 \
-Ddisable_libs=pipeline,table,port,fib,rib,reorder \
-Ddisable_apps=test \
-Dplatform=generic \
-Ddefault_library=static \
-Db_lto=true
enable_drivers와 disable_drivers를 동시에 지정하면 enable_drivers가 우선합니다. enable_drivers에 나열된 드라이버만 빌드 후보가 되고, 그중 disable_drivers에 해당하는 것이 제외됩니다. enable_drivers만 사용하는 것이 직관적입니다.
크기 제한 및 리소스 상수
DPDK의 내부 자료 구조 크기를 컴파일 타임에 결정하는 상수입니다. 기본값은 범용 서버를 기준으로 설정되어 있으며, 임베디드 환경이나 대규모 NIC 환경에서는 조정이 필요합니다.
| 옵션 / 매크로(Macro) | 기본값 | 영향 |
|---|---|---|
-Dmax_ethports | 32 | 최대 이더넷 포트 수. 포트별 배열 크기 결정 |
-Dmax_lcores | 128 | 최대 논리 코어 수. RTE_MAX_LCORE에 매핑 |
-Dmax_numa_nodes | 4 | 최대 NUMA 노드 수. 메모리 풀 분할에 영향 |
RTE_MAX_ETHPORTS | max_ethports | rte_ethdev 배열 크기. SmartNIC VF가 많으면 증가 필요 |
RTE_MAX_LCORE | max_lcores | per-lcore 변수 배열 크기. 대형 NUMA 서버는 256+ 필요 |
RTE_MAX_MEMSEG_LISTS | 64 | hugepage 메모리 세그먼트 목록 수. 다중 hugepage 크기 시 증가 |
RTE_MAX_MEM_MB | 524288 | 최대 hugepage 메모리 (MB). 512GB 기본, TB급 서버는 증가 필요 |
RTE_MAX_QUEUES_PER_PORT | 1024 | 포트당 최대 큐 수. RSS 해시 분산 범위에 영향 |
RTE_MAX_MEM_MB는 rte_eal_init() 시 실제 hugepage 할당량과는 무관한 상한값입니다.
기능 토글 옵션
빌드에 포함할 기능을 세밀하게 제어하는 옵션입니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
-Dtests | true | 유닛 테스트 빌드. CI에서는 true, 프로덕션 빌드에서는 false |
-Dexamples | 빈 문자열 | 빌드할 예제 (쉼표 구분). all로 전체 빌드 가능 |
-Denable_apps | 전체 | 빌드할 앱 선택 (test-pmd, proc-info, pdump 등) |
-Denable_docs | false | API 문서(Doxygen) 및 가이드(Sphinx) 빌드 |
-Denable_trace_fp | false | fast-path trace point 활성화. 성능 오버헤드 미미 |
-Dmbuf_refcnt_atomic | true | mbuf 참조 카운트(Reference Count) 원자적 연산(Atomic Operation). 단일 스레드면 false로 성능 향상 |
-Dper_library_versions | true | 라이브러리별 개별 SO 버전 관리. false면 단일 ABI 버전 |
-Denable_driver_sdk | false | 드라이버 개발용 내부 헤더 설치. out-of-tree PMD 개발 시 필요 |
-Dlog_default_level | info | 기본 로그 수준: emergency~debug (8단계) |
-Duse_hpet | false | HPET 타이머 사용. TSC가 정확한 현대 CPU에서는 불필요 |
크로스 컴파일 설정
DPDK는 Meson의 크로스 컴파일 파일(cross-file)을 사용하여 ARM, RISC-V 등 다양한 아키텍처용 빌드를 지원합니다.
| 크로스파일 항목 | 값 예시 | 설명 |
|---|---|---|
[binaries] c | aarch64-linux-gnu-gcc | 크로스 C 컴파일러 경로 |
[binaries] ar | aarch64-linux-gnu-ar | 크로스 아카이버 |
[binaries] strip | aarch64-linux-gnu-strip | 심볼 제거 도구 |
[binaries] pkgconfig | aarch64-linux-gnu-pkg-config | 크로스 pkg-config |
[host_machine] cpu_family | aarch64 | 타겟 CPU 패밀리 |
# ━━━ ARM64 크로스 컴파일 파일 (aarch64_cross.txt) ━━━
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = 'aarch64-linux-gnu-pkg-config'
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'armv8-a'
endian = 'little'
[properties]
platform = 'generic'
cpu_instruction_set = 'generic'
# ━━━ 크로스 컴파일 빌드 명령 ━━━
# ARM64 크로스 컴파일 (BlueField DPU 등)
meson setup build-arm64 \
--cross-file config/arm/arm64_bluefield_platform \
-Dplatform=generic \
-Denable_drivers=net/mlx5,common/mlx5,regex/mlx5,vdpa/mlx5 \
-Ddefault_library=static
ninja -C build-arm64
DESTDIR=/opt/dpdk-arm64 ninja -C build-arm64 install
# RISC-V 크로스 컴파일 (실험적, DPDK 23.11+)
meson setup build-riscv \
--cross-file config/riscv/riscv64_linux_gcc \
-Dplatform=generic
ninja -C build-riscv
apt install libnuma-dev:arm64 libpcap-dev:arm64로 설치합니다. sysroot를 별도로 구성할 경우 [properties] 섹션에 sys_root를 지정합니다.
make → Meson 전환 매핑
DPDK 21.11에서 make 빌드가 제거되었습니다. 기존 CONFIG_RTE_* 환경 변수와 대응하는 Meson 옵션 매핑입니다.
| make (CONFIG_RTE_*) | Meson (-D) | 비고 |
|---|---|---|
CONFIG_RTE_MACHINE | -Dmachine | GCC -march 값 |
CONFIG_RTE_ARCH | 자동 감지 | Meson이 호스트/크로스파일에서 결정 |
CONFIG_RTE_MAX_ETHPORTS | -Dmax_ethports | 동일 기능 |
CONFIG_RTE_MAX_LCORE | -Dmax_lcores | 동일 기능 |
CONFIG_RTE_MAX_NUMA_NODES | -Dmax_numa_nodes | 동일 기능 |
CONFIG_RTE_LIBRTE_PMD_* | -Denable_drivers / -Ddisable_drivers | 개별 옵션 → 통합 리스트 |
CONFIG_RTE_BUILD_SHARED_LIB | -Ddefault_library=shared | Meson 내장 옵션 |
CONFIG_RTE_EAL_IGB_UIO | -Denable_kmods | deprecated |
CONFIG_RTE_LIBRTE_VHOST | 자동 (libc 감지) | vhost-user 빌드 조건 자동 판단 |
CONFIG_RTE_APP_TEST | -Dtests | 테스트 빌드 토글 |
CONFIG_RTE_BUILD_EXAMPLES | -Dexamples | 쉼표 구분 리스트 또는 all |
CONFIG_RTE_EXEC_ENV_LINUX | 자동 감지 | 실행 환경(Linux/FreeBSD) 자동 판단 |
CONFIG_RTE_ENABLE_STDATOMIC | -Denable_stdatomic | DPDK 23.11+: C11 stdatomic 사용 |
CONFIG_RTE_USE_HPET | -Duse_hpet | HPET 타이머 |
T= (target) | --cross-file | 크로스 컴파일 대상 지정 |
O= (output dir) | meson setup <dir> | 빌드 디렉터리 지정 |
EXTRA_CFLAGS | -Dc_args | 추가 C 플래그 (Meson 내장) |
.config 파일의 CONFIG_RTE_* 항목을 수동으로 위 매핑표에 따라 -D 옵션으로 변환해야 합니다. meson_options.txt 파일에서 현재 버전의 모든 옵션을 확인할 수 있습니다.
실전 빌드 레시피
환경별 최적화된 빌드 명령 모음입니다.
# ━━━ 1. 최소 CI 빌드 (빠른 검증) ━━━
meson setup build-ci \
-Dbuildtype=debugoptimized \
-Dwerror=true \
-Dtests=true \
-Dexamples=l2fwd,l3fwd \
-Ddisable_drivers=raw/*,crypto/*,compress/*,regex/*,ml/* \
-Dcheck_includes=true
ninja -C build-ci
meson test -C build-ci --suite fast-tests
# ━━━ 2. 프로덕션 mlx5 빌드 (최대 성능) ━━━
meson setup build-prod \
-Dbuildtype=release \
-Dplatform=native \
-Denable_drivers=net/mlx5,common/mlx5,regex/mlx5 \
-Ddefault_library=static \
-Db_lto=true \
-Dmax_ethports=16 \
-Dmax_lcores=64 \
-Dtests=false \
-Denable_trace_fp=true \
-Dmbuf_refcnt_atomic=false \
-Dc_args='-march=icelake-server -mtune=icelake-server'
ninja -C build-prod
sudo ninja -C build-prod install && sudo ldconfig
# ━━━ 3. 디버그 빌드 (문제 추적) ━━━
meson setup build-debug \
-Dbuildtype=debug \
-Doptimization=0 \
-Ddebug=true \
-Dtests=true \
-Dexamples=all \
-Dlog_default_level=debug \
-Denable_trace_fp=true \
-Dc_args='-fsanitize=address,undefined' \
-Dc_link_args='-fsanitize=address,undefined'
ninja -C build-debug
# ━━━ 4. 문서 빌드 (Doxygen + Sphinx) ━━━
apt install doxygen python3-sphinx python3-sphinx-rtd-theme
meson setup build-doc \
-Denable_docs=true \
-Dtests=false \
-Dexamples=
ninja -C build-doc doc
# ━━━ 5. ARM64 DPU 빌드 (NVIDIA BlueField-3) ━━━
meson setup build-bf3 \
--cross-file config/arm/arm64_bluefield_platform \
-Dplatform=generic \
-Denable_drivers=net/mlx5,common/mlx5,regex/mlx5,vdpa/mlx5,compress/mlx5 \
-Ddefault_library=static \
-Db_lto=true \
-Dmax_ethports=8 \
-Dtests=false \
-Dprefix=/opt/dpdk
ninja -C build-bf3
DESTDIR=/mnt/bf3-rootfs ninja -C build-bf3 install
DPDK 주요 버전 변천
| 버전 | 연도 | 주요 변경 |
|---|---|---|
| 1.x~2.x | 2013~2015 | Intel 초기 릴리스, make 빌드, ixgbe/i40e PMD, 기본 mbuf/ring/mempool |
| 16.04~16.11 | 2016 | 연월 버전 체계 도입, rte_flow API 초안, Eventdev 프레임워크 |
| 17.11 LTS | 2017 | 첫 LTS, Cryptodev 성숙, Flow API 정식화, AF_XDP 실험 |
| 18.11 LTS | 2018 | ABI 안정성 정책 도입, Meson 빌드 추가, mlx5 bifurcated PMD |
| 19.11 LTS | 2019 | AF_XDP PMD 정식, Graph 라이브러리, Telemetry v2, RCU 라이브러리 |
| 20.11 LTS | 2020 | API 대정비 (rte_eth → rte_ethdev), dmadev 초안, IOAT 드라이버 |
| 21.11 LTS | 2021 | make 빌드 제거, Meson 전용, dmadev 정식, GPU 디바이스, Trace 라이브러리 |
| 22.11 LTS | 2022 | ABI 23.x 시리즈, Graph dispatch 모델, flow aging, CT offload 강화 |
| 23.11 LTS | 2023 | vhost DMA offload, rte_flow async, ethdev 텔레메트리 확장 |
| 24.11 LTS | 2024 | ABI 25.x 시리즈, ARM SVE2 최적화, GPUdev DMA, power intrinsics 강화 |
버전별 옵션 변경 이력
DPDK 버전 업그레이드 시 빌드 스크립트를 수정해야 하는 옵션 변경 사항입니다.
| 버전 | 추가된 옵션 | 변경된 옵션 | 제거/Deprecated |
|---|---|---|---|
| 18.11 | Meson 빌드 도입 (make과 병행) | — | — |
| 19.11 | enable_trace_fp | max_lcores 기본값 128→128 유지 | — |
| 20.11 | disable_apps, enable_apps | disable_drivers 와일드카드 지원 | enable_kmods deprecated |
| 21.11 | platform (기존 machine 대체) | machine은 platform 하위로 이동 | make 빌드 완전 제거 |
| 22.11 | enable_driver_sdk, log_default_level | max_ethports 기본값 32 유지 | KNI 라이브러리 deprecated |
| 23.11 | enable_stdatomic, RISC-V 크로스파일 | cpu_instruction_set ARM 확장 | igb_uio 소스 제거 |
| 24.03 | x86-64-v2~v4 platform 값 | developer_mode 동작 세분화 | — |
| 24.11 | ARM SVE2 cpu_instruction_set | max_numa_nodes 기본값 4→4 유지 | use_hpet deprecated 예고 |
diff <(cd dpdk-23.11 && meson configure build) <(cd dpdk-24.11 && meson configure build) 명령 또는 meson_options.txt 파일의 Git diff를 확인합니다. 릴리스 노트의 "Build System" 섹션도 참고하세요.
타 바이패스 프레임워크 비교
DPDK는 유일한 커널 바이패스 기술이 아닙니다. 각 프레임워크는 서로 다른 설계 철학과 트레이드오프를 가집니다.
| 항목 | DPDK | XDP (eBPF) | AF_XDP | Netmap | io_uring (net) |
|---|---|---|---|---|---|
| 실행 위치 | 유저 공간 | 커널 (NIC 드라이버 훅) | 유저 공간 (커널 XDP 경유) | 유저 공간 (커널 모듈) | 커널 (비동기 syscall) |
| NIC 점유 | 전용 (VFIO/UIO) | 공유 (커널 드라이버) | 큐별 분리 가능 | 전용/공유 혼합 | 공유 (커널 스택) |
| 처리량 (64B) | ~14.88 Mpps/core | ~24 Mpps (XDP_TX) | ~10 Mpps/core | ~12 Mpps/core | 아직 미성숙 |
| 지연 | 수백 ns | 수백 ns | ~1 μs | ~1 μs | 수 μs |
| 커널 기능 | 사용 불가 | 모두 가능 | 부분 가능 | 제한적 | 모두 가능 |
| 프로토콜 스택 | 없음 (VPP 등 별도) | 커널 TCP/IP | 없음/커널 혼합 | 없음 | 커널 TCP/IP |
| NIC 지원 범위 | 최대 (100+ PMD) | 드라이버 XDP 지원 필요 | AF_XDP 지원 NIC | 제한적 | 모든 NIC |
| 에코시스템 | OVS, VPP, SPDK, 상업 앱 | Cilium, Katran, bpfilter | DPDK AF_XDP PMD | 학술/소규모 | 범용 서버 |
| 학습 곡선 | 높음 | 중간 (eBPF) | 중간~높음 | 중간 | 낮음 |
| 적합 분야 | NFV, 통신, 전용 어플라이언스 | DDoS 방어, 로드밸런서, 커널 내 필터링 | 커널 기능 + 고성능 혼합 | 연구/교육 | 범용 서버 I/O 최적화 |
- 커널 기능(iptables, tc, routing)이 필요하다면 → XDP 또는 AF_XDP
- NIC을 다른 프로세스와 공유해야 하는 경우 → AF_XDP 또는 XDP
- 최대 처리량, 최소 지연이 절대 우선이라면 → DPDK
- 이미 VPP/OVS-DPDK 에코시스템을 사용 중이라면 → DPDK 유지
- L7 애플리케이션(웹 서버, DB) 최적화라면 → io_uring
DPDK 구동 시 NVMe 성능 저하 분석
DPDK와 NVMe 드라이버는 직접 코드 경로를 공유하지 않지만, PCIe 버스·CPU 코어·메모리 계층·커널 인프라 수준에서 다수의 간접 간섭 경로가 존재합니다. DPDK 애플리케이션을 기동한 뒤 NVMe SSD의 IOPS·지연이 갑자기 악화되는 현상은 대부분 아래 17가지 원인 중 하나(또는 복합)에 해당합니다.
1. PCIe 버스 대역폭 경합
DPDK 10 GbE 라인레이트(~14.88 Mpps, 64B 프레임)는 PCIe 3.0 x8 대역폭의 상당 부분을 소비합니다. NVMe가 같은 Root Complex 하위 Switch에 연결되면 TLP(Transaction Layer Packet) 수준에서 중재 경합이 발생합니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| 업스트림 대역폭 포화 | NIC과 NVMe가 동일 PCIe Switch 하위 → 업스트림 링크 대역폭을 공유 | NVMe 대역폭 30-50% 하락, avgqu-sz 증가 |
lspci -tv로 토폴로지 확인perf stat -e uncore_iio |
NVMe를 별도 PCIe Switch / CPU 직결 슬롯으로 이동 |
| TLP Credit 고갈 | DPDK PMD가 DMA descriptor를 대량 발급 → Posted/Non-Posted Credit 소진 → NVMe Completion 지연 | NVMe p99 지연 스파이크 (정상 대비 5~10×) | setpci로 Link Status 확인PCIe AER 로그 |
NIC tx_burst 크기 제한, PCIe MPS/MRRS 튜닝 |
| PCIe Ordering Rule | x86 PCIe는 Posted Write 순서 보장 필요 → NIC DMA와 NVMe DMA 간 Ordering Barrier 발생 | 단일 Switch에서만 NVMe 쓰기 지연 증가 | 분리 배치 후 지연 비교 A/B 테스트 | Relaxed Ordering 활성화 (setpci DEVCTL2) |
devargs에 mprq_en=1과 함께 설정하면 PCIe Ordering 부하를 줄일 수 있지만, 일부 플랫폼에서는 데이터 무결성(Integrity) 문제가 보고되었으므로 반드시 검증 후 적용하세요.
2. CPU 코어·IRQ Affinity 충돌
DPDK PMD는 전용 코어에서 100% CPU를 소비하는 폴링 루프를 실행합니다. NVMe 인터럽트(IRQ)가 같은 코어에 할당되면, IRQ 처리가 지연되어 NVMe I/O 완료 레이턴시가 급증합니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| DPDK 코어와 NVMe IRQ 동일 코어 | PMD 폴링 루프가 코어를 100% 점유 → NVMe MSI-X IRQ의 softirq 처리 기아(starvation) | NVMe await 급증, /proc/interrupts에서 특정 코어에 NVMe IRQ 편중 |
cat /proc/interrupts | grep nvmeDPDK --lcores 설정과 비교 |
NVMe IRQ를 DPDK 미사용 코어로 이동: echo <mask> > /proc/irq/<N>/smp_affinity |
| Hyper-Threading 간섭 | DPDK가 물리 코어의 한 HT를 점유하면, 형제 HT에서 NVMe IRQ 처리 성능 40~60% 하락 | HT 형제 코어에서 NVMe 지연 악화, LLC miss 증가 | lscpu -e로 HT 쌍 확인perf stat -C <core> |
HT 형제를 동시에 DPDK에 할당하거나, NVMe IRQ를 물리적으로 다른 코어에 할당 |
| irqbalance 충돌 | irqbalance 데몬이 NVMe IRQ를 DPDK 코어로 재배치(Relocation) |
운영 중 간헐적 NVMe 지연 스파이크 | systemctl status irqbalanceIRQ affinity 변화 모니터링 |
IRQBALANCE_BANNED_CPUS 환경변수로 DPDK 코어 제외, 또는 irqbalance 중지 후 수동 설정 |
# NVMe IRQ를 DPDK 미사용 코어(예: 코어 8~15)로 제한
for irq in $(grep nvme /proc/interrupts | awk '{print $1}' | tr -d ':'); do
echo ff00 > /proc/irq/$irq/smp_affinity # 코어 8~15 마스크
done
# irqbalance에서 DPDK 코어(0~7) 제외
echo 'IRQBALANCE_BANNED_CPUS="00ff"' >> /etc/default/irqbalance
systemctl restart irqbalance
3. NUMA 토폴로지 불일치
NUMA 시스템에서 NVMe 컨트롤러가 NUMA 노드 0에 연결되었는데, 애플리케이션이 NUMA 노드 1에서 I/O를 발급하면 Cross-NUMA 메모리 접근으로 지연이 30~100% 증가합니다. DPDK가 hugepage를 특정 NUMA 노드에서 대량 할당하면, 반대 노드의 NVMe 드라이버가 사용할 로컬 메모리가 부족해집니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| Cross-NUMA DMA | NVMe 컨트롤러와 NIC이 서로 다른 NUMA 노드 → DMA 완료 데이터가 리모트 메모리 통과 | NVMe 대역폭 저하, numastat에서 other_node 증가 |
cat /sys/block/nvme*/device/numa_nodenumastat -p <pid> |
NIC과 NVMe를 같은 NUMA 노드에 배치, DPDK --socket-mem 조정 |
| Hugepage NUMA 편향 | DPDK가 한 노드에서 hugepage를 대량 소비 → 해당 노드의 NVMe DMA 버퍼 할당 실패/폴백 | cat /sys/devices/system/node/node*/hugepages/*/free_hugepages에서 편향 |
numactl --hardwarehugepage 노드별 잔량 확인 |
노드별 hugepage 균등 할당: echo N > /sys/devices/system/node/nodeX/hugepages/.../nr_hugepages |
4. Hugepage 메모리 경합
DPDK는 기동 시 수 GB~수십 GB의 1G/2M hugepage를 예약합니다. 이로 인해 시스템 전체의 가용 메모리가 감소하여 NVMe 드라이버와 파일시스템(Filesystem)에 영향을 줍니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| Page cache 축소 | hugepage 예약이 일반 메모리를 압박 → page cache 크기 감소 → NVMe 버퍼 I/O 캐시 히트율 하락 | free -m에서 buff/cache 급감, NVMe 읽기 IOPS 하락 |
free -m, vmstat 1로 bi/bo 모니터링 |
DPDK hugepage를 필요한 최소량으로 제한, --socket-mem 명시 |
| THP compaction 스톰 | THP(Transparent Hugepage) 활성 상태에서 DPDK hugepage 예약이 메모리 단편화(Fragmentation) 유발 → compaction이 NVMe I/O 경로에서 stall | kcompactd CPU 사용 급증, NVMe 지연 간헐적 스파이크 |
grep compact /proc/vmstatperf top에서 compact_zone_order |
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled |
| OOM 킬러 간접 효과 | hugepage로 인한 메모리 부족 → OOM 킬러가 NVMe I/O를 발행하는 프로세스 종료 | dmesg에 OOM 메시지, 워크로드 중단 | dmesg | grep -i oom |
시스템 총 메모리 대비 DPDK hugepage 비율 20% 이하 유지 |
5. IOMMU/VFIO 오버헤드
DPDK는 vfio-pci를 통해 NIC에 접근하며, 이때 IOMMU가 DMA 주소 변환을 수행합니다. NVMe도 IOMMU를 통과하므로, IOTLB 경합과 IOMMU 그룹 공유 문제가 발생할 수 있습니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| IOTLB miss/flush | DPDK가 대량의 DMA 매핑을 생성 → IOTLB 엔트리 갱신 빈번 → NVMe DMA의 IOTLB miss 증가 | IOMMU 통과 시 NVMe 지연 증가 (passthrough 대비 5~15%) | perf stat -e dTLB-load-missesdmesg | grep -i iommu |
Intel: intel_iommu=on iommu=pt (pass-through 모드)DPDK: --iova-mode=pa (가능한 경우) |
| IOMMU 그룹 공유 | NIC과 NVMe가 같은 IOMMU 그룹 → VFIO가 그룹 전체를 잠금(Lock), NVMe 드라이버 바인딩 충돌 가능 | NVMe 장치 인식 실패 또는 성능 저하 | find /sys/kernel/iommu_groups/ -type l |
ACS(Access Control Services) 지원 스위치 사용, 물리적으로 분리된 슬롯 배치 |
iommu=pt는 DPDK가 VFIO를 사용할 때 다른 장치(NVMe 포함)의 DMA를 pass-through로 처리하여 IOMMU 오버헤드를 제거합니다. 단, DMA 격리가 약화되므로 보안이 중요한 환경에서는 주의가 필요합니다.
6. LLC 캐시 오염 / 메모리 대역폭 포화
DPDK PMD는 대량의 패킷 버퍼를 연속적으로 접근하며 LLC(Last Level Cache)를 오염시킵니다. NVMe 드라이버의 Completion Queue 엔트리와 I/O 메타데이터가 캐시에서 밀려나면 성능이 저하됩니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| LLC 오염 | DPDK mbuf 대량 prefetch → LLC way를 점유 → NVMe CQE·SQE 캐시 miss | NVMe 지연 증가, perf stat에서 LLC-load-misses 증가 |
perf stat -e LLC-load-misses -C <nvme_core>Intel RDT: pqos -m all:0-15 |
Intel CAT(Cache Allocation Technology): DPDK 코어의 LLC way 제한pqos -e "llc:1=0x00f;llc:2=0xff0" |
| 메모리 대역폭 포화 | DPDK DMA + NVMe DMA + CPU 접근이 메모리 채널 대역폭을 초과 | 양쪽 모두 성능 저하, pcm-memory.x에서 채널 사용률 80%+ |
pcm-memory.x 또는 perf stat -e uncore_imc |
Intel MBA(Memory Bandwidth Allocation)로 DPDK 대역폭 상한 설정 |
7. 전원 관리(Power Management)·스케줄러·RCU 간섭
DPDK PMD의 busy-poll 루프는 커널의 전원 관리, 스케줄러, RCU 콜백(Callback) 처리와 충돌할 수 있습니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| C-state / P-state 전환 | DPDK 미사용 코어가 deep C-state → NVMe IRQ 도착 시 웨이크업 지연 (수십 μs) | NVMe p99 지연에 간헐적 수십 μs 스파이크 | turbostat으로 C-state 분포 확인 |
NVMe 코어에 processor.max_cstate=1 또는cpupower idle-set -D 0 |
| 커널 스케줄러 간섭 | DPDK 코어가 isolcpus로 격리되지 않으면 커널 스레드(kworker 등)가 NVMe 처리를 방해 |
DPDK 코어에서 간헐적 패킷 드롭, NVMe 코어에서 스케줄링 지연 | ps -eo pid,psr,comm | grep kworker |
커널 부트 파라미터: isolcpus=0-7 nohz_full=0-7 rcu_nocbs=0-7 |
| RCU 콜백 지연 | DPDK 코어가 RCU grace period를 block → 커널 메모리 해제 지연 → NVMe 관련 slab 압박 | /sys/kernel/debug/rcu/rcu_preempt/rcudata에서 콜백 누적 |
cat /sys/kernel/debug/rcu/*/rcudata |
rcu_nocbs=<dpdk_cores>로 RCU 콜백을 다른 코어에 위임 |
# DPDK 코어 0~7 격리 + RCU 오프로드 (GRUB 커널 파라미터)
GRUB_CMDLINE_LINUX="isolcpus=0-7 nohz_full=0-7 rcu_nocbs=0-7 \
intel_idle.max_cstate=1 processor.max_cstate=1 \
intel_iommu=on iommu=pt"
8. blk-mq 큐 매핑·MSI-X·Thermal
NVMe 드라이버의 blk-mq 큐 할당, MSI-X 벡터 분배, 열 관리(Thermal Management) 등도 DPDK와의 상호작용에서 영향을 받습니다.
| 원인 | 메커니즘 | 증상 | 확인 방법 | 해결 |
|---|---|---|---|---|
| blk-mq 큐 매핑 왜곡 | isolcpus로 코어를 격리하면 blk-mq가 NVMe 큐를 남은 코어에만 매핑 → 큐 불균형 |
특정 NVMe 큐에 I/O 집중, /sys/block/nvme0n1/mq/*/cpu_list 편향 |
cat /sys/block/nvme0n1/mq/*/cpu_list |
managed_irq 파라미터 활용, blk_mq_update_nr_hw_queues() 재조정 |
| MSI-X 벡터 경합 | NIC과 NVMe의 MSI-X 벡터가 플랫폼 한계에 도달 → 인터럽트 공유/코얼레싱 | 인터럽트 공유로 NVMe 지연 증가 | cat /proc/interrupts에서 벡터 분포 확인 |
BIOS에서 MSI-X 벡터 풀 확대, 불필요한 장치 비활성화 |
| Thermal throttling | DPDK 코어의 100% 사용률 → CPU 패키지 온도 상승 → Thermal throttling이 NVMe 코어 클럭도 제한 | turbostat에서 PkgWatt 급증, 코어 클럭 하락 |
turbostat --show PkgWatt,CoreTmp |
적절한 냉각, DPDK power_pmd_mgmt API로 적응형 폴링 |
| NVMe poll 모드 경합 | 커널 NVMe 드라이버가 io_poll 모드(io_uring) 사용 시, DPDK PMD와 CPU 사이클 경합 |
양쪽 모두 처리량 하락 | cat /sys/block/nvme*/queue/io_poll |
NVMe poll 모드 비활성화 또는 전용 코어 분리 |
| SPDK와 DPDK 동시 사용 | SPDK(유저 공간 NVMe 드라이버)와 DPDK가 hugepage·코어·VFIO를 공유 → 리소스 경합 | 두 프레임워크 모두 성능 저하 | SPDK + DPDK 동시 기동 여부 확인 | hugepage·코어·IOMMU 그룹을 명시적으로 분리 할당 |
9. 종합 진단 체크리스트와 스크립트
아래 체크리스트를 순서대로 확인하면, DPDK–NVMe 간섭의 대부분을 빠르게 진단할 수 있습니다.
| # | 점검 항목 | 명령 / 확인 방법 | 정상 기준 |
|---|---|---|---|
| 1 | PCIe 토폴로지 분리 | lspci -tv | NIC과 NVMe가 다른 Switch 하위 |
| 2 | NUMA 노드 일치 | cat /sys/class/net/*/device/numa_nodecat /sys/block/nvme*/device/numa_node | NIC과 NVMe가 같은 NUMA 노드 |
| 3 | NVMe IRQ affinity | cat /proc/irq/*/smp_affinity_list (nvme) | DPDK 코어와 겹치지 않음 |
| 4 | HT 형제 분리 | lscpu -e | DPDK 코어의 HT 형제에 NVMe IRQ 없음 |
| 5 | irqbalance 제외 | grep BANNED /etc/default/irqbalance | DPDK 코어가 BANNED 목록에 포함 |
| 6 | Hugepage 잔량 | cat /sys/devices/system/node/node*/hugepages/*/free_hugepages | 각 노드에 충분한 여유 hugepage |
| 7 | 일반 메모리 여유 | free -m | buff/cache가 총 메모리의 20%+ |
| 8 | THP 설정 | cat /sys/kernel/mm/transparent_hugepage/enabled | madvise 또는 never |
| 9 | IOMMU 모드 | dmesg | grep -i iommu | iommu=pt 활성 |
| 10 | IOMMU 그룹 | find /sys/kernel/iommu_groups/ -type l | NIC과 NVMe가 다른 그룹 |
| 11 | LLC miss 비율 | perf stat -e LLC-load-misses -C <nvme_core> | DPDK 기동 전후 차이 <10% |
| 12 | 메모리 대역폭 | pcm-memory.x 또는 perf stat -e uncore_imc | 채널 사용률 <70% |
| 13 | C-state | turbostat --show Avg_MHz,Busy%,Bzy_MHz,PkgWatt | NVMe 코어가 deep C-state에 빠지지 않음 |
| 14 | Thermal | turbostat --show CoreTmp,PkgTmp | 패키지 온도 <85°C |
| 15 | blk-mq 큐 분포 | cat /sys/block/nvme0n1/mq/*/cpu_list | 큐가 NVMe NUMA 코어에 고르게 분포 |
| 16 | RCU 콜백 | cat /sys/kernel/debug/rcu/*/rcudata | DPDK 코어에서 콜백 누적 없음 |
#!/bin/bash
# dpdk-nvme-diag.sh — DPDK/NVMe 간섭 종합 진단 스크립트
set -euo pipefail
echo "=== 1. PCIe 토폴로지 ==="
lspci -tv 2>/dev/null | head -40
echo -e "\n=== 2. NUMA 노드 확인 ==="
for dev in /sys/class/net/*/device/numa_node; do
[ -f "$dev" ] && echo "$(dirname $(dirname $dev) | xargs basename): $(cat $dev)"
done
for dev in /sys/block/nvme*/device/numa_node; do
[ -f "$dev" ] && echo "$(echo $dev | grep -o 'nvme[0-9]*'): $(cat $dev)"
done
echo -e "\n=== 3. NVMe IRQ Affinity ==="
for irq in $(grep nvme /proc/interrupts 2>/dev/null | awk '{print $1}' | tr -d ':'); do
echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list 2>/dev/null)"
done
echo -e "\n=== 4. Hugepage 상태 (노드별) ==="
for node in /sys/devices/system/node/node*; do
for hp in "$node"/hugepages/hugepages-*; do
[ -d "$hp" ] && echo "$(basename $node) $(basename $hp): \
free=$(cat $hp/free_hugepages)/total=$(cat $hp/nr_hugepages)"
done
done
echo -e "\n=== 5. 메모리 상태 ==="
free -m
echo -e "\n=== 6. THP 설정 ==="
cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null
echo -e "\n=== 7. IOMMU 모드 ==="
dmesg 2>/dev/null | grep -i "iommu.*mode\|iommu.*pass" | tail -3
echo -e "\n=== 8. IOMMU 그룹 (NIC/NVMe) ==="
for dev in $(lspci -D | grep -iE 'nvme|ethernet' | awk '{print $1}'); do
grp=$(find /sys/kernel/iommu_groups/ -name "$dev" 2>/dev/null | grep -o 'iommu_groups/[0-9]*')
echo "$dev → $grp"
done
echo -e "\n=== 9. C-state (turbostat) ==="
command -v turbostat &>/dev/null && turbostat --show Avg_MHz,Busy%,CoreTmp -n 1 2>/dev/null | head -10 \
|| echo "turbostat not available"
echo -e "\n=== 10. blk-mq 큐 매핑 ==="
for mq in /sys/block/nvme*/mq/*/cpu_list; do
[ -f "$mq" ] && echo "$(echo $mq | grep -o 'nvme[^/]*/mq/[0-9]*'): $(cat $mq)"
done
echo -e "\n=== 진단 완료 ==="
- PCIe 토폴로지 분리 — 하드웨어 수준 경합 제거 (가장 큰 효과)
- NUMA 정렬 — NIC·NVMe·DPDK 코어를 같은 NUMA 노드에 배치
- CPU 코어/IRQ 분리 —
isolcpus+ IRQ affinity 설정 - Hugepage 최적화 — 노드별 균등 할당, 최소량 사용
- IOMMU 튜닝 —
iommu=pt, IOMMU 그룹 분리 - LLC/메모리 대역폭 격리 — Intel CAT/MBA (RDT) 적용
- 전원/스케줄러 튜닝 — C-state 제한,
nohz_full,rcu_nocbs - blk-mq/MSI-X/Thermal — 큐 재매핑, 냉각 확인
운영 하드닝과 트러블슈팅
DPDK 장애는 대개 "패킷 처리 코드"보다 권한, IOMMU 그룹, hugepage 배치, NUMA 불일치, 보안 부팅에서 먼저 발생합니다. 따라서 운영 체크리스트를 코드 리뷰와 별도로 가져가는 편이 안전합니다.
하드닝 체크리스트
| 항목 | 권장 설정 | 이유 |
|---|---|---|
| 장치 접근 | vfio-pci + IOMMU 기본 | DMA 격리와 비특권 실행 경로를 동시에 확보합니다. |
| UIO 사용 범위 | 랩/레거시 전용 | 공식 문서는 UEFI Secure Boot가 UIO 모듈 로드를 막을 수 있다고 경고합니다. |
| 코어 선택 | 코어 0은 OS용으로 남기고 sibling 충돌 회피 | Linux GSG도 코어 0 비사용을 권장합니다. |
| Hugepage 권한 | 전용 디렉터리, 최소 권한, 충분한 memlock | rootless 실행과 실패 원인 분리를 쉽게 합니다. |
| NUMA 정렬 | NIC, 워커 코어, mempool을 같은 노드에 둠 | 크로스소켓 지연과 IOTLB 비용을 줄입니다. |
| 호환성 고정 | OVS/DPDK/펌웨어 조합을 하나의 단위로 관리 | ABI와 PMD capability 차이로 인한 현장 편차를 줄입니다. |
자주 보는 증상과 원인
| 증상 | 원인 | 조치 |
|---|---|---|
cannot open /proc/self/pagemap | PA 모드 전제 또는 물리 주소 접근 권한 부족 | --iova-mode=va로 강제하거나 공식 문서가 요구하는 capability를 부여 |
VFIO group is not viable | IOMMU 그룹 안에 다른 함수/브리지 디바이스가 남아 있음 | 그룹 전체를 확인하고 같은 그룹의 불필요 디바이스를 커널에서 떼거나 ACS 토폴로지를 재검토 |
VFIO_IOMMU_MAP_DMA failed | memlock 부족 또는 dma_entry_limit 한계 | 리소스 한도를 올리고 hugepage 세그먼트 수를 줄이며, 필요 시 커널 dma_entry_limit를 확인 |
rx_nombuf 증가 | mempool 고갈, 잘못된 NUMA 배치, per-core 캐시 과소 | mempool 크기·캐시·NUMA를 다시 맞추고 show port xstats로 추적 |
| AF_XDP PMD가 zero-copy로 붙지 않음 | 드라이버 미지원 또는 XDP/queue 조합 불일치 | 커널 AF_XDP 드라이버 capability와 queue 구성을 먼저 확인 |
dpdk_initialized=false (OVS) | hugepage 디렉터리, socket-mem, VFIO 바인딩, ABI 조합 중 하나가 불일치 | ovs-vsctl get Open_vSwitch . dpdk_initialized, dpdk-devbind --status, OVS 로그를 같이 확인 |
| Secure Boot 환경에서 UIO 모듈 로드 실패 | 서명되지 않은 UIO 계열 모듈이 차단됨 | 가능하면 VFIO로 전환하고, 꼭 필요하면 MOK/모듈 서명 절차를 밟음 |
no-IOMMU 경로를 unsafe로 취급합니다.
테스트 랩에서만 쓰고, 운영 환경에서는 디바이스가 호스트 메모리 전체를 DMA로 건드릴 수 있는 전제를 항상 기억해야 합니다.
보안 공격 표면과 위협 모델
DPDK는 커널 네트워크 스택을 우회하므로 커널이 제공하는 보안 계층(Netfilter, SELinux, seccomp, 네임스페이스)을 사용할 수 없습니다. 따라서 DPDK 애플리케이션 자체가 신뢰 경계(trust boundary)가 되며, 공격 표면을 명시적으로 인식하고 완화해야 합니다.
| 위협 | 완화 방법 | 운영 체크 |
|---|---|---|
| DMA 공격 | VFIO + IOMMU 필수 사용, UIO/no-IOMMU 금지 | dmesg | grep -i iommu "enabled" 확인 |
| 패킷 파싱 | 모든 수신 패킷 길이/헤더 검증, fuzzing 테스트 | AFL/libFuzzer로 패킷 파서 정기 테스트 |
| Hugepage 노출 | 전용 디렉터리 + 최소 권한, --in-memory 사용 | ls -la /dev/hugepages/ 권한 확인 |
| Telemetry 소켓 | 소켓 파일 권한 제한 (0600), 전용 그룹 | ls -la /var/run/dpdk/ |
| 공유 메모리 | secondary 프로세스를 신뢰된 바이너리로 제한 | hugetlbfs 파일 소유권과 접근 로그 |
| Side-channel | DPDK 코어에서 타 워크로드 격리, CPU 취약점(Vulnerability) 패치 적용 | lscpu vulnerability 항목 확인 |
| 펌웨어 공급망 | NIC 펌웨어 서명 검증(Signature Verification), 공식 소스만 사용 | ethtool -i 펌웨어 버전 추적 |
| DoS | mempool 여유 모니터링, flow rule 수 제한, rate-limit | rx_nombuf xstat 감시 |
| IOMMU 그룹 | ACS 지원 확인, 불필요 디바이스 분리 | ls /sys/kernel/iommu_groups/*/devices/ |
- 1단계 —
dpdk-testpmd로 기본 포트 동작 이해 (io/mac/macswap 모드) - 2단계 —
examples/l2fwd소스 분석 (가장 단순한 DPDK 앱, ~300줄) - 3단계 —
examples/l3fwd분석 (LPM/EM 라우팅, RSS 활용) - 4단계 —
rte_ring,rte_mempool내부 구현 분석 - 5단계 — PMD 소스 분석 (
drivers/net/ixgbe/또는drivers/net/i40e/) - 6단계 — Eventdev, Cryptodev 등 고급 프레임워크
- VFIO — 가상화 (KVM) — VFIO (Virtual Function I/O)
- OVS 연동 — Open vSwitch (OVS) 커널 데이터패스
- AF_XDP / SmartNIC — BPF/XDP — SmartNIC과 DPDK/AF_XDP 연동
- Hugepage 관리 — 메모리
- IOMMU / DMA — DMA
- SmartNIC / DPU — SmartNIC / DPU 기반 네트워크 가속
- Secure Boot — Secure Boot
참고자료
공식 문서
- DPDK Programmer's Guide — EAL, mempool, ring, PMD 아키텍처 공식 가이드
- DPDK API Reference — rte_mbuf, rte_ring, rte_ethdev 등 전체 API
- DPDK Release Notes — 버전별 신규 기능, 제거 API, ABI 변경
- DPDK Getting Started Guide for Linux — 빌드, hugepage 설정, 드라이버 바인딩
- EAL (Environment Abstraction Layer) — 코어 바인딩, 메모리 관리, 인터럽트 처리
- Poll Mode Driver (PMD) — PMD 아키텍처, RX/TX 버스트, 오프로드 플래그
성능 튜닝
- Lcore and Thread 관리 — 코어 배치, 서비스 코어, 스레드 모델
- Mempool Library — 메모리 풀 아키텍처, 캐시, hugepage 활용
- Flow Bifurcation — 커널/DPDK 트래픽 분리
- rte_flow API — 하드웨어 플로우 분류/필터링/오프로드 규칙
- testpmd — 패킷 포워딩 테스트 및 성능 벤치마크 도구
드라이버 및 디바이스
- Network Interface Controller Drivers — PMD별 지원 기능, 제한 사항 매트릭스
- mlx5 (NVIDIA ConnectX) PMD — ConnectX-5/6/7, BlueField 지원, Flow 오프로드
- ice (Intel E810) PMD — E810 시리즈, ADQ, 파이프라인 모드
- virtio PMD — 가상 환경 PMD, vhost-user, packed virtqueue
- Cryptodev Drivers — QAT, AESNI, OpenSSL 가속 드라이버
- Eventdev Drivers — 이벤트 기반 패킷 처리, 스케줄링
프레임워크 및 통합
- Vhost Library — vhost-user 프로토콜, virtio 링, VM 네트워킹
- Packet Capture Framework — dpdk-dumpcap, 패킷 캡처/분석
- Graph Library — 그래프 기반 패킷 처리 프레임워크
- GSO (Generic Segmentation Offload) Library — SW 세그먼테이션 오프로드
- GRO (Generic Receive Offload) Library — SW 패킷 병합
주요 참고 글
- DPDK 공식 사이트 — 뉴스, 이벤트, 다운로드
- No, really, DPDK is not just about performance (LWN) — DPDK 설계 철학
- DPDK 소스 코드 (GitHub 미러)
- DPDK Roadmap — 향후 릴리스 계획, 신규 기능
커널 소스 경로 (DPDK 연동)
drivers/uio/uio_pci_generic.c— UIO PCI 드라이버 (DPDK UIO 바인딩)drivers/vfio/pci/vfio_pci_core.c— VFIO PCI 코어 (DPDK VFIO 바인딩)mm/hugetlb.c— Hugepage 관리 (DPDK 메모리 할당)drivers/net/virtio/— virtio-net 커널 드라이버 (vhost-user 대비)drivers/vhost/net.c— vhost-net 커널 모듈 (vhost-user 비교 대상)net/core/dev.c— XDP 훅 포인트 (AF_XDP 연동 시)
관련 문서
- Open vSwitch (OVS) 커널 데이터패스 — OVS-DPDK, PMD 스레드, EMC/dpcls, 운영 호환 매트릭스
- VPP (FD.io) — 고성능 유저스페이스 패킷 처리 — FD.io VPP 벡터 패킷 처리, 그래프 노드 아키텍처, DPDK 통합, 플러그인, 커널
- AF_XDP (XDP Sockets) — Linux 커널 AF_XDP 소켓 — xsk_socket, UMEM, XDP_SHARED_
- NAPI (New API) - 네트워크 패킷 처리 — NAPI 인터럽트 완화 메커니즘: napi_struct, 폴링 버짓, GRO, 멀티큐
- VFIO & mdev (디바이스 패스스루) — IOMMU 그룹, vfio-pci, 패스스루 디버깅
- Secure Boot — VFIO/UIO 모듈 서명, 실험 장비 부팅 정책 점검
- NFQUEUE & DPI 엔진 통합 — nfnetlink_queue 내부 구조, libnetfilter_queue API, Sur
- 네트워크 스택 고급 — NAPI, GRO, RSS/RPS/RFS, XPS, aRFS, 멀티코어 네트워크 분산
- IPSec & xfrm — Linux 커널 xfrm 프레임워크와 IPSec — SA/SP 데이터베이스, ESP/AH/