首页 / 资讯中心 / 文章详情

VPP源码解析:插件机制与node graph批处理框架实践

VPP源码解析:插件机制与node graph批处理框架实践 ★ FEATURED ARTICLE
简介VPPVector Packet Processing是FD.io社区的高性能数据包处理框架广泛用于网络功能虚拟化、路由与交换场景。面向网络开发工程师、VPP二次开发学习者及SDN/NFV研究者资料提供VPP完整源码和在其上编写示例插件的基础实例。包体共2000个文件、约22.26MB以C/C源码.c/.h/.cpp为主体辅以Python脚本、RST/API文档、YAML配置和编译构建说明便于读者对照源码理解VPP插件框架、node图调度与API注册机制。已有561人学习下载适合需要深入阅读网络转发源码并动手实现自定义插件的开发者NAT、ACL等既有插件代码也可直接作为改造模板。通过源码目录与插件示例的相互印证可快速掌握从工程编译到插件注册、运行的基本流程缩减从零起步的时间成本。1. VPP 源码解析从主程序到插件一次处理一大批包的框架如果把 VPP 源码摊开你会发现最值钱的部分不在主程序里而是那套把所有网络功能都塞进插件plugin、再靠 node graph 把插件串成流水线的组织方式。VPP 的思想反直觉别人一个一个处理包它一次拿一批包给一个节点跑同时摊平了缓存、指令流水线和锁开销。这份源码包带主干完整源码还配了一个可以照着抄的示例插件实例正适合想做 NAT、ACL、负载均衡这类用户态转发功能、又不想碰内核模块的开发者。这篇会从源码结构带你看清插件机制再手写一个能统计 IPv4 报文、可对指定源地址做丢包或放行的示例插件讲透它怎么编译、怎么加载、怎么改。2. 架构与源码结构node graph 和插件加载机制2.1 源码目录先找到 plugins 在哪VPP 的源码树有个特点几乎没有一个网络功能是写死在主程序里的全部以插件形式躺在 src/plugins 下。刚拿到源码时不要急着看 vpp/main.c先按目录把职责分清效率会高很多。目录职责关键内容src/vlib核心库矢量调度、buffer 池、node graph 框架vlib/main.c、vlib/node.c、vlib/buffer.csrc/vnet网络功能接口、L2/L3、隧道、feature arcvnet/interface.c、vnet/ip/src/vpp主程序、CLI 与 API 入口vpp/main.c、api/src/plugins官方插件集合也是自定义插件的参照物nat/、acl/、dpdk/ 等build-root编译输出目录构建完往里找 .so 和可执行文件install-vpp-native/vpp我拿到一份新源码习惯做三件事先grep -r VLIB_REGISTER_NODE数一下注册了多少个 node再看 src/plugins 里每个插件的目录命名规律基本上一个目录就是一个可加载的 .so最后把所有VNET_FEATURE_INIT里的 arc_name 抓出来搞清楚默认有哪些 feature 挂载点可以插队。做完这三步基本就能判断一个插件该写在哪个位置、挂到哪条链路上。src/vlib 是自下而上的地基src/vnet 是中层网络协议栈src/plugins 是各种功能插件的集合。写插件时你至少会同时碰到这三层vlib 提供 node 注册宏和 buffer 操作vnet 提供 feature arc 和协议头解析plugins 目录提供你参考的现成写法。2.2 核心机制一次处理一大批包VPP 跟内核协议栈最大的差异在包处理模型。内核里一个包一个包地进协议栈每次都要走一遍软中断、锁竞争、结构体分配释放VPP 则把网卡收上来的包攒成一个 vector以帧frame为单位投递给 node graph 里的某一个节点。帧里存的还不是 vlib_buffer_t 结构体本身而是一串紧凑的 u32 buffer 索引node 拿到后通过vlib_get_buffers批量换出指针处理完的空闲 buffer 直接回收分配开销被摊薄到一批包上。这种模型对现代 CPU 非常友好。几十个包的 descriptor 是连续数组遍历时预取命中率高分支预测也稳定再加上调度器会统计每个 node 的平均处理时间、向量长度和 next_node 选择分布自动调整后续批次的大小——处理快的节点每次都拿到更大的 vector整条流水线就跑得更满。这也是为什么“一轮循环处理 N 个包”比“N 轮循环处理 1 个包”在用户态转发场景里能高出好几个数量级的原因。有一条血泪经验值得提醒不要在 node 函数里对每个包做重量级调用尽量把判断收敛成查表或位运算让同一批包尽量走向同一个 next_node。node graph 会统计分支分布如果每个包都在分支上摇摆批处理效果会被明显稀释这是 VPP 特有的性能玄学压测时一测一个准。2.3 插件是如何被加载和调度的VPP 启动时扫描插件路径默认是系统安装目录下的 vpp_plugins 文件夹用 dlopen 把 .so 逐个拉起。每个 .so 内部通过VLIB_PLUGIN_REGISTER声明插件版本、描述、是否默认加载这些元信息。启动后可以用vppctl show plugins查看已加载列表也可以在运行中手动plugin load name或plugin unload name不需要重启进程。但加载成功不代表你的 node 会自动收到包。插件里的 node 要参与转发必须挂到某个调度位置上。挂法有两种一种是作为显式节点由前驱节点在 next_nodes 里通过索引指过来另一种更常用——把节点注册成 feature挂到某个 feature arc 上比如 ip4-unicast、arp、ethernet-output 这些用runs_before/runs_after控制它和同 arc 上其他 feature 的相对顺序。这样做的好处是不用改任何现成 node 的代码只靠配置就能把新功能插进转发链。两种挂法没有优劣之分语义差别很大显式 next_node 适合做独立的处理节点比如隧道解封装feature arc 适合做“改报文”或“拦报文”的中间逻辑比如 ACL、NAT、流量过滤。下面这个示例插件就选 feature arc贴近大多数实际场景。3. 手写示例插件从 .api 文件到 node 注册3.1 插件目录骨架和构建文件在 src/plugins 下新建一个 sample_plugin 目录里面放三个关键文件CMakeLists.txt、sample_plugin.api、sample_plugin.c。如果控制面逻辑多还可以拆一个 sample_plugin_API.c 单独放消息处理函数。CMakeLists.txt 是插件的门面一般长这样add_vpp_plugin(sample_plugin SOURCES sample_plugin.c sample_plugin_API.c API_FILES sample_plugin.api INSTALL_HEADERS sample_plugin.h )add_vpp_plugin是 VPP 构建系统封装好的函数它负责处理 .api 文件到 C 头文件的生成、链接 vlib/vnet 核心库、把产物 .so 放进 vpp_plugins 目录这些琐事。如果用原生 add_library 自己拼就得手动维护一堆 VPP 内部头文件的 include 路径VPP 版本一升级容易翻车。还有一个细节INSTALL_HEADERS 会把你暴露的插件头文件装到系统 include 目录后续写客户端程序或独立编译其他插件时会用到。如果只是验证插件逻辑这一项可以先不写但建议保留因为二次开发时大概率要用。3.2 .api 文件给插件加一个控制面插件的数据面 node 负责跑转发逻辑配置怎么下发、状态怎么查询属于控制面的事。VPP 用 .api 文件定义控制面消息这个文件经 vppapigen 编译后会同时生成 C 结构、序列化函数以及给外部客户端用的 Python 和 Go 消息类。下面定义两条消息一条让客户端设置“对某个源 IP 丢包或放行”一条返回操作结果。option version 1.0.0; import vnet/ip/ip_types.api; define sample_plugin_set_drop { u32 client_index; u32 context; vl_api_ip4_address_t src_addr; bool enable; }; define sample_plugin_set_drop_reply { i32 retval; };client_index 和 context 是 VPP API 消息的标准头字段client_index 标识是哪个客户端连接发来的context 由调用方写入VPP 异步应答时原样带回用来匹配请求和响应。业务字段只有 src_addr 和 enable 两个前者表示要匹配的源 IPv4 地址后者表示是开启丢包还是解除丢包。注意 .api 文件展开后的 C 结构体字段内存布局由生成代码保证。这意味着改完 .api 必须重新生成头文件并重编插件否则结构体偏移对不上会出现配置下发成功但节点读到乱值的灵异现象。后面避坑章节会专门讲这个。3.3 核心 node 函数实现现在写数据面最核心的节点处理函数。函数签名是 VPP 调度器规定死的vlib_main_t 是全局句柄vlib_node_runtime_t 是当前节点运行时的上下文vlib_frame_t 就是这一轮要处理的一批包。static uword sample_plugin_node_fn (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame) { u32 n_left frame-n_vectors; u32 * from vlib_frame_vector_args (frame); vlib_buffer_t * bufs[VLIB_FRAME_SIZE]; ip4_header_t * ip; u32 n_drop 0; while (n_left 0) { u32 n_now n_left; if (n_now VLIB_FRAME_SIZE) n_now VLIB_FRAME_SIZE; vlib_get_buffers (vm, from, bufs, n_now); for (u32 i 0; i n_now; i) { vlib_buffer_t * b bufs[i]; ip (ip4_header_t *) vlib_buffer_get_current (b); if (sample_plugin_match_src (ip-src_address)) { vlib_buffer_free_one (vm, b); n_drop; continue; } vlib_buffer_advance (b, 0); } from n_now; n_left - n_now; } vlib_increment_combined_counter (vm, sample_plugin_counts, 0, 1, n_drop); return frame-n_vectors; }逻辑说明vlib_frame_vector_args取出帧内的 buffer 索引数组首地址vlib_get_buffers一次批量换出 vlib_buffer_t 指针减少逐个查找的指针操作。循环体里用vlib_buffer_get_current拿到当前报文的起始地址强转成 ip4_header_t 后读源地址命中规则就vlib_buffer_free_one直接释放否则原样留下。函数返回值是本节点处理完仍存活的包数VPP 调度器会把这些包按 next_node 继续分发。参数说明VLIB_FRAME_SIZE 是单轮处理的批大小上限实际批大小由调度器动态调整vlib_buffer_get_current返回的是当前点在 buffer 里的数据指针不是 buffer 池指针千万别混。n_drop 只是示例计数器真实插件会更精细地分错误类别方便下一步 show errors 排查。3.4 把 node 挂进转发链node 函数写好后要用宏把它注册成 VPP nodeVLIB_REGISTER_NODE (sample_plugin_node) { .function sample_plugin_node_fn, .name sample-plugin, .vector_size sizeof (u32), .n_next_nodes 2, .next_nodes { [SAMPLE_PLUGIN_NEXT_DROP] error-drop, [SAMPLE_PLUGIN_NEXT_ACCEPT] ip4-lookup, }, };n_next_nodes 声明了这个节点有两个输出方向SAMPLE_PLUGIN_NEXT_DROP 指向 error-drop丢弃SAMPLE_PLUGIN_NEXT_ACCEPT 指向 ip4-lookup继续查路由转发。如果 node 函数没有显式指定 next_nodeVPP 会用枚举里的第一个方向兜底所以顺序别乱写。接着把 node 挂到 ip4-unicast 这个 feature arc 上让它跑在路由查询之前VNET_FEATURE_INIT (sample_plugin_feat, static) { .arc_name ip4-unicast, .node_name sample-plugin, .runs_before VNET_FEATURES (ip4-lookup), };这段的意思是只要报文走 IPv4 单播转发进 ip4-lookup 之前先过 sample-plugin 节点。控制面更新规则时node 读到的配置实时生效不需要重编译也不需要重启 VPP。这就是 VPP 插件“控制面与数据面分离”的基本形态也是插件体系最迷人的地方。4. 编译、安装、运行插件跑起来才是硬道理4.1 编译 VPP 源码在一台干净的 Ubuntu 机器上先把基础编译工具链装好VPP 的 Makefile 提供了统一入口make install-dep make buildinstall-dep 会把编译需要的依赖一次性装齐包括编译工具、Python 环境和若干库。build 全部走完会在 build-root/install-vpp-native/vpp 下生成完整产物首次编译耗时从二十分钟到一小时不等取决于机器核数和网络源速度长时间卡在下载依赖属正常换国内镜像能快很多。构建产物里值得关注的是 bin/vpp 主程序以及 lib/x86_64-linux-gnu/vpp_plugins 目录下的 .so 集合。往测试机部署可以make install也可以用 dpkg 安装 build-root 里打好的 .deb 包。开发调试阶段建议直接用 build-root 里的二进制跑省去安装步骤改完重编就能测。4.2 编译并加载你自己的插件插件代码放进 src/plugins 后不需要额外操作make build会把它一并编出来.so 出现在 build-root 对应插件目录里。如果只想单独加快插件迭代也可以进 build-root 用 CMake 单独编插件子目录只重编这一个目标比全量重编省不少时间。我一般开发期会写一个小脚本每次只调 cmake 编插件目标稳定后再走全量。启动 VPP 时通过 startup.conf 指定插件路径vpp -c /etc/vpp/startup.conf vppctl show pluginsstartup.conf 的 plugins 段可以这样写plugins { plugin path /path/to/build-root/install-vpp-native/vpp/lib/x86_64-linux-gnu/vpp_plugins plugin load sample_plugin.so }plugin path 让 VPP 去指定目录扫描插件plugin load 强制加载指定插件。如果不写 pathVPP 只扫系统默认目录手动拷 .so 也能识别但测试环境一升级或清理就会把旧 .so 留下版本错配对排查不友好。开发期宁可写死 path。4.3 用 CLI 验证插件行为没有物理网卡也能验证插件。VPP 可以创建 memif 接口配好地址后用 ping 或者流量工具触发报文。验证 node 是否被走到最直接的是 trace 和 graph 两条命令vppctl show vlib graph | grep sample vppctl clear trace vppctl trace add memif-input 10然后从外部 ping 一个经过 VPP 转发的目标地址再执行vppctl show vlib trace看报文是否经过 sample-plugin。如果走了trace 里会显示 sample-plugin 节点名和处理动作如果没出现回头看 3.4 的 feature 挂载是不是漏了使能。计数验证建议用vppctl show errors前提是 node 里注册了 error counter 并在命中时递增。没注册 counter 的话drop 统计不会出现在 show errors 里只能靠 trace 数包排查效率低不少。所以插件里哪怕先放一个简单的丢包计数也别省。5. 避坑跑 VPP 插件常见问题与排查5.1 加载 .so 报 symbol lookup error现象VPP 启动或 vppctl 手动加载时日志出现 undefined symbol 一类的报错插件加载失败。原因插件编译用的头文件路径或 VPP 版本和运行中的二进制不匹配。最常见是 A 版本源码编译的 .so被拷到 B 版本 VPP 环境里运行符号表自然对不上。解决先用vppctl show version确认运行中的版本再用同一份源码树重新编译插件。独立构建插件时include 路径必须指向与运行版本对应的 build-root 安装头文件别图省事混用系统目录里的旧头文件。5.2 node 挂了 feature 但流量不走现象show vlib graph 里能看到 sample-plugin 节点trace 里报文路径却根本没有它好像被透明忽略。原因只写了 VNET_FEATURE_INIT 并不会让 feature 自动生效。这个宏只是声明了“这个节点可以挂到哪个 arc 的哪个位置”真正在接口上启用要通过 vnet_feature_enable_disable 或对应 CLI 开启。解决在控制面代码里调用vnet_feature_enable_disable(ip4-unicast, sample-plugin, sw_if_index, enable_flag)或者先用vppctl show feature查看接口上已启用的 feature 列表确认是否处于 enable 状态。排查顺序是先看 feature 有没有开再看 node 有没有注册最后才看代码逻辑。5.3 改 .api 没重新生成头文件现象配置下发不报错但 node 里读到的 src_addr 是乱值偶发崩溃而且只在特定编译缓存状态下出现。原因.api 文件改了字段生成的头文件还是旧的插件编译时用了旧结构体布局造成字段偏移错位。这类问题非常隐蔽因为它不是编译期错误而是运行时数据错位。解决改 .api 后必须强制走一遍生成阶段再重编。最省心的方法是每次 make build 前把对应生成目录清掉宁可慢一点也不要吃残留缓存。从那以后我每次改 .api 都强制清一遍再编这个习惯救了好几次压测现场。5.4 大批量流量下 node 崩溃现象小流量测试正常一上压测工具就 segfault或者 buffer 池水位异常下降、回收不回来。原因高概率是把 buffer 索引当成指针解引用了或者对vlib_frame_vector_args返回的数组做了越界访问。小流量时 cache 和内存分布的偶然性掩盖了错误压测一上来就现原形。解决全部通过vlib_get_buffers批量取指针不要手动解引用索引数组frame-n_vectors 只读不要自行修改。压测前先vppctl show vlib buffer看 buffer 池水位如果池中空闲 buffer 持续减少说明有路径把 buffer 丢了没回收优先检查 drop 分支。5.5 show plugins 里看不到插件现象.so 明明已经放到插件目录vppctl show plugins 列表里却没有。原因插件扫描路径没生效或者 .so 扩展名不对、权限不足。开发期经常是路径写到了别的目录VPP 压根没扫到。解决startup.conf 里显式写 plugin path并用vppctl show plugins查看实际扫描路径做对比。排查时最直接的是vppctl plugin load /full/path/sample_plugin.so它会返回具体错误信息比翻启动日志快得多。6. 二次开发思路把示例插件改成真正的功能组件示例插件目前只支持一个固定源地址的丢包/放行要变成能用的功能组件至少要往两个方向改规则表化和统计可视化。规则表化是把sample_plugin_match_src里的单条比较换成查表。VPP 自带高层 hash 表和底层 bihash可以按 prefix 做 key、动作做 valuenode 循环里只做一次查表规则数量上来后性能不会线性劣化。数据面 node 里不要逐条遍历规则那是新手爱踩的性能坑。统计可视化是注册 error counter丢包时调用计数接口丢包原因通过vppctl show errors就能看到。还可以用VLIB_CLI_COMMAND注册自定义 vppctl 命令比如sample drop addr prefix这样联调时不用写 API 客户端脚本vppctl 里直接敲。验证方法固定成一套习惯改数据面代码后重编插件clear trace构造对应流量show vlib trace 确认路径和动件改 .api 后强制清理生成目录再编动 feature 挂载后先 show feature 确认 enable 状态。这套流程走熟了VPP 插件开发其实就是在 node 函数和 feature arc 之间来回打磨难度不在写代码而在搞清楚自己编写的节点被放到了哪条链路上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站