前言
通信本质是信号的传递,如声音、烽火狼烟、电话电报、数据网络等;具备信息源、信息通道、信息接受者;可以是实时的或跨时空的;可以是单向的或双向的或多向的;可以是定向的或无向的。但通信离不开介质,那怕量子纠缠,也跳不过物质;毕竟一切在物质世界的显化都需要以物质作为主导,同时需要能量参与其中。
这一过程可以称之为“编程”(自从GPT、Sora诞生,貌似整个世界的活动都可程序化/数字化),即通信编程。将物质或能量按照一定的规则进行编码、解码,可以理解为对物质和能量的雕刻-Coding。
这里Coding泛指硬件的或软件的,大多数时候两者是相辅相成的,硬件好比建设公路桥梁,软件好比在其上运行的车辆。但对通信而言,通信线缆、基站、网卡、网桥、路由交换是基础硬件;软件则是在其上运行的包协议栈。但自从网络IP化之后, 这种通用通信模型加速了网络的互联,协议栈基本被TCP/IP所替代。
VPP是来自于FDIO社区的一种高性能TCP/IP协议栈,由Cisco主导,其聚焦L2/L3转发,富含包括BD、ACL、NAT、PBR等功能。VPP基于指令级和调度级批处理机制、结合用户态IO技术,使用通用服务器即可实现网络数据包的线速转发。
矢量框架
报文处理不中断,且流速线型可谓之矢量。VPP如何保证处理的矢量性?针对VPP代码分析,个人认为主要从这些点:
- 用户态IO Polling技术: VPP使用DPDK库,直接在用户态轮询网卡数据,绕过了内核及网卡中断。Polling实现了报文的批量接收,是报文批量处理的前提。
- Node级批量调度: VPP在于业务组织逻辑,相同的业务Frame置于同一个Node,并在调度层面展开Node级调度,实现了业务的批量处理。使L3 Cache得到了最大化利用,这是框架级优化。
- Node内指令级批量:Node内逻辑使用了多个包的同质化处理,即并行编程,使L1-L2 Cache得到最大化利用。
- CPU指令流水线利用:指令级访存预取优化,对未来数据进行并行预取,缩短访存时间。
- 多队列多线程:VPP基于网卡多队列实现了多Worker并行无锁转发。
- 快慢分离:基于核间无损包Handoff技术,实现快任务和慢任务分离。
- 快转加速:部分业务实现了基于flow的快转支持。
DPDK
DPDK,数据面开发套件,支持包括UIO、VirtIO、MemIF、mbuf等工具库,覆盖了大多数网卡的UIO及零拷贝实现。可见,DPDK具备了丰富手IO开发套件,但其在协议处理上较弱,而VPP的协议处理为其强项,故而VPP+DPDK的组合仍是当前最优的数据面处理引擎。
VPP通过实现DPDK Plugin的方式将DPDK引入到VPP中。在DPDK Plugin中注册Node为INPUT、Polling类型,并挂载DPDK通用报文收发函数,DPDK通用收发函数集中自动为所有IO进行了适配,使用者VPP仅需要告知DPDK关联的网卡pci地址即可。VPP仅需要调用DPDK收包函数实现报文批量接收,并进行二次加工处理后,调用DPDK发包函数实现批量发送即可。整个过程聚焦协议处理本身。
Plugin
VPP采用插件框架,将数据面不同的业务功能置于不同的插件中,如ACL、ABF、NAT等,故而支持灵活裁剪。现有业务插件基于满足大多数应用场景,如有新业务需求,可基于现有插件架构新增。业务插件具备以统一的方式注册到VPP中,通常包括:
- 插件名及初始化顺序,由VLIB_INIT_FUNCTION定义
- 插件初始化及运行参数,由VLIB_CONFIG_FUNCTION定义
- 插件私有数据,与业务逻辑相关
- 插件直调API,即动态函数导出(如ACL)
- 插件配置vapi,支持能力开放
- 插件配置CLI,支持调测与运维
- 插件报文node,注册到转发图上,处理数据面报文
- 插件运行数据stateseg,实时处理的共享数据
Node
根据标议标准,TCP/IP协议栈涉及多层多类业务报文的处理,如ARP、IP、ICMP报文在转发面涉及多次修改。VPP将报文处理划分为多个阶段,每个阶段可注册不同的业务处理函数,这类函数统称为Node。VPP报文处理阶段由arc_name定义,有:
- arp
- nsh-output
- mpls-output
- mpls-input
- ip6-drop
- ip6-punt
- ip6-local
- ip6-output
- ip6-multicast
- ip6-unicast
- ip4-drop
- ip4-punt
- ip4-local
- ip4-output
- ip4-multicast
- ip4-unicast
- l2-output-nonip
- l2-input-nonip
- l2-output-ip6
- l2-input-ip6
- l2-output-ip4
- l2-input-ip4
- ethernet-output
- interface-output
- device-input
- l2-input
- l2-output
上述arc根据TCP/IP协议栈处理顺序划分,大的如L2/L3/L4阶段,每个阶段包括Input,Forwarding,Local,Output几种子阶段,业务Node根据处理需求注册到相同阶段即可,报文将按标准处理阶段依次调度。如BD业务注册在L2-input阶段; 如ACL业务通常注册在Ip4-unicast下,即L3/Input阶段;如NAT业务通常注册在Ip4-output阶段,即L3/Output阶段。
业务Node在注册到VPP时,除声明其调度阶段外,还需要声明其调度顺序及调度方式,调度顺序用于控制同一个ARC下不同node的处理顺序,由VNET_FEATURE_INIT声明并在VPP初始化时迭代出node graph;调度方式用于控制node的调度行为,包括:
- VLIB_NODE_TYPE_INTERNAL #中间业务Node主要类型
- VLIB_NODE_TYPE_INPUT #input node,通常为IO Node,即产生报文的Node
- VLIB_NODE_TYPE_PRE_INPUT #预处理node, 用于Sworker清理工作,如Nat Session
- VLIB_NODE_TYPE_PROCESS #协程node, 运行于Mworker,用于外部事件或周期事件
参见Nat out2in node注册:
Frame
Frame是VPP调度数据包的一种方式。试想VPP如何决定哪些包在哪些Node进行展开调度? 并同时要求矢量化? 这即是Frame调度需要考虑的。
传统数据包处理通常是单线函数栈方式,即逐一处理数据包,这种处理方式在专用芯片或特定指令集的CPU上性能仍然较好,但在通用服务器上性能极速拉低。通用服务器加速计算的有效方式是介于CPU和内存之间的Cache,无论是数据还是指令,VPP在这方面做到了极致。
如何实现数据包的批量调度? 这依赖于Frame的调度方式:
如Input Node产生数据包(如调用DPDK rx burst函数从Nic获取),最大数量为256。产生的数据包会递交给下一个Node处理,每一个Node都存在若干后续Node,但第一个Node为Input Node。在Node注册时,已经决定了Node的前后继关系。在当前Node中,会根据业务类型处理数据包,并将数据包分类。分类的结果决定了下一个Node,如当前Node处理了100个数据包,并将其分为4类,分别为A,B,C,D, 对应个数为:1,10,70,19。4类数据包会分别投递给4个不同的Node,重要的是形成Pending_frame,产生4个新的Pending_frame加入VPP调度序列中,Pending_frame中记录了其关联的Node,Node中挂载了相应的处理函数。
经过上述处理相同的业务报文被聚合,由于相同的业务报文处理逻辑基本是相同的,方便了VPP展开并行指令处理,最大化利用了CPU Cache。重要的Frame函数为:
- vlib_get_next_frame
- vlib_validate_buffer_enqueue_x1
- vlib_put_next_frame
Feature
上述讲到,Frame在Node中进行分类,那么如何进行分类呢?这里有两种方式:
- 默认调度 - 静态调度,在Node注册时已确定调度顺序
- 指定调度 - 动态调度,由代码根据具体业务子类决定
默认调度,通用的业务模型调度方式,即同一个arc下的调度顺序;指定调度,特定业务处理的动态调度方式,如:
ip4-unicast:
udping4-out2in
ip4-sv-reassembly-feature
nat44-out2in-worker-handoff
报文在ip4-unicast阶段,会默认逐一被udping,reassembly,nat-out2in处理,如在udping中处理时,默认调用vnet_feature_next设置next为reassembly; 但如果存在需要drop或其它业务处理的特定报文,则会修改next node。见上述Node注册时,已声明的处理的顺序,包括其业务可能的next node。
Others
- VPP针对配置数据的保护采用类似于调度锁方式,即将SWorker挂起后展开配置应用
- VPP trace提供了基于报文粒度的跟踪方式,并支持filter过滤
- VPP提供基于vapi的北向能力开放,并支持声明为mp_safe,针对非写数据的无锁访问