운영 · 플랫폼 · 확장
VPP 운영: 플러그인 시스템, CLI/API, 성능 최적화, 디버깅(Debugging), 설치, 활용 사례, CSIT 테스팅, 운영 가이드, 플러그인 개발, Kubernetes 통합, 모니터링, 실습 랩을 다룹니다.
linux-cp)이나 Feature Arc 구성이 달라질 수 있으므로, 실제 배포 전 공식 문서와 릴리스 노트를 반드시 교차 확인하시기 바랍니다.
핵심 요약
- 플러그인 시스템 —
VLIB_PLUGIN_REGISTER()매크로(Macro)로.so동적 라이브러리(Library)를 런타임에 로드하며, 각 플러그인은 노드·API·CLI 명령을 등록합니다. Feature Arc에 삽입하여 기존 그래프를 침습 없이 확장합니다. - CLI와 API —
vppctl은 대화형 제어용이며, Binary API는 공유 메모리 전송(Shared Memory Transport) 기반 고성능 프로그램용입니다. VAPI(C/C++), GoVPP(Go),vpp_papi(Python) 바인딩이 제공됩니다. - 성능 최적화 축 — CPU 핀닝(Pinning), NUMA 인식, NIC RSS 큐 분산, 폴링(Polling) 전략(busy/sleep/interrupt), 버퍼(Buffer) 풀 크기, Hugepage 분배 등 여러 축의 균형이 처리량(Throughput)과 지연(Latency)을 결정합니다.
- 디버깅 도구 —
trace add로 패킷(Packet)별 노드 통과 경로를,show runtime으로 노드별 벡터 크기·클럭 사이클을,pcap dispatch-trace로 vpp 내부 패킷 캡처를, 이벤트 로거(Event Logger)로 타이밍 이벤트를 기록합니다. - 설치와 빌드 — Debian/Ubuntu APT 패키지, RHEL/CentOS YUM 패키지, 소스 빌드 3가지 경로가 있으며,
startup.conf가 기동 시 모든 구성(CPU·메모리·DPDK·Unix)을 정의합니다. - Kubernetes 통합 — Calico-VPP가 가장 성숙한 CNI 통합입니다. NetworkPolicy를 VPP ACL로 매핑(Mapping)하고, memif CNI로 파드 간 제로카피 체이닝을 구현합니다.
- CSIT 테스팅 — FD.io Continuous System Integration Testing. 공식 성능 테스트 프레임워크로 릴리스별 NDR/PDR 처리량을 측정하며, 로컬에서도 일부 테스트를 재현할 수 있습니다.
- 모니터링 체계 — Stats Segment(공유 메모리 통계 영역)을 Prometheus exporter가 스크랩하고, Grafana 대시보드에서 시각화합니다. Worker별 드롭(Drop)·재전송(Retransmission)·벡터 크기를 상시 관찰합니다.
단계별 이해
- 설치
APT는apt install vpp vpp-plugin-core, 소스는make install-dep build-release로 빌드합니다. 커널 헤더·DPDK·Hugepage 설정이 전제되어야 합니다. - startup.conf 작성
unix(로그·CLI 소켓(Socket)),cpu(main-core·corelist-workers),buffers(buffers-per-numa),dpdk(dev·num-rx-queues)의 4개 섹션이 기본입니다. 운영 환경에서는api-segment·statseg도 명시합니다. - CLI 기본 조작
vppctl show interface(인터페이스 상태),show runtime(노드별 성능),show errors(에러 카운터),show hardware-interfaces(NIC 상세),show buffers(버퍼 풀 사용률)이 필수 명령입니다. - 플러그인 활성화/비활성화
plugins { plugin dpdk_plugin.so { enable } plugin acl_plugin.so { disable } }로 기동 시 선택적으로 로드합니다. 커스텀 플러그인 개발은src/plugins/sample/을 템플릿으로 시작합니다. - 성능 튜닝 체크리스트 적용
Hugepage 할당, isolcpus 격리(Isolation), NIC RSS 큐 수 = 워커 수,show runtime의 Vectors/Call ≥ 64,show errors의 에러 카운터 0 유지를 기준선으로 삼습니다. - 디버깅 워크플로우
장애 발생 시 (1)show errors로 에러 분류 확인 → (2)trace add dpdk-input 50+show trace로 패킷 경로 추적 → (3) 필요 시pcap dispatch-trace로 내부 캡처 → (4) 이벤트 로거로 타이밍 재구성 순으로 진단합니다.
플러그인 시스템
- 26.02 신규 플러그인 5개: NPol(Network Policies — Kubernetes NetworkPolicy 매핑), Selog(Shared Elog — 다중 프로세스 이벤트 로깅 공유), Soft RSS(소프트웨어 RSS — NIC가 RSS를 못 할 때 대체 경로), SFDP(StateFul Data Plane Services), SASC(Session-Aware Service Chaining). 릴리스 노트에 64개 신규 API 메시지(
npol_*,selog_*,sfdp_*등)가 함께 들어왔습니다. - 제거된 API: 26.02에서
avf_create,avf_delete관련 메시지가 제거됐습니다. 자동화 스크립트가 이 메시지를 직접 호출한다면 즉시 수정해야 합니다. - Deprecated API:
tap_create_v2(26.02에서 deprecated),pg_create_interface_v2(25.10에서 deprecated). 다음 릴리스에서 제거될 수 있으니 이관 계획을 세워 두시기 바랍니다. - Session 레이어: 25.06에 app 용 session eventing 인프라, 25.10에 per-FIFO 최대 메모리와 trusted CA 설정, proxy_write_early_data 콜백이 들어와 운영 측 관측·정책 지점이 늘어났습니다.
- Vector Library: 25.06에 SCHED 노드 타입과 CPU 설정의 "relative" 키워드가 추가되어 워커 배치를 상대 인덱스로 표현할 수 있게 됐습니다.
- 전체 변화 맵은 Host Stack 문서의 변경 요약 표에서 한눈에 볼 수 있습니다.
VPP는 모듈식 플러그인 아키텍처를 채택하여 기능을 동적으로 확장할 수 있습니다. 각 플러그인은 공유 라이브러리(Shared Library)(.so)로 빌드되며, VPP 시작 시 /usr/lib/vpp_plugins/에서 자동 로드됩니다.
플러그인 구조
VPP 플러그인의 기본 디렉터리 구조:
VPP 초기화 시퀀스
VPP는 vpp_main() 함수에서 시작하여, 메모리 초기화, UNIX 환경 설정, 플러그인 로딩, 노드 그래프 구성을 순차적으로 수행한 뒤 메인 루프에 진입합니다.
플러그인 로딩 메커니즘
VPP의 플러그인 시스템은 dlopen() 기반의 동적 로딩 방식을 사용합니다. 각 플러그인은 독립적인 SO 파일로 컴파일되며, 초기화 함수들은 매크로를 통해 등록됩니다.
/* 1. 플러그인 등록 매크로 (constructor로 자동 실행) */
VLIB_PLUGIN_REGISTER() = {
.version = "1.0",
.description = "My Custom Plugin",
.default_disabled = 0,
};
/* 2. 초기화 함수 — 의존성 지정으로 실행 순서 보장 */
static clib_error_t *
my_plugin_init (vlib_main_t *vm)
{
my_plugin_main_t *mp = &my_plugin_main;
mp->vlib_main = vm;
mp->vnet_main = vnet_get_main();
pool_alloc(mp->sessions, 1024);
return 0;
}
VLIB_INIT_FUNCTION(my_plugin_init) = {
.runs_after = VLIB_INITS("ip4_init", "ip6_init"),
};
/* 3. 워커 스레드별 초기화 */
static clib_error_t *
my_plugin_worker_init (vlib_main_t *vm)
{
/* 워커별 캐시, 통계 카운터 초기화 */
return 0;
}
VLIB_WORKER_INIT_FUNCTION(my_plugin_worker_init);
/* 4. 메인 루프 진입 시 콜백 — 모든 초기화 완료 후 실행 */
static clib_error_t *
my_plugin_main_loop_enter (vlib_main_t *vm)
{
vnet_feature_enable_disable(
"ip4-unicast", "my-plugin-node",
sw_if_index, 1, 0, 0);
return 0;
}
VLIB_MAIN_LOOP_ENTER_FUNCTION(my_plugin_main_loop_enter);
코드 설명
- VLIB_PLUGIN_REGISTER()
__attribute__((constructor))를 사용하여dlopen()시점에 자동으로 플러그인 정보를 등록합니다. - VLIB_INIT_FUNCTION
runs_after로 의존 init 함수를 지정하면, VPP가 토폴로지(Topology) 정렬을 통해 올바른 순서를 보장합니다. - VLIB_WORKER_INIT_FUNCTION각 워커 스레드(Thread)가 시작될 때 해당 스레드 컨텍스트에서 실행됩니다.
- VLIB_MAIN_LOOP_ENTER_FUNCTION메인 루프 진입 직전에 실행됩니다. 모든 노드 그래프가 준비된 상태이므로 Feature Arc 활성화 등을 수행합니다.
VLIB_INIT_FUNCTION에서 아직 초기화되지 않은 서브시스템에 접근하면 세그멘테이션 폴트가 발생할 수 있습니다. 반드시 runs_after로 의존성을 명시하시기 바랍니다. 순환 의존성이 감지되면 VPP는 시작에 실패합니다.
커스텀 플러그인 개발
/* my_plugin.c — 플러그인 초기화 */
#include <vnet/vnet.h>
#include <vlib/vlib.h>
#include <vpp/app/version.h>
typedef struct {
u32 sw_if_index;
u32 next_index;
} my_plugin_main_t;
my_plugin_main_t my_plugin_main;
static clib_error_t *
my_plugin_init (vlib_main_t *vm) {
my_plugin_main_t *mpm = &my_plugin_main;
mpm->sw_if_index = ~0;
return 0;
}
VLIB_INIT_FUNCTION(my_plugin_init);
/* 플러그인 등록 매크로 */
VLIB_PLUGIN_REGISTER() = {
.version = VPP_BUILD_VER,
.description = "My Custom Plugin",
};
코드 설명
-
7~11행
플러그인 전역 상태를 구조체(Struct)로 관리합니다.
sw_if_index를~0으로 초기화하여 "미설정" 상태를 표현하며, 이는 VPP에서 유효하지 않은 인덱스를 나타내는 관례입니다. -
15~20행
VLIB_INIT_FUNCTION매크로는 VPP 시작 시 자동으로 호출되는 초기화 함수를 등록합니다. 반환값이clib_error_t*이므로, 에러 시 VPP 시작을 중단할 수 있습니다. -
23~26행
VLIB_PLUGIN_REGISTER는 플러그인 메타데이터를 등록합니다. 버전 문자열은 빌드 시 자동 생성되며, VPP가 플러그인 호환성을 검증하는 데 사용됩니다.
/* node.c — 커스텀 그래프 노드 */
#include <vlib/vlib.h>
#include <vnet/vnet.h>
typedef enum {
MY_NODE_NEXT_DROP,
MY_NODE_NEXT_IFACE_OUTPUT,
MY_NODE_N_NEXT,
} my_node_next_t;
static uword
my_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t *frame) {
u32 n_left_from, *from, *to_next;
my_node_next_t next_index;
from = vlib_frame_vector_args(frame);
n_left_from = frame->n_vectors;
next_index = node->cached_next_index;
while (n_left_from > 0) {
u32 n_left_to_next;
vlib_get_next_frame(vm, node, next_index,
to_next, n_left_to_next);
while (n_left_from > 0 && n_left_to_next > 0) {
u32 bi0 = from[0];
vlib_buffer_t *b0 = vlib_get_buffer(vm, bi0);
u32 next0 = MY_NODE_NEXT_IFACE_OUTPUT;
/* 패킷 처리 로직 */
/* ... */
to_next[0] = bi0;
from += 1;
to_next += 1;
n_left_from -= 1;
n_left_to_next -= 1;
vlib_validate_buffer_enqueue_x1(vm, node, next_index,
to_next, n_left_to_next, bi0, next0);
}
vlib_put_next_frame(vm, node, next_index, n_left_to_next);
}
return frame->n_vectors;
}
VLIB_REGISTER_NODE(my_node) = {
.function = my_node_fn,
.name = "my-custom-node",
.vector_size = sizeof(u32),
.type = VLIB_NODE_TYPE_INTERNAL,
.n_next_nodes = MY_NODE_N_NEXT,
.next_nodes = {
[MY_NODE_NEXT_DROP] = "error-drop",
[MY_NODE_NEXT_IFACE_OUTPUT] = "interface-output",
},
};
코드 설명
-
12~15행
vlib_frame_vector_args()로 프레임의 버퍼 인덱스 배열을 획득하고,cached_next_index로 마지막 사용된 next 인덱스를 캐시(Cache)합니다. 대부분 패킷이 같은 경로로 가므로 프레임 전환 비용을 줄입니다. -
17~19행
vlib_get_next_frame()은 다음 노드의 프레임 슬롯과 남은 공간을 반환합니다. 프레임이 가득 차면 자동으로 새 프레임을 할당하여 연속 처리를 보장합니다. -
30~31행
vlib_validate_buffer_enqueue_x1()은 next 인덱스가 현재 프레임의 대상과 일치하는지 검증합니다. 불일치 시 프레임을 전환하여 다른 노드로 패킷을 분기합니다. -
33행
vlib_put_next_frame()으로 현재 프레임을 디스패처에 반환합니다. 프레임에 패킷이 있으면 pending_frames에 추가되어 다음 디스패치(Dispatch) 사이클에 처리됩니다. -
37~47행
VLIB_REGISTER_NODE에서type = VLIB_NODE_TYPE_INTERNAL은 이 노드가 이전 노드의 출력으로만 실행됨을 선언합니다. INPUT 타입과 달리 매 루프마다 폴링되지 않습니다.
VPP CLI 및 API
VPP CLI (vppctl)
VPP는 유닉스 도메인 소켓(/run/vpp/cli.sock)을 통한 CLI를 제공합니다:
# VPP CLI 접속
$ vppctl
_______ _ _ _____ ___
__/ __/ _ \ (_)__ | | / / _ \/ _ \
_/ _// // / / / _ \ | |/ / ___/ ___/
/_/ /____(_)_/\___/ |___/_/ /_/
vpp# show version
vpp# show interface
vpp# show hardware-interfaces
vpp# show runtime /* 노드별 성능 통계 */
vpp# show errors /* 에러 카운터 */
vpp# show trace /* 패킷 트레이스 */
# 패킷 트레이싱 (디버깅)
vpp# trace add dpdk-input 100 /* DPDK 입력 100 패킷 추적 */
vpp# show trace /* 추적 결과 확인 */
# 비대화형 실행
$ vppctl show interface
Binary API
VPP Binary API는 공유 메모리를 통한 고성능 프로그래밍 인터페이스입니다. .api 파일에서 메시지를 정의하고, 코드 생성기가 C/Python/Go 바인딩을 자동 생성합니다:
/* my_plugin.api — API 메시지 정의 */
autoreply define my_plugin_enable_disable {
u32 client_index;
u32 context;
bool enable_disable;
vl_api_interface_index_t sw_if_index;
};
define my_plugin_details {
u32 context;
vl_api_interface_index_t sw_if_index;
u64 packet_count;
};
VAPI (VPP API)
VAPI는 C/C++용 고수준 API 라이브러리로, Binary API 위에서 동기/비동기 호출, 콜백(Callback), 자동 직렬화(Serialization)를 제공합니다:
/* VAPI 사용 예제 (C) */
#include <vapi/vapi.h>
#include <vapi/vpe.api.vapi.h>
vapi_ctx_t ctx;
vapi_ctx_alloc(&ctx);
vapi_connect(ctx, "my_app", NULL,
64, /* max outstanding requests */
32, /* response queue depth */
VAPI_MODE_BLOCKING, true);
/* 인터페이스 목록 조회 */
vapi_msg_sw_interface_dump *msg = vapi_alloc_sw_interface_dump(ctx);
msg->payload.name_filter_valid = false;
vapi_sw_interface_dump(ctx, msg, callback_fn, NULL);
vapi_disconnect(ctx);
vapi_ctx_free(ctx);
GoVPP / Python bindings
# Python (vpp_papi) 예제
from vpp_papi import VPPApiClient
vpp = VPPApiClient(apifiles=['/usr/share/vpp/api/core/*.api.json'])
vpp.connect('my-python-app')
# 인터페이스 목록
ifaces = vpp.api.sw_interface_dump()
for iface in ifaces:
print(f"{iface.interface_name}: {iface.sw_if_index}")
# TAP 생성
rv = vpp.api.tap_create_v2(id=0, host_if_name='vpp-tap0',
host_ip4_prefix='192.168.1.1/24')
vpp.disconnect()
Python vpp_papi 자동화 예제
운영 환경에서는 CLI를 사람이 수동으로 입력하는 것보다, API 세션 생성 → 설정 적용 → 검증 → 통계 수집을 한 번의 자동화 흐름으로 묶는 편이 훨씬 안전합니다. 특히 VPP는 설정 성공 여부를 즉시 반환하므로, 배포 스크립트에서 실패 지점을 명확히 잡아낼 수 있습니다.
from vpp_papi import VPPApiClient
vpp = VPPApiClient(apifiles=[
"/usr/share/vpp/api/core/*.api.json",
"/usr/share/vpp/api/plugins/*.api.json",
])
vpp.connect("vpp-automation")
def run(cmd):
reply = vpp.api.cli_inband(cmd=cmd)
if reply.retval != 0:
raise RuntimeError(reply.reply)
return reply.reply
# 1. 구성 적용
run("create loopback interface instance 10")
run("set interface state loop10 up")
run("set interface ip address loop10 172.16.10.1/24")
run("ip route add 198.18.0.0/15 via 172.16.10.254 loop10")
# 2. 읽기 경로는 typed dump 사용
interfaces = vpp.api.sw_interface_dump()
for iface in interfaces:
name = iface.interface_name.rstrip("\\x00")
if name == "loop10":
print(f"loop10 index={iface.sw_if_index} mtu={iface.mtu[0]}")
# 3. 최종 검증
print(run("show interface loop10"))
print(run("show ip fib 198.18.0.0/15"))
vpp.disconnect()
코드 설명
-
1~7행
VPPApiClient는 API JSON 정의를 읽어 메시지 인코더/디코더를 준비합니다. 운영 환경에서는 core와 plugins API 파일을 함께 주는 편이 안전합니다. -
9~14행
cli_inband()를 래핑하여 실패 시 즉시 예외를 발생시킵니다. 초기 자동화에서는 typed 메시지보다 빠르게 구성 절차를 고정할 수 있습니다. - 17~20행 구성 적용 단계입니다. 루프백 생성, 링크 업, 주소 부여, 정적 경로 추가를 한 세션 안에서 수행하므로 중간 상태를 쉽게 검증할 수 있습니다.
-
23~27행
읽기 경로는
sw_interface_dump()처럼 typed API를 쓰면 필드 구조가 안정적으로 파싱됩니다. 특히 인터페이스 이름의 널 종료 바이트 제거가 자주 필요합니다. -
30~31행
운영 스크립트는 적용과 동시에 검증 출력을 남겨야 합니다.
show interface와show ip fib를 함께 저장하면 장애 시점의 상태 재현이 쉬워집니다.
| 자동화 단계 | 권장 API | 이유 |
|---|---|---|
| 초기 구성 프로토타입 | cli_inband() | 문서와 CLI 절차를 그대로 코드화하기 쉽습니다. |
| 대량 조회 | typed dump API | 문자열 파싱 없이 구조화된 필드를 바로 읽을 수 있습니다. |
| 주기적 모니터링 | Stats Segment | 잠금(Lock) 경합(Lock Contention)이 거의 없어 수집 오버헤드(Overhead)가 낮습니다. |
| 무중단 배포 | API + 사전 검증 로직 | 실패한 단계에서 즉시 롤백(Rollback) 기준을 세우기 쉽습니다. |
show interface와 show errors를 바로 읽으면, 설정 성공과 데이터 경로 정상 동작을 분리해서 판단할 수 있습니다.
Rust 클라이언트
VPP는 Rust 바인딩을 공식적으로 제공합니다. vpp-api-rs 크레이트는 Binary API를 직접 호출하거나 socket 전송을 이용하는 두 가지 방식 모두 지원합니다. 비동기 런타임(tokio)과 통합해 수천 개 연결을 하나의 프로세스에서 제어할 수 있어, Rust 기반 오케스트레이터(예: Kubernetes operator)에 적합합니다.
// Cargo.toml
// [dependencies]
// vpp-api-rs = "0.3"
// tokio = { version = "1", features = ["full"] }
use vpp_api_rs::VppClient;
async fn create_loopback() -> anyhow::Result<()> {
let mut client = VppClient::connect("/run/vpp/api.sock").await?;
let reply = client.create_loopback().await?;
println!("created sw_if_index={{}}", reply.sw_if_index);
Ok(())
}
govpp는 생산성에, Rust는 신뢰성에 유리합니다.
VAT2 — Binary API 테스트 도구
VAT2 (VPP API Test 2)는 VPP에 포함된 공식 Binary API 테스트 CLI입니다. 스크립트 파일에 API 호출을 순서대로 기술해 VPP 인스턴스에 전송할 수 있어, API 개발 시 reply 포맷 검증과 회귀 테스트에 쓰입니다.
$ vat2 -s /run/vpp/api.sock
# 또는 스크립트 실행
$ cat > /tmp/test.vat <<EOF
create_loopback
sw_interface_set_flags sw_if_index 1 flags 1
sw_interface_add_del_address sw_if_index 1 prefix 10.0.0.1/24
EOF
$ vat2 -s /run/vpp/api.sock -f /tmp/test.vat
vat2는 기존 vat(VAT1)의 후속이며, JSON 기반 API 정의에서 자동 생성된 명령을 제공합니다. CI 파이프라인에서 "API가 깨지지 않았는지" 빠르게 확인할 때 유용합니다.
libmemif — memif 프로그래밍 라이브러리
libmemif는 memif 공유 메모리 인터페이스를 VPP 외부 애플리케이션에서 직접 사용하기 위한 C 라이브러리입니다. 즉, VPP를 전혀 쓰지 않는 DPDK 앱이나 커스텀 데이터 평면 프로그램이 memif로 VPP와 제로카피 패킷 교환을 할 수 있게 해 줍니다.
/* libmemif 최소 클라이언트 */
#include <libmemif.h>
memif_conn_args_t args = { .role = MEMIF_ROLE_SLAVE, .socket_filename = "/run/vpp/memif.sock", .interface_id = 0 };
memif_conn_handle_t conn;
memif_create(&conn, &args, on_connect, on_disconnect, on_interrupt, NULL);
while (running) {
memif_poll_event(0); /* 콜백 기반으로 rx/tx 수행 */
}
주된 용도:
- SFC 에이전트 — VPP가 memif로 흘려주는 패킷을 외부 프로세스가 받아 L7 검사·DPI·DLP를 수행
- Kubernetes 사이드카 — Pod 사이드카가 libmemif로 Pod 트래픽을 가로챔
- 벤치마크 도구 — TRex와 별개로 자체 트래픽 생성기를 만들 때
govpp/memif, Rust 용 libmemif-sys 같은 바인딩이 커뮤니티에 존재합니다. 다만 공식 지원은 C 라이브러리만 보증됩니다.
Stats Segment
VPP의 Stats Segment는 통계 카운터를 외부 프로세스(Process)에 노출하는 공유 메모리 영역입니다. 배리어 없이 읽기 전용(Read-Only)으로 접근할 수 있어, 모니터링 시스템이 VPP 성능에 영향을 주지 않습니다.
/* C에서 Stats Segment 접근 */
#include <vpp-api/client/stat_client.h>
stat_client_main_t *sm = stat_client_get();
stat_segment_connect("/run/vpp/stats.sock");
/* 패턴으로 카운터 조회 */
u8 **patterns = 0;
vec_add1(patterns, (u8 *)"/if/rx");
stat_segment_data_t *data = stat_segment_dump(patterns);
/* Python에서 접근 */
# from vpp_papi.vpp_stats import VPPStats
# stats = VPPStats(socketname='/run/vpp/stats.sock')
# print(stats['/if/rx'])
| 카운터 경로 | 유형 | 설명 |
|---|---|---|
/if/rx | combined | 인터페이스별 RX 패킷/바이트 |
/if/tx | combined | 인터페이스별 TX 패킷/바이트 |
/if/drops | simple | 인터페이스별 드롭 패킷 |
/err/{node}/{error} | simple | 노드별 에러 카운터 |
/sys/heartbeat | scalar | VPP 활성 여부 (증가하는 카운터) |
/mem/statseg | scalar | Stats Segment 메모리 사용량 |
/if/names | name_vector | 인터페이스 이름 목록 |
vpp_prometheus_export 플러그인 또는 외부 vpp-exporter가 Stats Segment를 Prometheus 메트릭으로 변환합니다. /if/rx, /if/tx, /err/* 카운터를 Grafana 대시보드로 시각화하여 실시간(Real-time) 모니터링이 가능합니다.
성능 최적화
CPU 핀닝과 워커 스레드
VPP 성능의 핵심은 CPU 코어 격리와 적절한 워커 스레드 배치입니다:
/* startup.conf — CPU 핀닝 */
cpu {
main-core 0 /* 메인 스레드 (API, CLI 처리) */
corelist-workers 2,4,6,8 /* 워커: 물리 코어만 (HT 제외) */
skip-cores 1 /* 코어 1 건너뛰기 (OS용) */
}
# 커널 부팅 파라미터: VPP 코어를 OS 스케줄러에서 격리
GRUB_CMDLINE_LINUX="isolcpus=2,4,6,8 nohz_full=2,4,6,8 rcu_nocbs=2,4,6,8"
isolcpus는 지정 코어를 CFS 로드 밸런싱에서 제외합니다. nohz_full은 해당 코어의 스케줄러(Scheduler) 틱(Tick)을 비활성화하여 인터럽트(Interrupt)를 최소화합니다. rcu_nocbs는 RCU 콜백을 다른 코어로 오프로드합니다. 이 조합이 VPP의 busy-polling 성능을 극대화합니다.
NUMA 인식
NIC과 VPP 워커 스레드를 동일 NUMA 노드에 배치하여 원격 메모리 접근(remote memory access)을 방지합니다:
# NIC의 NUMA 노드 확인
$ cat /sys/bus/pci/devices/0000:03:00.0/numa_node
0
# NUMA 노드 0의 CPU 확인
$ lscpu | grep "NUMA node0"
NUMA node0 CPU(s): 0-7
/* startup.conf — NUMA 인식 설정 */
buffers {
buffers-per-numa 16384 /* NUMA 노드당 버퍼 수 */
default data-size 2048
}
dpdk {
dev 0000:03:00.0 { /* NUMA 0에 위치한 NIC */
num-rx-queues 4
}
}
cpu {
main-core 0 /* NUMA 0 코어 */
corelist-workers 2,4,6 /* NUMA 0 코어 */
}
인터럽트 모드 vs 폴링 모드
| 모드 | 장점 | 단점 | 적용 시나리오 |
|---|---|---|---|
| Polling (기본) | 최소 지연, 최대 throughput | CPU 100% 사용 | 고성능 필수 환경 |
| Interrupt | 유휴 시 CPU 절약 | 인터럽트 오버헤드 | 트래픽 변동이 큰 환경 |
| Adaptive | 부하에 따라 자동 전환 | 전환 지연 발생 가능 | 범용 환경 |
/* startup.conf — 인터럽트 모드 활성화 */
dpdk {
dev 0000:03:00.0 {
num-rx-queues 4
}
}
/* 적응형 모드: 유휴 시 sleep */
unix {
poll-sleep-usec 100 /* 유휴 시 100μs sleep */
}
성능 벤치마킹
# VPP 내장 패킷 생성기로 벤치마크
vpp# packet-generator new {
name pg0
limit 10000000
size 64-64
interface pg0
node ethernet-input
data { IP4: 1.2.3 -> 4.5.6
UDP: 192.168.1.1 -> 192.168.2.1
UDP: 1234 -> 5678
incrementing 100
}
}
vpp# packet-generator enable
# 런타임 통계 확인
vpp# show runtime
# 출력:
# Name State Calls Vectors Suspends Clocks Vectors/Call
# dpdk-input polling 1000000 10000000 0 24.50 10.00
# ethernet-input active 1000000 10000000 0 18.30 10.00
# ip4-input active 1000000 10000000 0 15.20 10.00
# ip4-lookup active 1000000 10000000 0 12.80 10.00
# TRex 외부 트래픽 생성기 사용
$ ./t-rex-64 -f stl/bench.py -m 10mpps -d 60
버퍼 풀 최적화
| NIC 속도 | 권장 buffers-per-numa | Hugepage 요구량 | 비고 |
|---|---|---|---|
| 1 GbE | 8,192 | ~32 MB (2M pages) | 저트래픽 환경 |
| 10 GbE | 32,768 | ~128 MB | 일반적 프로덕션 |
| 25 GbE | 65,536 | ~256 MB | 고트래픽 환경 |
| 40/100 GbE | 131,072+ | ~512 MB+ | 1G hugepage 권장 |
# 버퍼 풀 상태 확인
vpp# show buffers
Thread Name Index Size Alloc Free #Alloc #Free
0 default-numa-0 0 2048 32768 31200 1568 31200
1 default-numa-0 0 2048 32768 30800 1968 30800
# 부족 시 증상: show errors에서 no-buffer 증가
vpp# show errors
Count Node Reason
15230 dpdk-input no-buffer ← 버퍼 부족!
0 dpdk-input buffer-alloc-fail
dpdk-input의 no-buffer 에러가 증가하면 NIC에서 패킷을 가져오지 못합니다. buffers-per-numa를 늘리고, /proc/meminfo에서 HugePages_Free가 충분한지 확인하세요. 버퍼 한 개 = sizeof(vlib_buffer_t) + data-size ≈ 2,176바이트입니다.
perf/VPP 성능 카운터 분석
# perf stat로 VPP 워커의 CPU 카운터 측정
$ perf stat -e cycles,instructions,cache-misses,cache-references,\
branch-misses,branch-instructions -p $(pidof vpp) -t $(pgrep -P $(pidof vpp)) sleep 10
# VPP 내장 성능 카운터
vpp# show runtime
# Clocks/Call: 노드당 평균 CPU 사이클 (낮을수록 좋음)
# Vectors/Call: 호출당 평균 벡터 크기 (높을수록 좋음)
# 노드별 상세 통계
vpp# show runtime max
vpp# clear runtime
| CPU 카운터 | 정상 범위 (VPP) | 문제 징후 | 대응 |
|---|---|---|---|
| IPC (inst/cycle) | 2.0~3.5 | < 1.5 | 메모리 바운드, 프리페치 확인 |
| L1 I-cache miss% | < 2% | > 5% | 노드 그래프 단순화, Feature 최소화 |
| L1 D-cache miss% | < 5% | > 10% | CLIB_PREFETCH 추가, NUMA 확인 |
| Branch miss% | < 1% | > 3% | 벡터 크기 확인, 조건 분기 최소화 |
| LLC miss% | < 1% | > 3% | 워킹셋 크기 초과, FIB 크기 확인 |
VPP 성능 프로파일링(Profiling) 실전
VPP 성능 문제를 체계적으로 진단하려면, 내장 카운터 분석부터 시작하여 단계적으로 깊이를 늘려가야 합니다. 다음 의사결정 트리는 프로파일링 워크플로의 전체 흐름을 보여줍니다.
show runtime 출력 해석
show runtime은 VPP의 가장 기본적이면서도 강력한 성능 분석 도구입니다. 각 노드의 호출 횟수, 처리 벡터 수, 클럭 소비를 한눈에 보여줍니다.
vpp# show runtime
Time 120.3, 10 sec internal node vector rate 255.00
Thread 1 vpp_wk_0 (lcore 2)
Name State Calls Vectors Suspends Clocks Vectors/Call
dpdk-input polling 1523471 389688320 0 28.72 255.80
ethernet-input active 1523471 389688320 0 11.45 255.80
ip4-input active 1523468 389687552 0 8.33 255.80
ip4-lookup active 1523468 389687552 0 18.91 255.80
ip4-rewrite active 1523468 389687552 0 12.67 255.80
↑ ↑
Clocks/Call 낮을수록 좋음 Vectors/Call 높을수록 좋음
GigabitEthernet0/8/0-output active 1523468 389687552 0 6.22 255.80
GigabitEthernet0/8/0-tx active 1523468 389687552 0 9.81 255.80
Thread 2 vpp_wk_1 (lcore 4)
Name State Calls Vectors Suspends Clocks Vectors/Call
dpdk-input polling 1519832 388757248 0 29.01 255.78
ethernet-input active 1519832 388757248 0 11.52 255.78
ip4-input active 1519830 388756992 0 8.41 255.78
ip4-lookup active 1519830 388756992 0 19.05 255.78
ip4-rewrite active 1519830 388756992 0 12.74 255.78
핵심 지표(Metric)의 의미와 판단 기준은 다음과 같습니다:
| 지표 | 설명 | 양호 | 주의 | 나쁨 | 해석 |
|---|---|---|---|---|---|
| Vectors/Call | 한 번 호출에 처리한 패킷 수 | ≥ 200 | 50~200 | < 50 | 배치 효율. DPDK의 burst size(기본 256)에 가까울수록 좋습니다. |
| Clocks/Call | 한 번 호출에 소비한 CPU 사이클 | < 30 | 30~100 | > 100 | 노드 복잡도. ip4-lookup이 50 이상이면 FIB 크기나 캐시 문제를 의심하세요. |
| Calls | 노드가 호출된 총 횟수 | 워커 간 균등 분배 | 특정 워커의 Calls가 다른 워커의 2배 이상이면 RSS 불균형입니다. | ||
| Vectors | 처리된 총 패킷 수 | 시간 대비 선형 증가 | 총 처리량 계산: Vectors / Time = PPS | ||
| Suspends | 노드가 일시 중단된 횟수 | 0 | 간헐적 | 지속 발생 | 0이 아니면 버퍼 부족 또는 이벤트 대기 발생. dpdk-input의 Suspends > 0은 비정상입니다. |
| State: polling | 폴링 모드 동작 중 | dpdk-input은 항상 polling | polling이 아니면 인터럽트 모드이며, 고성능 시나리오에서는 부적합합니다. | ||
perf + FlameGraph 분석
show runtime의 Clocks/Call이 비정상적으로 높은 노드를 발견했다면, perf 도구로 해당 노드 내부의 CPU 핫스팟을 정밀 분석합니다.
# 1. VPP 워커 스레드 PID 확인
$ pgrep -a vpp
12345 /usr/bin/vpp -c /etc/vpp/startup.conf
# 2. 워커 스레드 TID 확인
$ ls /proc/12345/task/
12345 12347 12348 12349
# ↑ wk_0 ↑ wk_1 ↑ wk_2
# 3. perf top으로 실시간 핫스팟 확인 (특정 워커)
$ sudo perf top -t 12347 -g --no-children
# 4. perf record로 프로파일 기록 (30초)
$ sudo perf record -F 9999 -g --call-graph dwarf \
-t 12347 -o perf_vpp_wk0.data -- sleep 30
# 5. perf script로 텍스트 변환
$ sudo perf script -i perf_vpp_wk0.data > perf_vpp_wk0.txt
# 6. FlameGraph 생성
$ git clone https://github.com/brendangregg/FlameGraph.git
$ ./FlameGraph/stackcollapse-perf.pl perf_vpp_wk0.txt \
| ./FlameGraph/flamegraph.pl --title "VPP Worker 0" \
> vpp_wk0_flamegraph.svg
# 7. 브라우저에서 SVG 열기 (인터랙티브 탐색 가능)
$ xdg-open vpp_wk0_flamegraph.svg
FlameGraph에서 VPP 관련 함수를 읽는 방법은 다음과 같습니다:
- 그래프 노드 함수 (
*_node_fn):ip4_lookup_node_fn,ethernet_input_node_fn등. 이 함수들이 전체 폭의 대부분을 차지하는 것은 정상입니다. - 프레임워크 오버헤드:
vlib_main_loop,dispatch_node,vlib_frame_*함수들. 전체의 10~15% 이하여야 정상입니다. - 메모리 접근:
__memcpy_avx_unaligned,clib_memcpy등. 이 함수가 상위에 보이면 패킷 데이터 복사가 병목(Bottleneck)입니다. - FIB 조회:
ip4_fib_table_lookup_lb,mtrie_*함수. 이 부분이 넓으면 FIB 크기가 캐시를 초과한 것입니다.
vpp-dbg 패키지가 설치되어 있어야 합니다. Ubuntu/Debian에서는 apt install vpp-dbg, 직접 빌드한 경우 make build-release 대신 make build로 디버그 빌드를 사용하세요. 심볼이 없으면 FlameGraph에 16진수 주소만 표시됩니다.
병목 진단 시나리오별 가이드
실무에서 자주 발생하는 VPP 성능 문제와 그 진단·해결 방법을 정리합니다.
| 증상 | 가능한 원인 | 진단 명령 | 해결 방법 |
|---|---|---|---|
| Vectors/Call < 50 (낮은 배치 효율) |
트래픽 부족, 인터럽트 코얼레싱 과소, RSS 큐 과다 | show runtimeshow hardware-interfacesethtool -c <nic> |
코얼레싱 증가 (rx-usecs 100), RSS 큐 수를 워커 수와 일치, 폴링 모드 확인 |
| Clocks/Call > 100 (높은 노드 비용) |
캐시 미스, FIB 대형화, Feature arc 과다 | perf stat -e cache-misses -t <tid>show ip fib summary |
NUMA 바인딩 확인, FIB 프리픽스 집약, 불필요한 Feature 제거 |
| 워커 간 부하 불균형 (Calls 편차 > 2x) |
RSS 해시(Hash) 편향, 특정 플로우 과대 | show runtime 워커별 비교ethtool -x <nic> |
RSS 해시 키 변경, indirection table 재설정, toeplitz-key 조정 |
| L1 I-cache miss > 5% (명령어 캐시 부족) |
노드 그래프 너무 깊음, 플러그인 과다 | perf stat -e L1-icache-load-misses -t <tid>show node |
불필요한 플러그인 비활성화, Feature arc 최소화, 노드 병합 검토 |
| LLC miss > 3% (L3 캐시 초과) |
FIB 워킹셋 > LLC 크기, NUMA 원격 접근 | perf stat -e LLC-load-misses -t <tid>numactl -H |
NIC/메모리/코어를 동일 NUMA에 배치, FIB 크기 축소, Huge Pages 확인 |
| 메모리 대역폭(Bandwidth) 포화 (IPC < 1.5, LLC miss 높음) |
워커 수 과다, 메모리 채널 부족 | perf stat -e LLC-loads,LLC-load-missesdmidecode -t memory |
워커 수 감소, 메모리 채널 채움 (4ch → 8ch), 패킷 버퍼 크기 최적화 |
| dpdk-input no-buffer 에러 | 버퍼 풀 고갈, HugePages 부족 | show errorsshow bufferscat /proc/meminfo | grep Huge |
buffers-per-numa 증가, HugePages 할당 확대, 패킷 누수 노드 점검 |
| 특정 노드만 Clocks 급증 (예: nat44-out2in) |
세션 테이블 대형화, 해시 충돌 | show nat44 sessions summaryperf top -t <tid> |
최대 세션 수 제한, 세션 타임아웃 단축, 해시 버킷 수 조정 |
show runtime으로 전체 조망 → (2) show errors로 에러 카운터 확인 → (3) Vectors/Call 또는 Clocks/Call 이상 노드 특정 → (4) perf stat으로 CPU 카운터 확인 → (5) 필요 시 perf record + FlameGraph로 심층 분석.
앞 절에서 CPU 핀닝, NUMA 인식, 인터럽트/폴링 모드 비교를 다루었습니다. 이어지는 두 표는 폴링 전략 선택 가이드와 성능 튜닝 체크리스트로, 실제 운영 환경에서 성능 목표와 전력 소비 사이의 균형을 잡을 때 바로 활용할 수 있도록 정리한 요약입니다.
폴링 전략 비교
| 전략 | CPU 사용률 (유휴 시) | 지연 | 처리량 | 적합한 환경 |
|---|---|---|---|---|
| Busy poll (기본) | 100% | 최소 | 최대 | 전용 네트워크 어플라이언스 |
| poll-sleep-poll | 5~30% | 낮음 | 높음 | 범용 서버 (전력 절감) |
| Interrupt mode | ~0% (유휴) | 중간 (첫 패킷 지연) | 중간 | 저트래픽 환경 |
| Adaptive | 동적 | 동적 | 동적 | 가변 트래픽 패턴 |
# startup.conf — poll-sleep-poll 모드
cpu {
main-core 0
corelist-workers 1-3
}
# poll-sleep-poll: 패킷 없으면 짧은 sleep 후 재시도
unix {
poll-sleep-usec 100 /* 100μs sleep (유휴 시) */
}
# interrupt 모드 (AF_XDP 등 지원 드라이버)
dpdk {
dev 0000:00:08.0 {
num-rx-queues 2
}
}
# 런타임 모드 전환
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 interrupt
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 polling
vpp# set interface rx-mode GigabitEthernet0/8/0 queue 0 adaptive
성능 튜닝 체크리스트
| 항목 | 확인 방법 | 권장값 |
|---|---|---|
| Hugepage | cat /proc/meminfo | grep Huge | 워커당 2GB+, NUMA 균등 분배 |
| CPU 격리 | cat /proc/cmdline | isolcpus, nohz_full, rcu_nocbs |
| NIC RSS | show hardware-interfaces detail | 큐 수 = 워커 수 |
| 버퍼 크기 | show buffers | buffers-per-numa 32768+ |
| 벡터 효율 | show runtime → Vectors/Call | 64+ (높을수록 좋음) |
| 에러 카운터 | show errors | 0 (증가 시 원인 분석) |
| 인터럽트 친화도(Affinity) | /proc/interrupts | VPP 코어와 겹치지 않게 |
| NUMA 배치 | numactl --hardware | NIC/CPU/메모리 동일 노드 |
| MTU 최적화 | show interface | 점보프레임(9000) 가능 시 활성화 |
| TCP 튜닝 (VCL) | show session | 소켓 버퍼, 윈도우 크기 조정 |
Clocks/Call이 높은 노드가 병목이며, Vectors/Call이 1이면 배치 효율이 낮습니다. clear runtime → 부하 인가 → show runtime으로 실시간 프로파일링하세요.
RX 큐 → 워커 배치 (rx-placement)
RSS로 NIC이 만든 멀티 큐를 어느 워커가 폴링할지는 VPP가 자동으로 라운드로빈 배치하지만, 워커 부하가 편중되면 명시적으로 재배치할 수 있습니다. set interface rx-placement 명령은 NIC 큐를 특정 워커 스레드에 직접 매핑하며, 무중단으로 적용됩니다.
# 현재 큐 → 워커 매핑 확인
vpp# show interface rx-placement
# 큐 0을 워커 1번에, 큐 1을 워커 2번에 고정
vpp# set interface rx-placement GigabitEthernet0/8/0 queue 0 worker 1
vpp# set interface rx-placement GigabitEthernet0/8/0 queue 1 worker 2
# main 스레드(워커 0)로 되돌리기 — 일반적이지 않음
vpp# set interface rx-placement GigabitEthernet0/8/0 queue 0 main
show runtime에서 워커별Vectors/Call비교 — 한 워커만 유독 크면 RSS 해시 편향.show interface rx-placement로 큐 분배 확인 — 큐 수 < 워커 수면 일부 워커가 idle.- NIC RSS 큐 수를 워커 수와 일치시키거나,
num-rx-queues를 startup.conf에서 늘립니다. - 특정 플로우가 한 큐로 몰리면
show hardware-interfaces의 RSS hash key/types를 확인하고, 4-tuple(IP+port) 해시로 전환합니다. - 그래도 안 풀리면 Flow Director로 상위 N개 elephant flow를 명시 큐로 분산합니다.
하드웨어 오프로드 운영 런북 — 설정부터 장애 복구까지
하드웨어 오프로드의 세부 원리와 종류별 활성화 방법은 데이터 경로 문서의 하드웨어 오프로드 심화에서 다뤘습니다. 이 절은 운영 관점에서 (1) 도입 전 준비 → (2) 단계별 활성화 → (3) 상시 모니터링 → (4) 장애 대응 순으로 실전 런북을 제시합니다. 운영 엔지니어가 이 체크리스트만으로 오프로드 구성을 일관되게 관리할 수 있도록 구성했습니다.
도입 전 준비 — 5가지 확인
- NIC 펌웨어(Firmware) 버전 확인:
ethtool -i <pf>로 firmware를 읽어 벤더 릴리스 노트의 권장 버전과 비교. - DPDK PMD 버전 확인: VPP가 기대하는 DPDK와 NIC 펌웨어 호환 매트릭스 점검.
- Hugepages: 페이지 수와 NUMA 배치. DMA 버퍼 주소가 NIC가 속한 NUMA 노드에 있어야 합니다.
- PCIe 링크 상태:
lspci -vv | grep -i LnkSta로 Gen4 x16 등 기대 속도가 맞는지 확인. 한 단계만 떨어져도 100 G 라인레이트가 깨집니다. - IOMMU/VT-d:
dmesg | grep -i iommu로 사용 여부와 PassThru 모드 확인.
단계별 활성화 — 작은 기능부터 한 번에 하나씩
| 순서 | 기능 | 검증 카운터 | 문제 시 롤백 |
|---|---|---|---|
| 1 | 체크섬 오프로드 | ip4-input bad-checksum = 0 | 자동 폴백(VPP가 SW 계산) |
| 2 | RSS 멀티큐 | 워커별 vectors/call 균등 | 큐 1개로 축소 |
| 3 | TSO | 전송 처리량 상승, 재전송 0 | 인터페이스 재생성 시 gso 플래그 제거 또는 startup.conf 수정 |
| 4 | Flow Director | show flow 설치 성공 | 규칙 삭제 후 RSS만 사용 |
| 5 | Crypto (cryptodev) | show crypto engines 처리량 | crypto-engine native |
| 6 | IPsec inline | SA 카운터 일치 | SA 플래그에서 hw-offload 제거 |
상시 모니터링 지표
# (1) 오프로드 플래그가 살아 있는가
vpp# show hardware-interfaces GigabitEthernet0/8/0 | grep -E 'offload|rss|flow'
# (2) 오프로드 폴백 카운터 — 0이어야 정상
vpp# show errors | grep -iE 'fallback|software-csum|crypto-fallback'
# (3) 워커 부하 분포 — 편차가 심하면 RSS/flow 튜닝 필요
vpp# show runtime | awk 'NR>4 {print $1,$2,$4}' | sort -k3 -n
# (4) PCIe 에러 — 링크가 흔들리면 오프로드가 스스로 비활성화되기도 합니다
sudo lspci -vv -s 0000:81:00.0 | grep -iE 'lnksta|correctable|uncorrectable'
# (5) NIC 통계와 VPP 통계 일치성
vpp# show hardware-interfaces GigabitEthernet0/8/0 verbose | tail -30
운영 모니터링은 Prometheus로 수집하는 편이 편합니다. VPP stats segment(/run/vpp/stats.sock)를 vpp-exporter로 뽑아 내면 vpp_if_rx_packets, vpp_node_clocks 같은 지표가 자동 생성됩니다. 오프로드 폴백 카운터에 경보(> 100 events/s)를 걸어두면 조용한 퇴행(silent regression)을 잡을 수 있습니다.
장애 시나리오와 복구
| 증상 | 첫 확인 | 예상 원인 | 복구 절차 |
|---|---|---|---|
| 갑자기 체크섬 에러 급증 | show errors, dmesg | 펌웨어 업그레이드 후 호환성 또는 QinQ 트래픽 유입 | 체크섬 오프로드 일시 off, 원인 분석 후 재활성화 |
| 한 워커만 100% CPU | show runtime, RSS 해시 키 | 단일 IP 집중, 2-tuple 해시 | 4-tuple 해시로 전환, Flow Director로 분산 |
| 처리량이 어제보다 반토막 | lspci LnkSta, ethtool -S | PCIe 링크 downgrade, NIC thermal throttle | 카드 재시작, 물리 인터페이스 확인 |
| Flow 규칙이 사라짐 | show flow | PMD 리셋(링크 다운) 시 룰 소실 | 재설치 스크립트 자동 재적용 |
| cryptodev 지연 급증 | show crypto engines | QAT 디스크립터 부족 또는 펌웨어 오류 | num-crypto-mbufs 증설, 펌웨어 재적용 |
| inline IPsec replay 경고 | show ipsec sa | 시간 동기화 문제, anti-replay 윈도우 부족 | chrony 점검, 윈도우 확장 |
| SR-IOV VF 링크 다운 | 호스트 ethtool, PF 로그 | PF 리셋 전파, VF trust 정책 | VF 재바인딩, trust on 요청 |
변경 관리·롤아웃 원칙
- 카나리 먼저: 전체 클러스터의 5~10 % 노드에 먼저 오프로드를 켜고 24~72시간 관찰. 이후 단계적 확장.
- 동일 벤더/버전 고정: NIC 모델·펌웨어·DPDK 버전이 섞이면 동일한 오프로드 플래그가 다르게 동작합니다. 배치 단위로 버전을 고정하시기 바랍니다.
- 롤백 스크립트 준비: 각 오프로드를 끄는 단일 명령을 문서화하여, 장애 시 즉시 실행할 수 있도록 준비합니다.
- 펌웨어 업그레이드는 별개의 변경: 기능 롤아웃과 같은 창에서 섞지 마시기 바랍니다. 원인 분리가 불가능해집니다.
설치 및 설정
패키지 설치
# Ubuntu/Debian — FD.io 공식 저장소
$ curl -s https://packagecloud.io/install/repositories/fdio/release/script.deb.sh | sudo bash
$ sudo apt-get install vpp vpp-plugin-core vpp-plugin-dpdk vpp-dev
# CentOS/RHEL
$ curl -s https://packagecloud.io/install/repositories/fdio/release/script.rpm.sh | sudo bash
$ sudo yum install vpp vpp-plugins vpp-devel
# 서비스 시작
$ sudo systemctl start vpp
$ sudo systemctl enable vpp
소스 빌드
# 소스 클론
$ git clone https://gerrit.fd.io/r/vpp
$ cd vpp
# 의존성 설치 (Ubuntu)
$ make install-dep
$ make install-ext-deps
# 빌드
$ make build /* 디버그 빌드 */
$ make build-release /* 릴리스 빌드 */
# 실행
$ make run /* 빌드 디렉터리에서 직접 실행 */
startup.conf 설정
/* /etc/vpp/startup.conf — 프로덕션 설정 예제 */
unix {
nodaemon /* 포그라운드 실행 (디버깅 시) */
cli-listen /run/vpp/cli.sock /* CLI 소켓 경로 */
log /var/log/vpp/vpp.log /* 로그 파일 */
full-coredump /* 코어 덤프 활성화 */
exec /etc/vpp/setup.gate /* 시작 시 실행할 CLI 스크립트 */
}
api-trace {
on /* API 추적 활성화 */
}
api-segment {
gid vpp /* API 소켓 그룹 */
}
cpu {
main-core 0
corelist-workers 2,4,6,8
}
dpdk {
dev default {
num-rx-queues 4
num-tx-queues 4
num-rx-desc 1024
num-tx-desc 1024
}
dev 0000:03:00.0
dev 0000:03:00.1
uio-driver vfio-pci /* VFIO 사용 */
}
buffers {
buffers-per-numa 32768
default data-size 2048
}
plugins {
plugin default { disable } /* 기본 비활성화 */
plugin dpdk_plugin.so { enable }
plugin acl_plugin.so { enable }
plugin nat_plugin.so { enable }
plugin linux_cp_plugin.so { enable }
}
startup.conf 섹션별 레퍼런스
앞 절의 startup-conf 예시는 실전 템플릿입니다. 여기에는 각 섹션의 전체 옵션 표를 정리해 두어, 특정 파라미터를 찾을 때 빠르게 참조할 수 있도록 합니다. 표에 등장하는 옵션 이름은 VPP 26.02 기준이며, 릴리스마다 일부가 추가/삭제될 수 있으므로 정확한 현재 값은 공식 26.02 문서와 src/vpp/conf/startup.conf를 병행 확인하시기 바랍니다.
unix { } — 프로세스/터미널
| 옵션 | 설명 |
|---|---|
interactive | 터미널에 CLI 프롬프트 노출 (foreground 실행 시) |
nodaemon | 데몬화하지 않고 포그라운드 유지. systemd 서비스와 함께 사용 |
log /var/log/vpp/vpp.log | 로그 파일 경로 |
full-coredump | 코어 덤프에 모든 메모리 포함 (디버그용) |
cli-listen /run/vpp/cli.sock | vppctl 접속용 유닉스 소켓 |
gid vpp / uid vpp | 프로세스 권한 강하 |
exec <file> | 부팅 시 자동 실행할 CLI 스크립트 |
cli-no-banner | CLI 배너 숨김 |
poll-sleep-usec <N> | idle 시 usleep 기간 (전력 절약) |
cpu { } — 워커 스레드
| 옵션 | 설명 |
|---|---|
main-core <N> | 메인 스레드를 핀닝할 CPU 코어 |
corelist-workers <list> | 워커 스레드를 배치할 코어 목록 (예: 1-3,5-7) |
workers <N> | 워커 스레드 개수 (corelist-workers와 택1) |
skip-cores <N> | 0번부터 N개 코어 건너뛰기 |
scheduler-policy fifo | 실시간 스케줄러 사용 (fifo/rr/other) |
scheduler-priority <N> | 실시간 스케줄 우선순위 (1~99) |
buffers { } — 버퍼 풀
| 옵션 | 설명 |
|---|---|
buffers-per-numa <N> | NUMA 노드당 사전 할당 버퍼 수 (기본 16384, 고PPS는 128K+) |
default data-size <N> | 기본 버퍼 데이터 크기 (기본 2048 바이트) |
page-size default-hugepage | Hugepage 크기(2M/1G) 선택 |
dpdk { } — DPDK
| 옵션 | 설명 |
|---|---|
dev <PCI-addr> | DPDK에 바인딩할 NIC PCI 주소 |
dev default { num-rx-queues <N> } | 기본 RX 큐 수 |
num-mbufs <N> | DPDK mbuf 풀 크기 |
no-pci | PCI 스캔 생략 (DPDK 완전 비활성) |
uio-driver vfio-pci / uio_pci_generic | IOMMU 드라이버 선택 |
socket-mem <mb> | NUMA별 DPDK 메모리 할당 |
dev default { tso on } | TSO 오프로드 활성화 |
plugins { } — 플러그인 로드/제외
| 옵션 | 설명 |
|---|---|
plugin default { disable } | 모든 플러그인 기본 비활성 — 이어서 필요한 것만 enable |
plugin dpdk_plugin.so { enable } | 특정 플러그인 활성 |
plugin acl_plugin.so { disable } | 특정 플러그인 비활성 |
path /usr/lib/x86_64-linux-gnu/vpp_plugins | 플러그인 검색 경로 |
session { } — 세션 레이어
| 옵션 | 설명 |
|---|---|
enable | 세션 레이어 활성 (호스트 스택 쓸 때 필수) |
event-queue-length <N> | 애플리케이션-세션 이벤트 큐 크기 |
preallocated-sessions <N> | 부팅 시 사전 할당 세션 수 |
preallocated-half-open-sessions <N> | half-open 세션 사전 할당 |
local-endpoints-table-memory <size> | 로컬 엔드포인트 테이블 메모리 |
v4-session-table-buckets <N> | IPv4 세션 해시 버킷 수 |
tls { } — TLS 엔진
| 옵션 | 설명 |
|---|---|
use-test-cert-in-ca | 개발용 자체 서명 CA 자동 로드 |
ca-dir /etc/ssl/certs | 시스템 CA 저장소 |
engine openssl / picotls / mbedtls | 기본 TLS 엔진 선택 |
first-segment-size <size> | TLS 첫 세그먼트 크기 (튜닝) |
statseg { } — Stats Segment
| 옵션 | 설명 |
|---|---|
size <size> | 공유 메모리 크기 (기본 96M) |
per-node-counters on | 노드별 카운터 노출 (perfmon/Prometheus에서 필수) |
update-interval <sec> | 갱신 주기 (기본 10초) |
socket-name /run/vpp/stats.sock | stats 소켓 경로 |
socksvr { } — 바이너리 API 소켓
| 옵션 | 설명 |
|---|---|
socket-name /run/vpp/api.sock | Binary API 유닉스 소켓 경로. govpp·vat2·papi가 사용 |
default | 기본 설정 사용 |
api-trace { } — API 트레이스
| 옵션 | 설명 |
|---|---|
on | API 호출 기록 활성 (디버그·감사용) |
nitems <N> | 트레이스 링 버퍼 크기 |
save-api-table <file> | 종료 시 트레이스 저장 경로 |
memif { } — memif 기본값
| 옵션 | 설명 |
|---|---|
socket-filename /run/vpp/memif.sock | memif 제어 소켓 경로 |
vpp -c /etc/vpp/startup.conf -f로 foreground 실행해 파싱 에러를 즉시 확인할 수 있습니다. 또한 부팅 후 show version verbose와 show threads로 적용된 CPU 핀닝과 플러그인 로드를 재확인하시기 바랍니다.
활용 사례
VPP는 라인레이트 패킷 처리가 필요한 여러 분야에서 쓰입니다. 전형적인 배치 형태와 사용 플러그인을 한 표로 정리합니다. 각 용도의 세부 구성은 해당 주제의 기술 문서에서 다룹니다.
| 용도 | 역할 | 핵심 플러그인 · 기능 | 관련 문서 |
|---|---|---|---|
| 가상 스위치(vSwitch) | VM/Pod 간 고성능 스위칭 | vhost-user, L2 브릿징, Classify | 데이터 경로 (L2~L4) |
| CPE · VNF | 통신사 고객 구내 장비 소프트웨어화 | 라우팅·NAT·IPsec·QoS 통합 | 보안과 터널링, 오버레이 · 실전 시나리오 |
| 5G UPF | 모바일 코어의 User Plane Function | GTP-U 종단, SRv6, PFCP, upf 플러그인 | SRv6 |
| Kubernetes CNI | Pod 네트워킹 가속 | Calico/VPP, BGP, ACL→NetworkPolicy | VPP + Kubernetes 통합 |
| L4 로드 밸런서 | 고성능 분산 | lb 플러그인, Maglev 해시, DSR | L3 라우팅 (FIB) |
| 보안 게이트웨이 | IDS/IPS·SSL Inspection 연동 | ACL, TPROXY, SSL Inspection | SSL Inspection |
| IPsec VPN 게이트웨이 | 사이트 간 암호화 터널 | ipsec 플러그인, IKEv2, cryptodev 오프로드 | 실전 IPsec VPN |
VPP 운영 가이드
프로덕션 환경에서 VPP를 안정적으로 운영하려면 무중단 설정 변경, 재시작(Reboot) 절차, 고가용성 구성, 용량 산정, 로깅 체계를 체계적으로 관리해야 합니다. 이 섹션에서는 실무에서 반드시 알아야 할 운영 지침을 다룹니다.
무중단 설정 변경
VPP는 많은 설정을 런타임에 변경할 수 있지만, 일부 설정은 프로세스 재시작이 필요합니다. 운영 중 서비스 중단 없이 변경 가능한 항목과 그렇지 않은 항목을 명확히 구분해야 합니다.
| 설정 항목 | 런타임 변경 | 재시작 필요 | 비고 |
|---|---|---|---|
| 인터페이스 IP 주소 | O | set interface ip address | |
| 라우팅 테이블(Routing Table) (route 추가/삭제) | O | ip route add/del | |
| ACL 규칙 | O | acl-plugin CLI | |
| NAT44 매핑 추가/삭제 | O | nat44 add static mapping | |
| 인터페이스 생성 (tap, memif 등) | O | 새 인터페이스 동적 생성 가능 | |
| 로그 레벨 변경 | O | set logging class | |
| Hugepage 크기/수량 | O | startup.conf buffers 섹션 | |
| 워커 스레드 수 | O | startup.conf cpu 섹션 | |
| DPDK 인터페이스 바인딩 | O | startup.conf dpdk 섹션 | |
| 메인 코어 지정 | O | startup.conf cpu 섹션 | |
| 플러그인 로드/언로드 | O | startup.conf plugins 섹션 | |
| 소켓 경로 변경 | O | startup.conf unix 섹션 |
# 안전한 런타임 설정 변경 예시
# 1) 인터페이스 IP 변경
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
# 2) 라우팅 테이블 업데이트
vpp# ip route add 10.0.0.0/8 via 192.168.1.254
vpp# ip route del 172.16.0.0/12 via 192.168.1.254
# 3) NAT44 매핑 추가
vpp# nat44 add static mapping local 10.0.0.100 22 external 203.0.113.50 2222 tcp
vpp# show nat44 static mappings
# 4) ACL 규칙 동적 적용
vpp# set acl-plugin acl permit src 10.0.0.0/24 dst 0.0.0.0/0
vpp# set acl-plugin interface GigabitEthernet0/8/0 input acl 0
exec 명령을 사용하면 여러 CLI 명령을 하나의 스크립트 파일로 묶어 원자적(Atomic)으로 실행할 수 있습니다. 개별 명령을 하나씩 입력하는 것보다 설정 오류를 줄이고, 일관된 상태를 보장합니다.
# /etc/vpp/reconfigure-nat.cli — NAT 재구성 배치 스크립트
nat44 del static mapping local 10.0.0.100 22 external 203.0.113.50 2222 tcp
nat44 add static mapping local 10.0.0.200 22 external 203.0.113.50 2222 tcp
nat44 add static mapping local 10.0.0.201 80 external 203.0.113.51 80 tcp
nat44 add static mapping local 10.0.0.201 443 external 203.0.113.51 443 tcp
# VPP CLI에서 배치 실행
vpp# exec /etc/vpp/reconfigure-nat.cli
# 결과 확인
vpp# show nat44 static mappings
delete interface)는 해당 인터페이스에 연결된 모든 세션, 라우팅, NAT 매핑을 즉시 제거합니다. 프로덕션에서는 먼저 set interface state ... down으로 비활성화하고 트래픽이 다른 경로로 전환된 것을 확인한 후 삭제하세요.
우아한 재시작과 설정 영속성
VPP는 자체적인 설정 영속성 메커니즘을 제공하지 않습니다. startup.conf의 exec 지시문을 통해 부팅 시 CLI 스크립트를 자동 실행하여 원하는 상태를 재구성합니다.
# /etc/vpp/startup.conf — exec 스크립트 지정
unix {
nodaemon
log /var/log/vpp/vpp.log
full-coredump
cli-listen /run/vpp/cli.sock
exec /etc/vpp/setup.gate ← 부팅 시 자동 실행
}
cpu {
main-core 0
corelist-workers 1-3
}
buffers {
buffers-per-numa 16384
default data-size 2048
}
dpdk {
dev 0000:00:08.0
}
# /etc/vpp/setup.gate — 부팅 시 자동 실행 스크립트
# 순서가 중요합니다: 인터페이스 → IP → 라우팅 → NAT → ACL
# 1단계: 인터페이스 활성화
set interface state GigabitEthernet0/8/0 up
set interface state GigabitEthernet0/9/0 up
# 2단계: IP 주소 할당
set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
set interface ip address GigabitEthernet0/9/0 10.0.0.1/24
# 3단계: 라우팅 설정
ip route add 0.0.0.0/0 via 192.168.1.254
ip route add 172.16.0.0/12 via 10.0.0.254
# 4단계: NAT44 설정
nat44 plugin enable sessions 131072
set interface nat44 in GigabitEthernet0/9/0 out GigabitEthernet0/8/0
nat44 add static mapping local 10.0.0.100 22 external 192.168.1.1 2222 tcp
nat44 add static mapping local 10.0.0.101 80 external 192.168.1.1 80 tcp
# 5단계: ACL 설정
set acl-plugin acl permit+reflect src 10.0.0.0/24 dst 0.0.0.0/0
set acl-plugin interface GigabitEthernet0/9/0 input acl 0
고가용성(HA)과 장애 조치
프로덕션 환경에서는 단일 VPP 인스턴스의 장애가 서비스 중단으로 이어지지 않도록 고가용성 구성이 필수입니다. VPP는 여러 HA 패턴을 지원합니다.
| HA 패턴 | 구성 방식 | 장점 | 한계 |
|---|---|---|---|
| Active-Standby (VRRP) | linux-cp 플러그인으로 커널 VRRP와 연동 | 구성 단순, 검증된 방식 | 절체 시 1~3초 다운타임 |
| Active-Active (ECMP) | 상위 라우터에서 ECMP로 분산 | 다운타임 없음, 확장 용이 | 세션 고정(affinity) 필요 |
| NAT 세션 미러링 | nat44 ha CLI로 standby에 세션 복제 | NAT 세션 유지 | 대역폭 소모, 지연 발생 가능 |
| IPsec SA 재수립 | IKEv2 re-keying으로 SA 재수립 | 표준 기반, 호환성 높음 | 재수립 동안 패킷 손실 가능 |
# NAT44 HA 구성 — 세션 미러링
# Primary 노드 설정
vpp# nat44 ha listener 10.0.0.1:20000 path-mtu 1400
vpp# nat44 ha failover 10.0.0.2:20000
# Standby 노드 설정
vpp# nat44 ha listener 10.0.0.2:20000 path-mtu 1400
vpp# nat44 ha failover 10.0.0.1:20000
# 세션 동기화 확인
vpp# show nat44 ha
listener 10.0.0.1:20000
failover 10.0.0.2:20000
in-sync sessions: 45832
pending updates: 3 ← 0에 가까울수록 양호
last resync: 2.3s ago
# Active-Standby VRRP 연동 (linux-cp 플러그인)
# linux-cp 활성화: startup.conf에 추가
# plugins { plugin linux_cp_plugin.so { enable } }
# 커널에 미러 인터페이스 생성
vpp# lcp create GigabitEthernet0/8/0 host-if eth-vpp0
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
# 커널에서 keepalived(VRRP) 구성
# /etc/keepalived/keepalived.conf에서 eth-vpp0 사용
# VIP 192.168.1.254를 VRRP로 관리
VPP 인스턴스의 활성(liveness) 상태를 외부에서 확인하는 방법은 다음과 같습니다.
# 방법 1: CLI 소켓을 통한 헬스 체크
$ vppctl -s /run/vpp/cli.sock show version 2>&1 | grep -q "vpp" && echo "UP" || echo "DOWN"
# 방법 2: API 소켓 존재 여부 확인
$ test -S /run/vpp/api.sock && echo "API socket exists"
# 방법 3: systemd 상태 확인
$ systemctl is-active vpp.service
# 방법 4: 프로세스 체크 + CLI 응답 확인 (모니터링 스크립트)
#!/bin/bash
if pgrep -x vpp_main > /dev/null; then
RESULT=$(timeout 3 vppctl show runtime 2>&1)
if [ $? -eq 0 ]; then
echo "VPP healthy"
else
echo "VPP process alive but CLI unresponsive"
fi
else
echo "VPP process not running"
fi
용량 산정과 사이징
VPP의 성능은 메모리, CPU 코어 수, Hugepage 설정에 크게 의존합니다. 워크로드에 맞는 리소스를 사전에 산정해야 운영 중 성능 저하나 OOM(Out of Memory) 장애를 방지할 수 있습니다.
| 리소스 항목 | 산정 공식 | 참고 수치 |
|---|---|---|
| 버퍼 풀 메모리 | buffers-per-numa × 2.2KB | 16K 버퍼 ≈ 35MB, 32K ≈ 70MB |
| FIB 테이블 (BGP 풀 테이블) | 경로 수 비례 | ~900K 경로 ≈ ~200MB |
| NAT44 세션 테이블 | 세션 수 × ~500B | 1M 세션 ≈ ~500MB |
| TCP 호스트 스택 세션 | 세션 수 × ~2KB (세션 + FIFO 쌍) | 1M 세션 ≈ ~2GB |
| TLS 컨텍스트 (OpenSSL) | 동시 연결 × ~50KB | 10K 연결 ≈ ~500MB |
| TLS 컨텍스트 (picotls) | 동시 연결 × ~5KB | 10K 연결 ≈ ~50MB |
| 워크로드 | 코어당 처리량 (64B 패킷) | 코어당 처리량 (IMIX) | 비고 |
|---|---|---|---|
| L3 포워딩 (IPv4) | ~15 Mpps | ~10 Gbps | 기본 ip4-lookup 노드 |
| NAT44 (endpoint-dependent) | ~5 Mpps | ~6 Gbps | 세션 수에 따라 감소 |
| IPsec (AES-128-GCM) | ~3 Mpps | ~5 Gbps | QAT 가속 시 ~8 Gbps |
| TLS 종단 (RSA-2048) | ~2K CPS | ~5 Gbps (bulk) | 핸드셰이크가 병목 |
| ACL (stateful, 1K 규칙) | ~8 Mpps | ~8 Gbps | 규칙 수에 따라 감소 |
# Hugepage 산정 예시: 25GbE NIC, 워커 3개
# 1) 필요 버퍼 수 계산
# NIC 링 크기: rx 2048 + tx 2048 = 4096/NIC
# 파이프라인 버퍼: 워커당 ~8192개 (벡터 처리 + 큐잉)
# 총 버퍼: 4096 + (3 × 8192) = 28672 → 32768 (2의 거듭제곱)
# 2) 메모리 = 32768 × 2.2KB ≈ 72MB (버퍼)
# + FIB/NAT/세션 등 추가 메모리
# → 총 1GB (여유 포함) ~ 2GB 권장
# 3) 2MB Hugepage 기준: 1GB / 2MB = 512페이지
# 1GB Hugepage 기준: 1개 (또는 2개 권장)
# 커널 부트 파라미터
GRUB_CMDLINE_LINUX="default_hugepagesz=2M hugepagesz=2M hugepages=1024"
# 또는 런타임 할당 (권장하지 않음 — 단편화 가능)
$ echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 할당 상태 확인
$ cat /proc/meminfo | grep Huge
HugePages_Total: 1024
HugePages_Free: 980
HugePages_Rsvd: 44
Hugepagesize: 2048 kB
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages로 노드별 할당이 가능합니다. startup.conf의 buffers-per-numa도 NUMA 노드별로 적용됩니다.
로깅과 Syslog 통합
VPP는 모듈별 로그 레벨 제어, 파일 로깅, syslog 전달을 지원합니다. 운영 환경에서는 로그를 중앙 로그 시스템으로 전달하여 장애 추적과 감사(audit)에 활용합니다.
# 파일 로그: startup.conf에서 설정
unix {
log /var/log/vpp/vpp.log ← VPP 메인 로그 파일
full-coredump
}
# 로그 파일 로테이션: logrotate 연동
# /etc/logrotate.d/vpp
# /var/log/vpp/vpp.log {
# daily
# rotate 14
# compress
# missingok
# notifempty
# postrotate
# vppctl set logging size 0 ← 로그 버퍼 리셋
# endscript
# }
# 모듈별 로그 레벨 설정 (런타임)
# 현재 로그 설정 확인
vpp# show logging
default level: warn
unthrottled: 0
# 특정 모듈의 로그 레벨 변경
vpp# set logging class dpdk level debug
vpp# set logging class nat level info
vpp# set logging class acl-plugin level warn
vpp# set logging class session level notice
# 사용 가능한 로그 클래스 확인
vpp# show logging class
Name Level
acl-plugin warn
dpdk debug
nat info
session notice
...
# Syslog 전달 설정 — 버전별로 CLI 이름이 달라질 수 있어
# 먼저 '? logging' 으로 사용 가능한 하위 명령을 확인하시기 바랍니다.
vpp# ? logging
# 반환되는 목록에서 실제 원격 전송 명령을 선택해 적용합니다.
# 예: set logging syslog-sender <ipv4> (릴리스에 따라 명령 이름 상이)
# 실시간 로그 확인 (디버깅 시 유용)
vpp# show log
# 특정 로그 엔트리 수 제한하여 확인
vpp# show logging count 50
# 로그 버퍼 크기 설정
vpp# set logging size 4096
warn으로 유지하세요. debug 레벨은 대량의 로그를 생성하여 성능에 영향을 줄 수 있으므로, 장애 추적 시에만 일시적으로 활성화하고 완료 후 원래 레벨로 복원하세요. set logging class dpdk level warn으로 즉시 변경 가능합니다.
VPP 플러그인 개발
VPP 플러그인은 그래프 노드(graph node)를 등록하여 패킷 처리 파이프라인(Pipeline)에 새로운 기능을 삽입합니다. 커널 모듈(Kernel Module)과 달리 유저스페이스에서 동작하므로 크래시가 시스템 전체에 영향을 주지 않으며, 핫 리로드가 가능합니다.
VPP 핵심 API 함수 상세 분석
VPP 플러그인 개발에서 가장 빈번하게 사용되는 핵심 API 함수들을 상세히 분석합니다. 이 함수들은 패킷 처리 노드의 fn() 콜백 내에서 호출되며, 각 함수의 동작 원리와 올바른 사용 패턴을 이해하는 것이 고성능 플러그인 작성의 핵심입니다.
validate_buffer_enqueue_x4는 quad-loop 최적화와 함께 익히는 편이 효율적이고, ⑤의 카운터/트레이스는 관측성을 붙이기 위해 마지막에 추가합니다.
vlib_frame_vector_args() — 프레임에서 버퍼 인덱스 배열 획득
vlib_frame_vector_args()는 노드 함수에 전달된 프레임(vlib_frame_t)에서 버퍼 인덱스 배열의 시작 포인터를 반환합니다. 반환 타입은 u32*이며, 각 원소는 vlib_buffer_pool 내의 버퍼 인덱스입니다. 배열의 유효 크기는 frame->n_vectors로 확인할 수 있습니다.
/* vlib_frame_vector_args() 기본 사용 패턴 */
static uword
my_node_fn (vlib_main_t *vm,
vlib_node_runtime_t *node,
vlib_frame_t *frame)
{
u32 n_left_from, *from;
/* 프레임에서 버퍼 인덱스 배열 포인터 획득 */
from = vlib_frame_vector_args (frame);
/* 처리할 패킷 수 (1~VLIB_FRAME_SIZE, 최대 256) */
n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
u32 bi0 = from[0];
vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);
/* b0->data + b0->current_data 위치부터 패킷 데이터 접근 */
ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* 패킷 처리 로직 ... */
from += 1;
n_left_from -= 1;
}
return frame->n_vectors;
}
vlib_frame_vector_args()가 반환하는 포인터는 프레임 구조체 바로 뒤에 위치한 인라인 배열을 가리킵니다. 프레임은 노드 런타임에 의해 사전 할당되므로 별도의 메모리 할당이 발생하지 않습니다. 반환 값의 수명은 현재 노드 함수 실행 동안에만 유효합니다.
vlib_get_next_frame() / vlib_put_next_frame() — 다음 노드로의 프레임 관리
vlib_get_next_frame()은 다음 노드로 패킷을 전달하기 위한 출력 프레임의 참조를 획득합니다. vlib_put_next_frame()은 처리 완료 후 프레임을 반환하며, 실제로 전달할 벡터 수를 갱신합니다. 이 두 함수는 반드시 쌍(pair)으로 호출해야 합니다.
/* get_next_frame / put_next_frame 쌍 사용 패턴 */
u32 n_left_from, *from;
u32 n_left_to_next, *to_next;
u32 next_index;
from = vlib_frame_vector_args (frame);
n_left_from = frame->n_vectors;
next_index = node->cached_next_index;
while (n_left_from > 0)
{
/* 다음 노드의 출력 프레임 참조 획득 */
vlib_get_next_frame (vm, node, next_index,
to_next, n_left_to_next);
while (n_left_from > 0 && n_left_to_next > 0)
{
u32 bi0 = from[0];
u32 next0 = next_index;
to_next[0] = bi0;
from += 1;
to_next += 1;
n_left_from -= 1;
n_left_to_next -= 1;
/* next0가 변경될 수 있는 경우 validate_buffer_enqueue 사용 */
vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
to_next, n_left_to_next,
bi0, next0);
}
/* 프레임 반환: n_left_to_next 값으로 실제 전달 수 계산 */
vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}
vlib_get_next_frame()은 내부적으로 next_index에 해당하는 다음 노드의 보류 프레임(pending frame)을 조회합니다. 보류 프레임이 없으면 새로 할당합니다. to_next는 해당 프레임의 버퍼 인덱스 배열 끝 위치를, n_left_to_next는 남은 슬롯 수를 반환합니다.
vlib_put_next_frame()은 n_left_to_next 값을 사용하여 실제로 프레임에 추가된 벡터 수를 계산하고, 프레임을 보류 큐에 등록합니다. 이 함수를 호출하지 않으면 프레임이 다음 노드에 전달되지 않으며, 버퍼가 누수됩니다. 반대로 get 없이 put을 호출하면 초기화되지 않은 상태에서 접근하여 정의되지 않은 동작이 발생합니다.
vlib_validate_buffer_enqueue_x1/x2/x4() — 버퍼 큐잉 최적화
vlib_validate_buffer_enqueue 매크로 계열은 패킷을 다음 노드로 전달할 때 next_index 변경을 효율적으로 처리합니다. 같은 next_index로 연속 전달 시에는 fast-path로 동작하여 추가 복사가 불필요하지만, next_index가 변경되면 slow-path에서 현재 프레임을 반환하고 새 프레임을 획득합니다.
/* vlib_validate_buffer_enqueue_x1 매크로 의사 코드 (pseudo-code) */
/*
* 매개변수:
* vm — vlib_main_t*
* node — vlib_node_runtime_t*
* next_index — 현재 프레임이 가리키는 다음 노드 인덱스 (변경 가능)
* to_next — 출력 프레임 버퍼 인덱스 배열 포인터
* n_left_to_next — 출력 프레임 남은 슬롯 수
* bi0 — 현재 패킷 버퍼 인덱스
* next0 — 이 패킷이 가야 할 다음 노드 인덱스
*/
if (PREDICT_FALSE (next0 != next_index))
{
/* slow-path: next_index가 변경됨 */
/* 1. 현재 프레임에서 방금 기록한 bi0를 제거 (to_next 롤백) */
/* 2. 현재 프레임을 반환 (put_next_frame) */
/* 3. next0에 해당하는 새 프레임 획득 (get_next_frame) */
/* 4. 새 프레임에 bi0 기록 */
/* 5. 새 프레임 반환 (put_next_frame) */
/* 6. 원래 next_index의 프레임 재획득 (get_next_frame) */
vlib_set_next_frame_buffer (vm, node, next0, bi0);
n_left_to_next += 1; /* 롤백한 슬롯 복원 */
}
/* fast-path: next0 == next_index → 이미 to_next에 기록되어 있으므로 추가 작업 없음 */
x2와 x4 변형은 각각 2개, 4개의 패킷을 동시에 처리하여 명령어 수준 병렬성(ILP, Instruction-Level Parallelism)을 활용합니다. 현대 CPU의 슈퍼스칼라 파이프라인에서 독립적인 메모리 접근과 분기 예측(Branch Prediction)을 병렬로 수행할 수 있습니다.
/* x2 변형: 2개 패킷 동시 큐잉 */
while (n_left_from >= 4 && n_left_to_next >= 2)
{
u32 bi0, bi1;
u32 next0, next1;
vlib_buffer_t *b0, *b1;
/* 프리페치: 2개 앞의 패킷을 미리 캐시에 적재 */
{
vlib_buffer_t *p2, *p3;
p2 = vlib_get_buffer (vm, from[2]);
p3 = vlib_get_buffer (vm, from[3]);
vlib_prefetch_buffer_header (p2, LOAD);
vlib_prefetch_buffer_header (p3, LOAD);
}
bi0 = from[0]; bi1 = from[1];
to_next[0] = bi0; to_next[1] = bi1;
b0 = vlib_get_buffer (vm, bi0);
b1 = vlib_get_buffer (vm, bi1);
/* 각 패킷의 next_index 결정 */
next0 = process_packet (b0);
next1 = process_packet (b1);
from += 2; to_next += 2;
n_left_from -= 2; n_left_to_next -= 2;
vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
to_next, n_left_to_next,
bi0, bi1, next0, next1);
}
x1은 패킷 단위 처리로 코드가 단순하지만 ILP를 활용하지 못합니다. x2는 2개 패킷을 병렬 처리하여 L1/L2 캐시 미스 대기 시간(Latency)을 숨길 수 있으며, x4는 4개 패킷을 병렬 처리하여 최대 처리량을 달성합니다. 일반적으로 dual-loop(x2)와 single-loop(x1)를 조합하여 n_left_from이 2 미만일 때 x1으로 나머지를 처리하는 패턴을 사용합니다.
vlib_buffer_advance() — 헤더 파싱을 위한 포인터 이동
vlib_buffer_advance()는 버퍼의 current_data 오프셋(Offset)과 current_length를 동시에 조정하여 패킷 데이터 내에서 논리적 시작 위치를 이동합니다. 양수 값을 전달하면 헤더를 건너뛰어 디캡슐화(Decapsulation) 효과를 내고, 음수 값을 전달하면 헤더 공간을 확보하여 캡슐화(Encapsulation)를 수행합니다.
/* vlib_buffer_advance() 내부 동작 */
/*
* b->current_data += advance;
* b->current_length -= advance;
*
* 양수(+N): current_data를 N바이트 뒤로 이동 → 헤더 스킵
* 음수(-N): current_data를 N바이트 앞으로 이동 → 헤더 공간 확보
*/
/* 예시 1: 이더넷 헤더 스킵 후 IP 헤더 접근 (디캡슐화) */
ethernet_header_t *eth = vlib_buffer_get_current (b0);
u16 ethertype = clib_net_to_host_u16 (eth->type);
/* current_data += sizeof(ethernet_header_t) */
vlib_buffer_advance (b0, sizeof (ethernet_header_t));
/* 이제 vlib_buffer_get_current()는 IP 헤더를 가리킵니다 */
ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* 예시 2: 새 헤더 추가 (캡슐화) */
/* current_data -= sizeof(my_header_t) → 앞쪽 headroom 사용 */
vlib_buffer_advance (b0, -(i32) sizeof (my_header_t));
my_header_t *hdr = vlib_buffer_get_current (b0);
hdr->version = 1;
hdr->length = clib_host_to_net_u16 (payload_len);
캡슐화 시에는 current_data가 감소하면서 사전에 확보된 headroom 영역을 사용합니다. 기본 headroom은 VLIB_BUFFER_PRE_DATA_SIZE(기본 128바이트)이며, 이 범위를 초과하면 데이터 손상이 발생합니다. 체인 버퍼의 경우 vlib_buffer_advance()는 첫 번째 세그먼트의 current_data만 조정합니다. total_length_not_including_first_buffer 필드는 변경되지 않으므로, 전체 패킷 길이를 계산할 때는 b->current_length + b->total_length_not_including_first_buffer를 사용해야 합니다.
vnet_buffer() / vnet_buffer2() — 패킷 메타데이터 접근
vnet_buffer()와 vnet_buffer2() 매크로는 vlib_buffer_t 내의 opaque[10] 및 opaque2[12] 배열을 프로토콜별 메타데이터 구조체로 오버레이(Overlay)하여 접근합니다. 이 영역은 패킷이 노드 그래프를 따라 전달될 때 노드 간 정보를 공유하는 공유 메모리 역할을 합니다.
/* vnet_buffer_opaque_t — opaque[10] 영역의 union 구조 (일부) */
typedef struct
{
union
{
struct
{
u32 sw_if_index[VLIB_N_RX_TX]; /* [0]=RX, [1]=TX 인터페이스 */
i16 l2_hdr_offset; /* L2 헤더 시작 오프셋 */
i16 l3_hdr_offset; /* L3 헤더 시작 오프셋 */
i16 l4_hdr_offset; /* L4 헤더 시작 오프셋 */
u8 feature_arc_index; /* 피처 아크 인덱스 */
};
struct { u32 adj_index[VLIB_N_RX_TX]; } ip;
struct { u32 fib_index; } tcp;
struct { u32 sa_index; u32 seq; } ipsec;
};
} vnet_buffer_opaque_t;
/* 노드 함수에서의 메타데이터 접근 */
vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);
/* 수신 인터페이스 인덱스 읽기 */
u32 rx_sw_if_index = vnet_buffer(b0)->sw_if_index[VLIB_RX];
/* 송신 인터페이스 설정 */
vnet_buffer(b0)->sw_if_index[VLIB_TX] = tx_sw_if_index;
/* IP adjacency 인덱스 접근 */
u32 adj_idx = vnet_buffer(b0)->ip.adj_index[VLIB_TX];
/* vnet_buffer2: 확장 메타데이터 (opaque2[12] 영역) */
vnet_buffer2(b0)->loop_counter = 0;
플러그인에서 커스텀 메타데이터를 저장하려면 opaque/opaque2의 사용되지 않는 영역을 활용합니다. 다만 노드 그래프 상의 다른 노드와 필드가 겹치지 않도록 주의해야 합니다. 가장 안전한 방법은 vnet_buffer_opaque_t의 union에 플러그인 전용 구조체를 추가하고, 해당 필드가 사용되는 구간(노드 경로)에서만 유효하도록 문서화하는 것입니다.
/* 플러그인 커스텀 메타데이터 정의 예시 */
/* vnet_buffer_opaque_t union에 추가 */
struct
{
u32 session_id;
u16 action_flags;
u16 reserved;
} my_plugin;
/* 사용 */
vnet_buffer(b0)->my_plugin.session_id = sid;
vnet_buffer(b0)->my_plugin.action_flags = MY_ACTION_FORWARD;
CLIB_PREFETCH() — 성능 최적화를 위한 캐시 프리페치
CLIB_PREFETCH()는 지정된 메모리 주소의 캐시 라인(Cache Line)을 L1 캐시에 미리 적재하는 프리페치 명령을 생성합니다. 패킷 처리에서 메모리 접근 지연(L3 캐시 미스 시 수십~수백 나노초)이 처리량의 주된 병목이므로, 프리페치는 VPP 성능 최적화의 핵심 기법입니다.
/* CLIB_PREFETCH 힌트 유형 */
/*
* LOAD (T0) — 읽기 전용 접근 예정, 공유 상태로 캐시 적재
* STORE (T1) — 쓰기 접근 예정, 배타적 상태로 캐시 적재
* WRITE = STORE의 별칭, 동일 동작
*/
/* dual-loop에서의 프리페치 패턴 */
while (n_left_from >= 4 && n_left_to_next >= 2)
{
/* 2개 앞 패킷의 버퍼 헤더를 프리페치 */
{
vlib_buffer_t *p2, *p3;
p2 = vlib_get_buffer (vm, from[2]);
p3 = vlib_get_buffer (vm, from[3]);
/* 버퍼 헤더 프리페치 (메타데이터 읽기용) */
vlib_prefetch_buffer_header (p2, LOAD);
vlib_prefetch_buffer_header (p3, LOAD);
/* 패킷 데이터 프리페치 (헤더 파싱용) */
CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, LOAD);
CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, LOAD);
}
/* 현재 2개 패킷 처리 — 프리페치된 데이터 활용 */
u32 bi0 = from[0];
u32 bi1 = from[1];
vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);
vlib_buffer_t *b1 = vlib_get_buffer (vm, bi1);
/* 패킷 처리 ... */
from += 2;
n_left_from -= 2;
to_next += 2;
n_left_to_next -= 2;
}
프리페치 거리(prefetch distance)는 "현재 처리 중인 패킷"과 "프리페치 대상 패킷" 사이의 간격입니다. dual-loop에서는 거리가 2(현재 0,1번 처리하면서 2,3번 프리페치)이며, quad-loop에서는 거리가 4입니다. 이 거리는 프리페치 명령 발행 후 실제 캐시 적재까지의 지연 시간 동안 CPU가 현재 패킷을 처리할 수 있도록 설정합니다. 거리가 너무 짧으면 프리페치 효과가 없고, 너무 길면 프리페치된 데이터가 캐시에서 밀려날 수 있습니다.
VPP 핵심 API 요약
| API | 용도 | 호출 위치 | 성능 영향 |
|---|---|---|---|
vlib_frame_vector_args() | 입력 프레임에서 버퍼 인덱스 배열 획득 | 노드 함수 시작부 | O(1), 포인터 연산만 수행 |
vlib_get_next_frame() | 다음 노드 출력 프레임 참조 획득 | 패킷 처리 루프 시작 | 보류 프레임 조회/할당 비용 |
vlib_put_next_frame() | 출력 프레임 반환 및 전달 등록 | 패킷 처리 루프 종료 | O(1), 벡터 수 갱신만 수행 |
vlib_validate_buffer_enqueue_x1/x2/x4() | next_index 변경 시 프레임 교체 자동 처리 | 패킷 큐잉 시점 | fast-path O(1), slow-path 프레임 교체 비용 |
vlib_buffer_advance() | 패킷 데이터 포인터 이동 (디캡슐화/캡슐화) | 헤더 파싱/추가 시 | O(1), 정수 덧셈 2회 |
vnet_buffer() / vnet_buffer2() | 패킷 메타데이터(sw_if_index, adj 등) 접근 | 패킷 처리 전 구간 | O(1), 구조체 포인터 캐스팅 |
CLIB_PREFETCH() | 다음 패킷 데이터를 L1 캐시에 미리 적재 | dual/quad loop 선두 | 캐시 미스 50~200ns 회피 |
VPP + Kubernetes 통합
Kubernetes 환경에서 VPP는 CNI 플러그인의 데이터플레인으로 동작하여 Pod 네트워킹을 가속합니다. 현재 Calico/VPP가 사실상의 표준이며, 기존 Contiv-VPP는 유지보수가 중단되었습니다.
Calico/VPP 배포 및 설정
# 1. Calico operator 설치
$ kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/tigera-operator.yaml
# 2. Calico/VPP 커스텀 리소스 생성
$ cat <<'EOF' | kubectl apply -f -
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
cni:
type: Calico
calicoNetwork:
linuxDataplane: VPP
bgp: Enabled
ipPools:
- cidr: 10.244.0.0/16
encapsulation: None
natOutgoing: Enabled
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
name: default
spec: {}
EOF
# 3. VPP 관련 ConfigMap (워커 노드별 설정)
$ cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: calico-vpp-config
namespace: calico-vpp-dataplane
data:
CALICOVPP_INTERFACES: |-
{
"uplinkInterfaces": [{
"interfaceName": "eth0",
"vppDriver": "af_xdp"
}]
}
CALICOVPP_CONFIG_TEMPLATE: |-
unix {
nodaemon
full-coredump
cli-listen /var/run/vpp/cli.sock
}
cpu { main-core 0 workers 2 }
buffers { buffers-per-numa 32768 }
EOF
# 4. 확인
$ kubectl get pods -n calico-vpp-dataplane
NAME READY STATUS RESTARTS
calico-vpp-node-xxxxx 2/2 Running 0
# 5. VPP CLI 접근 (노드 내에서)
$ kubectl exec -n calico-vpp-dataplane calico-vpp-node-xxxxx \
-c vpp -- vppctl show interface
$ kubectl exec -n calico-vpp-dataplane calico-vpp-node-xxxxx \
-c vpp -- vppctl show runtime
NetworkPolicy 적용
# Calico/VPP는 Kubernetes NetworkPolicy를 VPP ACL로 변환
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-only
namespace: production
spec:
podSelector:
matchLabels:
app: web-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
# VPP에서 변환된 ACL 확인
vpp# show acl-plugin acl
# acl-index 0: permit proto 6 src 10.244.0.0/16 dport 80 ...
# Pod 간 memif 직접 연결 확인 (동일 노드 내)
vpp# show memif
# interface memif0/0 ... status: connected (Pod A)
# interface memif0/1 ... status: connected (Pod B)
vppDriver: "af_xdp"를 사용하면 NIC를 커널과 공유할 수 있어 노드 관리(SSH 등)가 편리합니다. 최대 성능이 필요하면 "dpdk"를 사용하되, 관리 인터페이스를 별도 확보하세요.
VPP 모니터링 체계
VPP는 stats segment(공유 메모리 기반 통계 영역)를 통해 실시간 성능 지표를 제공합니다. Prometheus, Grafana와 연동하여 운영 가시성을 확보할 수 있습니다.
Stats Segment 구조
| 카테고리 | 경로 패턴 | 예시 | 설명 |
|---|---|---|---|
| 인터페이스 | /if/<stat> | /if/rx-packets, /if/tx-bytes | 인터페이스별 패킷/바이트 카운터 |
| 노드 | /node/<name>/<stat> | /node/ip4-input/calls | 노드별 호출 횟수, 벡터, 클럭 |
| 에러 | /err/<node>/<error> | /err/ip4-input/ip4 spoofed | 노드별 에러 카운터 |
| 시스템 | /sys/<stat> | /sys/vector_rate | 글로벌 벡터 처리율 |
| 버퍼 | /buffer-pools/<pool> | /buffer-pools/default/available | 버퍼 풀 사용률 |
| NAT | /nat44/<stat> | /nat44/total-sessions | NAT 세션 수 |
# stats segment 직접 접근 (C 클라이언트)
$ vpp_get_stats dump /if/rx-packets
[0]: 1523456
[1]: 892341
# CLI로 확인
vpp# show stats /if/rx
vpp# show stats /sys/vector_rate
vpp# show stats dump
# Python API로 접근
$ python3 -c "
from vpp_papi.vpp_stats import VPPStats
stats = VPPStats(socketname='/run/vpp/stats.sock')
stats.connect()
print('Vector rate:', stats['/sys/vector_rate'])
print('RX packets:', stats['/if/rx-packets'])
stats.disconnect()
"
Prometheus Exporter 연동
# vpp-exporter 실행 (공식 vpp_prometheus_export 또는 서드파티)
$ vpp_prometheus_export --stats-socket /run/vpp/stats.sock \
--listen 0.0.0.0:9482
# Prometheus 설정 (prometheus.yml)
scrape_configs:
- job_name: 'vpp'
static_configs:
- targets: ['vpp-host:9482']
scrape_interval: 5s
# 주요 메트릭
# vpp_interface_rx_packets{interface="GigabitEthernet0/8/0"} 1523456
# vpp_interface_tx_bytes{interface="GigabitEthernet0/9/0"} 892341234
# vpp_node_calls{node="ip4-input"} 7654321
# vpp_node_vectors_per_call{node="ip4-lookup"} 42.3
# vpp_buffer_available{pool="default"} 28672
# vpp_nat44_sessions_total 12345
# vpp_system_vector_rate 2.4e6
Grafana 대시보드 주요 패널
| 패널 | 쿼리 (PromQL) | 의미 |
|---|---|---|
| 인터페이스 PPS | rate(vpp_interface_rx_packets[5m]) | 초당 수신 패킷 수 |
| 벡터 효율 | vpp_node_vectors_per_call{node="dpdk-input"} | 배치 크기 (높을수록 효율적) |
| 버퍼 사용률 | 1 - vpp_buffer_available / vpp_buffer_total | 버퍼 풀 소진 경고 |
| 드롭 비율 | rate(vpp_interface_drops[5m]) / rate(vpp_interface_rx_packets[5m]) | 패킷 드롭 비율 |
| NAT 세션 | vpp_nat44_sessions_total | 활성 NAT 세션 수 |
| CPU 사이클/패킷 | rate(vpp_node_clocks[5m]) / rate(vpp_node_vectors[5m]) | 노드별 처리 효율 |
show errors에서 증가하는 에러 카운터도 Prometheus로 수집하여 조기 경고에 활용할 수 있습니다.
실습 랩
랩 토폴로지
3개의 네트워크 네임스페이스(Namespace)와 VPP TAP 인터페이스를 사용한 라우팅/NAT 실습 환경입니다. 물리 NIC 없이 로컬에서 VPP의 핵심 기능을 검증할 수 있습니다.
환경 구성
#!/bin/bash — VPP 실습 랩 환경 구성
# 1. 네임스페이스 생성
sudo ip netns add ns-client
sudo ip netns add ns-server
# 2. VPP 시작 (minimal startup.conf)
cat <<'EOF' | sudo tee /tmp/lab-startup.conf
unix {
cli-listen /run/vpp/cli.sock
log /tmp/vpp-lab.log
nodaemon
}
api-segment { gid $(id -gn) }
plugins {
plugin default { disable }
plugin nat_plugin.so { enable }
plugin acl_plugin.so { enable }
plugin ping_plugin.so { enable }
}
EOF
sudo vpp -c /tmp/lab-startup.conf &
sleep 2
# 3. TAP 인터페이스 생성
sudo vppctl create tap id 0 host-if-name vpp-tap0 host-ns ns-client \
host-ip4-addr 192.168.1.1/24
sudo vppctl set interface ip address tap0 192.168.1.2/24
sudo vppctl set interface state tap0 up
sudo vppctl create tap id 1 host-if-name vpp-tap1 host-ns ns-server \
host-ip4-addr 10.0.0.2/24
sudo vppctl set interface ip address tap1 10.0.0.1/24
sudo vppctl set interface state tap1 up
# 4. 네임스페이스 기본 게이트웨이 설정
sudo ip netns exec ns-client ip route add default via 192.168.1.2
sudo ip netns exec ns-server ip route add default via 10.0.0.1
echo "Lab ready! Test: sudo ip netns exec ns-client ping 10.0.0.2"
라우팅 및 NAT 실습
# IP 라우팅 확인
vpp# show ip fib
# 192.168.1.0/24 → tap0, 10.0.0.0/24 → tap1 확인
# NAT44 설정 (ns-client → ns-server 시 주소 변환)
vpp# nat44 plugin enable sessions 1024
vpp# nat44 add interface address tap1
vpp# set interface nat44 in tap0 out tap1
# 정적 매핑 (ns-server의 포트 8080 → ns-client)
vpp# nat44 add static mapping local 192.168.1.1 8080 \
external tap1 8080 tcp
# ACL 설정 (ICMP만 허용 테스트)
vpp# set acl-plugin acl permit proto 1 /* ICMP */
vpp# set acl-plugin acl permit proto 6 dport 8080 /* TCP 8080 */
vpp# set acl-plugin acl deny
vpp# set acl-plugin interface tap0 input acl 0
vpp# set acl-plugin interface tap0 input acl 1
vpp# set acl-plugin interface tap0 input acl 2
검증 및 모니터링
# 1. 기본 연결 테스트
$ sudo ip netns exec ns-client ping -c 3 10.0.0.2
PING 10.0.0.2: 64 bytes from 10.0.0.2: icmp_seq=1 ttl=63 time=0.12 ms
# 2. NAT 세션 확인
vpp# show nat44 sessions
192.168.1.1:45000 -> 10.0.0.1:45000 -> 10.0.0.2:8080 [TCP]
# 3. 패킷 트레이싱으로 전체 경로 확인
vpp# trace add virtio-input 10
$ sudo ip netns exec ns-client ping -c 1 10.0.0.2
vpp# show trace
# virtio-input → ethernet-input → ip4-input → nat44-in2out →
# ip4-lookup → ip4-rewrite → tap1-output 경로 확인
# 4. 런타임 통계
vpp# show runtime
# 각 노드의 Calls, Vectors, Clocks/Call 확인
# 5. 에러 확인
vpp# show errors
# 에러 카운터가 0이면 정상
# 6. 정리
$ sudo killall vpp
$ sudo ip netns del ns-client
$ sudo ip netns del ns-server
trace add virtio-input 10 → ping → show trace는 VPP 학습의 가장 효과적인 방법입니다. 각 패킷이 어떤 노드를 거치고, 어떤 결정(FIB lookup, NAT 변환, ACL 매칭)을 받는지 한눈에 파악할 수 있습니다.
trace 출력 해석 예시
실습 랩에서 가장 가치 있는 학습 자료는 show trace 원문입니다. 노드 이름만 읽고 끝내지 말고, 각 줄이 어떤 상태 변경을 반영하는지 해석해야 다음 장애 때 바로 원인을 좁힐 수 있습니다.
Packet 1
virtio-input
sw_if_index 1, next-index 4, hw_if_index 1
ethernet-input
dst 02:fe:54:00:00:01 src 02:fe:54:00:00:02 type 0x0800
ip4-input
src 192.168.1.1 dst 10.0.0.2 ttl 64 checksum 0x6b21
nat44-in2out
translate 192.168.1.1:45000 -> 10.0.0.1:45000
ip4-lookup
fib 0 dpo-idx 7 flow hash 0x18a220e4
ip4-rewrite
tx_sw_if_index 2 rewrite 14 bytes
tap1-output
enqueue to host-if vpp-tap1
| trace 줄 | 무엇을 뜻하는가 | 문제가 보일 때 의심할 지점 |
|---|---|---|
virtio-input | 패킷이 어떤 입력 인터페이스에서 들어왔는지 보여줍니다. | 입력 포트가 다르면 인터페이스 바인딩이나 TAP 연결을 먼저 봐야 합니다. |
ethernet-input | 목적지/출발지 MAC과 EtherType이 기대값인지 확인합니다. | EtherType이 다르면 VLAN, QinQ, 잘못된 L2 캡슐화를 의심해야 합니다. |
ip4-input | TTL, 체크섬(Checksum), 주소가 정상인지 보여줍니다. | 여기서 드롭되면 입력 ACL보다 기본 L3 검사가 먼저 실패한 것입니다. |
nat44-in2out | 실제 변환된 주소와 포트를 보여줍니다. | 번역 결과가 없으면 NAT 인터페이스 방향이나 세션 용량을 점검해야 합니다. |
ip4-lookup | 어느 FIB와 DPO가 선택되었는지 확인합니다. | 예상과 다른 FIB면 VRF 분리나 라우트 우선순위(Priority) 문제일 가능성이 큽니다. |
ip4-rewrite | 최종 송신 인터페이스와 rewrite 길이를 보여줍니다. | rewrite 바이트가 0이거나 인터페이스가 다르면 adjacency가 잘못된 것입니다. |
tap1-output | 패킷이 실제 호스트 측 인터페이스로 나가는 마지막 단계입니다. | 여기까지 왔는데 상대가 못 받으면 네임스페이스 쪽 라우팅과 호스트 스택을 봐야 합니다. |
nat44-in2out까지 정상인데 ip4-lookup이 이상하면 NAT가 아니라 라우팅/FIB 문제입니다. 반대로 ethernet-input 단계부터 값이 이상하면 정책 이전의 L2 입력 문제입니다.
실습 랩 장애 진단 런북
랩에서는 기능이 많지 않기 때문에, 장애를 보면 오히려 어디부터 볼지 더 막막해질 수 있습니다. 가장 빠른 방법은 호스트 네임스페이스, VPP 인터페이스 상태, FIB/NAT, trace를 아래 순서대로 잘라 보는 것입니다. 순서를 지키면 원인 후보를 크게 좁힐 수 있습니다.
# 1. 네임스페이스 자체 확인
sudo ip netns exec ns-client ip addr
sudo ip netns exec ns-client ip route
sudo ip netns exec ns-client ping -c 1 192.168.1.2
# 2. VPP 인터페이스와 주소 확인
vpp# show interface
vpp# show interface addr
# 3. 라우팅과 NAT 상태 확인
vpp# show ip fib
vpp# show nat44 sessions
# 4. 패킷이 VPP에 들어오는지 확인
vpp# clear trace
vpp# trace add virtio-input 5
sudo ip netns exec ns-client ping -c 1 10.0.0.2
vpp# show trace
# 5. 최종 수신 측 확인
sudo ip netns exec ns-server ip addr
sudo ip netns exec ns-server tcpdump -ni vpp-tap1 -c 5
| 단계 | 정상일 때 보이는 것 | 비정상일 때 바로 의심할 것 |
|---|---|---|
| ns-client 확인 | 기본 게이트웨이가 192.168.1.2로 보이고, 게이트웨이 핑이 응답합니다. | TAP 생성 실패, 네임스페이스 이동 실패, 호스트 측 주소 배치 오류를 먼저 봅니다. |
| VPP 인터페이스 | tap0, tap1가 모두 up이고 주소가 기대값과 같습니다. | set interface state 누락, 주소 부여 누락, 잘못된 인터페이스 이름 사용이 흔합니다. |
| FIB/NAT | 직접 연결 경로와 NAT 세션이 기대한 만큼만 보입니다. | 라우트 누락, NAT 인터페이스 방향 반대, 이전 테스트 세션 잔존 여부를 확인합니다. |
| trace | virtio-input부터 tap1-output까지 연속으로 이어집니다. | 중간 노드가 끊기면 그 지점 바로 앞의 정책이나 라우팅 상태를 보면 됩니다. |
| 수신 네임스페이스 | tcpdump에 실제 패킷이 보입니다. | VPP까지는 정상이고 호스트 네트워크 스택(Network Stack) 또는 네임스페이스 라우팅이 문제일 가능성이 큽니다. |
show interface, show ip fib, show trace 세 개를 먼저 모으는 편이 훨씬 빠릅니다. 이 세 출력만으로도 "입력 문제", "정책 문제", "출력 문제"를 거의 항상 구분할 수 있습니다.
L7 모니터링 — HTTP 메트릭과 대시보드
지금까지의 모니터링 절은 인터페이스 통계·노드 처리량·CPU 점유율 같은 L2~L4 지표에 집중했습니다. 그러나 FD.io VPP가 호스트 스택과 HTTP 게이트웨이를 운영하기 시작하면 운영 관점도 한 단계 위로 올라가야 합니다. 사용자가 체감하는 지표는 패킷 수가 아니라 요청 성공률·평균 응답 시간·캐시 적중률이기 때문입니다. 이 절은 VPP가 L7 워크로드에서 노출해야 할 핵심 메트릭과, 그것을 Prometheus·Grafana 같은 외부 시스템으로 흘려보내는 패턴을 정리합니다.
핵심 L7 메트릭 카탈로그
| 메트릭 | 유형 | 관찰점 | 경보 임계 예시 |
|---|---|---|---|
vpp_http_requests_total | counter (라벨: method, status, vhost) | 요청 수 추세 | 5xx 비율 > 1% |
vpp_http_request_duration_seconds | histogram | 처리 시간 분포 | p99 > 200ms |
vpp_http_in_flight | gauge | 처리 중 요청 수 | 워커 큐 포화 신호 |
vpp_http_cache_hits_total / misses_total | counter | 캐시 효율 | 적중률 < 70% |
vpp_http_parse_errors_total | counter (라벨: kind) | 잘못된 요청·헤더 폭주 | 스파이크 = 공격 신호 |
vpp_http_upstream_failures_total | counter (라벨: upstream) | 업스트림 건강도 | 임계 도달 시 회로 차단 |
vpp_http2_streams_active | gauge (라벨: worker) | 워커별 스트림 분포 | 한 워커 편중 |
vpp_http2_window_stalls_total | counter | 흐름 제어 막힘 | BDP 대비 윈도 부족 |
vpp_tls_handshakes_total | counter (라벨: result, version) | TLS 협상 결과 | 실패율 > 0.5% |
vpp_tls_session_resumptions_total | counter | 세션 재사용 효율 | 재사용률 추적 |
메트릭 수집 파이프라인
핵심은 per-worker 카운터의 무락 합산입니다. VPP의 모든 카운터 매크로(vlib_increment_simple_counter, vlib_increment_combined_counter)는 워커별 thread-local 슬롯에 쓰고, 통계 세그먼트는 메인 스레드가 사이클 단위로 합산해 노출합니다. 이 분리 덕분에 메트릭 갱신은 워커 핫패스에서 명령 1~2개로 끝나며, 외부 스크래퍼는 합산된 결과만 읽습니다.
L7 카운터 등록과 갱신
/* HTTP 카운터 정의 */
#define foreach_http_counter \
_(REQUESTS_TOTAL, "requests_total") \
_(REQUESTS_2XX, "requests_2xx") \
_(REQUESTS_4XX, "requests_4xx") \
_(REQUESTS_5XX, "requests_5xx") \
_(PARSE_ERRORS, "parse_errors") \
_(CACHE_HITS, "cache_hits") \
_(CACHE_MISSES, "cache_misses") \
_(UPSTREAM_FAILURES, "upstream_failures")
typedef enum {
#define _(N, s) HTTP_CNT_##N,
foreach_http_counter
#undef _
HTTP_N_COUNTERS
} http_counter_t;
static vlib_simple_counter_main_t http_counters[HTTP_N_COUNTERS];
/* 초기화: stat segment에 등록 */
clib_error_t *
http_stats_init (vlib_main_t *vm)
{
#define _(N, s) \
http_counters[HTTP_CNT_##N].name = "/http/" s; \
http_counters[HTTP_CNT_##N].stat_segment_name = "/http/" s; \
vlib_validate_simple_counter (&http_counters[HTTP_CNT_##N], 0); \
vlib_zero_simple_counter (&http_counters[HTTP_CNT_##N], 0);
foreach_http_counter
#undef _
return 0;
}
/* 갱신: 핫패스에서 호출 */
static inline void
http_count (http_counter_t c, u32 thread_index)
{
vlib_increment_simple_counter (&http_counters[c], thread_index, 0, 1);
}
/* RX 콜백에서 사용 */
static int
my_http_rx (session_t *s)
{
u32 ti = s->thread_index;
http_count (HTTP_CNT_REQUESTS_TOTAL, ti);
http_msg_t msg;
if (http1_parse (s->rx_fifo, &msg) < 0) {
http_count (HTTP_CNT_PARSE_ERRORS, ti);
return -1;
}
/* ... 처리 후 응답 status에 따라 분류 카운터 증가 ... */
return 0;
}
통계 노출 — vpp_prometheus_export
VPP에는 stat segment를 읽어 Prometheus 텍스트 포맷으로 변환하는 도구가 함께 들어 있습니다.
# 1) Prometheus exporter 실행 (포트 9482)
vpp_prometheus_export --port 9482 \
--used-only \
--include '/http/.*' \
--include '/tls/.*' \
--include '/sys/.*'
# 2) 카운터 직접 조회 (vppctl)
vppctl show stat /http/requests_total
vppctl show stat /http/cache_hits
# 3) JSON으로 한 번에 출력
vppctl show stat | jq '.["/http/"]'
# 4) Prometheus 서버에서 스크랩 설정
# scrape_configs:
# - job_name: 'vpp'
# static_configs:
# - targets: ['vpp-host-1:9482', 'vpp-host-2:9482']
# metric_relabel_configs:
# - source_labels: [__name__]
# regex: 'http_requests_total'
# action: keep
PromQL 예시 — 주요 SLI 계산
# 분당 요청 수
sum(rate(http_requests_total[1m])) by (vhost)
# 5xx 비율
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# p99 지연
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
# 캐시 적중률
sum(rate(http_cache_hits[5m]))
/ (sum(rate(http_cache_hits[5m])) + sum(rate(http_cache_misses[5m])))
# 워커 편중 — 표준편차
stddev(http2_streams_active) / avg(http2_streams_active)
Binary API에 자작 메시지 추가하기
VPP CLI 및 API가 기존 메시지를 사용하는 쪽을 다뤘다면, 여기서는 반대 방향으로 플러그인이나 패치에서 새 Binary API 메시지를 정의하고 Python·Go 바인딩까지 자동 생성하는 전체 흐름을 정리합니다. VPP의 API 계층은 IDL(Interface Definition Language) 한 파일에서 C 핸들러, 언어별 바인딩, JSON 스키마를 모두 파생시키는 구조이므로, IDL 한 곳만 정확히 작성하면 나머지는 빌드 과정에서 자동으로 만들어집니다.
IDL 문법 — .api 파일
*.api 파일은 VPP 고유의 작은 IDL입니다. 메시지, 타입, 이벤트를 정의하며 vppapigen 도구가 이를 파싱하여 C 헤더·JSON·CRC 값을 생성합니다. 아래는 "샘플 카운터 플러그인"이 정의하는 가상의 파일입니다.
/* src/plugins/sample_counter/sample_counter.api */
option version = "1.0.0";
import "vnet/interface_types.api";
/* 인터페이스에 카운터 기능을 활성/비활성 */
autoreply define sample_counter_enable_disable
{
u32 client_index;
u32 context;
vl_api_interface_index_t sw_if_index;
bool enable_disable [default=true];
};
/* 특정 인터페이스의 카운터 한 개 요청 */
define sample_counter_get
{
u32 client_index;
u32 context;
vl_api_interface_index_t sw_if_index;
};
define sample_counter_get_reply
{
u32 context;
i32 retval;
u64 packets;
u64 bytes;
};
/* 전체 인터페이스 덤프: details 스트림 */
define sample_counter_dump
{
u32 client_index;
u32 context;
};
define sample_counter_details
{
u32 context;
vl_api_interface_index_t sw_if_index;
u64 packets;
u64 bytes;
};
/* 비동기 이벤트: 임계치 초과 통지 */
define sample_counter_threshold_event
{
u32 client_index;
u32 pid;
vl_api_interface_index_t sw_if_index;
u64 packets;
};
service {
rpc sample_counter_dump returns null
stream sample_counter_details;
rpc want_sample_counter_events returns sample_counter_enable_disable_reply
events sample_counter_threshold_event;
};
문법 요점은 다음과 같습니다.
autoreply는 자동으로_reply메시지를 만들어 주며 필드는retval(i32)만 가집니다.client_index·context는 거의 모든 요청의 표준 앞자리입니다. 클라이언트가 요청/응답을 매칭하는 용도입니다.import로 공용 타입을 재사용합니다.vl_api_interface_index_t는vnet/interface_types.api에 정의되어 있습니다.- 덤프 계열은 스트림 패턴입니다. 요청 하나에 다수의
_details이벤트가 응답으로 흐르고, 클라이언트는control_ping_reply로 끝을 감지합니다. service { rpc ... }블록은 요청·스트림·이벤트 관계를 선언하여 VAPI 같은 타입 세이프 바인딩이 이를 활용합니다.[default=true],[limit=256],[variable_size=count]같은 속성으로 기본값·길이 제한·가변 배열을 지정합니다.
C 핸들러 작성
vppapigen은 메시지 이름별로 vl_api_<name>_t_handler 심볼을 기대합니다. 플러그인은 이 심볼을 구현하고 setup_message_id_table을 통해 런타임에 ID를 바인딩합니다.
/* src/plugins/sample_counter/sample_counter_api.c */
#include <vnet/vnet.h>
#include <vnet/plugin/plugin.h>
#include <sample_counter/sample_counter.h>
#include <vnet/format_fns.h>
#include <sample_counter/sample_counter.api_enum.h>
#include <sample_counter/sample_counter.api_types.h>
#define REPLY_MSG_ID_BASE smcm->msg_id_base
#include <vlibapi/api_helper_macros.h>
static void
vl_api_sample_counter_enable_disable_t_handler(
vl_api_sample_counter_enable_disable_t *mp)
{
sample_counter_main_t *smcm = &sample_counter_main;
vl_api_sample_counter_enable_disable_reply_t *rmp;
u32 sw_if_index = ntohl(mp->sw_if_index);
int rv;
VALIDATE_SW_IF_INDEX(mp);
rv = sample_counter_enable_disable(smcm, sw_if_index, (int)(mp->enable_disable));
BAD_SW_IF_INDEX_LABEL;
REPLY_MACRO(VL_API_SAMPLE_COUNTER_ENABLE_DISABLE_REPLY);
}
static void
vl_api_sample_counter_get_t_handler(vl_api_sample_counter_get_t *mp)
{
sample_counter_main_t *smcm = &sample_counter_main;
vl_api_sample_counter_get_reply_t *rmp;
u32 sw_if_index = ntohl(mp->sw_if_index);
u64 packets = 0, bytes = 0;
int rv;
VALIDATE_SW_IF_INDEX(mp);
rv = sample_counter_read(smcm, sw_if_index, &packets, &bytes);
BAD_SW_IF_INDEX_LABEL;
REPLY_MACRO2(VL_API_SAMPLE_COUNTER_GET_REPLY,
({
rmp->packets = clib_host_to_net_u64(packets);
rmp->bytes = clib_host_to_net_u64(bytes);
}));
}
static void
vl_api_sample_counter_dump_t_handler(vl_api_sample_counter_dump_t *mp)
{
sample_counter_main_t *smcm = &sample_counter_main;
vl_api_registration_t *reg;
vl_api_sample_counter_details_t *rmp;
u32 sw_if_index;
u64 packets, bytes;
reg = vl_api_client_index_to_registration(mp->client_index);
if (!reg) return;
pool_foreach_index (sw_if_index, smcm->counters)
{
sample_counter_read(smcm, sw_if_index, &packets, &bytes);
rmp = vl_msg_api_alloc_zero(sizeof(*rmp));
rmp->_vl_msg_id = ntohs(VL_API_SAMPLE_COUNTER_DETAILS + smcm->msg_id_base);
rmp->context = mp->context;
rmp->sw_if_index = htonl(sw_if_index);
rmp->packets = clib_host_to_net_u64(packets);
rmp->bytes = clib_host_to_net_u64(bytes);
vl_api_send_msg(reg, (u8 *) rmp);
}
}
/* 빌드 시 vppapigen이 생성한 메시지 리스트를 포함 */
#include <sample_counter/sample_counter.api.c>
static clib_error_t *
sample_counter_api_hookup(vlib_main_t *vm)
{
sample_counter_main_t *smcm = &sample_counter_main;
smcm->msg_id_base = setup_message_id_table();
return 0;
}
VLIB_API_INIT_FUNCTION(sample_counter_api_hookup);
매크로 REPLY_MACRO, REPLY_MACRO2는 vlibapi/api_helper_macros.h에 정의되어 있습니다. 이들은 요청 메시지의 client_index·context를 회신에 복사하고, 전역 메시지 ID 베이스(REPLY_MSG_ID_BASE)를 더해 런타임 메시지 ID를 계산합니다. 플러그인은 자신만의 베이스를 setup_message_id_table()(vppapigen이 생성)에서 등록하므로, 여러 플러그인이 동시에 로드되어도 ID 충돌이 없습니다.
메시지 CRC와 호환성
각 메시지는 필드 레이아웃 기반의 32비트 CRC를 가집니다. 클라이언트가 연결 시 VPP가 로드한 메시지 목록과 CRC를 내려주고, 클라이언트 측 IDL과 CRC가 달라지면 해당 메시지는 "호환되지 않음"으로 표시됩니다. 따라서 배포 중인 메시지 필드를 변경하려면 다음 중 하나를 택해야 합니다.
- 새 메시지 이름으로 병행 추가: 기존
sample_counter_get은 유지한 채sample_counter_get_v2를 추가합니다. 클라이언트가 새 CRC를 인식하면 v2를 우선 사용합니다. - in_progress 속성: 안정화 전이라면 메시지 정의에
option in_progress;를 붙여 변경 가능 표식을 달아둡니다. 이 경우 VPP는 호환성 경고를 약화합니다.
Python 바인딩 (vpp_papi)
Python 바인딩은 빌드가 필요 없습니다. vpp_papi가 런타임에 JSON IDL을 로드해 메시지를 동적 생성하기 때문에, 플러그인이 설치된 후 JSON 경로를 전달하기만 하면 됩니다.
# /usr/share/vpp/api/plugins/sample_counter.api.json 이 설치되어 있어야 함
from vpp_papi import VPPApiClient
import os, glob
apidir = '/usr/share/vpp/api'
api_files = glob.glob(os.path.join(apidir, '**/*.api.json'), recursive=True)
vpp = VPPApiClient(apifiles=api_files)
vpp.connect('sample-ctrl')
# 활성화
r = vpp.api.sample_counter_enable_disable(sw_if_index=1, enable_disable=True)
assert r.retval == 0
# 단건 조회
r = vpp.api.sample_counter_get(sw_if_index=1)
print(f"pkts={r.packets} bytes={r.bytes}")
# 덤프 스트림
for d in vpp.api.sample_counter_dump():
print(f"if={d.sw_if_index} pkts={d.packets}")
# 비동기 이벤트 구독
def on_event(msg_name, msg):
if msg_name == 'sample_counter_threshold_event':
print(f"ALERT if={msg.sw_if_index} pkts={msg.packets}")
vpp.register_event_callback(on_event)
vpp.api.want_sample_counter_events(enable_disable=True, pid=os.getpid())
vpp.disconnect()
Go 바인딩 (GoVPP)
Go 쪽은 빌드 시점 코드 생성이 필요합니다. binapi-generator가 *.api.json을 읽어 타입과 RPC 래퍼를 생성합니다.
# 바인딩 생성
$ binapi-generator \
--input-dir=/usr/share/vpp/api/plugins \
--output-dir=./binapi \
sample_counter
# 결과:
./binapi/sample_counter/sample_counter.ba.go
./binapi/sample_counter/sample_counter_rpc.ba.go
package main
import (
"context"
"fmt"
"log"
"git.fd.io/govpp.git"
"git.fd.io/govpp.git/api"
"./binapi/sample_counter"
)
func main() {
conn, err := govpp.Connect("/run/vpp/api.sock")
if err != nil {
log.Fatal(err)
}
defer conn.Disconnect()
ch, err := conn.NewAPIChannel()
if err != nil {
log.Fatal(err)
}
defer ch.Close()
// 단건 조회
req := &sample_counter.SampleCounterGet{SwIfIndex: 1}
reply := &sample_counter.SampleCounterGetReply{}
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
log.Fatal(err)
}
fmt.Printf("pkts=%d bytes=%d\n", reply.Packets, reply.Bytes)
// 스트림 덤프
rpc := sample_counter.NewServiceClient(conn)
stream, _ := rpc.SampleCounterDump(context.Background(), &sample_counter.SampleCounterDump{})
for {
d, err := stream.Recv()
if err == api.EOF {
break
}
fmt.Printf("if=%d pkts=%d\n", d.SwIfIndex, d.Packets)
}
}
전송 계층 선택 — 소켓 vs 공유 메모리
Binary API는 두 전송을 지원합니다. 두 경로는 같은 메시지 포맷을 공유하므로 클라이언트 코드는 동일하며, 성능·격리 특성만 다릅니다.
- 공유 메모리(SVM): 같은 호스트·같은 namespace에서 최저 지연. 클라이언트가 VPP와 같은 공유 세그먼트를 매핑합니다. 권한 분리가 약해 신뢰 경계를 넘어가는 용도에는 부적합합니다.
- 유닉스 도메인 소켓: 기본 경로
/run/vpp/api.sock. 길이-프리픽스 바이트스트림입니다. 파일 권한으로 격리가 가능하며 컨테이너·권한 분리 시나리오에 적합합니다. 덤프처럼 많은 메시지를 한 번에 주고받을 때 복사 비용이 shmem보다 크지만, 대부분의 운영 작업에서 유의미한 병목은 아닙니다.
# 소켓 전송 강제 (권장: 운영 자동화)
vpp = VPPApiClient(apifiles=api_files, server_address='/run/vpp/api.sock')
vpp.connect('sample-ctrl', do_async=False)
vpp-agent, 자체 HTTP·gRPC 게이트웨이)를 앞에 두고 로컬 소켓으로 VPP와 통신하는 구조가 표준 패턴입니다. 소켓 파일을 TCP로 노출하는 것은 인증·암호화 계층이 없어 권장하지 않습니다.
참고자료
공식 문서
- FD.io 프로젝트 공식 사이트 — VPP, HICN, CSIT 프로젝트 허브
- VPP 26.02 공식 문서 — 현재 공개된 최상위 문서 트리
- VPP 26.02 릴리스 노트 — 최근 변경점과 API 차이
- VPP Supported Features — 기능별 성숙도와 소스 경로
- VPP Configuration Reference —
cpu,tls,session설정 - Linux Control Plane Integration —
linux_cp,linux_nl동작 원리
아키텍처 및 설계
- Feature Arcs — 피처 아크 메커니즘, 노드 체이닝
- Writing a VPP Plugin — 플러그인 개발 가이드
- VLIB (Vector Processing Library) — 벡터 처리 엔진 내부 구조
- VNET (VPP Network Stack) — 네트워킹 기능 구현
- VPP 빌드/실행/개발 가이드
성능 및 벤치마크
- CSIT Performance Report — VPP 성능 테스트 결과 (NDR/PDR, MRR)
- CSIT Trending Dashboard — 버전별 성능 트렌드
- CSIT (Continuous System Integration and Testing) — 테스트 프레임워크 개요
통합 및 생태계
- VPP Host Stack 관련 기능 목록 — Session Layer, VCL, TLS, QUIC 상태
- linux-cp Plugin — 커널 네트워크 스택 동기화
- HICN (Hybrid Information-Centric Networking) — ICN 프로토콜 VPP 구현
- VPP 소스 코드 (GitHub) — 메인 저장소
주요 참고 글
- FD.io and VPP: rising up the software data plane (LWN)
- Red Hat: VPP Vector Packet Processing 소개
- VPP Use Cases — 라우터, 방화벽(Firewall), CGN, vSwitch 사례
커널 소스 경로 (VPP 연동)
drivers/net/tun.c— TUN/TAP 드라이버 (VPP TAP 인터페이스)drivers/vhost/net.c— vhost-net (vhost-user 백엔드)net/packet/af_packet.c— AF_PACKET (host-interface)net/xdp/xsk.c— AF_XDP 소켓 (AF_XDP 인터페이스)drivers/uio/uio_pci_generic.c— UIO (DPDK/VPP 디바이스 바인딩)drivers/vfio/pci/vfio_pci_core.c— VFIO PCI (IOMMU 격리 바인딩)
커널 관련 소스 및 참고 코드
VPP가 활용하는 커널 서브시스템과 관련 소스 파일 매핑:
| 커널 서브시스템 | 소스 파일 | VPP 연관 |
|---|---|---|
| TUN/TAP | drivers/net/tun.c | VPP TAP 인터페이스, /dev/net/tun |
| vhost | drivers/vhost/net.c, drivers/vhost/vhost.c | vhost-user 백엔드, virtio 링 |
| virtio | drivers/virtio/virtio_ring.c | TAP virtio, VM 네트워킹 |
| AF_PACKET | net/packet/af_packet.c | host-interface, PACKET_MMAP |
| AF_XDP | net/xdp/xsk.c, net/xdp/xsk_buff_pool.c | AF_XDP 인터페이스 |
| XDP | net/core/dev.c (XDP hooks) | AF_XDP용 XDP 프로그램 |
| UIO | drivers/uio/uio_pci_generic.c | DPDK UIO 드라이버 |
| VFIO | drivers/vfio/pci/vfio_pci_core.c | DPDK VFIO 드라이버 (IOMMU) |
| Hugepages | mm/hugetlb.c, fs/hugetlbfs/ | DPDK/VPP 메모리 할당 |
| IOMMU | drivers/iommu/intel/iommu.c, drivers/iommu/amd/iommu.c | VFIO DMA 격리 |
| Netlink | net/netlink/af_netlink.c | linux-cp 플러그인, 라우팅 동기화 |
| Namespace | net/core/net_namespace.c | TAP 네임스페이스 격리 |
https://gerrit.fd.io/r/gitweb?p=vpp.git에서 확인할 수 있습니다. 주요 디렉터리: src/vnet/(네트워킹 코어), src/vlib/(벡터 처리 엔진), src/plugins/(플러그인), src/vpp/(메인 프로세스).
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.
오픈소스 코드 인용 고지
| 프로젝트 | 저작권자 | 라이선스 | 공식 저장소 |
|---|---|---|---|
| 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 |
코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.