기초와 아키텍처

FD.io VPP: 벡터 패킷(Packet) 처리 모델, 그래프 노드 아키텍처, 메모리 관리, 멀티스레딩, clib/vlib/vnet API 계층 등 기초와 아키텍처를 다룹니다.

전제 조건: TCP/IP 네트워크 스택과 IP 라우팅의 기본 개념을 먼저 이해하시기 바랍니다. 제어 평면과 데이터 평면이 분리되어 동작하므로, 규칙 갱신 시점과 실제 적용 지연(Latency)을 함께 확인해야 합니다.
일상 비유: 이 주제는 도로 표지판 갱신과 교통 흐름 제어(Flow Control)와 비슷합니다. 표지판을 바꿔도 차량 흐름 반영에는 시간이 필요하듯이, 경로/정책 갱신과 패킷 처리 시점은 분리해서 봐야 합니다.

핵심 요약

  • 벡터 패킷 처리 (Vector Packet Processing) — 패킷을 하나씩 처리하지 않고 벡터(배열) 단위로 묶어 동일한 노드를 통과시켜 I-캐시(Cache) 지역성과 분기 예측(Branch Prediction) 효율을 극대화합니다.
  • 그래프 노드(Graph Node) 아키텍처 — 패킷 처리 파이프라인(Pipeline)을 방향성 그래프로 구성합니다. 각 노드가 하나의 처리 단계(파싱, 분류, 포워딩 등)를 담당하며, 벡터가 노드 간을 이동합니다.
  • vlib 프레임워크 — VPP의 핵심 인프라 라이브러리로, 노드 스케줄링, 메인 루프, 타이머(Timer), 이벤트 로거 등을 제공합니다.
  • vlib_buffer_t — 패킷 데이터와 메타데이터(오프셋, 다음 노드 인덱스, 에러 상태)를 묶은 VPP의 핵심 버퍼(Buffer) 구조체로, 커널 sk_buff에 해당하는 역할을 합니다.
  • 메모리 관리 — mheap 기반 힙(Heap)과 버퍼 풀(Buffer Pool)의 2층 구조, NUMA 인식 할당, 워커별 독립 풀로 할당 경로를 단순화합니다.
  • 멀티스레딩 모델 — 메인 스레드(Thread) + N개 워커 스레드, 프레임 핸드오프(Frame Handoff)를 통한 워커 간 패킷 전달, 배리어 동기화(Barrier Sync)로 구성 변경을 안전하게 적용합니다.
  • clib 인프라 라이브러리clib_bihash(고성능 해시), clib_pool(인덱스 기반 객체 풀), clib_vec(동적 배열), 원자적(Atomic) 연산, 고해상도 시간 측정 등 저수준 프리미티브를 제공합니다.
  • API 계층 — clib / vlib / vnet / VCL / TLS 의 역할 분담과 접두사(clib_*, vlib_*, vnet_*) 명명 규칙으로 계층을 판별할 수 있습니다.

단계별 이해

  1. 벡터 처리 모델 이해 — 스칼라 처리와 벡터 처리의 차이를 파악합니다.

    전통적인 패킷별(scalar) 처리는 노드를 매번 전환하여 I-캐시 미스가 발생합니다. VPP는 동일 노드에서 수백 개 패킷을 연속 처리(벡터)하여 캐시 효율과 쿼드 루프 패턴을 통한 ILP(Instruction-Level Parallelism)를 확보합니다.

  2. 그래프 노드 구조 학습 — 패킷이 노드 그래프를 따라 이동하는 원리를 이해합니다.

    각 노드는 vlib_node_registration_t로 등록되며, 프레임(Frame)을 입력으로 받아 패킷을 처리하고 다음 노드 인덱스를 설정합니다. ip4-input → ip4-lookup → ip4-rewrite → interface-output 같은 기본 포워딩 경로와 Feature Arc 삽입 지점을 추적합니다.

  3. vlib_buffer_t 구조 파악 — 패킷 + 메타데이터 구조체를 해부합니다.

    버퍼 인덱스만 노드 간에 전달되며, 실제 데이터는 current_data 오프셋부터 시작합니다. next_buffer 체인으로 다중 세그먼트 패킷을, error·flags 필드로 노드 간 상태를 전달합니다.

  4. 메인 루프와 스레드 모델 이해 — 메인 스레드와 워커 스레드의 역할을 구분합니다.

    메인 스레드는 제어 평면(API, CLI)을, 워커 스레드는 데이터 평면을 담당합니다. 구성 변경 시 vlib_worker_thread_barrier_sync()로 워커를 일시 정지시켜 일관성을 보장하고, 워커 간 패킷 전달은 프레임 핸드오프 RPC로 수행합니다.

  5. clib 자료구조 숙지 — 실제 코드에서 자주 쓰는 프리미티브를 파악합니다.

    clib_bihash는 FIB, ACL, 세션 테이블의 기반이 되며 버킷·스플라이스 기반으로 락(Lock) 경합을 최소화합니다. clib_pool은 인덱스 기반 객체 할당으로 포인터 체이닝 없이 캐시 효율을 높이고, clib_vec은 동적 배열로 가변 길이 데이터를 다룹니다.

  6. API 계층 구분 — 접두사만 보고 어느 계층의 함수인지 판별합니다.

    clib_*은 순수 C 인프라(플랫폼 독립), vlib_*은 VPP 런타임(노드·버퍼·스레드), vnet_*은 네트워크 스택(인터페이스·FIB·ACL), vppcom_*/vls_*은 호스트 스택 진입점입니다. 함수 서명만 봐도 책임 경계가 드러나므로 코드 읽기 속도가 크게 빨라집니다.

  7. 다음 단계로 이동 — 기초를 다졌다면 다음 주제로 진행합니다.

    실제 패킷이 L2~L4 노드를 어떻게 통과하는지는 데이터 경로, 보안·터널링 기능은 보안과 터널링, 호스트 스택·TLS·QUIC은 호스트 스택 개요, 설치·CLI·플러그인 개발·운영·디버깅(Debugging)은 운영 · 플랫폼 · 확장에서 이어집니다.

관련 표준: FD.io VPP Architecture — 고성능 패킷 처리 프레임워크 아키텍처입니다. 관련 표준·규격은 본 페이지 하단의 참고자료 절을 확인하시기 바랍니다.
관련 기술: VPP는 커널 네트워크 스택을 우회(bypass)하여 사용자 공간(User Space)에서 패킷 입출력(I/O)을 처리하는 프레임워크입니다. 실제 데이터 경로에서는 버퍼 인덱스를 중심으로 패킷을 디스패치(Dispatch)하여 캐시 지역성, 분기 예측(Branch Prediction), 대기 시간(Latency) 측면에서 유리합니다. 커널의 TAP/TUN(drivers/net/tun.c), vhost(drivers/vhost/), AF_PACKET(net/packet/af_packet.c), AF_XDP(net/xdp/xsk.c), UIO/VFIO(drivers/uio/, drivers/vfio/) 서브시스템과 밀접하게 연관됩니다.

VPP 개요

📌 문서 기준 버전: 본 시리즈는 v26.02(2026-02-25 릴리스, 최신 stable)를 기준으로 작성되어 있습니다. 구버전 비교 대상은 v25.02이며, 두 릴리스 사이의 차이(25.06 / 25.10 / 26.02)는 호스트 스택 문서의 "v25.02 → v26.02 주요 변경 요약"에 한 표로 정리되어 있습니다. 각 페이지의 주요 변경 지점에는 인라인 🔄 박스로 "25.02 대비 변경"을 표기합니다.

VPP(Vector Packet Processing)는 Linux Foundation 산하 FD.io(Fast Data Input/Output) 프로젝트의 핵심 구성 요소로, 고성능 유저스페이스 패킷 처리 플랫폼입니다. 원래 Cisco에서 개발한 상용 코드를 2016년에 오픈소스로 공개하였으며, 현재 Linux Foundation이 호스팅하는 커뮤니티 주도 프로젝트입니다. 주요 기능은 메일링 리스트와 공개 리뷰를 거치는 패치(Patch) 시리즈를 통해 계속 확장됩니다.

VPP의 핵심 설계 철학은 벡터 패킷 처리(Vector Packet Processing)입니다. 전통적인 커널 네트워크 스택이 패킷 하나씩 전체 처리 경로를 순회하는 scalar 방식인 반면, VPP는 여러 패킷을 하나의 벡터(배열)로 묶어서 동일한 처리 노드를 일괄 통과시킵니다. 이를 통해 CPU의 명령어 캐시 히트율을 극대화하고, 분기 예측 효율을 높여 범용 x86 CPU에서도 초당 수천만 패킷(수십 Mpps)을 처리할 수 있습니다.

VPP 전체 아키텍처 빅 픽처 (Big Picture) Linux 커널 (제어 평면/관리용) 소켓 API / netlink TCP/IP 스택 netfilter / qdisc 커널 NIC 드라이버 → Scalar, 인터럽트 syscall/컨텍스트 스위치 ~1 Mpps/core 커널 통로 TAP / TUN AF_PACKET AF_XDP vhost-user linux-cp VPP 유저스페이스 프로세스 (kernel-bypass) 호스트 스택 & 애플리케이션 API VCL / VLS LD_PRELOAD Binary API / VAPI Stats Segment 패킷 처리 그래프 (Graph Node Architecture) 모든 노드는 한 번에 패킷 벡터(최대 256개)를 처리 → i-cache 핫, 분기 예측 효율 ↑ dpdk-input ethernet-input ip4-lookup ip4-rewrite tx 플러그인 노드가 Feature Arc에 삽입 — ACL · NAT · IPsec · SRv6 · TLS · QUIC · L2/L3 Feature Arc (ip4-unicast, ip6-unicast, device-input, ...) VLIB / VNET / CLIB 인프라 vlib_buffer_t buffer pool 메인 루프 워커 스레드 mheap / bihash frame handoff 배리어 동기화 NUMA-aware 유저스페이스 드라이버 / 고속 I/O DPDK PMD RDMA AVF / iavf VMXNET3 memif 폴링 모드 · Hugepage · zero-copy 하드웨어 / 외부 NIC (RX/TX ring) RSS · Flow Director QAT / Crypto HW SR-IOV / VF 배치 형태 vRouter / vSwitch CNF · 5G UPF IPsec VPN GW Calico/VPP CNI 성능 기준점 64B, 단일 코어: ~12 Mpps AES-GCM (QAT): ~40 Gbps TLS CPS: ~80K+ 한 프레임에서 보기: 커널을 우회해 벡터 단위로 그래프를 돌리고, 플러그인으로 기능을 조립하며, 제어 평면은 linux-cp/netlink로 커널과 동기화합니다.

벡터 패킷 처리 모델

Scalar 처리 vs Vector 처리

전통적인 커널 네트워크 스택의 Scalar 처리는 패킷 하나를 수신하면 L2 → L3 → L4 → 소켓(Socket) 전달까지 전체 경로를 완주한 후 다음 패킷을 처리합니다. 각 단계마다 다른 함수 코드가 I-cache에 로드되므로 캐시 미스가 빈번합니다.

VPP의 Vector 처리는 다수의 패킷(벡터)을 동시에 한 노드에서 처리한 후, 다음 노드로 벡터 전체를 넘깁니다. 동일한 코드가 수백 패킷에 반복 적용되므로 I-cache에 상주하며, 분기 예측이 안정되어 파이프라인 버블이 최소화됩니다.

Scalar 처리 vs Vector 처리 Scalar 처리 (커널) Pkt 1 L2 L3 L4 완료 Pkt 2 L2 L3 ... 순차 반복 I-cache miss ↕ Vector 처리 (VPP) Vector (256 pkts) Pkt 1, Pkt 2, ... ... Pkt N L2 Node 모든 패킷 일괄 L3 Node 모든 패킷 일괄 L4 Node 모든 패킷 일괄 완료 ✓ I-cache hot — 동일 코드 반복 실행 Scalar: 패킷마다 전체 경로 순회 → I-cache 냉각 | Vector: 노드별 일괄 처리 → I-cache 상주 VPP 기본 벡터 크기: VLIB_FRAME_SIZE = 256

캐시 효율성과 성능

VPP의 성능 우위는 CPU 마이크로아키텍처 수준의 최적화에서 비롯됩니다:

/* VPP 노드의 전형적인 dual-loop 패턴 (간략화) */
while (n_left_from >= 4) {
    /* 다음 2개 패킷 프리페치 */
    vlib_buffer_t *p2, *p3;
    p2 = vlib_get_buffer(vm, from[2]);
    p3 = vlib_get_buffer(vm, from[3]);
    CLIB_PREFETCH(p2->data, CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(p3->data, CLIB_CACHE_LINE_BYTES, LOAD);

    /* 현재 2개 패킷 처리 */
    vlib_buffer_t *b0, *b1;
    b0 = vlib_get_buffer(vm, from[0]);
    b1 = vlib_get_buffer(vm, from[1]);

    /* 패킷 처리 로직... */
    next0 = process_packet(b0);
    next1 = process_packet(b1);

    vlib_validate_buffer_enqueue_x2(vm, node, next_index,
                                     to_next, n_left_to_next,
                                     bi0, bi1, next0, next1);
    from += 2;
    n_left_from -= 2;
}
코드 설명
  • 3~8행 CLIB_PREFETCH로 다음 2개 패킷 데이터를 L1 캐시에 미리 로드합니다. 현재 패킷 처리 동안 메모리 지연를 숨기는 핵심 최적화입니다.
  • 11~13행 현재 2개 패킷을 동시에 가져와 처리합니다. CPU의 ILP(Instruction-Level Parallelism)를 활용하여 파이프라인 스톨을 최소화합니다.
  • 19~21행 vlib_validate_buffer_enqueue_x2는 2개 패킷의 next 노드가 동일한지 확인하고, 같으면 배치로 한 번에 큐잉합니다. 다르면 개별적으로 프레임을 분리합니다.
  • 22~23행 포인터를 2씩 전진시키며 남은 패킷 수를 감소합니다. 4개 미만이 남으면 루프를 종료하고 single-loop으로 전환합니다.

쿼드 루프 패턴과 SIMD 최적화

VPP 노드의 가장 최적화된 형태는 quad-loop 패턴입니다. 4개의 패킷을 동시에 처리하여 CPU의 ILP(Instruction-Level Parallelism)를 극대화하고, 프리페치 거리를 확보합니다. 컴파일러가 SIMD 벡터화를 적용할 가능성도 높아집니다.

/* quad-loop 패턴 — 최고 성능 경로 */
while (n_left_from >= 8) {
    /* 8개 앞 패킷 프리페치 (4개 처리 동안 캐시 로드) */
    CLIB_PREFETCH(vlib_get_buffer(vm, from[4])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[5])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[6])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);
    CLIB_PREFETCH(vlib_get_buffer(vm, from[7])->data,
                  CLIB_CACHE_LINE_BYTES, LOAD);

    /* 현재 4개 패킷 일괄 처리 */
    vlib_buffer_t *b0, *b1, *b2, *b3;
    b0 = vlib_get_buffer(vm, from[0]);
    b1 = vlib_get_buffer(vm, from[1]);
    b2 = vlib_get_buffer(vm, from[2]);
    b3 = vlib_get_buffer(vm, from[3]);

    next0 = process_packet(b0);
    next1 = process_packet(b1);
    next2 = process_packet(b2);
    next3 = process_packet(b3);

    vlib_validate_buffer_enqueue_x4(vm, node, next_index,
        to_next, n_left_to_next,
        bi0, bi1, bi2, bi3, next0, next1, next2, next3);
    from += 4;
    n_left_from -= 4;
}

/* single-loop: 나머지 패킷 처리 (1~3개) */
while (n_left_from > 0) {
    vlib_buffer_t *b0 = vlib_get_buffer(vm, from[0]);
    next0 = process_packet(b0);
    vlib_validate_buffer_enqueue_x1(vm, node, next_index,
        to_next, n_left_to_next, bi0, next0);
    from += 1;
    n_left_from -= 1;
}
코드 설명
  • 3~11행 8개 앞 패킷을 프리페치하여 4개 처리 동안 캐시 워밍을 완료합니다. 프리페치 거리가 dual-loop(2)보다 2배 길어 메모리 지연 은닉 효과가 극대화됩니다.
  • 14~18행 4개 패킷을 동시에 가져와 독립적으로 처리합니다. 컴파일러가 SIMD 벡터화를 적용하기 쉬운 구조이며, 현대 CPU의 슈퍼스칼라 파이프라인을 최대한 활용합니다.
  • 20~26행 vlib_validate_buffer_enqueue_x4는 4개 패킷의 next 인덱스를 비교하여 동일 경로인 패킷을 배치 큐잉합니다. 경로가 분산되면 개별 처리로 폴백합니다.
  • 29~36행 잔여 패킷(1~3개)을 single-loop으로 처리합니다. quad-loop 진입 조건(8개 이상)을 충족하지 못하는 경우의 보완 경로입니다.
루프 패턴패킷/반복프리페치 거리ILP 활용적합한 상황
Single-loop10~1낮음복잡한 상태 기계, 잔여 패킷 처리
Dual-loop22중간중간 복잡도 노드, 분기 많은 처리
Quad-loop44~8높음단순 처리 (lookup, rewrite, forward)
패턴 선택: 노드의 처리 로직이 단순할수록 quad-loop이 유리합니다. ip4-lookup, l2-fwd 같은 핫 패스 노드는 quad-loop을 사용하고, 복잡한 상태 머신이 필요한 tcp-input은 single-loop으로 구현합니다.

벡터 크기 튜닝과 영향

VLIB_FRAME_SIZE는 한 번에 노드에 전달되는 최대 패킷 수를 결정합니다. 기본값 256은 대부분의 상황에서 최적이지만, 특수 워크로드에서는 조정이 필요할 수 있습니다.

벡터 크기I-cache 효과처리량(Throughput)테일 지연메모리 사용
64보통낮음최소 (좋음)낮음
128좋음중간낮음중간
256 (기본)최적높음중간중간
512최적최고높음 (나쁨)높음
벡터 크기와 테일 지연: 벡터가 클수록 처리량은 높아지지만, 벡터의 마지막 패킷은 첫 패킷이 노드에 도착한 후 전체 벡터 처리가 끝날 때까지 대기합니다. VoIP/실시간(Real-time) 트래픽이 중요한 환경에서는 벡터 크기를 줄여 테일 지연를 낮출 수 있습니다.

운영 환경 벡터 크기 튜닝 실전

기본값 256을 변경하려면 VLIB_FRAME_SIZE 매크로를 재정의하고 VPP를 재빌드해야 합니다(build-data/platforms/의 CMake 옵션). 런타임 변경은 불가능하지만, 입력 노드의 버스트 크기(dpdk-inputVLIB_FRAME_SIZE 버스트 읽기 개수)핸드오프 큐 깊이(frame-queue-nthreads)startup.conf에서 조정할 수 있습니다. 세 파라미터를 조합해 실제 워크로드에 맞춰야 합니다.

# startup.conf — 벡터·핸드오프 관련 핵심 파라미터
buffers {
  buffers-per-numa 262144   # 워커 수 × VLIB_FRAME_SIZE × 4 이상
  default data-size 2048
}

dpdk {
  dev 0000:04:00.0 {
    num-rx-queues 4
    num-rx-desc 2048        # NIC 링 깊이 — 벡터보다 충분히 커야
    num-tx-desc 2048
  }
}

cpu {
  main-core 0
  corelist-workers 1,2,3,4
}

# handoff 큐 — 워커 간 재분배 시 벡터 버퍼링
plugins {
  plugin dpdk_plugin.so { enable }
}
# DPDK 입력측 burst 크기 변경
vpp# set dpdk interface placement TenGigabitEthernet4/0/0 queue 0 thread 1
워크로드권장 벡터num-rx-deschandoff 깊이목표 p99 지연
VoIP / 실시간 게임6451264< 80μs
일반 라우터 / NAT256 (기본)2048256< 500μs
CDN 엣지 / 대용량 TCP2564096512< 2ms
배치 암호화 (IPsec bulk)5124096512관심 없음

튜닝 절차:show runtimevectors/call이 지속적으로 VLIB_FRAME_SIZE 근처(예: 240~256)이면 포화 상태이므로 num-rx-desc를 늘려 버스트 흡수력을 확보합니다. ② 반대로 평균 벡터가 2~4 수준이면 트래픽이 적은 것이므로 벡터를 늘릴 이유가 없습니다. ③ p99 지연이 목표를 초과하면 벡터를 줄이고 손실 처리량을 워커 추가로 보상합니다. 벡터와 지연의 트레이드오프는 근본적이므로 "양쪽 모두 좋게" 만들 수는 없습니다.

VPP 아키텍처

VPP의 아키텍처는 이 장에서 8개의 핵심 주제로 전개됩니다. 패킷 처리 그래프(Graph Node)로 노드 간 벡터 흐름을 구성하고, vlib_buffer_t로 패킷과 메타데이터를 함께 다룹니다. 메인 루프(Main Loop)가 워커 스레드의 폴링과 노드 디스패치를 담당하며, Feature Arc 메커니즘으로 기존 그래프에 기능을 침습 없이 삽입합니다. 이어서 패킷 1개가 RX 디스크립터에서 TX 큐까지 가는 실제 경로를 따라가고, L2 → L3 → L4 통합 그래프 흐름으로 계층별 상호작용을 확인합니다. 마지막으로 핵심 그래프 노드 내부 구현추가 그래프 노드 내부 구현에서 개별 노드 코드를 분석합니다. 먼저 아래 다이어그램으로 전체 그림을 조망한 뒤, 이어지는 각 절에서 주제별 상세를 차례로 펼칩니다.

VPP 패킷 처리 그래프 아키텍처 NIC DPDK/AF_XDP dpdk-input INPUT node ethernet-input L2 분류 ip4-input IPv4 처리 ip4-lookup FIB 검색 ip4-rewrite MAC 재작성 NAT44 ACL ip6-input IPv6 처리 l2-input L2 브릿징 l2-fwd MAC 학습/포워딩 interface-output TX queue 범례: INPUT/INTERNAL 노드 L3 처리 노드 기능 플러그인 (ACL, NAT...)

패킷 처리 그래프 (Graph Node Architecture)

VPP의 핵심은 방향성 비순환 그래프(DAG) 기반 패킷 처리 엔진입니다. 각 처리 단계는 노드(Node)로 구현되며, 노드 간 연결은 arc로 표현됩니다. 패킷(벡터)은 노드에서 노드로 arc를 따라 이동하며, 각 노드는 패킷의 다음 목적지 노드를 결정합니다.

/* 그래프 노드 등록 예제 — src/vnet/ethernet/node.c */
VLIB_REGISTER_NODE (ethernet_input_node) = {
    .function = ethernet_input,          /* 노드 처리 함수 */
    .name = "ethernet-input",
    .vector_size = sizeof(u32),          /* 벡터 원소 크기 (버퍼 인덱스) */
    .n_errors = ETHERNET_N_ERROR,
    .error_strings = ethernet_error_strings,
    .n_next_nodes = ETHERNET_INPUT_N_NEXT,
    .next_nodes = {
        [ETHERNET_INPUT_NEXT_IP4_INPUT] = "ip4-input",
        [ETHERNET_INPUT_NEXT_IP6_INPUT] = "ip6-input",
        [ETHERNET_INPUT_NEXT_L2_INPUT]  = "l2-input",
        [ETHERNET_INPUT_NEXT_DROP]      = "error-drop",
    },
};
코드 설명
  • 2행 VLIB_REGISTER_NODE 매크로는 정적 초기화자로 노드를 그래프에 등록합니다. 컴파일 타임에 노드 메타데이터가 확정되어 런타임 등록 비용이 없습니다.
  • 3행 .function 필드는 노드의 벡터 처리 함수를 지정합니다. 디스패처가 프레임을 전달할 때 이 함수를 호출하여 패킷 배치를 처리합니다.
  • 5행 vector_size = sizeof(u32)는 벡터 원소가 32비트 버퍼 인덱스임을 선언합니다. 64비트 포인터 대신 인덱스를 사용하여 프레임 크기를 절반으로 줄이고 캐시 효율을 높입니다.
  • 8~13행 next_nodes 배열은 이 노드의 출력 엣지를 정의합니다. 각 엣지에 문자열 이름을 매핑(Mapping)하여, 그래프 초기화 시 이름 기반으로 노드 인덱스가 자동 해석됩니다.

노드 유형 (INTERNAL, INPUT, PROCESS, PRE_INPUT)

노드 유형설명실행 방식대표 예
VLIB_NODE_TYPE_INTERNAL그래프 중간 처리 노드이전 노드가 벡터를 전달할 때 실행ip4-input, ip4-lookup, l2-fwd
VLIB_NODE_TYPE_INPUT패킷 입력 노드매 메인 루프마다 폴링(Polling)dpdk-input, af-packet-input
VLIB_NODE_TYPE_PROCESS코루틴 기반 프로세스(Process)이벤트/타이머(Timer) 기반 실행dhcp-client-process, arp-reply
VLIB_NODE_TYPE_PRE_INPUT입력 전 처리INPUT 노드보다 먼저 실행epoll-input (이벤트 폴링)

벡터 크기와 처리 흐름

VPP의 벡터 프레임 크기는 VLIB_FRAME_SIZE로 정의되며, 기본값은 256입니다. 메인 루프(vlib_main_loop)는 다음 순서로 동작합니다:

  1. PRE_INPUT 노드 실행 (epoll 이벤트 수집 등)
  2. INPUT 노드 실행 (dpdk-input이 NIC에서 패킷 수집 → 벡터 생성)
  3. INTERNAL 노드: 보류 중인 프레임이 있는 노드를 순회하며 벡터 처리
  4. PROCESS 노드: 타이머/이벤트 만료된 코루틴 실행
  5. 1번으로 돌아감 (busy-loop 또는 sleep)
성능 팁: VPP의 메인 루프는 기본적으로 busy-polling이며, 패킷이 없을 때도 CPU를 100% 사용합니다. startup.confpoll-sleep-usec 옵션으로 유휴 시 절전 모드(Suspend)를 활성화할 수 있지만, 지연가 증가할 수 있습니다.

메인 루프 내부 동작

VPP의 vlib_main_loop()는 모든 패킷 처리의 진입점(Entry Point)입니다. 단일 스레드 내에서 4단계를 반복 실행하며, 각 단계의 순서가 성능에 직접 영향을 미칩니다.

VPP 메인 루프 (vlib_main_loop) 흐름 1. PRE_INPUT 노드 epoll, timer wheel 체크 2. INPUT 노드 폴링 dpdk-input → 벡터 생성 3. INTERNAL 노드 디스패치 보류 프레임이 있는 노드를 순회 ethernet-input → ip4-input → ip4-lookup → ... 4. PROCESS 노드 코루틴: ARP, DHCP, BGP 등 poll-sleep-usec > 0이면 sleep 단계별 소요 시간 PRE_INPUT: ~0.1 μs INPUT: ~2~5 μs (NIC 폴링) INTERNAL: 벡터 크기에 비례 PROCESS: 이벤트 시에만 실행 busy-loop 반복 루프 1회 = 1 dispatch cycle | show runtime의 Clocks/Call = INTERNAL 노드 기준
/* vlib_main_loop 의사코드 (src/vlib/main.c) */
static void
vlib_main_loop (vlib_main_t *vm) {
    while (1) {
        /* 1단계: PRE_INPUT — epoll, 시그널 체크 */
        vlib_node_runtime_update_stats(vm);
        dispatch_pre_input_nodes(vm);

        /* 2단계: INPUT — NIC 폴링, 벡터 수집 */
        dispatch_input_nodes(vm);
        /* dpdk-input이 NIC RX 링에서 패킷 배치를 가져와
           ethernet-input으로 향하는 프레임을 생성 */

        /* 3단계: INTERNAL — 보류 프레임 디스패치 */
        for (pending_frame in vm->pending_frames) {
            node = pending_frame->node;
            frame = pending_frame->frame;
            node->function(vm, node, frame);
            /* 노드가 새 프레임을 생성하면 pending 추가 */
        }

        /* 4단계: PROCESS — 코루틴 타이머/이벤트 체크 */
        dispatch_process_nodes(vm);

        /* 유휴 시 선택적 sleep */
        if (no_work && vm->poll_sleep_usec)
            usleep(vm->poll_sleep_usec);
    }
}
코드 설명
  • 5~7행 PRE_INPUT 단계에서 epoll 이벤트와 시그널(Signal)을 체크합니다. CLI 소켓, API 연결, 타이머 휠 등 비패킷 이벤트를 먼저 처리하여 제어 평면 응답성을 보장합니다.
  • 9~12행 INPUT 단계에서 모든 입력 노드(dpdk-input 등)가 NIC를 폴링합니다. 수집된 패킷은 프레임으로 묶여 pending_frames에 등록됩니다.
  • 14~19행 INTERNAL 단계는 보류 중인 프레임을 순회하며 노드 함수를 호출합니다. 한 노드의 출력이 다른 노드의 입력이 되어, 그래프 전체가 깊이 우선으로 처리됩니다.
  • 25~27행 poll_sleep_usec가 설정되면 유휴 상태(Idle State)에서 usleep()으로 CPU를 양보(Yield)합니다. 기본값은 busy-polling이므로 CPU 사용률이 100%이지만, 지연가 가장 낮습니다.
루프 단계실행 노드 유형실행 빈도주요 동작
PRE_INPUTVLIB_NODE_TYPE_PRE_INPUT매 사이클epoll 이벤트 수집, 타이머 휠 갱신
INPUTVLIB_NODE_TYPE_INPUT매 사이클NIC 폴링, 패킷 벡터 생성
INTERNALVLIB_NODE_TYPE_INTERNAL프레임 존재 시그래프 노드 순회, 패킷 처리
PROCESSVLIB_NODE_TYPE_PROCESS이벤트/타이머 시코루틴 재개 (ARP, DHCP 등)

Feature Arc 메커니즘

Feature Arc는 VPP의 핵심 확장 메커니즘으로, 패킷 처리 경로에 기능을 동적으로 삽입하거나 제거할 수 있게 합니다. 커널의 Netfilter 후크(Hook) 체인과 유사한 개념이지만, 컴파일 타임이 아닌 런타임에 노드 체인을 재구성합니다.

Feature Arc: ip4-unicast 경로 동적 삽입 기본 (Feature 없음): ip4-input ip4-lookup ip4-rewrite Feature Arc 활성 (ACL + NAT + IPsec 삽입): ip4-input ACL ip4-inacl NAT44 nat44-in2out IPsec ipsec-input ip4-lookup ip4-rewrite Feature Arc은 인터페이스별로 활성화 → 패킷의 current_config_index가 체인을 따라 이동 주요 내장 Feature Arc: ip4-unicast | ip4-multicast | ip4-output | ip6-unicast | ip6-multicast | ip6-output ethernet-output | interface-output | mpls-input | mpls-output | nsh-output
/* Feature Arc에 노드 등록 (VNET_FEATURE_INIT 매크로) */
VNET_FEATURE_INIT (my_acl_feature, static) = {
    .arc_name = "ip4-unicast",       /* 소속 Feature Arc */
    .node_name = "my-acl-node",       /* 노드 이름 */
    .runs_before = VNET_FEATURES("ip4-lookup"),  /* 이 노드보다 먼저 실행 */
    .runs_after  = VNET_FEATURES("ip4-input-no-checksum"),
};

/* 런타임에 인터페이스별 Feature 활성화 */
vpp# set interface feature GigabitEthernet0/8/0 my-acl-node arc ip4-unicast

/* Feature Arc 상태 확인 */
vpp# show features verbose
코드 설명
  • 2행 VNET_FEATURE_INIT 매크로는 컴파일 타임에 노드를 Feature Arc에 등록합니다. static 키워드로 링커(Linker) 섹션에 배치되어 자동 수집됩니다.
  • 3행 arc_name은 이 feature가 소속될 arc를 지정합니다. ip4-unicast arc는 ip4-input에서 시작하여 ip4-lookup으로 끝나는 체인입니다.
  • 5~6행 runs_before/runs_after는 위상 정렬(topological sort) 제약을 정의합니다. 런타임에 이 제약을 기반으로 feature 체인의 실행 순서가 자동으로 결정됩니다.
  • 9행 Feature 활성화는 인터페이스별로 수행됩니다. 활성화하면 해당 인터페이스의 패킷만 이 노드를 거치며, 다른 인터페이스는 영향을 받지 않습니다.
Feature Arc시작 노드기본 종착 노드용도
ip4-unicastip4-inputip4-lookupIPv4 유니캐스트 인입 처리
ip4-outputip4-rewriteinterface-outputIPv4 송출 처리
ip4-multicastip4-inputip4-mfib-forward-lookupIPv4 멀티캐스트
ip6-unicastip6-inputip6-lookupIPv6 유니캐스트 인입
ethernet-outputadj-l2-midchaininterface-output이더넷 출력 경로
interface-outputinterface-outputinterface-tx최종 인터페이스 출력

패킷 1개가 RX 디스크립터에서 TX 큐까지 가는 실제 경로

VPP를 처음 접할 때 가장 헷갈리는 지점은 "패킷이 정확히 어느 시점에 vlib_buffer_t가 되고, 어느 시점에 next 인덱스가 바뀌며, 어느 시점에 실제 NIC 송신 큐에 들어가는가"입니다. 아래 흐름을 기준으로 보면, 추상적인 그래프 모델과 실제 구현 객체가 한 번에 연결됩니다.

패킷 1개가 VPP 내부를 이동하는 실제 구현 경로 NIC RX Ring descriptor DMA 완료 dpdk-input burst 수집 mbuf → buffer index frame 생성 vlib_buffer_t current_data current_length opaque / error ethernet-input EtherType 분류 VLAN 파싱 next = ip4-input ip4-input / Feature TTL / checksum 검사 ACL / NAT / IPsec next = ip4-lookup ip4-lookup mtrie LPM 검색 adj_index 계산 fib_index 결정 ip4-rewrite L2 헤더 재작성 MAC / VLAN push next = interface-output interface-output per-thread TX frame queue 선택 burst 송신 준비 NIC TX Ring descriptor 기록 doorbell 하드웨어 송신 핵심은 "패킷 복사"가 아니라 "버퍼 인덱스와 메타데이터 이동"입니다. 노드는 데이터를 직접 옮기기보다 next 인덱스와 opaque 필드를 갱신합니다. 따라서 디버깅도 payload 자체보다 current_data, adj_index, error, tx_sw_if_index가 어떻게 바뀌는지 추적해야 합니다.
  1. NIC가 RX 디스크립터를 채웁니다. 하드웨어는 DMA로 패킷을 메모리에 써 두고, RX 링의 소유권을 소프트웨어 쪽으로 넘깁니다.
  2. dpdk-input이 burst 단위로 회수합니다. 이 시점에 성능은 rx_burst 크기, 디스크립터 수, NUMA 일치 여부에 크게 좌우됩니다.
  3. 패킷은 vlib_buffer_t로 표현됩니다. 실제 데이터는 이미 hugepage 메모리에 존재하고, 이후 그래프는 주로 버퍼 인덱스 배열을 이동시킵니다.
  4. ethernet-input이 첫 분류를 수행합니다. EtherType과 VLAN을 읽고 ip4-input, ip6-input, l2-input 중 어느 경로로 보낼지 결정합니다.
  5. ip4-input과 Feature Arc가 정책을 적용합니다. ACL, NAT, IPsec, 사용자 플러그인이 이 구간에 삽입되며, 패킷을 드롭하거나 메타데이터를 보강할 수 있습니다.
  6. ip4-lookup이 FIB를 조회합니다. 결과는 adjacency 인덱스로 정리되어 vnet_buffer(b0) 계열 메타데이터에 저장됩니다.
  7. ip4-rewriteinterface-output이 송신을 마무리합니다. 최종 L2 헤더를 재작성한 뒤 per-thread TX 프레임에 넣고, 마지막에 NIC TX 링으로 burst 송신합니다.
단계핵심 객체주로 바뀌는 값확인 명령
RX 수집RX descriptor, mbufburst 길이, queue 인덱스show hardware-interfaces
그래프 진입vlib_frame_tn_vectors, cached_next_indexshow runtime
L2 분류ethernet_header_tEtherType, VLAN 정보trace add ethernet-input
정책 적용current_config_indexfeature 체인 위치, drop 여부show features verbose
FIB 조회mtrie, adjacencyadj_index, fib_indexshow ip fib
송신 준비rewrite header, TX frametx_sw_if_index, rewrite 길이show trace
실무 관찰 포인트: VPP는 데이터 복사를 줄이는 대신 "상태 전달"을 많이 합니다. 패킷이 예상과 다른 인터페이스로 나간다면 payload 자체보다 adj_index, current_config_index, b0->error, tx_sw_if_index를 먼저 확인하는 편이 빠릅니다.

L2 → L3 → L4 통합 그래프 흐름

앞 절까지는 RX 디스크립터부터 TX 링까지의 수평 경로와 개별 노드를 따로 살펴보았습니다. 이 다이어그램은 동일한 경로를 OSI 계층(L1~L7)에 맞춰 세로로 재배치하여, 프레임이 어느 계층에서 어느 VPP 그래프 노드의 책임으로 처리되는지를 한눈에 보여 줍니다. L2 브릿징 경로와 L3 라우팅 경로, 그리고 VPP 호스트 스택(Host Stack)의 L4/세션(Session) 계층까지를 하나의 그림으로 잇습니다.

VPP 그래프 노드의 L2 → L3 → L4 계층별 통합 흐름 L1 / Driver L2 / Ethernet L2 Bridging L3 / IP L4 / UDP·TCP Session / App dpdk-input af-packet-input memif-input af-xdp-input vlib_buffer_t ethernet-input sparse vector로 EtherType O(1) 분류 VLAN tag pop · subinterface 매칭 interface.l2==1 → 브릿지 EtherType=0x0800 / 0x86DD → L3 ARP / LLDP / 기타 l2-input BD 매핑 · split-horizon l2-learn SMAC→L2 FIB 학습 l2-fwd DMAC 해시 lookup l2-flood BUM 복제 l2-output → tx same-BD egress Feature Arc (device-input, l2-input): MACsec · ACL-L2 · VXLAN/GENEVE decap · mirror ip4-input checksum · TTL ip6-input 병렬 ip4-lookup mtrie 8-8-8-8 LPM DPO 체인 선택 ip4-rewrite adj: MAC/TTL/MTU → interface-output ip4-local ip4-punt ip4-mcast error-drop Feature Arc (ip4-unicast): ACL · NAT44 · IPsec Policy · Policer · SRv6 · 커스텀 플러그인 udp-local / udp46-input UDP 포트 → listener VXLAN/GTP/QUIC decap 진입 tcp4-input-nolookup 4-tuple → tcp_connection_t 상태 머신 디스패치 tcp4-established ACK · SACK · CC · RTO (SYN-sent / LISTEN 등 병렬) session-queue 워커 바인딩 · 이벤트 큐 RX FIFO에 payload 적재 Session Layer session_t · 공유 메모리 FIFO 이벤트 큐 (mq) TLS / QUIC Engine OpenSSL · picotls · Quicly Record 경계 · 핸드셰이크 VCL / VLS / LD_PRELOAD vppcom_session_* · POSIX shim 애플리케이션 (nginx / Envoy 등) 실선은 핫 패스 전이, 점선은 조건부 분기와 드롭/punt 경로입니다. 동일한 vlib_buffer_t가 계층을 가로질러 전달되며 next_index만 갱신됩니다. L2 브릿징 경로(상단 녹색)와 L3 라우팅 경로(중단)는 sw_interface의 L2/L3 모드 플래그로 양자택일됩니다.
다이어그램 읽는 법: 이 그림은 세 가지 관점을 하나로 겹쳐 둔 것입니다. ① 가로 방향은 처리 진행 순서(드라이버 → TX/Session) ② 세로 방향은 OSI 계층 ③ 점선 상자는 Feature Arc 삽입 지점입니다. 특정 플러그인(ACL, NAT44, IPsec 등)이 어느 계층의 Feature Arc에 걸려 있는지를 파악하면, show features verbose 출력과 실제 패킷 경로를 머릿속에서 빠르게 맞추어 볼 수 있습니다.

메모리 관리(Memory Management) 아키텍처

힙(Heap)/mheap 관리

VPP는 여러 개의 독립적인 메모리 영역을 관리합니다. 모든 영역은 hugepage 위에 할당되며, clib_mem 인프라가 NUMA 인식 할당을 제공합니다.

메모리 영역기본 크기용도설정 위치
Main Heap1 GBFIB, 세션 테이블, 일반 할당memory { main-heap-size 2G }
Buffer PoolsNUMA별 개별패킷 데이터 (vlib_buffer_t + data)buffers { buffers-per-numa 32768 }
API Segment64 MBBinary API 공유 메모리api-segment { global-size 256M }
Stats Segment32 MB통계 카운터 공유 메모리statseg { size 64M }
# VPP 메모리 사용량 확인
vpp# show memory main-heap
Thread 0 vpp_main
  base 0x7f8a00000000, size 1g, locked, unmap-on-destroy
    page stats: page-size 2M, total 512, mapped 310, not-mapped 202
    total: 1073741824, used: 536870912, free: 536870912

vpp# show memory api-segment
  Shared API segment:
    base 0x7f8900000000, size 64m

# NUMA별 메모리 분포
vpp# show memory numa

show memory verbose 출력 해석

운영 중 메모리 누수나 단편화(Fragmentation)가 의심될 때 가장 먼저 실행하는 명령이 show memory main-heap verbose입니다. 출력이 길고 무엇을 봐야 하는지 헷갈리기 쉬우므로, 아래 표의 네 가지 수치만 집중해서 읽으면 95%의 상황을 판정할 수 있습니다.

vpp# show memory main-heap verbose
Thread 0 vpp_main
  base 0x7f8a00000000, size 2g, locked, unmap-on-destroy
    page stats: page-size 2M, total 1024, mapped 820, not-mapped 204
    total: 2147483648, used: 1288490188, free: 858993460, trimmable: 156823552
    free chunks 4821 free fastbin blks 12
    max total allocated 1932735283
    chunk headers: 4821
    largest contiguous free block: 4456448            ← ★ 핵심 ①
    fragmentation ratio: 18.3%                        ← ★ 핵심 ②
    max block size allocated: 268435456
    small blocks (< 256B) allocated: 892341          ← ★ 핵심 ③
    allocation histogram:
      [0,64)     : 412385
      [64,256)   : 479956
      [256,1024) : 128234
      [1024,4K)  : 45612
      [4K,16K)   : 8723
      [16K,64K)  : 1204
      [64K,1M)   : 89
      [1M+)      : 12                                 ← ★ 핵심 ④
지표건강 기준경고 기준해석
① largest contiguous free block> 총 free의 30%< 1MB작으면 큰 할당(FIB 확장, bihash 리사이즈)이 실패할 수 있음
② fragmentation ratio< 20%> 40%높으면 reheal 또는 reboot 고려
③ small blocks allocated증가 추세시간에 따라 꾸준히 증가 = 메모리 누수 의심
④ 1M+ 버킷안정급증세션/FIB 폭주의 선행 지표

mheap 단편화와 재정리(Reheal) 전략

VPP의 mheap(Martin Heap, clib 인프라)은 dlmalloc 계열이지만 coalescing(인접 free 블록 병합)이 매우 공격적으로 동작합니다. 그럼에도 장기 운영 시 단편화가 누적되는 이유는 bihash 페이지 확장·수축이 대형 연속 블록을 요구하는데, 중간에 작은 장기 객체(예: 영구 FIB 엔트리)가 끼어 있어 병합이 차단되기 때문입니다. 이 현상은 수일~수주 단위로 진행되며, 어느 순간 "show errors에서 bihash add failed가 폭증하고, 메모리는 절반 이상 free인데 할당이 실패"하는 형태로 표면화됩니다.

대응은 세 단계로 나뉩니다:

  1. 즉시 완화clib_mem_trim()에 해당하는 show memory main-heap trim(일부 빌드) 또는 memory trace off; memory trace on으로 fastbin을 비우고 상단 블록을 OS에 반환합니다. 단편화는 감소하지 않지만 trimmable 영역이 확보됩니다.
  2. 구조적 해결startup.conf에서 main-heap-size를 평소 사용량의 2.5~3배로 설정하고, 대형 bihash는 고정 크기(fixed pre-size)로 미리 할당해 동적 리사이즈를 회피합니다. 예: nat { translation hash buckets 524288 }로 처음부터 큰 크기로 생성.
  3. 주기적 Reheal — 무중단이 필수가 아닌 환경에서는 주 1회 blue-green 재시작(Reboot)으로 mheap을 초기화합니다. 상태(세션·FIB)는 외부 저장소 → 재기동 후 재주입 패턴으로 복원합니다. LCP 구성에서는 리눅스 라우팅 테이블이 진실의 원천이므로 이 방식이 안전합니다.
# 단편화 진단 주간 스크립트
#!/bin/bash
LARGEST=$(vppctl show memory main-heap verbose | \
          awk '/largest contiguous/ {print $NF}')
FREE=$(vppctl show memory main-heap | awk '/free:/ {print $6}')
RATIO=$(( LARGEST * 100 / FREE ))

if [ $RATIO -lt 30 ]; then
  logger -t vpp-mem "Fragmentation warning: largest/free=$RATIO%"
  # 필요 시 알림 시스템(PagerDuty, Slack) 연동
fi

# 메모리 추적(allocation profiling) 활성화 — 누수 추적
vpp# memory trace on main-heap
... 수 분 운영 ...
vpp# show memory main-heap verbose | head -50
vpp# memory trace off main-heap

진단 주의점: memory trace on은 모든 할당에 콜 스택(call stack)을 기록하므로 성능 오버헤드가 20~40% 증가합니다. 운영 환경에서는 짧게(수 분) 활성화한 뒤 반드시 끄고, 가능하면 카나리 인스턴스에서만 수행하세요. 또한 main-heap-size를 늘리면 hugepage 요구량도 같이 증가하므로 hugepage 예약을 먼저 확인해야 합니다.

버퍼 풀 아키텍처

VPP 버퍼 풀은 고성능 패킷 처리의 핵심입니다. NUMA 노드별로 독립된 풀을 유지하여 원격 메모리 접근을 방지하고, 프리 리스트와 재활용(Recycling) 메커니즘으로 할당/해제 오버헤드를 최소화합니다.

VPP 버퍼 풀 아키텍처 (per-NUMA) NUMA Node 0 Hugepages (2MB/1GB 페이지) Buffer Pool 0 buf 0 buf 1 buf 2 ... buf N Free List (LIFO) Thread 0 캐시 32개 버퍼 로컬 Thread 1 캐시 32개 버퍼 로컬 NUMA Node 1 Hugepages (2MB/1GB 페이지) Buffer Pool 1 buf 0 buf 1 buf 2 ... buf N Free List (LIFO) Thread 2 캐시 32개 버퍼 로컬 Thread 3 캐시 32개 버퍼 로컬 버퍼 인덱스(u32)로 접근 — 포인터 대신 인덱스를 사용하여 32비트로 버퍼 참조 (메모리 절약)
/* 버퍼 풀 생성과 생명주기 */

/* 1. 시작 시: hugepage 위에 버퍼 풀 사전 할당 */
/*    buffers { buffers-per-numa 32768 } → NUMA별 32768개 버퍼 */

/* 2. INPUT 노드: 버퍼 할당 */
u32 n_alloc = vlib_buffer_alloc(vm, buffer_indices, n_buffers);
/* 스레드 로컬 캐시에서 먼저 할당 → 부족 시 글로벌 free list에서 보충 */

/* 3. 패킷 처리 중: 버퍼 체인 (점보 프레임 등) */
if (b->flags & VLIB_BUFFER_NEXT_PRESENT)
    next_b = vlib_get_buffer(vm, b->next_buffer);

/* 4. 처리 완료: 버퍼 해제 (free list 반환) */
vlib_buffer_free(vm, buffer_indices, n_buffers);
/* 로컬 캐시가 가득 차면 글로벌 free list로 반환 */
포인터 대신 인덱스(u32): VPP는 버퍼를 64비트 포인터 대신 32비트 인덱스로 참조합니다. 벡터 프레임의 각 원소가 4바이트(u32)이므로, 256개 패킷 벡터가 1KB만 차지합니다. 포인터라면 2KB가 필요합니다. 이 설계는 캐시 라인(Cache Line) 효율을 높이고, 프레임 복사/이동 비용을 절반으로 줄입니다.

멀티스레딩 아키텍처

워커 스레드 모델

VPP는 메인 스레드 + N개 워커 스레드 모델을 사용합니다. 메인 스레드는 CLI/API 처리와 제어 평면을 담당하고, 워커 스레드는 데이터 평면(패킷 처리)에 전담합니다. 각 워커는 독립된 메인 루프를 실행합니다.

VPP 멀티스레딩 아키텍처 NIC RX Queue 0 RX Queue 1 RX Queue 2 Main Thread (Core 0) CLI, API, 제어 평면, PROCESS 노드 Worker 0 (Core 2) dpdk-input → graph dispatch → interface-output Worker 1 (Core 4) dpdk-input → graph dispatch → interface-output Worker 2 (Core 6) dpdk-input → graph dispatch → interface-output Frame Handoff Worker 0 → Worker 1 (플로우 기반) lockfree ring per-thread queue Barrier Sync 설정 변경 시 전체 동기화 각 워커 = 독립 메인 루프 + 전용 RX 큐 | 워커 간 공유 데이터 없음 (lockfree)
항목Main ThreadWorker Thread
CPU 코어main-core (보통 0)corelist-workers (2,4,6...)
역할CLI, API, 제어 평면패킷 처리 (데이터 평면)
INPUT 노드실행 가능 (기본 비활성)dpdk-input 폴링
PROCESS 노드ARP, DHCP, BGP 등실행하지 않음
NIC 큐 매핑1:1 (큐 N → 워커 N)
공유 데이터 접근배리어 내에서 수정읽기 전용(Read-Only) (RCU 유사)
/* startup.conf — 스레딩 설정 */
cpu {
    main-core 0                /* 메인 스레드: 코어 0 */
    corelist-workers 2,4,6,8   /* 워커 4개: 물리 코어만 (HT 제외) */
    skip-cores 1               /* 코어 1 건너뛰기 (OS 예약) */
    scheduler-policy fifo      /* RT 스케줄링 (선택) */
    scheduler-priority 50      /* RT 우선순위 */
}

/* 워커 수에 따라 NIC RX 큐를 자동 매핑 */
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4        /* 워커 수와 일치 */
    }
}

프레임 핸드오프

NIC의 RSS(Receive Side Scaling)는 5-tuple 해시를 기반으로 수신 큐를 분산합니다. 하지만 NAT44 세션 룩업, IPsec SA 선택, TCP 세그먼트 재조립 등 상태 기반 처리(Stateful Processing)는 동일 플로우의 모든 패킷이 반드시 같은 워커에서 처리되어야 합니다. RSS 해시가 이를 보장하지 못할 때, VPP의 frame handoff 메커니즘이 패킷을 올바른 워커로 재분배합니다.

Handoff 동작 원리

VPP의 handoff는 그래프 노드로 구현되며, 다음과 같은 단계로 동작합니다:

  1. 플로우 해시 계산: handoff 노드가 패킷의 5-tuple(src IP, dst IP, src port, dst port, protocol)로부터 해시를 계산합니다.
  2. 대상 워커 결정: hash % n_workers 연산으로 패킷이 처리될 워커 인덱스를 결정합니다.
  3. 바이패스 최적화: 패킷이 이미 올바른 워커에 있으면 handoff 없이 다음 노드로 직접 전달합니다. 이 경우 추가 비용이 발생하지 않습니다.
  4. 크로스 워커 전달: 다른 워커로 이동해야 하면, 해당 워커의 per-worker lockfree ring에 버퍼 인덱스를 enqueue합니다.
  5. 대상 워커 수신: 대상 워커가 다음 메인 루프(main loop) 반복에서 자신의 ring을 dequeue하여 패킷을 수신하고 그래프 처리를 계속합니다.
  6. Symmetrical 모드: symmetrical 옵션을 활성화하면, 양방향 플로우(A→B와 B→A)가 동일한 해시를 생성하여 같은 워커에서 처리됩니다. 이를 위해 src/dst를 정렬한 후 해시를 계산합니다.
Worker 0 (패킷 수신) handoff 노드 flow hash 계산 hash % n_workers = 2 → Worker 2로 전달 Lockfree Ring Worker 2 전용 큐 enqueue(buf_idx) Worker 2 dequeue → 그래프 처리 다음 노드 처리 nat44-in2out / ipsec 등 이미 올바른 워커 → bypass (비용 0)

핵심 구조체: vlib_frame_queue_t

Handoff의 핵심은 per-worker lockfree ring buffervlib_frame_queue_t 구조체입니다. 각 워커마다 하나의 ring이 존재하며, 다른 워커들이 이 ring에 패킷을 enqueue합니다:

필드타입설명
headvolatile u32Consumer(수신 워커)가 읽는 위치. 대상 워커만 증가시킵니다.
tailvolatile u32Producer(송신 워커)가 쓰는 위치. Atomic CAS로 증가시킵니다.
elts[]vlib_frame_queue_elt_tRing의 각 슬롯. 버퍼 인덱스 배열(buffer_index[VLIB_FRAME_SIZE])과 프레임 메타데이터를 포함합니다.
n_in_useu32현재 사용 중인 슬롯 수 (오버플로 감지용)
neltsu32Ring 크기. 기본값은 64이며, frame-queue-nelts 설정으로 조정합니다.
/* vlib_buffer_enqueue_to_thread() — 핵심 handoff 함수 (의사 코드) */
static_always_inline u32
vlib_buffer_enqueue_to_thread (vlib_main_t *vm,
                               vlib_node_runtime_t *node,
                               u32 *buffers, u16 *thread_indices,
                               u32 n_packets, u32 drop_on_full)
{
  vlib_frame_queue_t *fq;
  u32 n_left = n_packets;

  while (n_left)
  {
    u16 target_thread = thread_indices[0];

    /* 같은 워커이면 bypass — handoff 비용 없음 */
    if (target_thread == vm->thread_index)
    {
      /* 현재 노드의 next로 직접 enqueue */
      vlib_buffer_enqueue_to_next (vm, node, buffers, nexts, count);
      continue;
    }

    /* 대상 워커의 frame queue 획득 */
    fq = vlib_get_frame_queue (vm, fq_index, target_thread);

    /* ring tail을 atomic CAS로 확보 */
    u32 tail = __atomic_load_n (&fq->tail, __ATOMIC_ACQUIRE);
    u32 new_tail = tail + 1;

    /* 오버플로 검사 */
    if (PREDICT_FALSE (new_tail - fq->head >= fq->nelts))
    {
      if (drop_on_full)
        vlib_buffer_free (vm, buffers, count);
      continue;
    }

    /* 버퍼 인덱스를 ring 슬롯에 복사 */
    vlib_frame_queue_elt_t *elt = &fq->elts[tail & (fq->nelts - 1)];
    clib_memcpy_fast (elt->buffer_index, buffers, count * sizeof (u32));
    elt->n_vectors = count;

    /* tail 전진 (atomic store) — 대상 워커가 볼 수 있게 됨 */
    __atomic_store_n (&fq->tail, new_tail, __ATOMIC_RELEASE);

    /* 대상 워커의 pending interrupt 신호 */
    vlib_node_set_interrupt_pending (
        vlib_get_main_by_index (target_thread), node->node_index);
  }

  return n_packets;
}
코드 설명
  • 10~14행 대상 워커가 현재 스레드와 같으면 handoff를 건너뛰고 직접 enqueue합니다. 불필요한 cross-thread 통신을 피하여 자체 워커 패킷의 지연를 제거합니다.
  • 20~21행 __atomic_load_n으로 frame queue의 tail을 acquire 시맨틱으로 읽습니다. 잠금(Lock)프리 링 버퍼(Ring Buffer) 구현으로, 명시적 잠금이나 뮤텍스(Mutex) 없이 워커 간 패킷을 전달합니다.
  • 24~29행 링 오버플로 시 drop_on_full 플래그에 따라 패킷을 폐기합니다. 이는 backpressure 메커니즘으로, 대상 워커가 과부하 상태임을 의미합니다.
  • 32~37행 버퍼 인덱스를 링 슬롯에 복사한 후 __atomic_store_n으로 tail을 release 시맨틱으로 전진시킵니다. 이 시점에서 대상 워커가 새 패킷을 볼 수 있게 됩니다.

Handoff CLI 명령

명령설명예시
set interface handoff인터페이스에 handoff 노드를 삽입합니다set interface handoff GigabitEthernet0/8/0 workers 0-3
symmetrical양방향 플로우가 같은 워커에서 처리되도록 합니다set interface handoff ... workers 0-3 symmetrical
show handoff워커별 handoff 큐 상태 및 드롭 수를 표시합니다show handoff
frame-queue-neltsstartup.conf에서 ring 크기를 조정합니다cpu { frame-queue-nelts 128 }
show runtimehandoff 노드의 벡터 수/클록 사이클을 확인합니다show runtime
# handoff 노드 삽입 (nat44 이전에 플로우 해시 기반 재분배)
vpp# set interface handoff GigabitEthernet0/8/0 workers 0-3 symmetrical

# handoff 큐 상태 확인
vpp# show handoff
  worker 0: 250000 frames, 0 drops
  worker 1: 248000 frames, 0 drops
  worker 2: 251000 frames, 0 drops
  worker 3: 249000 frames, 0 drops

Handoff 성능 영향

항목Handoff 없음Handoff 있음비고
패킷당 추가 사이클0~50-80 사이클캐시 미스 포함. bypass 시 ~5 사이클
메모리 대역폭(Bandwidth)기본+10-15%크로스 워커 버퍼 인덱스 복사
캐시 효율높음L1/L2 미스 증가패킷 메타데이터가 다른 코어 캐시에 존재
플로우 일관성보장 안 됨보장됨상태 기반 기능 정상 동작
frame queue 메모리0워커당 ~512KB64 슬롯 × VLIB_FRAME_SIZE 기준

실전 시나리오별 Handoff 설정

시나리오Handoff 필요 이유권장 설정
NAT44세션 테이블이 per-thread이므로 동일 플로우가 다른 워커에서 처리되면 세션 미스(Miss)가 발생합니다set interface handoff <if> workers 0-N symmetrical
IPsec (터널(Tunnel) 모드)SA(Security Association) 상태와 시퀀스 번호(Sequence Number)가 per-thread이므로 플로우 분산 시 replay 검사가 실패합니다set interface handoff <if> workers 0-N symmetrical
TCP 스택 (Host Stack)TCP 연결 상태(윈도우, ACK 추적)는 단일 워커에서 관리해야 합니다set interface handoff <if> workers 0-N symmetrical
순수 L3 포워딩FIB 룩업은 stateless이므로 handoff가 불필요하며, 오히려 성능을 저하시킵니다Handoff 미설정 (RSS만 사용)
ACL 기반 필터링ACL 매칭은 stateless이므로 handoff 없이도 정상 동작합니다Handoff 미설정 (RSS만 사용)
핸드오프 큐 오버플로: 특정 워커가 과부하되면 handoff 큐가 가득 차서 패킷이 드롭됩니다. show handoff에서 drops가 증가하면, show runtime으로 해당 워커의 병목 노드를 확인하세요. RSS 해시가 불균형한 경우가 흔하며, frame-queue-nelts를 128이나 256으로 늘려 일시적 버스(Bus)트를 흡수할 수 있습니다.

배리어 동기화

VPP의 데이터 평면은 lockfree로 동작하지만, FIB 업데이트나 인터페이스 추가 같은 제어 평면 변경은 모든 워커를 일시 정지시키는 배리어 동기화가 필요합니다.

작업배리어 필요이유
FIB 엔트리 추가/삭제필요mtrie 구조체 수정
인터페이스 생성/삭제필요sw_interface 벡터 재할당
ACL 규칙 변경필요ACL 매치 테이블 교체
통계 카운터 읽기불필요per-thread 카운터 (lockfree)
패킷 트레이싱불필요per-thread 트레이스 버퍼
show runtime불필요읽기 전용
배리어 장기 홀드: 배리어가 활성화되면 모든 워커의 패킷 처리가 정지합니다. show barrier로 배리어 점유 시간을 모니터링하세요. 대량의 FIB 업데이트(BGP 풀 테이블 로딩 등)는 배리어를 장시간 홀드하여 패킷 드롭을 유발할 수 있습니다. FIB 배치 업데이트 API를 사용하면 배리어 점유를 최소화합니다.

큐 어피니티(Queue Affinity)와 플로우 스티키

큐 어피니티(Queue Affinity)는 특정 플로우의 패킷이 항상 동일한 워커 스레드에서 처리되도록 보장하는 메커니즘입니다. NIC의 RSS, 플로우 디렉터(Flow Director), DPDK rte_flow API, VPP의 핸드오프(handoff) 노드 등 여러 계층에서 구현됩니다. 이를 통해 세션 상태의 경합(Contention) 없는 처리와 캐시 지역성(Cache Locality)을 극대화합니다.

NIC 계층 — 하드웨어 플로우 분류 RSS (5-tuple 해시) Toeplitz 해시 → indirection table src/dst IP, port, proto Flow Director (exact match) 특정 플로우 → 지정 큐 NIC 하드웨어 필터 rte_flow (flexible match) 프로그래밍 가능 분류 규칙 prefix, mask, action chain DPDK 계층 — 큐 할당 RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 VPP 계층 — 워커 어피니티 Worker 0 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 1 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 2 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정 Worker 3 세션 어피니티 바인딩 TCP/TLS FIFO 전용 handoff 보정

RSS(Receive Side Scaling) 상세

RSS는 NIC 하드웨어에서 Toeplitz 해시 함수를 사용하여 5-tuple(소스 IP, 목적지 IP, 소스 포트, 목적지 포트, 프로토콜)에 대한 해시값을 계산하고, 그 결과를 인다이렉션 테이블(Indirection Table)을 통해 RX 큐에 매핑합니다.

/* Toeplitz 해시 계산 과정 (개념적 수도코드) */
static u32 toeplitz_hash(const u8 *key, const u8 *input, int len)
{
    u32 hash = 0;
    for (int i = 0; i < len * 8; i++) {
        if (input[i / 8] & (1 << (7 - (i % 8))))
            hash ^= get_key_word(key, i);
    }
    return hash;
}

/* 해시 → 큐 매핑 */
queue_index = indirection_table[hash & (table_size - 1)];
# VPP startup.conf에서 RSS 설정
dpdk {
    dev 0000:03:00.0 {
        num-rx-queues 4           # 워커 수에 맞춰 큐 수 설정
        rss-fn ipv4-tcp           # TCP/IPv4에 대해 RSS 활성화
    }
}
RSS 한계: Toeplitz 해시는 확률적 분배이므로 해시 충돌로 인한 큐 불균형이 발생할 수 있습니다. 또한 요청과 응답의 src/dst가 반전되어 서로 다른 큐에 도착하는 비대칭 문제가 있으며, 이를 해결하려면 대칭 해시를 사용해야 합니다.

Flow Director / rte_flow 기반 정밀 제어

Flow Director는 NIC 하드웨어에서 exact-match 또는 prefix-match 규칙으로 특정 플로우를 지정된 큐로 스티어링(steering)합니다. DPDK의 rte_flow API는 이를 프로그래밍 가능한 방식으로 추상화합니다.

/* rte_flow API로 특정 VIP 트래픽을 큐 2로 스티어링하는 예시 */
struct rte_flow_attr attr = { .ingress = 1 };
struct rte_flow_item pattern[] = {
    { .type = RTE_FLOW_ITEM_TYPE_ETH },
    { .type = RTE_FLOW_ITEM_TYPE_IPV4,
      .spec = &(struct rte_flow_item_ipv4){
          .hdr.dst_addr = rte_cpu_to_be_32(RTE_IPV4(10,0,0,100))
      }},
    { .type = RTE_FLOW_ITEM_TYPE_END },
};
struct rte_flow_action actions[] = {
    { .type = RTE_FLOW_ACTION_TYPE_QUEUE,
      .conf = &(struct rte_flow_action_queue){ .index = 2 }},
    { .type = RTE_FLOW_ACTION_TYPE_END },
};
flow = rte_flow_create(port_id, &attr, pattern, actions, &error);
# VPP CLI에서 DPDK 플로우 분류 설정
set dpdk flow-classify interface GigabitEthernet3/0/0 \
    ip4-table 0

# 사용 사례: 특정 VIP(10.0.0.100) 트래픽을 전용 워커에 할당
# → NIC 하드웨어가 패킷을 지정 큐로 스티어링
# → 해당 큐에 매핑된 워커가 전담 처리

VPP 세션 워커 어피니티

TCP/TLS 세션은 accept 또는 connect 시점에 현재 워커 스레드에 바인딩됩니다. session_alloc() 함수가 vlib_get_thread_index()로 현재 워커 인덱스를 확인하고, 해당 세션을 그 워커에 고정합니다. 세션의 전체 생명주기(lifetime) 동안 워커 변경은 불가능하며, 이는 FIFO 버퍼를 스레드 간에 공유하지 않기 때문입니다.

/* src/vnet/session/session.c — 세션 할당과 워커 바인딩 */
session_t *
session_alloc (u32 thread_index)
{
    session_worker_t *wrk = &session_main.wrk[thread_index];
    session_t *s;

    /* per-worker 풀에서 세션 할당 — 락 불필요 */
    pool_get_aligned(wrk->sessions, s, CLIB_CACHE_LINE_BYTES);
    clib_memset(s, 0, sizeof(*s));
    s->session_index = s - wrk->sessions;
    s->thread_index = thread_index;  /* 현재 워커에 고정 */

    return s;
}

/* handoff 노드 — RSS 불일치 보정 */
/* 패킷이 잘못된 워커에 도착하면 올바른 워커로 재전달 */
static uword
session_queue_node_fn (vlib_main_t *vm, ...)
{
    /* session의 thread_index와 현재 thread_index 비교 */
    if (s->thread_index != thread_index)
        session_send_rpc_evt_to_thread(s->thread_index, ...);
        /* → 올바른 워커로 handoff */
}

대칭 플로우 해시

symmetrical 옵션을 사용하면 소스와 목적지를 교환(src↔dst swap)해도 동일한 해시값이 나오도록 보장합니다. 이 기능은 NAT, IPsec 등에서 인바운드/아웃바운드 경로가 같은 워커에서 처리되도록 하는 데 필수적입니다.

/* src/vnet/ip/ip4_forward.c — 대칭 해시 구현 */
static inline u32
ip4_compute_flow_hash (const ip4_header_t *ip,
                        flow_hash_config_t cfg)
{
    u32 a, b, c;
    u32 src = clib_net_to_host_u32(ip->src_address.as_u32);
    u32 dst = clib_net_to_host_u32(ip->dst_address.as_u32);

    /* 대칭 해시: src/dst를 정렬하여 방향 무관하게 동일 해시 */
    if (cfg & IP_FLOW_HASH_SYMMETRIC) {
        if (src > dst) {
            u32 tmp = src; src = dst; dst = tmp;
        }
    }
    a = src; b = dst; c = ip->protocol;
    hash_v3_mix32(&a, &b, &c);
    return c;
}
# VPP CLI에서 대칭 해시 활성화
set ip flow-hash table 0 src dst sport dport proto symmetric

# 확인
show ip flow-hash table 0
# 출력: src dst sport dport proto symmetric

실전 설정 가이드

NIC 속도권장 큐 수권장 워커 수handoff 설정비고
1 GbE1–21–2불필요단일 큐로 충분
10 GbE2–42–4세션 앱 시 필요RSS 기본 분배
25 GbE4–84–8권장Flow Director 병용 권장
40 GbE4–84–8권장NUMA 교차 배치 주의
100 GbE8–168–16필수rte_flow + 대칭 해시 필수

문제 진단 CLI 명령

# NIC 큐 ↔ 워커 매핑 확인
vpp# show interface placement
Thread 1 (vpp_wk_0):
  GigabitEthernet3/0/0 queue 0
Thread 2 (vpp_wk_1):
  GigabitEthernet3/0/0 queue 1

# 핸드오프 통계 — 큐 불일치로 인한 핸드오프 빈도 확인
vpp# show handoff
  worker 0: 15234 handoffs
  worker 1: 12891 handoffs

# 세션별 워커 확인 — 세션이 어느 워커에 바인딩되었는지 확인
vpp# show session verbose
  [0:0] 10.0.0.1:80->10.0.0.2:54321 ESTABLISHED thread 1
  [0:1] 10.0.0.1:80->10.0.0.3:54322 ESTABLISHED thread 2

# DPDK 큐 배치 — DPDK 레벨 큐-코어 매핑
vpp# show dpdk interface placement
  GigabitEthernet3/0/0
    queue 0 -> core 2 (worker 0)
    queue 1 -> core 4 (worker 1)
    queue 2 -> core 6 (worker 2)
    queue 3 -> core 8 (worker 3)

L7 미리보기 — HTTP/2 멀티플렉싱과 세션 워커 분산

지금까지의 절은 모두 L2~L4 그래프 노드와 vppinfra 인프라에 집중했습니다. 그러나 FD.io VPP의 호스트 스택(호스트 스택)이 HTTP/2를 처리할 때는 한 가지 새로운 차원이 추가됩니다. 한 TCP 연결 안의 여러 스트림이 어떻게 워커 스레드에 분산되는가입니다. 이 절은 본 페이지의 멀티스레딩 모델과 호스트 스택을 잇는 짧은 다리 역할이며, 본격적인 세부 사항은 호스트 스택 페이지의 HTTP 프로토콜 절에서 다룹니다.

TCP 연결 1개 5튜플 → RSS hash → Worker 0 고정 Stream 1 (GET /a) stream_id=1 Stream 3 (POST /b) stream_id=3 Stream 5 (GET /c) stream_id=5 Stream 7 (GET /d) stream_id=7 Worker 0 — 모든 스트림 을 동일 워커에서 처리 HPACK 디코더 1개 flow-ctrl per-stream 캐시 친화적 Worker 1 (다른 연결 처리) Worker 2 (다른 연결 처리) Worker 3 (다른 연결 처리) RSS로 연결 단위 분산 스트림 단위 다중화(Multiplexing)는 같은 워커 내부

이 다이어그램의 핵심은 두 가지 원칙이 서로 다른 계층에서 동시에 작동한다는 점입니다.

이 분리가 가져오는 결과는 예측 가능한 확장성입니다. 워커 N개를 늘리면 동시 연결 수와 RPS가 거의 선형으로 증가합니다. 다만 한 클라이언트가 단일 TCP 연결 위에서 수천 스트림을 동시에 열면 그 전체 부하가 한 워커에 집중되므로, 헤비 클라이언트는 SETTINGS_MAX_CONCURRENT_STREAMS로 묶어 두는 편이 안전합니다. HPACK 동적 테이블·우선순위 트리·플로 제어 윈도의 구체적인 동작은 호스트 스택 페이지의 HTTP/2 절에서 이어 다룹니다.

커널 네트워크 스택과의 비교

성능 비교

시나리오커널 스택VPP (DPDK)VPP (AF_XDP)
L3 포워딩 (64B, 10GbE)~1.2 Mpps~14.8 Mpps (라인레이트)~11 Mpps
L2 브릿징 (64B, 10GbE)~2 Mpps~14.8 Mpps (라인레이트)~12 Mpps
IPsec (AES-GCM-256, 1500B)~2 Gbps~20 Gbps (AES-NI)
NAT44 (64B)~800 Kpps~10 Mpps~8 Mpps
TCP 연결 처리우수 (네이티브)제한적 (VCL)제한적
주의: 성능 수치는 하드웨어, 패킷 크기, 규칙 수에 따라 크게 달라집니다. 위 표는 단일 코어, 10GbE NIC 기준의 대략적인 참고값입니다.

왜 VPP는 빠른가 — 다섯 가지 기제

위 표의 10배 이상 격차는 단일 트릭이 아니라, 서로 다른 계층의 최적화 기제 다섯 가지가 곱해져 만들어집니다. 각 기제는 앞서 다룬 절들에서 따로 설명했지만, 이 절은 "수치적 차이가 어디서 오는가"를 한눈에 정리합니다.

#기제제거하는 비용대략 기여(단일 코어 L3 fwd 기준)
1커널/유저 경계 제거 (DPDK PMD)syscall, context switch, skb 할당, softirq, netfilter hook~5~10× (1.2 Mpps → ~10 Mpps)
2인터럽트 → 폴링per-packet IRQ, top/bottom-half, scheduler 개입~1.5× (지연 분산 제거)
3벡터 처리 (그래프 노드 × 256 packets)I-cache 미스, 함수 호출 오버헤드, 분기 예측 실패~2× (instruction cache 효과)
4quad-loop + prefetchL1/L2 D-cache 미스 stall (수십~수백 cycle/패킷)~1.5× (메모리 latency 은닉)
52-cacheline 버퍼 레이아웃 + HugepageTLB 미스, cacheline 교차, NUMA 원격 접근~1.2~1.5×
곱셈으로 보는 직관: 5 × 1.5 × 2 × 1.5 × 1.3 ≈ 30×. 실제로는 노드 수와 패킷 크기에 따라 5~15배 사이로 수렴하지만, 라인레이트 64B 포워딩이 가능해지는 이유는 이 "곱셈 효과" 때문입니다. 한 가지만 도입해서는 큰 차이가 나지 않으며, 다섯 기제가 모두 맞물려야 합니다.

왜 quad-loop가 결정적인가 — IPC와 prefetch 거리

현대 CPU는 한 cycle에 평균 3~4개의 명령어를 retire할 수 있지만, 메모리에서 데이터를 기다리면 IPC(Instructions Per Cycle)가 0.5 이하로 떨어집니다. L1 D-cache 미스는 ~12 cycle, L2 미스는 ~40 cycle, L3 미스(원격 NUMA)는 ~300 cycle을 stall로 유발합니다. 패킷마다 FIB lookup·헤더 재기록을 하면 D-cache 미스가 일상적이고, 이 stall이 처리량을 결정합니다.

quad-loop는 4개 패킷을 함께 다루면서 다음 상황을 만들어냅니다:

이 효과는 노드 코드가 단순할수록 두드러집니다. ip4-lookup, l2-fwd처럼 핫 패스 노드가 quad-loop을 쓰는 이유이며, 복잡한 상태 머신을 가진 tcp-input이 single-loop인 이유이기도 합니다(쿼드 루프 패턴 절 참조).

왜 2-cacheline 버퍼 레이아웃이 중요한가

vlib_buffer_t는 의도적으로 두 개의 64바이트 cacheline에 맞춰 설계되어 있습니다. 첫 번째 cacheline은 핫 필드(current_data, current_length, flags, next_buffer, current_config_index 등)로, 모든 노드가 매 패킷에서 읽고 쓰는 항목입니다. 두 번째 cacheline은 콜드 필드(trace_handle, opaque, opaque2)로, 일부 노드만 접근합니다.

이 레이아웃과 quad-loop, prefetch는 따로 떼어 보면 작은 최적화이지만, 결합하면 cycles/packet을 ~150에서 ~30 미만으로 내리는 결정적 차이를 만듭니다. CSIT 보고서의 노드별 cycles/packet 수치를 보면 ip4-lookup이 한 자릿수 cycle인 이유가 여기에 있습니다.

적용 시나리오 선택 기준

기준커널 스택 적합VPP 적합
TCP 애플리케이션웹 서버, DB, 범용 서비스L3/L4 포워딩, 터널링
성능 요구< 5 Gbps, 범용 처리> 10 Gbps, 라인레이트 포워딩
기능 복잡도소켓 API, iptables, tcL2/L3/VPN/NAT 고성능 처리
운영 편의성표준 Linux 도구 사용별도 CLI/API 학습 필요
NIC 공유여러 앱이 NIC 공유NIC 독점(DPDK) 또는 AF_XDP
에코시스템모든 Linux 도구 호환linux-cp로 일부 통합

참고자료

이 문서는 VPP 25.10 / 26.02 기준으로 작성되어 있으며, 아래 링크는 공식·권위 있는 1차 자료 위주로 정리했습니다. 릴리스마다 API와 노드 이름이 변할 수 있으므로 실제 배포 전에는 반드시 본인이 쓰는 버전의 문서를 교차 확인하시기 바랍니다.

FD.io · VPP 공식 문서

VLIB 벡터 처리와 그래프 노드

vppinfra (clib) 인프라 라이브러리

기초 표준 및 관련 RFC

VPP 소스 트리 탐색

소스 탐색 팁: git.fd.io/vpp의 cgit은 브랜치별로 코드를 볼 수 있으므로, 글에 등장한 함수(예: vlib_node_runtime_t)를 현재 쓰시는 릴리스 브랜치에서 직접 확인하는 편이 가장 정확합니다. GitHub 미러는 UI가 편하지만 최신 반영이 수 시간 늦을 수 있습니다.

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.

오픈소스 코드 인용 고지

라이선스 고지: 이 문서의 코드 예제에는 아래 오픈소스 프로젝트의 소스 코드에서 발췌·간략화한 내용이 포함되어 있습니다. 해당 코드 블록에는 원본 프로젝트의 라이선스가 그대로 적용되며, 본 사이트의 CC BY-NC-SA 4.0 라이선스 대상에서 제외됩니다. 이들 코드의 포함은 한국 저작권법 제28조 및 제35조의5에 근거한 교육 목적의 공정 이용에 해당합니다.
프로젝트저작권자라이선스공식 저장소
VPP (Vector Packet Processing) FD.io contributors Apache License 2.0 github.com/FDio/vpp
DPDK (Data Plane Development Kit) DPDK contributors BSD 3-Clause License github.com/DPDK/dpdk

코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.