某运营商的 5G UPF 需要 100Gbps 的转发能力,基于内核的转发方案只能做到 30Gbps。换用 VPP 后,单核转发能力从 3Mpps 提升到 15Mpps,整个节点轻松达到线速。VPP 的秘诀在于向量化处理——一次处理一批包,而不是一个一个来。
当你用 DPDK 把网卡收包拉到用户态之后,下一个问题来了:如何组织数据包的处理逻辑? 一个 IPv4 转发需要经过以太网解析、IP 校验、路由查找、MAC 重写、出接口选择等多个步骤——如果每个包都逐个走完这条流水线,CPU 的指令缓存(I-Cache)会被反复冲刷,性能大打折扣。
VPP(Vector Packet Processing)正是为解决这个问题而生的。它用矢量处理替代了传统的标量处理:不是让一个包走完全部流程,而是让一批包(典型 256 个)一起通过同一个处理节点,然后再一起进入下一个节点。这种”批量流水线”模式让 CPU 的 I-Cache 命中率飙升,单核 IPv4 转发性能轻松突破 10Mpps——这就是 VPP 的核心秘密。
VPP 不仅是 FD.io(Fast Data Project)的数据平面引擎,更是当今工业级高性能网络软件的基石。Cisco 的 CSR1000v 虚拟路由器、Comcast 的宽带网关、3GPP 5G UPF 参考实现——它们的底层都是 VPP。理解 VPP,就是理解下一代数据平面的架构范式。
本章将深入 VPP 的核心设计:矢量包处理为何比标量处理快、图节点架构如何组织处理流水线、插件框架如何实现可扩展性、CLI 与 binary API 如何与外部交互、性能优化的关键技巧,以及 VPP 与 DPDK 的集成方式。最后通过四个动手实践,让你从安装配置到编写自定义插件,真正掌握 VPP。
一、VPP 是什么?
1.1 从 Cisco 到 FD.io
VPP 的历史可以追溯到 2002 年。当时 Cisco 内部需要一个高性能的用户态数据平面引擎,用于其路由器产品线。经过十余年的打磨,VPP 在 Cisco 内部已经是一个非常成熟的商业级数据平面——但它是闭源的。
2016 年,Cisco 将 VPP 的核心代码以 Apache 2.0 许可证捐赠给 Linux Foundation,成立了 FD.io(Fast Data Project)。这一举动改变了高性能网络领域的格局:一个经过十余年工业验证的数据平面引擎,突然变成了开源社区的。
关键时间线:
| 时间 | 事件 |
|---|---|
| 2002 | Cisco 内部启动 VPP 项目,用于高性能路由器数据平面 |
| 2010 | VPP 在 Cisco CSR1000v 虚拟路由器中大规模部署 |
| 2016 | Cisco 将 VPP 捐赠给 Linux Foundation,FD.io 项目成立 |
| 2017 | FD.io 成为 LFN(Linux Foundation Networking)项目 |
| 2018 | FD.io 从 LFN 孵化器毕业,成为成熟项目 |
| 2019 | Comcast 宣布基于 VPP 构建宽带网关,日处理数十亿包 |
| 2021 | VPP 成为 3GPP 5G UPF 参考实现的核心数据平面 |
| 2025 | VPP 持续演进,支持 P4 可编程、SRv6、WireGuard 等新特性 |
1.2 设计哲学
VPP 的设计哲学可以概括为三个核心原则:
1. 矢量处理(Vector Processing)
传统网络软件采用标量处理:一个包走完所有处理步骤,再处理下一个包。VPP 反其道而行:一批包(矢量)一起通过同一个处理节点,然后一起进入下一个节点。这种模式极大地提高了 I-Cache 命中率,是 VPP 性能的根基。
2. 图节点流水线(Graph-based Pipeline)
VPP 将数据包处理逻辑组织为有向图:每个处理步骤是一个节点(Node),数据包沿着图的边从一个节点流向下一个节点。这种设计让处理逻辑高度模块化——添加新功能只需添加新节点和边,无需修改已有代码。
3. 插件可扩展(Plugin Extensibility)
VPP 的核心功能(以太网解析、IP 路由、ARP 等)以图节点形式实现,而新功能可以通过插件(Plugin) 动态加载。插件可以注册新的图节点、添加 CLI 命令、扩展 binary API——几乎可以扩展 VPP 的一切。
这三个原则不是孤立的,而是相互支撑的:图节点架构是矢量处理的前提(只有把处理逻辑拆成节点,才能批量通过),插件框架是图节点架构的延伸(插件就是动态注册的节点集合)。理解了这三点,就理解了 VPP 的全部设计哲学。
1.3 VPP 在高性能网络生态中的位置
在第 3 章中详细分析了 DPDK 的架构——DPDK 解决的是”如何高效收发包”的问题。VPP 则解决的是”如何高效处理包”的问题。两者是互补关系:
- DPDK:用户态轮询驱动、零拷贝内存、无锁队列——数据包从网卡到用户态的基础设施
- VPP:矢量处理、图节点流水线、插件框架——数据包在用户态的高效处理引擎
二、矢量包处理:I-Cache 友好的秘密
2.1 标量处理:I-Cache 的噩梦
传统的网络包处理采用标量(Scalar) 模式:取一个包,让它走完所有处理步骤(以太网解析 → IP 校验 → 路由查找 → MAC 重写 → 发送),然后再取下一个包。
假设每个处理步骤的代码大小为 4 KB,整条流水线共 5 个步骤,总计 20 KB。现代 CPU 的 L1 I-Cache 通常为 32 KB。看起来 20 KB 完全放得下?问题在于——当第一个包走完”以太网解析”进入”IP 校验”时,“以太网解析”的代码就可能被挤出 I-Cache;当第二个包到来时,“以太网解析”的代码又需要重新从 L2 Cache 或主存加载。
这就是 I-Cache Thrashing(指令缓存颠簸):CPU 花费大量时间等待指令从低级缓存或主存加载,而不是执行指令。当流水线步骤越多、代码越大,这个问题越严重。
2.2 矢量处理:批量通过,I-Cache 友好
VPP 的矢量处理采用了完全不同的策略:不是让一个包走完所有步骤,而是让一批包(矢量)一起通过同一个步骤,然后这批包一起进入下一个步骤。
具体来说:
- 输入节点(如
dpdk-input)一次从网卡收取 N 个包(典型 N=256) - 这 256 个包一起进入
ethernet-input节点——此时ethernet-input的代码被加载到 I-Cache - 256 个包全部处理完后,一起进入
ip4-input节点——此时ip4-input的代码被加载到 I-Cache - 以此类推,直到所有包走完流水线
关键洞察:当 256 个包连续执行同一段代码时,这段代码在 I-Cache 中是”热的”——第一个包执行时加载代码,后续 255 个包直接命中 I-Cache。I-Cache 的加载开销被 256 个包分摊,每个包的均摊开销几乎为零。
2.3 为什么是 256?
VPP 默认的矢量大小(VLIB_FRAME_SIZE)为 256。这个数字不是随意选择的,而是基于以下考量:
- L1 I-Cache 大小:现代 CPU 的 L1 I-Cache 通常为 32 KB。一个典型节点的代码大小在 1~8 KB 之间,256 个包的批量处理确保代码在 I-Cache 中保持热度
- 均摊调度开销:每个节点调度有一次固定开销(函数指针调用、矢量初始化等),256 个包分摊后,每个包的调度开销可忽略
- L1 D-Cache 局部性:256 个包的元数据(
vlib_buffer_t)在内存中连续,预取效率高 - 延迟与吞吐的平衡:矢量越大,吞吐越高,但延迟也越大(需要等矢量填满)。256 是一个工程上的甜蜜点
矢量大小并非越大越好。过大的矢量会导致:(1) 延迟增加——必须等更多包到达才能开始处理;(2) L1 D-Cache 压力增大——256 个包的元数据已经接近 L1 D-Cache 容量;(3) 尾部延迟——矢量末尾的包等待时间过长。在延迟敏感场景中,可以减小矢量大小(如 64 或 128),牺牲部分吞吐来换取更低延迟。
2.4 标量 vs 矢量:直观对比
2.5 性能数据
在典型的 x86 服务器上(Intel Xeon Gold 6230 @ 2.1 GHz),VPP 的 IPv4 转发性能数据:
| 配置 | 吞吐量 | 延迟(P50) | 备注 |
|---|---|---|---|
| 单核,64 字节包 | ~12 Mpps | ~2 μs | 纯 IPv4 转发,无 ACL |
| 单核,512 字节包 | ~8 Mpps | ~3 μs | 路由表 1000 条 |
| 单核,1518 字节包 | ~1.2 Mpps | ~5 μs | 接近 10G 线速 |
| 4 核,64 字节包 | ~45 Mpps | ~3 μs | RSS 分流 + worker 线程 |
| 8 核,64 字节包 | ~85 Mpps | ~4 μs | 线性扩展接近理想 |
以上数据基于 VPP 24.06 + DPDK 23.11,使用 Intel XL710 (40GbE) 网卡。实际性能取决于硬件配置、路由表大小、启用的特性等因素。VPP 的单核 IPv4 转发性能通常比基于 Linux 内核的转发快 510 倍,比基于 OVS 内核态的转发快 35 倍。
三、图节点架构
3.1 节点类型
VPP 的图节点有三种类型,对应不同的处理阶段:
1. VLIB_NODE_TYPE_INPUT(输入节点)
输入节点是数据包进入 VPP 的入口。它们负责从外部源(网卡、AF_XDP socket、tap 接口等)获取数据包,并将其注入 VPP 的图节点流水线。输入节点在每个调度周期中被优先调用。
典型的输入节点:
dpdk-input:从 DPDK PMD 收包(最常用)af-xdp-input:从 AF_XDP socket 收包tap-input:从 tap 接口收包unix-epoll-input:从 Unix socket 收包(用于 CLI 通信)
2. VLIB_NODE_TYPE_INTERNAL(内部节点)
内部节点是 VPP 图节点的主体,负责数据包的实际处理逻辑。以太网解析、IP 路由查找、ACL 过滤、NAT 转换等都是内部节点。
典型的内部节点:
ethernet-input:以太网帧解析,根据 EtherType 分发到下一节点ip4-input:IPv4 头部校验、选项处理ip4-lookup:IPv4 路由表查找(FIB 查找)ip4-rewrite:根据路由结果重写 MAC 地址和 TTLip4-icmp-input:ICMP 消息处理
3. VLIB_NODE_TYPE_OUTPUT(输出节点)
输出节点是数据包离开 VPP 的出口。它们负责将处理完毕的数据包发送到外部目标(网卡、tap 接口等)。
典型的输出节点:
interface-output:通用输出节点,根据接口类型分发dpdk-output:通过 DPDK PMD 发包tap-output:通过 tap 接口发包
3.2 VLIB_REGISTER_NODE 宏
VPP 使用 VLIB_REGISTER_NODE 宏将节点注册到图节点系统中。这个宏在编译时生成注册信息,在 VPP 启动时自动执行注册。
/* 注册一个 IPv4 查找节点 */VLIB_REGISTER_NODE (ip4_lookup_node) ={ .name = "ip4-lookup", /* 节点名称,用于 CLI 和调试 */ .vector_size = sizeof (u32), /* 矢量元素大小(next_index) */ .format_trace = format_ip4_lookup_trace, /* 追踪格式化函数 */ .n_next_nodes = IP4_LOOKUP_N_NEXT, /* 下一个节点数量 */ .next_nodes = { [IP4_LOOKUP_NEXT_REWRITE] = "ip4-rewrite", [IP4_LOOKUP_NEXT_DROP] = "ip4-drop", [IP4_LOOKUP_NEXT_PUNT] = "ip4-punt", },};关键字段解释:
- name:节点的唯一标识符,在 CLI 和日志中使用
- vector_size:矢量中每个元素的大小,通常是
sizeof(u32)(即 next_index) - n_next_nodes 和 next_nodes:定义该节点的后继节点。每个包通过
next_index选择下一个节点 - format_trace:当启用包追踪时,格式化该节点的追踪信息
3.3 节点函数签名
每个图节点必须实现一个节点函数(Node Function),其签名为:
/** * 图节点函数签名 * * @param vm 虚拟机实例,包含全局状态 * @param node 当前节点描述符,包含节点统计和配置 * @param frame 输入矢量帧,包含待处理的数据包索引数组 * @return 处理的数据包数量 */static uwordip4_lookup_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame){ u32 n_left_from, *from; u32 next_index;
/* 获取输入矢量(待处理的包索引数组) */ from = vlib_frame_vector_args (frame); n_left_from = frame->n_vectors; /* 矢量中的包数量 */
/* 获取下一个节点的索引数组 */ u32 *to_next; u32 n_left_to_next;
while (n_left_from > 0) { /* 获取当前节点的下一个矢量空间 */ vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
/* 双循环处理数据包(内循环 4 路展开) */ while (n_left_from > 0 && n_left_to_next > 0) { u32 bi0; /* buffer index */ vlib_buffer_t *b0; /* buffer 指针 */ u32 next0; /* 下一个节点索引 */
/* 取包 */ bi0 = from[0]; b0 = vlib_get_buffer (vm, bi0);
/* --- 数据包处理逻辑 --- */ ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* FIB 查找 */ ip4_fib_mtrie_lookup_t mtrie0; next0 = ip4_lookup_mtrie (&mtrie0, ip0, &next0);
/* 设置下一个节点 */ to_next[0] = bi0; to_next += 1; n_left_to_next -= 1;
/* 预取下一个包的数据 */ if (n_left_from > 1) { u32 bi1 = from[1]; vlib_buffer_t *b1 = vlib_get_buffer (vm, bi1); CLIB_PREFETCH (b1, 64, LOAD); }
from += 1; n_left_from -= 1; }
/* 将处理完的矢量放入下一个节点 */ vlib_put_next_frame (vm, node, next_index, n_left_to_next); }
return frame->n_vectors;}3.4 next_index:逐包选择下一个节点
VPP 图节点的核心灵活性来自 next_index 机制。每个包在处理过程中决定自己的下一个节点——不是所有包都必须走同一条路径。
例如,在 ip4-lookup 节点中:
- 路由查找命中 →
next_index = IP4_LOOKUP_NEXT_REWRITE→ 进入ip4-rewrite - 路由查找未命中 →
next_index = IP4_LOOKUP_NEXT_DROP→ 进入ip4-drop - 路由查找命中但需要 punt →
next_index = IP4_LOOKUP_NEXT_PUNT→ 进入ip4-punt
这意味着同一个矢量中的 256 个包,可能被分发到不同的下一个节点。VPP 的调度器会自动将包分组,形成新的子矢量,分别发送到对应的后继节点。
3.5 VPP 图节点拓扑
一个典型的 IPv4 转发路径在 VPP 中经过以下节点:
上图展示的是最简单的 IPv4 单播转发路径。实际 VPP 的图节点拓扑要复杂得多——ARP 解析、ICMP 处理、ACL 过滤、NAT 转换、QoS 分类等都是独立的节点,通过 next_index 动态选择。你可以用 show vlib graph CLI 命令查看完整的节点拓扑。
3.6 完整节点实现示例
以下是一个完整的自定义节点实现——一个简单的 IPv4 包计数器节点:
#include <vlib/vlib.h>#include <vnet/ip/ip.h>
/* 定义下一个节点的枚举 */typedef enum{ MY_COUNTER_NEXT_IP4_LOOKUP, /* 正常转发 → ip4-lookup */ MY_COUNTER_NEXT_DROP, /* 丢弃 */ MY_COUNTER_N_NEXT,} my_counter_next_t;
/* 节点函数 */static uwordmy_counter_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame){ u32 n_left_from, *from; u32 pkts_counted = 0;
from = vlib_frame_vector_args (frame); n_left_from = frame->n_vectors;
while (n_left_from > 0) { u32 bi0 = from[0]; vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0); ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* 统计 IPv4 包 */ if (ip0->protocol == IP_PROTOCOL_TCP) pkts_counted++;
/* 所有包都转发到下一个节点 */ from[0] = bi0; from += 1; n_left_from -= 1; }
/* 更新节点计数器 */ vlib_node_increment_counter (vm, node->node_index, 0, pkts_counted);
/* 将所有包发送到 ip4-lookup */ vlib_buffer_enqueue_to_next (vm, node, from, frame->n_vectors, MY_COUNTER_NEXT_IP4_LOOKUP);
return frame->n_vectors;}
/* 注册节点 */VLIB_REGISTER_NODE (my_counter_node) ={ .name = "my-counter", .vector_size = sizeof (u32), .n_next_nodes = MY_COUNTER_N_NEXT, .next_nodes = { [MY_COUNTER_NEXT_IP4_LOOKUP] = "ip4-lookup", [MY_COUNTER_NEXT_DROP] = "error-drop", },};
/* 将节点函数与注册信息关联 */VLIB_NODE_FUNCTION (my_counter_node, my_counter_node_fn);四、插件框架
4.1 为什么需要插件?
VPP 的核心功能(以太网解析、IP 路由、ARP 等)以图节点形式内置在 vnet 库中。但实际部署中,用户往往需要自定义功能:专有的 NAT 逻辑、定制的 ACL 规则、特定的 QoS 策略等。
如果每次添加新功能都要修改 VPP 核心代码并重新编译,那将是一场噩梦——代码维护困难、升级风险高、不同用户的需求互相冲突。VPP 的插件框架正是为解决这个问题而设计的:
- 动态加载:插件以共享库(
.so)形式存在,VPP 启动时自动加载 - 隔离性:插件有独立的源码目录和编译流程,与 VPP 核心代码解耦
- 完整 API 访问:插件可以调用 VPP 的全部公开 API(vlib、vnet、vppinfra)
- 热插拔:通过配置文件控制插件的加载/卸载,无需修改 VPP 核心代码
4.2 VLIB_PLUGIN_REGISTER
每个插件必须使用 VLIB_PLUGIN_REGISTER 宏声明其元信息:
#include <vlib/vlib.h>#include <vlibplugin/plugin.h>
VLIB_PLUGIN_REGISTER () ={ .version = "1.0.0", /* 插件版本 */ .description = "My custom VPP plugin", /* 插件描述 */ .default_disabled = 0, /* 默认是否禁用(0=启用) */};VPP 启动时会扫描插件目录(默认 /usr/lib/x86_64-linux-gnu/vpp_plugins/),加载所有非禁用的插件共享库,并调用其初始化函数。
4.3 插件目录结构
一个标准的 VPP 插件项目结构如下:
my_plugin/├── CMakeLists.txt # 构建配置├── my_plugin.c # 插件入口 + 初始化├── my_plugin_node.c # 图节点实现├── my_plugin_api.c # Binary API 定义├── my_plugin_cli.c # CLI 命令注册├── my_plugin.h # 内部头文件├── my_plugin.api # Binary API 消息定义└── setup.py # 安装脚本(可选)4.4 插件初始化流程
VPP 的插件初始化遵循以下流程:
- VPP 主进程启动,解析命令行参数和配置文件
- 扫描插件目录,发现所有
.so文件 - 对每个插件,检查
VLIB_PLUGIN_REGISTER的default_disabled字段 - 加载非禁用的插件共享库(
dlopen) - 调用插件的初始化函数(
VLIB_INIT_FUNCTION) - 初始化函数中注册图节点、CLI 命令、binary API 等
- 所有插件初始化完成后,VPP 进入主循环
/* 插件初始化函数 */static clib_error_t *my_plugin_init (vlib_main_t *vm){ /* 初始化插件的全局状态 */ my_plugin_main_t *mpm = &my_plugin_main; clib_memset (mpm, 0, sizeof (*mpm));
/* 注册图节点(已在 VLIB_REGISTER_NODE 中完成) */ /* 注册 CLI 命令 */ /* 注册 binary API */
return 0; /* 返回 NULL 表示成功 */}
/* 将初始化函数注册到 VPP 启动流程 */VLIB_INIT_FUNCTION (my_plugin_init);4.5 完整插件骨架
以下是一个完整的 VPP 插件骨架,包含节点、CLI 和初始化:
/* ========== my_plugin.h ========== */#ifndef __MY_PLUGIN_H__#define __MY_PLUGIN_H__
#include <vlib/vlib.h>
typedef struct{ u32 packet_count; /* 包计数器 */ u32 byte_count; /* 字节计数器 */} my_plugin_main_t;
extern my_plugin_main_t my_plugin_main;
#endif /* __MY_PLUGIN_H__ */
/* ========== my_plugin.c ========== */#include "my_plugin.h"#include <vlibplugin/plugin.h>
my_plugin_main_t my_plugin_main;
VLIB_PLUGIN_REGISTER () ={ .version = "1.0.0", .description = "My packet counter plugin",};
static clib_error_t *my_plugin_init (vlib_main_t *vm){ my_plugin_main_t *mpm = &my_plugin_main; clib_memset (mpm, 0, sizeof (*mpm)); return 0;}
VLIB_INIT_FUNCTION (my_plugin_init);
/* ========== my_plugin_node.c ========== */#include "my_plugin.h"
typedef enum{ MY_PLUGIN_NEXT_IP4_LOOKUP, MY_PLUGIN_NEXT_DROP, MY_PLUGIN_N_NEXT,} my_plugin_next_t;
static uwordmy_plugin_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame){ my_plugin_main_t *mpm = &my_plugin_main; u32 n_left_from, *from;
from = vlib_frame_vector_args (frame); n_left_from = frame->n_vectors;
while (n_left_from > 0) { u32 bi0 = from[0]; vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);
/* 统计 */ mpm->packet_count++; mpm->byte_count += vlib_buffer_length_in_chain (vm, b0);
from += 1; n_left_from -= 1; }
/* 所有包转发到 ip4-lookup */ vlib_buffer_enqueue_to_next (vm, node, from, frame->n_vectors, MY_PLUGIN_NEXT_IP4_LOOKUP);
return frame->n_vectors;}
VLIB_REGISTER_NODE (my_plugin_node) ={ .name = "my-plugin-node", .vector_size = sizeof (u32), .n_next_nodes = MY_PLUGIN_N_NEXT, .next_nodes = { [MY_PLUGIN_NEXT_IP4_LOOKUP] = "ip4-lookup", [MY_PLUGIN_NEXT_DROP] = "error-drop", },};
VLIB_NODE_FUNCTION (my_plugin_node, my_plugin_node_fn);
/* ========== my_plugin_cli.c ========== */#include "my_plugin.h"
static clib_error_t *show_my_plugin_stats_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd){ my_plugin_main_t *mpm = &my_plugin_main;
vlib_cli_output (vm, "My Plugin Statistics:"); vlib_cli_output (vm, " Packets: %u", mpm->packet_count); vlib_cli_output (vm, " Bytes: %u", mpm->byte_count);
return 0;}
/* 注册 CLI 命令: show my-plugin stats */VLIB_CLI_COMMAND (show_my_plugin_stats_cmd, static) ={ .path = "show my-plugin stats", .short_help = "Show my plugin packet statistics", .function = show_my_plugin_stats_fn,};插件中的图节点函数运行在 VPP 的主数据路径上,任何性能问题都会直接影响整体吞吐。在节点函数中绝对不能做以下事情:(1) 调用阻塞 I/O(printf、文件读写等);(2) 使用互斥锁(会导致 worker 线程阻塞);(3) 分配大块内存(使用 VPP 的内存池 vec_alloc 代替 malloc);(4) 执行耗时的计算(超过微秒级)。违反这些规则会导致 VPP 性能急剧下降。
五、CLI 与 binary API
5.1 VPP CLI(vppctl)
VPP 提供了一个功能丰富的命令行接口,通过 vppctl 工具访问。VPP CLI 是调试、配置和监控 VPP 的主要方式。
# 连接到 VPP 的 CLIsudo vppctl
# 或者直接执行单条命令sudo vppctl show interface常用的 VPP CLI 命令分为三大类:
show 命令(查看状态)
# 查看接口信息show interfaceshow interface address
# 查看路由表show ip fibshow ip6 fib
# 查看 ARP 表show ip arp
# 查看图节点统计show vlib graphshow node counters
# 查看运行时信息show runtimeshow threads
# 查看插件列表show plugins
# 查看硬件信息show hardware
# 查看包追踪trace add dpdk-input 100show traceclear traceset 命令(配置)
# 设置接口 IP 地址set interface ip address GigabitEthernet0/8/0 192.168.1.1/24set interface ip address del GigabitEthernet0/8/0 192.168.1.1/24
# 启用/禁用接口set interface state GigabitEthernet0/8/0 upset interface state GigabitEthernet0/8/0 down
# 设置路由ip route add 10.0.0.0/8 via 192.168.1.254ip route del 10.0.0.0/8
# 设置 ARP 条目set ip arp GigabitEthernet0/8/0 192.168.1.254 00:11:22:33:44:55clear 命令(清除状态)
# 清除接口统计clear interface
# 清除 ARP 表clear ip arp
# 清除路由表clear ip fib
# 清除节点计数器clear node counters5.2 Binary API
VPP CLI 适合人工交互,但程序化控制 VPP 需要一种更高效的接口——Binary API。
Binary API 是 VPP 的进程间通信(IPC)机制,基于共享内存实现:
- 低延迟:客户端和 VPP 之间通过共享内存交换消息,无需系统调用
- 类型安全:消息格式由
.api文件定义,自动生成 C 代码 - 双向通信:客户端可以发送请求并接收回复,也可以注册回调接收 VPP 的事件通知
Binary API 的工作流程:
- 客户端连接到 VPP 的共享内存区域(
/dev/shm/vpe-api) - 客户端发送请求消息(如
sw_interface_dump) - VPP 处理请求,将回复消息写入共享内存
- 客户端读取回复
5.3 API 消息定义(.api 文件)
VPP 使用 .api 文件定义 Binary API 的消息格式。这些文件使用一种类似 Protobuf 的 IDL(接口定义语言):
/* my_plugin.api - 自定义插件的 API 定义 */
/* 请求消息:查询包统计 */define my_plugin_stats_get{ u32 client_index; /* 客户端标识 */ u32 context; /* 请求上下文 */};
/* 回复消息:返回包统计 */define my_plugin_stats_reply{ u32 context; /* 请求上下文 */ i32 retval; /* 返回值(0=成功) */ u64 packet_count; /* 包计数 */ u64 byte_count; /* 字节计数 */};VPP 的构建系统会自动从 .api 文件生成 C 代码(消息结构体、序列化/反序列化函数、类型安全的 API 函数),开发者无需手动编写这些样板代码。
5.4 VAT(VPP API Tester)
VAT 是 VPP 自带的 Binary API 测试工具,可以直接通过命令行发送 Binary API 消息:
# 启动 VATsudo vpp-api-test
# 在 VAT 中执行 API 命令vat# sw_interface_dumpvat# ip_route_add_dst_prefix 10.0.0.0/8 via 192.168.1.254vat# my_plugin_stats_getVAT 对于调试 Binary API 和验证插件功能非常有用。
5.5 CLI 命令注册
在 VPP 中注册自定义 CLI 命令非常简单,使用 VLIB_CLI_COMMAND 宏:
#include <vlib/vlib.h>
static clib_error_t *my_set_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd){ u32 value = 0;
/* 解析命令参数 */ while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) { if (unformat (input, "value %d", &value)) ; else return clib_error_return (0, "unknown input `%U'", format_unformat_error, input); }
vlib_cli_output (vm, "Set value to %u\n", value); return 0;}
/* 注册 CLI 命令: set my-command value <n> */VLIB_CLI_COMMAND (my_set_command, static) ={ .path = "set my-command", .short_help = "Set my custom command value", .function = my_set_command_fn,};VPP CLI 命令的路径是层级式的。set my-command 中的 set 是顶级命令,my-command 是子命令。VPP 会自动处理命令补全和帮助信息。你可以在 VPP CLI 中输入 set ? 查看所有 set 子命令。
六、性能优化技术
6.1 预取(Prefetch)
预取是 VPP 性能优化的第一利器。现代 CPU 的内存访问延迟与 Cache 命中与否差距巨大:
| 访问类型 | 典型延迟 |
|---|---|
| L1 Cache 命中 | ~1 ns |
| L2 Cache 命中 | ~4 ns |
| L3 Cache 命中 | ~12 ns |
| 主存访问 | ~80 ns |
如果每次处理数据包都要从主存加载 vlib_buffer_t 和数据包内容,延迟将是灾难性的。VPP 通过提前预取下一个包的数据来解决这个问题:
/* VPP 节点中的典型预取模式 */while (n_left_from >= 4){ /* 预取距离为 2 的包数据 */ vlib_buffer_t *b2, *b3; b2 = vlib_get_buffer (vm, from[2]); b3 = vlib_get_buffer (vm, from[3]);
/* 预取 buffer 元数据(64 字节) */ CLIB_PREFETCH (b2, 64, LOAD); CLIB_PREFETCH (b3, 64, LOAD);
/* 预取数据包内容(64 字节) */ CLIB_PREFETCH (vlib_buffer_get_current (b2), 64, LOAD); CLIB_PREFETCH (vlib_buffer_get_current (b3), 64, LOAD);
/* 处理当前包 b0 和 b1 */ /* ... */
from += 2; n_left_from -= 2;}CLIB_PREFETCH 宏展开为 GCC 的 __builtin_prefetch,它生成 PREFETCHT0 指令,告诉 CPU 提前将数据加载到 L1 Cache 中。预取距离(lookahead distance)通常为 24 个包——在处理当前包时,提前加载 24 个包之后的数据。
6.2 矢量大小调优
VPP 的默认矢量大小为 256(VLIB_FRAME_SIZE),但可以根据场景调整:
- 高吞吐场景:使用较大的矢量(256 或更大),最大化 I-Cache 命中率
- 低延迟场景:使用较小的矢量(64 或 128),减少等待矢量填满的延迟
- 混合场景:使用 VPP 的自适应矢量大小调整(
vlib_frame_size运行时可调)
# 在 VPP 启动配置中调整矢量大小node { frame-size 128 # 减小矢量大小以降低延迟}6.3 节点调度优化
VPP 的节点调度器负责决定下一个执行哪个节点。调度策略直接影响性能:
1. 输入节点优先
输入节点(dpdk-input)在每个调度周期中被优先调用,确保数据包持续进入流水线。如果输入节点没有包,调度器才会处理内部节点。
2. 间接调用优化
VPP 的节点调度通过函数指针实现。为了减少间接调用的开销,VPP 将活跃节点的函数指针缓存在 vlib_node_runtime_t 结构中,确保 CPU 的分支预测器能准确预测目标地址。
3. 节点合并
对于简单的处理步骤,VPP 支持将多个逻辑步骤合并到一个节点函数中,减少节点调度次数。例如,ip4-rewrite 节点同时完成 MAC 重写、TTL 递减和校验和更新——而不是分成三个独立节点。
6.4 多核架构
VPP 的多核架构采用主线程 + worker 线程模型:
关键设计:
- 主线程:负责 VPP 初始化、CLI 处理、binary API 处理、控制面操作。主线程不参与数据包转发
- Worker 线程:每个 worker 线程独立运行完整的图节点流水线,从输入节点到输出节点。Worker 线程之间无共享状态,无需加锁
- RSS 分流:网卡通过 RSS(Receive Side Scaling)将数据包哈希到不同的接收队列,每个 worker 线程绑定一个队列,实现无锁并行
# /etc/vpp/startup.conf - 多核配置示例cpu { main-core 0 # 主线程绑定 CPU 0 corelist-workers 2-5 # Worker 线程绑定 CPU 2-5}
dpdk { dev 0000:3b:00.0 { num-rx-queues 4 # 4 个接收队列,对应 4 个 worker num-tx-queues 4 # 4 个发送队列 }}VPP 的 worker 线程必须是无锁的——任何共享状态的访问都会导致 cache line bouncing,严重降低多核扩展性。如果你的插件需要在 worker 之间共享数据,请使用 VPP 提供的无锁数据结构(如 clib_spinlock、vlib_frame_queue),或者将共享操作放到主线程中执行。
七、VPP + DPDK 集成
7.1 dpdk-input 节点
VPP 通过 dpdk-input 节点从 DPDK PMD 接收数据包。这个节点是 VPP 与 DPDK 的桥梁,其工作流程如下:
- VPP 的 worker 线程调用
dpdk-input节点函数 - 节点函数调用
rte_eth_rx_burst()从 DPDK PMD 批量收包 - 收到的
rte_mbuf被转换为 VPP 的vlib_buffer_t格式 - 数据包索引被放入矢量帧,传递给下一个节点(
ethernet-input)
关键代码逻辑(简化版):
static uworddpdk_input_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame){ dpdk_main_t *dm = &dpdk_main; u32 n_packets;
/* 遍历所有 DPDK 接口 */ for (int i = 0; i < vec_len (dm->devices); i++) { dpdk_device_t *xd = dm->devices[i]; u16 queue_id = vlib_get_thread_index (); /* 每个 worker 一个队列 */
/* 从 DPDK PMD 批量收包 */ struct rte_mbuf *mbufs[VLIB_FRAME_SIZE]; n_packets = rte_eth_rx_burst (xd->port_id, queue_id, mbufs, VLIB_FRAME_SIZE);
if (n_packets == 0) continue;
/* 将 rte_mbuf 转换为 vlib_buffer_t 并加入矢量 */ u32 *to_next; vlib_get_next_frame (vm, node, DPDK_INPUT_NEXT_ETHERNET, to_next, n_left_to_next);
for (int j = 0; j < n_packets; j++) { /* rte_mbuf 与 vlib_buffer_t 共享内存布局 */ u32 bi = rte_mbuf_to_vlib_buffer (mbufs[j]); to_next[j] = bi; }
vlib_put_next_frame (vm, node, DPDK_INPUT_NEXT_ETHERNET, n_left_to_next - n_packets); }
return n_packets;}VPP 的 vlib_buffer_t 和 DPDK 的 rte_mbuf 在内存布局上是兼容的——VPP 在 rte_mbuf 的头部预留了空间用于存储 vlib_buffer_t 的元数据。这意味着从 rte_mbuf 到 vlib_buffer_t 的转换几乎零开销,只是指针的简单偏移。这种设计是 VPP + DPDK 高效集成的关键之一。
7.2 dpdk-output 节点
dpdk-output 节点负责将处理完毕的数据包通过 DPDK PMD 发送出去:
- 数据包从上游节点(如
interface-output)到达dpdk-output - 节点函数将
vlib_buffer_t转换回rte_mbuf格式 - 调用
rte_eth_tx_burst()批量发送数据包 - 发送完毕后释放已发送的 buffer
7.3 配置 DPDK 接口
VPP 通过 startup.conf 配置文件指定 DPDK 接口:
dpdk { # 指定 DPDK EAL 参数 dev 0000:3b:00.0 # 绑定 PCI 地址为 0000:3b:00.0 的网卡 dev 0000:3b:00.1 # 绑定第二块网卡
# 或者使用网卡描述 dev 0000:3b:00.0 { num-rx-queues 4 # 接收队列数(对应 worker 线程数) num-tx-queues 4 # 发送队列数 rx-offload 0x0 # 接收卸载配置 tx-offload 0x0 # 发送卸载配置 }
# 大页内存配置 huge-dir /dev/hugepages socket-mem 2048,2048 # 每个 NUMA 节点的内存大小(MB)
# 日志级别 log-level debug}配置完成后,VPP 启动时会自动初始化 DPDK EAL、绑定网卡、创建收发队列。之后 dpdk-input 和 dpdk-output 节点就可以正常工作了。
7.4 VPP + DPDK 性能
VPP + DPDK 的组合是当今开源领域最高性能的用户态网络方案之一。在典型的 x86 服务器上,VPP + DPDK 的 IPv4 转发性能:
| 包大小 | 单核吞吐 | 4 核吞吐 | 8 核吞吐 | 线速比 |
|---|---|---|---|---|
| 64 B | 12 Mpps | 45 Mpps | 85 Mpps | ~56% @ 10GbE |
| 128 B | 12 Mpps | 46 Mpps | 88 Mpps | ~85% @ 10GbE |
| 256 B | 11 Mpps | 42 Mpps | 80 Mpps | ~98% @ 10GbE |
| 512 B | 8 Mpps | 30 Mpps | 58 Mpps | ~100% @ 10GbE |
| 1518 B | 1.2 Mpps | 4.6 Mpps | 8.8 Mpps | ~100% @ 10GbE |
64 字节小包场景下,VPP + DPDK 的单核吞吐约 12 Mpps,这已经接近 x86 架构的理论极限。64 字节包在 10GbE 上的线速为 14.88 Mpps,VPP 达到了线速的 80%+。在 40GbE 和 100GbE 场景下,需要更多核心来达到线速。与第 3 章中分析的 DPDK testpmd 性能相比,VPP 的 IPv4 转发(包含路由查找和 MAC 重写)仅比 testpmd 的纯 L2 转发慢约 10~15%,说明 VPP 的图节点调度开销非常低。
八、动手实践
实践 1:安装和启动 VPP
目标:在 Ubuntu 22.04 上安装 VPP 并验证其正常运行。
# 1. 添加 FD.io 软件源sudo apt updatesudo apt install -y curl apt-transport-https
# 添加 FD.io VPP 仓库(以 VPP 24.06 为例)curl -fsSL https://packagecloud.io/fdio/2406/gpgkey | \ sudo gpg --dearmor -o /usr/share/keyrings/fdio.gpgecho "deb [signed-by=/usr/share/keyrings/fdio.gpg] \ https://packagecloud.io/fdio/2406/ubuntu jammy main" | \ sudo tee /etc/apt/sources.list.d/fdio.list
# 2. 安装 VPPsudo apt updatesudo apt install -y vpp vpp-plugin-core vpp-plugin-dpdk
# 3. 配置大页内存(VPP + DPDK 必需)echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 4. 创建 VPP 配置文件sudo tee /etc/vpp/startup.conf << 'EOF'unix { nodaemon # 前台运行(调试用) log /var/log/vpp/vpp.log cli-listen /run/vpp/cli.sock}
cpu { main-core 0 corelist-workers 2-3}
dpdk { huge-dir /dev/hugepages socket-mem 1024}
# 不绑定网卡,使用 af-packet 接口(虚拟机环境友好)# dpdk {# dev 0000:3b:00.0# }EOF
# 5. 启动 VPPsudo systemctl start vpp
# 6. 验证 VPP 运行状态sudo vppctl show version# 预期输出类似:# vpp v24.06-release built by root on ...
sudo vppctl show runtime# 查看 VPP 运行时信息,包括线程、节点统计等
sudo vppctl show plugins# 查看已加载的插件列表实践 2:配置 DPDK 接口
目标:在 VPP 中配置 DPDK 接口,实现物理网卡的收发包。
此实践需要物理网卡和 DPDK 兼容的硬件。虚拟机环境下可以使用 af-packet 接口替代 DPDK 接口进行测试。
# 1. 查看系统中的网卡 PCI 地址lspci | grep -i ethernet# 输出类似:# 3b:00.0 Ethernet controller: Intel Corporation ...
# 2. 将网卡绑定到 DPDK 兼容驱动(vfio-pci)sudo modprobe vfio-pcisudo dpdk-devbind.py --bind=vfio-pci 0000:3b:00.0
# 3. 修改 VPP 配置,添加 DPDK 接口sudo tee /etc/vpp/startup.conf << 'EOF'unix { nodaemon log /var/log/vpp/vpp.log cli-listen /run/vpp/cli.sock}
cpu { main-core 0 corelist-workers 2-3}
dpdk { dev 0000:3b:00.0 { num-rx-queues 2 num-tx-queues 2 } huge-dir /dev/hugepages socket-mem 1024}EOF
# 4. 重启 VPPsudo systemctl restart vpp
# 5. 验证 DPDK 接口sudo vppctl show interface# 预期输出类似:# Name Idx State MTU (L3) IP Address# GigabitEthernet0/3b/0/0 1 down 9000
# 6. 启用接口并配置 IP 地址sudo vppctl set interface state GigabitEthernet0/3b/0/0 upsudo vppctl set interface ip address GigabitEthernet0/3b/0/0 192.168.1.1/24
# 7. 验证接口状态sudo vppctl show interface address GigabitEthernet0/3b/0/0sudo vppctl show hardware实践 3:使用 VPP CLI 配置 IPv4 转发
目标:配置 VPP 的 IPv4 路由,实现两台主机之间的转发。
假设网络拓扑如下:
主机A (192.168.1.100) ←→ [VPP 接口1: 192.168.1.1] — VPP — [VPP 接口2: 192.168.2.1] ←→ 主机B (192.168.2.100)# 1. 配置接口 IP 地址(假设接口已启用)sudo vppctl set interface ip address GigabitEthernet0/3b/0/0 192.168.1.1/24sudo vppctl set interface ip address GigabitEthernet0/3b/0/1 192.168.2.1/24
# 2. 添加静态路由sudo vppctl ip route add 10.0.0.0/8 via 192.168.1.254 GigabitEthernet0/3b/0/0sudo vppctl ip route add 172.16.0.0/12 via 192.168.2.254 GigabitEthernet0/3b/0/1
# 3. 查看 FIB(转发信息表)sudo vppctl show ip fib# 预期输出包含:# Destination Packets Bytes Adjacency# 10.0.0.0/8 0 0 via 192.168.1.254, GigabitEthernet0/3b/0/0# 192.168.1.0/24 0 0 via local, GigabitEthernet0/3b/0/0# 192.168.2.0/24 0 0 via local, GigabitEthernet0/3b/0/1
# 4. 查看 ARP 表sudo vppctl show ip arp
# 5. 手动添加 ARP 条目(如果对端不支持 ARP)sudo vppctl set ip arp GigabitEthernet0/3b/0/0 192.168.1.254 00:11:22:33:44:55
# 6. 使用包追踪调试# 启用追踪:捕获 100 个从 dpdk-input 进入的包sudo vppctl trace add dpdk-input 100
# 发送测试流量(从主机 A ping 主机 B)# ...
# 查看追踪结果sudo vppctl show trace# 预期输出类似:# Packet 1# 00:00:00:000000: dpdk-input# GigabitEthernet0/3b/0/0 rx queue 0# 00:00:00:000001: ethernet-input# IP4: 00:11:22:33:44:55 -> 66:77:88:99:aa:bb# 00:00:00:000002: ip4-input# TCP: 192.168.1.100 -> 192.168.2.100# 00:00:00:000003: ip4-lookup# fib 0 adj 1# 00:00:00:000004: ip4-rewrite# tx_sw_if_index 2# 00:00:00:000005: interface-output# GigabitEthernet0/3b/0/1
# 清除追踪sudo vppctl clear trace实践 4:编写自定义 VPP 插件
目标:编写一个简单的 VPP 插件,统计 TCP/UDP/ICMP 包的数量,并通过 CLI 查看统计结果。
步骤 1:创建插件目录
# 在 VPP 源码中创建插件目录cd ~/vpp/src/pluginsmkdir -p my_proto_countercd my_proto_counter步骤 2:编写插件代码
/* my_proto_counter.c - 协议包计数器插件 */
#include <vlib/vlib.h>#include <vnet/ip/ip.h>#include <vlibplugin/plugin.h>
/* ========== 插件全局状态 ========== */typedef struct{ u64 tcp_count; u64 udp_count; u64 icmp_count; u64 other_count;} my_proto_counter_main_t;
static my_proto_counter_main_t my_proto_counter_main;
/* ========== 图节点实现 ========== */typedef enum{ MY_PROTO_COUNTER_NEXT_IP4_INPUT, MY_PROTO_COUNTER_NEXT_DROP, MY_PROTO_COUNTER_N_NEXT,} my_proto_counter_next_t;
static uwordmy_proto_counter_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame){ my_proto_counter_main_t *mpm = &my_proto_counter_main; u32 n_left_from, *from;
from = vlib_frame_vector_args (frame); n_left_from = frame->n_vectors;
while (n_left_from > 0) { u32 bi0 = from[0]; vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0); ip4_header_t *ip0 = vlib_buffer_get_current (b0);
/* 根据协议类型计数 */ switch (ip0->protocol) { case IP_PROTOCOL_TCP: mpm->tcp_count++; break; case IP_PROTOCOL_UDP: mpm->udp_count++; break; case IP_PROTOCOL_ICMP: mpm->icmp_count++; break; default: mpm->other_count++; break; }
/* 预取下一个包 */ if (n_left_from > 1) { u32 bi1 = from[1]; vlib_buffer_t *b1 = vlib_get_buffer (vm, bi1); CLIB_PREFETCH (b1, 64, LOAD); }
from += 1; n_left_from -= 1; }
/* 所有包转发到 ip4-input */ vlib_buffer_enqueue_to_next (vm, node, from, frame->n_vectors, MY_PROTO_COUNTER_NEXT_IP4_INPUT);
return frame->n_vectors;}
VLIB_REGISTER_NODE (my_proto_counter_node) ={ .name = "my-proto-counter", .vector_size = sizeof (u32), .n_next_nodes = MY_PROTO_COUNTER_N_NEXT, .next_nodes = { [MY_PROTO_COUNTER_NEXT_IP4_INPUT] = "ip4-input", [MY_PROTO_COUNTER_NEXT_DROP] = "error-drop", },};
VLIB_NODE_FUNCTION (my_proto_counter_node, my_proto_counter_node_fn);
/* ========== CLI 命令 ========== */static clib_error_t *show_proto_counter_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd){ my_proto_counter_main_t *mpm = &my_proto_counter_main;
vlib_cli_output (vm, "Protocol Counter Statistics:"); vlib_cli_output (vm, " TCP: %lu packets", mpm->tcp_count); vlib_cli_output (vm, " UDP: %lu packets", mpm->udp_count); vlib_cli_output (vm, " ICMP: %lu packets", mpm->icmp_count); vlib_cli_output (vm, " Other: %lu packets", mpm->other_count);
return 0;}
VLIB_CLI_COMMAND (show_proto_counter_cmd, static) ={ .path = "show proto-counter", .short_help = "Show protocol counter statistics", .function = show_proto_counter_fn,};
static clib_error_t *clear_proto_counter_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t *cmd){ my_proto_counter_main_t *mpm = &my_proto_counter_main; clib_memset (mpm, 0, sizeof (*mpm)); vlib_cli_output (vm, "Protocol counters cleared."); return 0;}
VLIB_CLI_COMMAND (clear_proto_counter_cmd, static) ={ .path = "clear proto-counter", .short_help = "Clear protocol counter statistics", .function = clear_proto_counter_fn,};
/* ========== 插件注册与初始化 ========== */VLIB_PLUGIN_REGISTER () ={ .version = "1.0.0", .description = "Protocol packet counter plugin",};
static clib_error_t *my_proto_counter_init (vlib_main_t *vm){ my_proto_counter_main_t *mpm = &my_proto_counter_main; clib_memset (mpm, 0, sizeof (*mpm));
/* 注意:此插件节点需要在 VPP 图中插入到 ethernet-input 和 ip4-input 之间 * 这通常通过 feature arc 机制实现,此处简化处理 */ clib_warning ("my_proto_counter plugin initialized");
return 0;}
VLIB_INIT_FUNCTION (my_proto_counter_init);步骤 3:编写 CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
add_vpp_plugin(my_proto_counter SOURCES my_proto_counter.c)步骤 4:编译和安装
# 在 VPP 源码根目录编译cd ~/vppmkdir -p build && cd buildcmake .. -DCMAKE_BUILD_TYPE=releasemake my_proto_counter -j$(nproc)
# 安装插件sudo cp src/plugins/my_proto_counter/my_proto_counter.so \ /usr/lib/x86_64-linux-gnu/vpp_plugins/
# 重启 VPPsudo systemctl restart vpp
# 验证插件加载sudo vppctl show plugins | grep my_proto_counter
# 查看统计sudo vppctl show proto-counter在实际生产中,自定义节点需要通过 VPP 的 feature arc 机制插入到已有的图节点流水线中,而不是简单地替换 ip4-input 的前驱节点。Feature arc 允许你在不修改核心代码的情况下,将自定义节点动态插入到特定位置(如 IP 输入之前、路由查找之后等)。这超出了本章的范围,感兴趣的读者可以参考 VPP 源码中 vnet/feature 目录的实现。
小结
本章深入剖析了 VPP 与 FD.io 数据平面的核心设计与实现。回顾关键要点:
1. 矢量处理是 VPP 的灵魂
VPP 用”一批包一起通过一个节点”替代了”一个包走完所有节点”,彻底解决了 I-Cache 颠簸问题。默认 256 个包的矢量大小在吞吐和延迟之间取得了工程上的最优平衡,单核 IPv4 转发可达 12 Mpps。
2. 图节点架构提供了极致的模块化
每个处理步骤是一个独立节点,通过 next_index 机制逐包选择后继节点。这种设计让 VPP 的处理流水线高度灵活——添加新功能只需添加新节点,无需修改已有代码。
3. 插件框架实现了真正的可扩展性
VPP 的核心功能与扩展功能完全解耦。插件以共享库形式动态加载,可以注册图节点、CLI 命令和 binary API,几乎可以扩展 VPP 的一切。
4. 性能优化是系统工程
预取、矢量大小调优、节点合并、无锁多核架构——VPP 的性能不是单一优化带来的,而是系统级优化的结果。每一个优化都建立在前一个的基础之上。
5. VPP + DPDK 是工业级数据平面的标准组合
DPDK 负责高效收发包(第 3~7 章详解),VPP 负责高效处理包。两者通过 dpdk-input 和 dpdk-output 节点无缝集成,rte_mbuf 与 vlib_buffer_t 的内存布局兼容性使得转换几乎零开销。
VPP 不是象牙塔中的学术项目——它在 Cisco 路由器、Comcast 宽带网关、5G UPF 等工业级场景中经过了大规模验证。理解 VPP 的架构与编程模型,就是掌握了下一代数据平面的设计范式。
参考资料
官方文档
- FD.io VPP 官方文档 — VPP 架构、API 参考与编程指南
- FD.io VPP Wiki — VPP 图节点架构与插件开发文档
- VPP 源码 — VPP 完整源码,包含所有核心库和插件
技术论文与演讲
- “VPP: A Brief History and Future Direction” — FD.io Summit 2019,VPP 的设计演进
- “Vector Packet Processing: A Fast Software Router” — Cisco 技术报告,矢量处理的原始论文
- “FD.io VPP Performance Report” — FD.io 定期发布的性能基准测试报告
开源项目
- FD.io VPP — VPP 官方 GitHub 镜像
- vppsb (VPP Strong Back) — VPP 补充插件集合
- hicn (Hybrid ICN) — 基于 VPP 的信息中心网络插件
相关章节
- 第 3 章:DPDK 架构全景与核心概念 — DPDK 的用户态轮询驱动模型,VPP 的收包基础设施
- 第 5 章:DPDK 轮询模式驱动 — PMD 收发包机制,VPP 的
dpdk-input节点底层 - 第 7 章:DPDK 多核与并发模型 — RSS 分流与 lcore 绑核,VPP 多核架构的基础
- 第 12 章:OVS-DPDK 与虚拟交换 — OVS-DPDK 数据路径,与 VPP 的架构对比
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






