ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VPP数据平面:从批量包处理到高性能网络转发实战

VPP数据平面:从批量包处理到高性能网络转发实战 如果你在搜索引擎里敲下VPP三个字母大概率会看到两种完全不同的语境一种是DDR4内存超频教程里“VPP给谁供电”的电源参数另一种是网络领域大名鼎鼎的Vector Packet Processing。后者正是FD.io社区的核心数据平面也是高性能网络数据包处理绕不开的关键技术。写这篇东西的背景很简单这两年做网络转发面的项目从内核协议栈转向用户态数据平面VPP是其中绕不开的重量级选手。它解决的痛点一句话就能说明白——当端口速率从1G涨到10G、100G、400G时CPU的处理能力和网络吞吐之间的剪刀差越来越大传统的逐包处理方式到了物理极限VPP用“批量处理”的思路在软件平面上重新找回了性能。1. 一个包到底多难处理从带宽焦虑到批量思维网络设备经常会说“线速转发”很多人以为线速只是线性放大带宽。真正卡脖子的是小包。以太网上每个包除了数据本身还有前导码和帧间隙的固定开销64字节小包满线速下的pps大致是固定的万兆网卡约14.88Mpps40G约59.52Mpps100G约148.8Mpps。这意味着一台100G网卡若想保持满线速处理64字节的小包CPU每秒得处理接近一亿五千万个包。1.1 传统内核路径的算力缺口传统内核协议栈里一个包从网卡中断到应用socket中途要经历中断响应、软中断、协议栈逐层解析、系统调用、用户态拷贝等环节粗略估算下来单个包消耗两三千周期并不夸张。哪怕CPU主频有3GHz单核每秒也就三十亿个周期按3000 cycle/包算单核上限只有1Mpps左右——刚够扛住千兆线速距离万兆相差十数倍距离100G相差上百倍。这就是带宽焦虑的根源网口速率是摩尔定律驱动的CPU单核性能却早已撞上频率墙。1.2 多核不是银弹锁竞争才是大头看到这里有人自然想到加核。但网络转发是一个非常依赖共享状态的任务路由表、ARP表、会话表、计数器本质上是全局可见的。多核并发访问这些结构要么加锁要么加原子操作。锁一旦上了吞吐再高也会被串行化CPU在等锁时什么都干不了即使能并行cache line在核间同步也会带来巨大的总线流量这叫cache line bouncing。真要去调优通常能从perf里看到异常的spinlock开销和cache-miss比例此时32核可能剩下的有效算力连8核都不到。多核扩展还有一个隐藏问题跨核报文分发。网卡多队列可以把报文哈希到不同CPU但哈希不均衡时会形成热点核另一个核却闲着。真实流量里极少有均匀的hash分布所以生产环境里经常要配合RSS或流表调度优化。总而言之光靠加核解决不了根本问题核心思路必须转变。1.3 批量处理把“一个一个”改成“一叠一叠”VPP给出的答案技术上叫vector日常里就是“攒够一叠再处理”。类比快递分拣如果有一千件包裹要录入系统一件一件扫码肯定不比十条流水线一次扫一筐来得快。VPP的思路是每轮循环从网卡批量收包比如一次收256个包然后再统一对这个batch做协议解析、查表、转发。这样做有几个直接收益中断和上下文的固定开销被摊薄了256倍包处理中的指令预取更友好同类决策比如批量查同一个路由表、批量做同一个MAC改写的cache命中率大幅提升。后面几章我会把VPP的核心机制、运行图架构、性能取舍以及从编译到排查的完整过程展开讲。如果你之前只做过内核网络编程或者刚接触DPDK想找一个完整的数据面框架这篇应该能帮你把VPP的底细一次性摸清楚。2. Vector Packet Processing的核心机制一次处理一叠包的工程艺术VPP不只是一个库而是一个自带数据平面框架、内存管理和调度器的完整运行时。理解它的核心机制重点是理解它如何把“批量”这件事落实到每一个细节中。2.1 这里的vector是“包批”不是SIMD指令初次接触VPP的人容易把vector和CPU的SIMD向量指令弄混。SIMD是对同一条指令操作多份数据比如一次处理8个浮点数VPP的vector是数据包的集合——一次调度中由网卡收进来的一批包。每个包在VPP内部由一个buffer描述包体存在共享内存池中vector本身是一个buffer索引数组。这个区别很关键。SIMD的并行是数据级并行能加速加解密、checksum这类计算密集操作而VPP的vector并行是包级并行它解决的是“控制流开销”问题——函数调用、循环、分支决策、内存访问都是按批分摊。两者不冲突VPP中的很多节点内部也会使用硬件加速指令但整个框架的编排单位是包向量。2.2 Burst收包、预取和连续内存批量处理的三板斧VPP能批处理的先决条件是能从网卡一次性拿到足够多的包。基于DPDK的输入节点通常使用rte_eth_rx_burst这类接口一次调用就收下一批包。DPDK的PMD驱动在收包时已经做了预取优化网卡通过DMA把数据写入内存后CPU轮询时先批量预取部分cache line避免紧接着逐包访问时等待内存。然后是内存池设计。VPP为包buffer分配的是大页支持下的连续物理内存耗尽时通过命名内存池管理。连续内存带来的直接好处是TLB页表缓存miss大幅减少如果一个buffer池跨越几百个4K页CPU访问每个新页都可能触发一次TLB miss换2MB大页之后同样的内存区域只需要几百个页表项。这只是第一个层面buffer结构本身也考虑了对齐和cache line复用避免频繁伪共享。2.3 Batch大小是延迟和吞吐的杠铃批次大小是VPP里最微妙的一个参数。默认的frame size通常在256这个量级也就是一个节点每次处理256个包。256这个数字不是拍脑袋定的批次太小分摊固定开销不足性能逼近逐包处理批次太大网卡和节点之间的队列缓冲时间拉长端到端延迟显著上升。现代DDR4内存带宽足够时单方向延迟增加几十微秒通常可接受但并不是所有场景都能承受。所以低延迟场景下比如旁挂的网关设备、高频交易网络很多人会调小burst或改用中断收发模式保时延高吞吐场景比如骨干转发、CDN边缘则用默认甚至调大batch换极致pps。这个权衡没有绝对正确答案务必用实际业务负载压测不要照抄网上的参数。3. 从graph节点看数据面重构VPP的架构设计逻辑如果只把VPP理解成“批处理收发包”就太可惜了。它真正大胆的地方是把整个数据转发路径重构为一张运行图graph所有处理环节都变成图上的节点。3.1 一切皆是node把转发路径拆成有向图内核协议栈的转发逻辑是层层函数调用从二层解析到IP路由再到输出每一步深度耦合。VPP相反每个功能单元是一个节点节点之间用有向边连接形成一个图。经典的L3转发路径可以简化为dpdk-input - ethernet-input - ip4-input - ip4-lookup - ip4-rewrite - interface-output手里拿着这样一张图你可以很直观地看出“新增一个解析器”只是插入一个节点“丢弃某种流量”只需要让边导向drop节点而不必触碰其他任何代码。VPP的插件体系正是基于这种设计每个插件注册自己的节点和边动态加入运行图。3.2 frame与vector一次调度一叠包走完一个节点VPP中更准确的术语是frame和vector一个frame是一个节点一次被调度时拿到的包集合内部包含一个vector的buffer索引。调度器从源头节点开始把收到的包按向量广播到后续节点每个节点执行完逻辑后把包传给后继节点的下一个frame。整个过程不拷贝数据只传递buffer索引数据的实际内容始终留在共享内存池里。这种传递模型带来的副产品是天然的流水线节点A处理完256个包时节点B可能已经处理上一条256个包不同的CPU核则可以并行处理同一张图的不同分支。你可以在show node counters里看到每个节点累计处理的包数和丢弃数那其实是在跑一张大数据流图的统计视图。3.3 无锁与私有状态为什么多核扩展能接近线性VPP的多核扩展做得几乎线性核心原因在于它的设计哲学每个worker线程拥有独立的graph实例和尽可能多的节点实例大部分节点维护的是线程私有的状态不存在跨线程共享修改。私有的ARP表缓存、私有的统计计数、私有的速率整形器状态这些数据都在各自的cache里没有跨核同步开销。不得已要共享的数据比如全局的FIB表则用无锁或读写锁方式访问读取路径以RCU或版本号机制避免锁等待。加上网卡多队列本身就把不同的流分散到不同核核间需要通信的机会很少。在规模化场景中VPP的丢包率通常与核数无关反而是哈希均衡、NUMA拓扑、内存带宽成为新的瓶颈。3.4 一个最小转发图的走读走一遍上图dpdk-input从网卡队列批量收包构造vectorethernet-input解析MAC头部按ethertype分发给下一个节点IPv4走ip4-inputIPv6走ip6-inputip4-input校验IP头并查找FIB如果匹配到近端转发则进入ip4-rewrite改写MAC后交给interface-outputinterface-output按网卡队列做环形队列入队最后触发发送。每个环节里都有prefetch、批量循环、switch分支合并的实现细节。看VPP源码时可以先看几个最基础的节点很多高阶节点都是在这些基础节点上做扩展的。理解了这张图你就掌握了VPP的灵魂。4. 性能数字背后的取舍为什么快、快在哪、代价是什么说一堆原理最终还是要落到数字上。这里先声明网络性能测试极其依赖硬件平台、网卡驱动、包大小、路由条目规模、编译选项不同环境差一个数量级都有可能。下面数字是社区公开benchmark和常见部署环境下的量级参考不要拿去当精确指标。4.1 单核能力与典型实测数字在接近最优配置的x86平台上DPDK并用VPP跑L3转发路径单核做到每秒2000万到4000万包是常见水平若路由表很小、纯转发路径成熟5千万pps也有出现。作为参照同一平台上内核协议栈单核通常只有几十万到一两百万pps。这意味着什么一块同样价格的CPUVPP能用两个核扛下万兆小包线速给控制面和管理面留出大量余力。时延方面在批量模式下VPP的单跳时延通常只有个位数到几十微秒取决于burst的大小和是否使用中断模式。对比内核路径动辄几十乃至上百微秒它更接近专用硬件的表现。这里尤其要说VPP的最高性能依赖“专网专核”也就是网卡驱动由VPP独占、CPU核被隔离不跑别的进程一旦混布业务进程实时性和确定性都会大打折扣。对比项内核协议栈DPDK裸转发VPP包处理方式逐包中断/软中断批量收包逻辑自写批量运行图调度单核小包性能数十万到一两百万pps依赖实现千万到数千万pps开发成本低socket编程即可高要自己实现转发逻辑中高但插件框架成熟可编程性弱NFV/流量控制困难强强节点自由扩展适用场景控制面、轻量转发高性能数据面库完整数据面框架4.2 多核扩展几乎线性但有几个条件多核性能接近线性的前提是流向均匀和NUMA拓扑合理。如果网卡插在NUMA节点0但worker绑到了节点1跨节点访问内存的延迟会显著拉高每个包的成本性能掉一半非常常见。所以生产部署时网卡的PCIe位置、大页内存所在节点、worker线程的CPU亲和性必须对齐这部分我在第六章会用具体命令展开。第一优先是把网卡和worker放同一个NUMA节点其次才是考虑哈希均衡。很多团队一上来就调RSS接收侧缩放其实是把顺序搞反了。网卡RSS只解决多队列分发问题解决不了跨NUMA节点访存问题。4.3 代价与边界哪些场景不适合VPPVPP不是银弹。首先它要求网卡支持用户态驱动DPDK PMD市面上很多低端网卡或虚拟化环境并不提供合适的绑定方式。其次它本质是“用户态轮询”CPU会持续跑转发循环空闲时也占100%的单核不像内核那样能低功耗待机。第三开发一个自定义节点需要掌握VPP的buffer、graph、trace等内部API学习曲线明显比普通socket编程陡峭很多。如果你只是偶尔转发少量控制消息VPP的大炮打蚊子毫无必要。另外VPP对内存的胃口也比普通应用大每个buffer是预分配的大页内存默认配置下几个GB并不奇怪。嵌入式设备或内存吃紧的环境中要先认真计算buffer总量否则内存不够会直接拒绝启动。5. 从零跑通VPP编译、启动、配置与基础转发原理聊透了下面进入实战。这一节我会带着你从一台干净的Linux机器开始把VPP跑起来顺便配一个最基础的二层桥接和三层路由。5.1 环境准备中的三个关键动作巨页、CPU隔离、网卡驱动第一步是巨页。VPP和DPDK依赖hugepages分配连续内存建议至少预留1-2GB。临时生效可以echo 2048 /proc/sys/vm/nr_hugepages永久配置则改/etc/sysctl.conf中的vm.nr_hugepages。随后用cat /proc/meminfo | grep Huge确认。第二步是CPU隔离。把专供VPP的CPU核从内核调度器中剥离避免其他进程抢占。修改内核启动参数isolcpus2-5假设我们想把2-5留给VPP重启后生效。没有隔离也没关系定向绑核也能用但隔离能让性能稳定得多。第三步是准备网卡驱动。VPP通过DPDK访问网卡需要把物理网卡从内核驱动解绑绑定到vfio-pci需要开启IOMMU或igb_uio。DPDK自带的dpdk-devbind.py脚本做这件事最方便modprobe vfio-pci dpdk-devbind.py -b vfio-pci 0000:01:00.0注意绑卡前一定要确认这块网卡不承担管理网络否则SSH会断。这是新手最容易踩的坑我在下一节还会强调。5.2 编译安装与startup.conf最小配置编译VPP本身不难官方仓库用make管理git clone https://git.fd.io/vpp.git cd vpp make install-dep make build-release编译产物在build-root/下也可以用make install安装到系统。接着写一份最小的配置文件startup.confunix { interactive log /var/log/vpp/vpp.log full-coredump } cpu { main-core 2 workers 4 } dpdk { dev 0000:01:00.0 dev 0000:01:00.1 no-multi-seg }main-core是VPP主线程绑定的核workers是数据面worker线程数一般取物理核的一半到全部。dpdk下的dev写网卡的PCI地址用刚才lspci查到的实际地址替换。不同VPP版本字段略有差异启动后看日志确认即可。然后启动vpp -c startup.conf5.3 二层桥接与三层路由的快速配置启动后通过vppctl进入交互式CLI。先看接口名vppctl show interfaceVPP识别到DPDK网卡后接口名通常是GigabitEthernet0/0/0这种格式由PCI位置生成。二层桥接的配置如下vppctl set interface state GigabitEthernet0/0/0 up vppctl set interface state GigabitEthernet0/0/1 up vppctl set interface l2 bridge GigabitEthernet0/0/0 1 vppctl set interface l2 bridge GigabitEthernet0/0/1 1这是最简二层桥把所有流量转发到同桥接域的其他口。三层路由更简单vppctl set interface ip address GigabitEthernet0/0/0 192.168.1.1/24 vppctl set interface state GigabitEthernet0/0/0 up vppctl ip route add 10.0.0.0/8 via 192.168.1.2 GigabitEthernet0/0/0配置完成后用vppctl show ip fib确认路由表用vppctl show interface看收发包计数。通了就成功了。5.4 查看运行状态从show命令到节点级计数VPP最有价值的排障手段是节点计数器。show node counters会列出每个节点的累计包数和丢弃数show node counters all会连计数节点都输出。如果在dpdk-input节点有大量包流入却在ip4-lookup之后数量大幅减少说明丢包发生在那段路径上。配合trace抓入口节点前几十个包可以逐字节查看包被哪个节点改写了。6. 实际部署里最常见的坑从收包为零到性能折半我见过不少团队从0开始部署VPP遇到的问题高度雷同。这里列四个高频坑和完整的排查链路。6.1 接口下全是0先查驱动再查巨页最经典的“我配了接口但show interface收包为0”问题路径往往是三层叠buff。排查顺序先看show hardware-interfaces确认VPP是否真的接管了网卡如果没接管检查PCI地址是否写对vfio-pci是否绑定成功。再确认网卡光模块和对端链路正常VPP CLI里show interface的link状态是否up。然后确认巨页配置是不是够用内存不足时dpdk-input节点会报错或静默丢包。最后用show node counters all看入口节点到底有没有包进来再进行下一步深挖。这个顺序的重要性在于前一项是后一项的前提。很多人一上来就怀疑哈希、怀疑RSS折腾半天其实只是驱动绑错。6.2 buffer耗尽与内存调整VPP启动后buffer池大小是可配置的。如果某次高压测试后发现no buffers available这类提示先看show buffers确认当前使用率和最大容量。常见诱因有三个buffer池配得太小、单个包链使用了超长多seg多段包导致buffer碎片、或者某些session泄漏了buffer没有归还。线上环境中调buffer参数要结合流量模型计算不要盲目加内存。6.3 NUMA跨节点引发的性能折半前文已说NUMA错位会让跨节点访存延迟抵消掉大部分批处理收益。排查手段是先用lspci -vv或lstopo确认网卡所在NUMA节点再用numactl --hardware确认CPU分配。若网卡在node0但worker绑到node1启动VPP时补一句numactl --cpunodebind0 vpp -c startup.conf或者直接把worker的CPU亲和性改到node0。这一步调整前后转发性能往往立竿见影从一半以下恢复到九成以上。6.4 用节点计数器和trace命令定位丢包点性能不达标或丢包时最该问的问题是“丢在哪一步”。VPP把整个路径拆成了节点相当于每一步都有仪表盘。show node counters显示的l2-discard、ip4-discard节点计数增长位置就是丢包发生的功能模块。比如ethernet-input的rx_bytes有10万ip4-input却只有5万那问题大概率出在EtherType分发或校验上。这时再加tracevppctl trace add dpdk-input 100然后触发流量show packet trace查看抓到的报文细节。这套方法比盲调参数有效得多也适合日常做性能分析时找出热点节点。6.5 低延迟场景burst size怎么调如果业务对低延迟有硬性要求VPP的分批模型确实会引入排队延迟。常见调法是把送入节点的frame size调小让每批的积压时间缩短更彻底的做法是改用中断收包模式让包到达即处理不再攒批。代价是吞吐明显下降中断反复化开销上升。我实测下来用合理的frame size比如64或128配合强绑核多数转发场景能做到微秒级时延基本够用。要压到亚微秒还得回到专用硬件或用户态RDMA那种赛道。7. 学习路径与扩展方向如何把这份能力真正用起来到了这里VPP的主干已经走通了。最后说说我认为比较务实的进阶路线和个人感受。7.1 推荐的顺序、资料与上手路径别一上来就啃VPP源码。建议顺序是先扎实理解内核协议栈的转发路径和瓶颈再跑一遍DPDK基础示例理解PMD收包和轮询模型最后进入VPP。VPP官方文档和FD.io wiki把概念讲得比较清楚可以先通读src/vnet和src/vlib的代码注释这两个目录是graph调度和buffer管理的核心。上手时建议用VIRTIO网卡或虚拟化环境先跑通逻辑再上物理环境能少折腾一半驱动问题。7.2 扩展方向SRv6、NAT与负载均衡VPP最吸引人的地方在于它不是一个只能做转发的玩具而是一个完整的可编程数据面。SRv6在VPP中已经有了相当完整的实现能直接做分段路由和网络编程NAT44、哈希负载均衡、ACL过滤这类节点开箱即用。对于做云原生网络的人VPP也逐步收敛进CNF云原生网络功能的版图vhost-user接口可以连QEMU虚拟机AF_PACKET接口可以接普通内核接口整个体系能串成一套高性能虚拟化转发路径。7.3 分享我的一个小习惯最后分享一个我自己长期保持的习惯每接入一个新业务场景前先写一个针对该场景的最小测试拓扑把VPP的节点计数、buffer使用率、延迟分布都记录下来存档。理由是网络性能问题往往“这次调好、下次又崩”没有基线数据很难判断回归。VPP这类用户态数据面尤其如此一个驱动改动、一次内核升级都可能让性能从满血变残血。有基线、有计数器、有trace问题出现时才有底气去定位而不是靠玄学调参。学习VPP的过程不算轻松但把它跑通后的收益非常实在你等于在用软件的方式把一台通用服务器变成了接近专用转发设备的性能体。这种能力在流量越来越卷的时代是相当有含金量的。
返回列表