mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
7176 字
20 分钟
VPP 与 FD.io 数据平面
2025-07-21

某运营商的 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)。这一举动改变了高性能网络领域的格局:一个经过十余年工业验证的数据平面引擎,突然变成了开源社区的。

关键时间线:

时间事件
2002Cisco 内部启动 VPP 项目,用于高性能路由器数据平面
2010VPP 在 Cisco CSR1000v 虚拟路由器中大规模部署
2016Cisco 将 VPP 捐赠给 Linux Foundation,FD.io 项目成立
2017FD.io 成为 LFN(Linux Foundation Networking)项目
2018FD.io 从 LFN 孵化器毕业,成为成熟项目
2019Comcast 宣布基于 VPP 构建宽带网关,日处理数十亿包
2021VPP 成为 3GPP 5G UPF 参考实现的核心数据平面
2025VPP 持续演进,支持 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 的一切。

Note

这三个原则不是孤立的,而是相互支撑的:图节点架构是矢量处理的前提(只有把处理逻辑拆成节点,才能批量通过),插件框架是图节点架构的延伸(插件就是动态注册的节点集合)。理解了这三点,就理解了 VPP 的全部设计哲学。

1.3 VPP 在高性能网络生态中的位置#

在第 3 章中详细分析了 DPDK 的架构——DPDK 解决的是”如何高效收发包”的问题。VPP 则解决的是”如何高效处理包”的问题。两者是互补关系:

  • DPDK:用户态轮询驱动、零拷贝内存、无锁队列——数据包从网卡到用户态的基础设施
  • VPP:矢量处理、图节点流水线、插件框架——数据包在用户态的高效处理引擎
graph TB subgraph VPP应用层["VPP 应用层"] CLI[VPP CLI / binary API] PLUGIN[插件层<br/>NAT / ACL / QoS / ...] end subgraph VPP核心层["VPP 核心层"] VLIB[vlib 库<br/>图节点调度 / 矢量处理] VNET[vnet 库<br/>L2/L3/L4 协议节点] VPPINFRA[vppinfra 库<br/>基础数据结构 / 内存池] end subgraph DPDK层["DPDK 基础层(第3-7章)"] PMD[PMD 轮询驱动] MBUF[rte_mbuf / mempool] EAL[EAL 抽象层] end subgraph 硬件层["硬件层"] NIC[网卡<br/>Intel / Mellanox / ...] end CLI --> PLUGIN CLI --> VNET PLUGIN --> VLIB VNET --> VLIB VLIB --> VPPINFRA VLIB --> PMD PMD --> MBUF MBUF --> EAL EAL --> NIC style VPP核心层 fill:#e8f5e9,stroke:#2e7d32 style DPDK层 fill:#e3f2fd,stroke:#1565c0 style 硬件层 fill:#fce4ec,stroke:#c62828

二、矢量包处理: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 的矢量处理采用了完全不同的策略:不是让一个包走完所有步骤,而是让一批包(矢量)一起通过同一个步骤,然后这批包一起进入下一个步骤。

具体来说:

  1. 输入节点(如 dpdk-input)一次从网卡收取 N 个包(典型 N=256)
  2. 这 256 个包一起进入 ethernet-input 节点——此时 ethernet-input 的代码被加载到 I-Cache
  3. 256 个包全部处理完后,一起进入 ip4-input 节点——此时 ip4-input 的代码被加载到 I-Cache
  4. 以此类推,直到所有包走完流水线

关键洞察:当 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 是一个工程上的甜蜜点
Warning

矢量大小并非越大越好。过大的矢量会导致:(1) 延迟增加——必须等更多包到达才能开始处理;(2) L1 D-Cache 压力增大——256 个包的元数据已经接近 L1 D-Cache 容量;(3) 尾部延迟——矢量末尾的包等待时间过长。在延迟敏感场景中,可以减小矢量大小(如 64 或 128),牺牲部分吞吐来换取更低延迟。

2.4 标量 vs 矢量:直观对比#

graph TB subgraph 标量处理["标量处理(Scalar)— I-Cache 颠簸"] direction LR S1["包1: eth-input → ip4-input → ip4-lookup → ip4-rewrite → output"] S2["包2: eth-input → ip4-input → ip4-lookup → ip4-rewrite → output"] S3["包3: eth-input → ip4-input → ip4-lookup → ip4-rewrite → output"] end subgraph 矢量处理["矢量处理(Vector)— I-Cache 友好"] direction LR V1["包1~256: eth-input"] V2["包1~256: ip4-input"] V3["包1~256: ip4-lookup"] V4["包1~256: ip4-rewrite"] V5["包1~256: output"] V1 --> V2 --> V3 --> V4 --> V5 end style 标量处理 fill:#ffcdd2,stroke:#c62828 style 矢量处理 fill:#c8e6c9,stroke:#2e7d32

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 μsRSS 分流 + worker 线程
8 核,64 字节包~85 Mpps~4 μs线性扩展接近理想
Note

以上数据基于 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 地址和 TTL
  • ip4-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_nodesnext_nodes:定义该节点的后继节点。每个包通过 next_index 选择下一个节点
  • format_trace:当启用包追踪时,格式化该节点的追踪信息

3.3 节点函数签名#

每个图节点必须实现一个节点函数(Node Function),其签名为:

/**
* 图节点函数签名
*
* @param vm 虚拟机实例,包含全局状态
* @param node 当前节点描述符,包含节点统计和配置
* @param frame 输入矢量帧,包含待处理的数据包索引数组
* @return 处理的数据包数量
*/
static uword
ip4_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 中经过以下节点:

graph LR DPDK_IN["dpdk-input<br/>从 DPDK PMD 收包"] ETH_IN["ethernet-input<br/>解析以太网帧头"] IP4_IN["ip4-input<br/>IPv4 头部校验"] IP4_LK["ip4-lookup<br/>FIB 路由查找"] IP4_RW["ip4-rewrite<br/>MAC 重写 + TTL 递减"] INT_OUT["interface-output<br/>选择出接口"] DPDK_OUT["dpdk-output<br/>通过 DPDK PMD 发包"] DPDK_IN --> ETH_IN ETH_IN --> IP4_IN IP4_IN --> IP4_LK IP4_LK -->|查找命中| IP4_RW IP4_LK -->|查找未命中| DROP["ip4-drop"] IP4_RW --> INT_OUT INT_OUT --> DPDK_OUT style DPDK_IN fill:#4CAF50,color:#fff style DPDK_OUT fill:#2196F3,color:#fff style DROP fill:#f44336,color:#fff style IP4_LK fill:#FF9800,color:#fff
Note

上图展示的是最简单的 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 uword
my_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 的插件初始化遵循以下流程:

  1. VPP 主进程启动,解析命令行参数和配置文件
  2. 扫描插件目录,发现所有 .so 文件
  3. 对每个插件,检查 VLIB_PLUGIN_REGISTERdefault_disabled 字段
  4. 加载非禁用的插件共享库(dlopen
  5. 调用插件的初始化函数(VLIB_INIT_FUNCTION
  6. 初始化函数中注册图节点、CLI 命令、binary API 等
  7. 所有插件初始化完成后,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 uword
my_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,
};
Warning

插件中的图节点函数运行在 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 的 CLI
sudo vppctl
# 或者直接执行单条命令
sudo vppctl show interface

常用的 VPP CLI 命令分为三大类:

show 命令(查看状态)

# 查看接口信息
show interface
show interface address
# 查看路由表
show ip fib
show ip6 fib
# 查看 ARP 表
show ip arp
# 查看图节点统计
show vlib graph
show node counters
# 查看运行时信息
show runtime
show threads
# 查看插件列表
show plugins
# 查看硬件信息
show hardware
# 查看包追踪
trace add dpdk-input 100
show trace
clear trace

set 命令(配置)

# 设置接口 IP 地址
set interface ip address GigabitEthernet0/8/0 192.168.1.1/24
set interface ip address del GigabitEthernet0/8/0 192.168.1.1/24
# 启用/禁用接口
set interface state GigabitEthernet0/8/0 up
set interface state GigabitEthernet0/8/0 down
# 设置路由
ip route add 10.0.0.0/8 via 192.168.1.254
ip route del 10.0.0.0/8
# 设置 ARP 条目
set ip arp GigabitEthernet0/8/0 192.168.1.254 00:11:22:33:44:55

clear 命令(清除状态)

# 清除接口统计
clear interface
# 清除 ARP 表
clear ip arp
# 清除路由表
clear ip fib
# 清除节点计数器
clear node counters

5.2 Binary API#

VPP CLI 适合人工交互,但程序化控制 VPP 需要一种更高效的接口——Binary API

Binary API 是 VPP 的进程间通信(IPC)机制,基于共享内存实现:

  • 低延迟:客户端和 VPP 之间通过共享内存交换消息,无需系统调用
  • 类型安全:消息格式由 .api 文件定义,自动生成 C 代码
  • 双向通信:客户端可以发送请求并接收回复,也可以注册回调接收 VPP 的事件通知

Binary API 的工作流程:

  1. 客户端连接到 VPP 的共享内存区域(/dev/shm/vpe-api
  2. 客户端发送请求消息(如 sw_interface_dump
  3. VPP 处理请求,将回复消息写入共享内存
  4. 客户端读取回复

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 消息:

# 启动 VAT
sudo vpp-api-test
# 在 VAT 中执行 API 命令
vat# sw_interface_dump
vat# ip_route_add_dst_prefix 10.0.0.0/8 via 192.168.1.254
vat# my_plugin_stats_get

VAT 对于调试 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,
};
Note

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 运行时可调)
/etc/vpp/startup.conf
# 在 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 线程模型:

graph TB subgraph 主线程["主线程(Main Thread)"] MAIN["vlib_main<br/>CLI / binary API<br/>节点调度<br/>控制面操作"] end subgraph Worker线程["Worker 线程"] W1["Worker 0<br/>dpdk-input → ... → dpdk-output<br/>绑定 CPU 核心 2"] W2["Worker 1<br/>dpdk-input → ... → dpdk-output<br/>绑定 CPU 核心 3"] W3["Worker 2<br/>dpdk-input → ... → dpdk-output<br/>绑定 CPU 核心 4"] W4["Worker 3<br/>dpdk-input → ... → dpdk-output<br/>绑定 CPU 核心 5"] end NIC["网卡(多队列 RSS)"] NIC -->|队列 0| W1 NIC -->|队列 1| W2 NIC -->|队列 2| W3 NIC -->|队列 3| W4 MAIN -.->|控制面| W1 MAIN -.->|控制面| W2 MAIN -.->|控制面| W3 MAIN -.->|控制面| W4 style 主线程 fill:#e3f2fd,stroke:#1565c0 style Worker线程 fill:#e8f5e9,stroke:#2e7d32

关键设计:

  • 主线程:负责 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 个发送队列
}
}
Warning

VPP 的 worker 线程必须是无锁的——任何共享状态的访问都会导致 cache line bouncing,严重降低多核扩展性。如果你的插件需要在 worker 之间共享数据,请使用 VPP 提供的无锁数据结构(如 clib_spinlockvlib_frame_queue),或者将共享操作放到主线程中执行。

七、VPP + DPDK 集成#

7.1 dpdk-input 节点#

VPP 通过 dpdk-input 节点从 DPDK PMD 接收数据包。这个节点是 VPP 与 DPDK 的桥梁,其工作流程如下:

  1. VPP 的 worker 线程调用 dpdk-input 节点函数
  2. 节点函数调用 rte_eth_rx_burst() 从 DPDK PMD 批量收包
  3. 收到的 rte_mbuf 被转换为 VPP 的 vlib_buffer_t 格式
  4. 数据包索引被放入矢量帧,传递给下一个节点(ethernet-input

关键代码逻辑(简化版):

static uword
dpdk_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;
}
Note

VPP 的 vlib_buffer_t 和 DPDK 的 rte_mbuf 在内存布局上是兼容的——VPP 在 rte_mbuf 的头部预留了空间用于存储 vlib_buffer_t 的元数据。这意味着从 rte_mbufvlib_buffer_t 的转换几乎零开销,只是指针的简单偏移。这种设计是 VPP + DPDK 高效集成的关键之一。

7.2 dpdk-output 节点#

dpdk-output 节点负责将处理完毕的数据包通过 DPDK PMD 发送出去:

  1. 数据包从上游节点(如 interface-output)到达 dpdk-output
  2. 节点函数将 vlib_buffer_t 转换回 rte_mbuf 格式
  3. 调用 rte_eth_tx_burst() 批量发送数据包
  4. 发送完毕后释放已发送的 buffer

7.3 配置 DPDK 接口#

VPP 通过 startup.conf 配置文件指定 DPDK 接口:

/etc/vpp/startup.conf
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-inputdpdk-output 节点就可以正常工作了。

7.4 VPP + DPDK 性能#

VPP + DPDK 的组合是当今开源领域最高性能的用户态网络方案之一。在典型的 x86 服务器上,VPP + DPDK 的 IPv4 转发性能:

包大小单核吞吐4 核吞吐8 核吞吐线速比
64 B12 Mpps45 Mpps85 Mpps~56% @ 10GbE
128 B12 Mpps46 Mpps88 Mpps~85% @ 10GbE
256 B11 Mpps42 Mpps80 Mpps~98% @ 10GbE
512 B8 Mpps30 Mpps58 Mpps~100% @ 10GbE
1518 B1.2 Mpps4.6 Mpps8.8 Mpps~100% @ 10GbE
Note

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 update
sudo 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.gpg
echo "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. 安装 VPP
sudo apt update
sudo 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. 启动 VPP
sudo 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 接口,实现物理网卡的收发包。

Warning

此实践需要物理网卡和 DPDK 兼容的硬件。虚拟机环境下可以使用 af-packet 接口替代 DPDK 接口进行测试。

# 1. 查看系统中的网卡 PCI 地址
lspci | grep -i ethernet
# 输出类似:
# 3b:00.0 Ethernet controller: Intel Corporation ...
# 2. 将网卡绑定到 DPDK 兼容驱动(vfio-pci)
sudo modprobe vfio-pci
sudo 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. 重启 VPP
sudo 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 up
sudo vppctl set interface ip address GigabitEthernet0/3b/0/0 192.168.1.1/24
# 7. 验证接口状态
sudo vppctl show interface address GigabitEthernet0/3b/0/0
sudo 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/24
sudo 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/0
sudo 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/plugins
mkdir -p my_proto_counter
cd 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 uword
my_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

CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
add_vpp_plugin(my_proto_counter
SOURCES
my_proto_counter.c
)

步骤 4:编译和安装

# 在 VPP 源码根目录编译
cd ~/vpp
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=release
make my_proto_counter -j$(nproc)
# 安装插件
sudo cp src/plugins/my_proto_counter/my_proto_counter.so \
/usr/lib/x86_64-linux-gnu/vpp_plugins/
# 重启 VPP
sudo systemctl restart vpp
# 验证插件加载
sudo vppctl show plugins | grep my_proto_counter
# 查看统计
sudo vppctl show proto-counter
Note

在实际生产中,自定义节点需要通过 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-inputdpdk-output 节点无缝集成,rte_mbufvlib_buffer_t 的内存布局兼容性使得转换几乎零开销。

VPP 不是象牙塔中的学术项目——它在 Cisco 路由器、Comcast 宽带网关、5G UPF 等工业级场景中经过了大规模验证。理解 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 定期发布的性能基准测试报告

开源项目#

相关章节#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

VPP 与 FD.io 数据平面
https://blog.souloss.com/posts/high-perf-networking/high-perf-networking-vpp-fdio-data-plane/
作者
Souloss
发布于
2025-07-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
DPDK 数据平面核心机制
高性能网络 深入 DPDK 数据平面核心机制——rte_ring 无锁环形缓冲区(SPSC/MPMC/RTS/HTS)、rte_mbuf 分段链与零拷贝、包解析辅助库(rte_net/rte_ether/rte_ip/rte_tcp)、CRC/Hash 硬件卸载、TSO/LRO、Scatter-Gather I/O——掌握数据包在 DPDK 应用中的高效处理全链路。
2
SmartNIC 与 DPU
高性能网络 深入 SmartNIC 与 DPU——硬件卸载概念与收益、SmartNIC 架构(固定功能 vs 可编程)、DPU 产品矩阵(NVIDIA BlueField/AMD Pensando/Intel IPU)、OVS 硬件卸载(tc/rte_flow/ASAP²)、P4 编程入门——掌握硬件加速网络的完整技术栈。
3
DPDK 多核与并发模型
高性能网络 深入 DPDK 多核与并发模型——lcore 模型与 CPU 亲和性、Run-to-Completion 模型、Pipeline 模型与 rte_ring 跨核通信、原子操作与内存屏障、RCU 机制(rte_rcu_qsbr)、Eventdev 事件驱动框架——掌握多核数据平面编程的完整技术栈。
4
OVS-DPDK 与虚拟交换
高性能网络 深入 OVS-DPDK 与虚拟交换——OVS-DPDK 架构与内核 OVS 对比、dpdkvhostuser 端口类型、vhost-user 协议与共享 virtio 环、virtio 前后端、VM-to-VM 交换路径、流分类(EMC/DPCLS/megaflows)、性能调优——掌握云环境虚拟网络加速的完整技术栈。
5
SPDK 与存储旁通
高性能网络 深入 SPDK 与存储旁通——SPDK 架构与 DPDK EAL 共享、用户态 NVMe 驱动(MMIO/SQ/CQ)、vhost-user 协议与共享内存、bdev 抽象层与模块、blobstore 持久化对象存储、iSCSI/NVMe-oF Target——掌握用户态存储的完整技术栈。