传统数据中心中硬件服务器上运行linux,linux用硬件网卡收发包,硬件网卡有broadcom的有mellanox的有intel的等各式各样的,硬件网卡连接到硬件交换机上,硬件交换机有H3C的有cisco的,交换机进行包转发实现服务器之间互通。在云计算环境下,对计算资源进行了切分,服务器上运行的是一个个虚拟机,虚拟机也要有网卡实现互连互通,但虚拟机的网卡不是物理的,是虚拟的网卡,虚拟的网卡连接到虚拟的交换机上,虚拟的交换机对同一个服务器上的虚拟机之间流量进行转发,如果虚拟交换机再连接到服务器的硬件网卡,那么虚拟机就可以和服务器外面通信了。
硬件网卡收包时,CPU先分配内存,然后告诉硬件网卡内存的地址,报文从硬件交换机出来后进入硬件网卡的队列,硬件网卡通过DMA功能把包从物理网卡搬运到内存中。然后中断CPU说报文收上来了,CPU处理中断,软中断执行内核协议栈处理,最后通知应用程序收包。
虚拟网卡是CPU模拟出来的,它的队列也是模拟出来的,就是服务器上的一块内存。要模拟DMA功能就得进行一次内存拷贝,这次内存拷贝比较特殊,从服务器上拷贝到虚拟机里,虚拟机运行在服务器上,用的也是服务器上的物理内存,相当于服务器上物理内存之间的拷贝,只是地址转换比较复杂。
应用程序调用发包时,内核协议栈模块分配一块内存,把用户态要发的内容拷贝到内核。然后经过复杂的协议处理,最后地址告诉硬件网卡说我要发包你发完了告诉我一声,硬件网卡的DMA就把要发的数据从内存中搬运到物理网卡的队列中,然后告诉CPU说发完了,你可以回收内存了。
虚拟网卡发包和物理网卡发包类似,包从虚拟机中搬运到物理服务器内存中,然后经过软件交换机,最后从物理网卡出去。虚拟网卡有e1000,virtio等,为什么云计算环境最终选择了virtio?因为virtio首先提供了一种虚拟机和物理服务器数据交换的通用机制,虚拟网卡虚拟硬盘虚拟显卡虚拟串口等都可以用。其次virtio还分frontend和backend,frontend运行在虚拟机中,backend运行在物理服务器上,frontend和backend配合实现具体的网络功能,frontend和backend之间用virtio通用机制交换数据,设计真是十分巧妙。 virtio得到了大多的hypervisor支持,成了虚拟化事实上的标准。
硬件交换机是进行二层转发的,学习MAC进行转发,用STP协议防止环路出现,为了减小arp这样的广播风暴又增加了vlan隔离。软件交换机有linux bridge,vpp,ovs等,为什么云计算环境中大多用了ovs?因为云计算是最自然的SDN应用场景,不再需要复杂的控制面协议,如STP。另外云计算要求灵活,vpp pipeline和linux bridge设计时考虑更多的是二三层转发原理和转发性能,要加入新功能异常艰难,而ovs采用了openflow pipeline,多table和多group灵活跳转。pipeline设计考虑更多的是功能实现,性能方面由datapath支撑,并且datapath upstream到kernel中了,顺手就能用了,所以选择ovs。
这张图来自于redhat的博客,博客地址在参考文献中,非常完美的一张图,就没必要重复造轮子了。操作硬件网卡就是读写硬件网卡的寄存器,现在的外设都是PCIE外设,有配置空间和BAR空间。配置空间是PCIE标准定义,BAR空间硬件自己定义,读写寄存器就会触发硬件相应的操作,比如发送包,会构造一个ring,每个ring指向skb,然后把ring的头写到一个寄存器,再写一下触发发包的寄存器,硬件就读取ring,把ring指向的skb都从内存搬运到网卡上去。
虚拟机也有模拟的主板芯片和PCIE总线,virtio-net是PCIE总线上的一个外设。在这张图中frontend就是guest kernel中的virtio-net driver,而backend是qemu模拟的virtio-net device。virtio-net driver读写寄存器,qemu就要把原来硬件干的活模拟一遍。因为虚拟网卡寄存器是不存在的,kvm就把一块内存做特殊标志当作寄存器,virtio-net driver一写这块内存,cpu就从guest中exit出来,停止执行guest,开始执行qemu代码模拟guest触发的动作。qemu把外设模拟分为两种:控制面模拟和数据面模拟。控制面模拟有feature协商,vring地址交换等;数据面模拟有数据搬运和消息传递,其中消息传递就是guest通知vhost-net数据准备好了,我要发送,vhost-net发送完后告诉guest我帮你发送完了。在这张图中qemu把数据面模拟的工作交给了内核的vhost-net模拟来完成。
上面说虚拟网卡模拟DMA是内存拷贝,既然是内存拷贝那干脆共享内存就不用拷贝了。vhost-net就是这么干的,不管是内存拷贝还是内存共享都要进行内存地址换算。guest中的virtio-net driver设置的都是guest physical address,而vhost-net只知道host virtual address,但别忘记guest的内存是qemu分配的,qemu知道换算关系,qemu就通过vhost protoco告诉vhost-net模块换算关系。剩下就是消息传递了,kvm给qemu提供了api,qemu创建出两个eventfd传递给kvm,qemu通知guest,写一个fd,kvm读这个fd,kvm再把中断注入guest中,这个方向的通知叫做call。guest通知qemu,guest写寄存器,kvm拦截,kvm写另一个fd,qemu读这个fd,这个方向的通知叫做kick,qemu干脆一狠心,把这两个fd仍给了vhost-net,说以后这两个fd的读写就交给你了。
guest在内核中分配skb,把地址写到vring中,kick kvm,kvm再通知vhost。vhost是内核线程,vhost地址换算拿到了skb,复制skb,通知guest发送完成,guest回收sbk,vhost继续给skb找netdev,加入到一个cpu的backlog,触发softirq,这个cpu的softirq发现netdev绑定在ovs桥上,查ovs流表找到出接口,调用物理网卡的驱动发送出去,物理网卡发送完成,把skb释放。
guest中的virtio-net driver分配skb,设置到vring上,物理网卡驱动分配sbk设置给网卡,网卡DMA,中断触发,在softirq发现物理网卡绑在桥上,查ovs流表找到虚拟机的tap口,把skb放入tap的队列中,叫醒vhost worker,vhost worker醒来一看skb到了tap队列中,把skb拷贝到guest分配的skb上,通知kvm包来了,kvm再中断guest。
这张图也来自于redhat的博客,我做了一点修改,原图guest中运行的是virtio-net-pmd而且还有vIOMMU。考虑到guest中的难度,我觉得公有云上是不现实的,让用户在guest中搞hugepage和dpdk pmd难度很大,而且目前没有配套的成熟用户态协议栈,修改一下假设guest是最普通的guest,相比vhost-net一点也不变。这种模式ovs不再用内核的datapath,物理网卡绑定了DPDK,物理网卡直接把包DMA到用户态ovs,ovs进程和qemu进程共享内存把包传递到qemu进程中,qemu进程地址换算一下包就到了guest中。virtio-net控制面模拟还在qemu中,原来给vhost-net下配置的vhost protoco变成了vhost-user protoco把配置交给了ovs-dpdk进程,qemu和ovs-dpdk之间建立了unix domain socket。这个socket的神奇之处在于不仅能传递virtio配置信息,还能传递qemu和kvm之间通信的kick/call fd。qemu把virtio-net数据面模拟交给了ovs-dpdk进程,消息通道还是靠kvm。guest中一直poll不现实,但ovs-dpdk从guest中拿包时可以一直poll。
这张图也来自于redhat博客,根据我自己的理解修改一把,保持guest不变,用户在物理机上怎么部署业务,在虚拟机中也怎么部署业务,不能让用户感觉到不习惯或者不舒服。整体原理就是把virtio backend都由硬件实现了,然后用passthrough功能,和普通的物理网卡passthrough一模一样,只是这块卡实现了virtio标准。 passthrough和dpdk都用了vfio-pci,原理一样,把物理网卡的pcie配置空间映射到qemu进程或者ovs-dpdk进程空间中,ovs-dpdk就直接读写,但qemu还得再地址转换一下给了guest,guest中的virtio-net driver就可以直接读写了,所以virtio kick就好搞了,但call还是经过vfio-pci,vfio-pci通知kvm,kvm再中断注入guest中。数据搬运就是硬件直接DMA到qemu进程或者ovs-dpdk进程中,进程在虚拟地址空间中分配内存,交给硬件的地址都是进程的虚拟地址。然后进程把地址信息给vfio,vfio把进程虚拟地址转换成物理地址给IOMMU,IOMMU在外设给进程虚拟地址搬运数据时把地址转换成这个物理地址。qemu相比ovs-dpdk多了一道手续,guest driver给硬件配置的地址是自己的物理地址,qemu和vfio得把guest的物理地址转换成host的物理地址。
passthrough的问题是虚拟机不能热迁移,热迁移要把网卡pcie空间的数据迁移走,而且还要知道硬件DMA修改了那些guest的内存,要把修改的内存也迁移过去,但硬件都没有提供这样的接口,提供了接口qemu还得有办法获取。既然有其它物理网卡能passthrough了干嘛还要实现virtio物理网卡来passthrough,这样做没什么用。
这张图也来自于redhat博客,图太多可能博客作者搞混了图,原图是不对,当时可能想把vpda往vfio/mdev方面靠,首先mdev并不要求必须遵循virtio标准,vdpa实现了数据面的virtio标准,并且把datapath offload到了硬件,控制面没有offload继续由qemu模拟,硬件设备虚拟出来的vdpa未必实现了virtio标准要求的pcie功能,显然用vfio不行,没有vfio就不能利用vfio控制iommu的代码,vdpa需要自己开发,内核中没有upstream,控制面利用vhost协议,vhost再调用vhost-vdpa,vhost-vdpa调用硬件厂商提供的接口把控制信息下放到硬件中。
这种方法硬件网卡直接把包DMA到guest中了,问题是kick和call还是很麻烦,kick到了kvm,kvm通知vhost-vdpa,vhost-vdpa调用硬件驱动通知硬件。硬件call时中断给了硬件驱动,硬件驱动通知vhsot-vdpa,vhost-vdpa通知kvm,kvm把中断注入guest,如果guest用dpdk poll mode driver,kick和call就不成问题了。
搞了这么复杂,第一是为了让硬件网卡实现起来容易,硬件网卡厂商基于SRIOV和Scable IOV实现资源分割,再模拟一下vring操作就可以包装出vdpa。第二是为了实现热迁移,寄希望于硬件厂商的driver能提供接口获取DMA写了那些guest内存。第三guest和host用vdpa统一用virtio-net驱动,qemu只增加一个vhost-vdpa模块,qemu也简单,硬件的不同让硬件厂商的驱动屏蔽,厂商同时提供硬件和驱动,其它模块都不动就能适配不同厂商。
guest保持不变,用的都是virtio-net驱动,中断有开销,协议栈有开销,这是避免不了的。定性分析一下三种转发模型的利弊,忽略virtio full offload。首先上限受制于PCIE插槽的物理能力,当然真实场景下很难达到物理上限,只有dpdk l2fwd这些简单模型下才能达到。包处理就是一条流水线,达到无缝配合才能实现性能最高,并行几条流水线处理就更好了。
简单总结一下影响性能的几个因素,分析每种因素在三种模块中的影响。
vhost-net在物理网卡收发包用了中断,vhost-user用了dpdk pmd没有中断开销。
vdpa就是zero-copy。vhost-net 和vhost-user rx无法zero-copy,假如rx要实现zero copy,guest alloc skb,backend把skb给了硬件网卡驱动,驱动设置给硬件网卡,硬件哪知道要来的包是给哪个虚拟机,假如硬件网卡一个队列对应一个虚拟机,那guest不提供skb,硬件网卡就丢包了,rx skb只能由硬件网卡驱动分配,分配时能不能从guest内存中分配,好像也不行,没法和guest同步,只能从内核或者ovs空间中分配skb,分配的skb也没法共享给guest,必须拷贝。vhost-net和vhsot-user tx都能实现zero-copy,guest alloc skb,backend直到等物理网卡驱动DMA走再通知guest发送完了,你可以回收skb了,有点麻烦,第一guest可能得等好久才能回收,第二等硬件网卡发送完了通知哪个guest回收难实现。所以vhost-net和vhost-user zero-copy都不现实。
vhost-user模型中qemu和ovs-dpdk都用到了hugepage,坏处是内存不能超卖。
就是说参与转发的CPU多少,从串行变成并行,当然能提高性能了,但CPU资源是有限的,到底几个队列好呢,物理网卡有多队列,virtio-net也有多队列,按流分队列还是按包分队列,不管怎样进哪个队列由物理网卡来实现,软件实现不划算,物理网卡计算一个hash值写到skb中,到了virtio-net再根据这个值再入virtio-net队列。发送最简单配置就是一个cpu一个队列。
减少同步的开销,DPDK中有无锁队列,CPU之间传递skb用无锁队列开销就小。另一方面就是查流表和写流表实现无锁,用urcu等等。
pipeline设计问题,接力干就是一个cpu把skb处理一会然后把skb放入队列交由下一下cpu处理,下一个cpu poll还是上个cpu通知下一个cpu开始干活,poll没sbk就白白浪费cpu,通知机制有开销和时延。一口气干到底就是就是一个cpu收skb处理skb最后发送skb,把所有活都干了,多个cpu同时开干,配合多队列好一点。这两种模式的不同涉及到cpu cache/prefech等技术的利用,接力干对cache/prefetch友好。vhost-net是接力干的,vhost-user两个模式都能用,用户态修改代码和调试都容易一点。
| vhost-net | vhost-user | vdpa |
---|---|---|---|
interrupt | 有开销 | 无开销 | 无开销 |
zero-copy | 不能用 | 不能用 | 没必要用 |
hugepage | 不能用 | 用了 | 没必要用 |
mutilple queue | 能用 | 能用 | 能用 |
lock | 有 | 没有 | 不存在 |
pipeline | 分工衔接通知复杂CPU不能专心干活 | 灵活设计,CPU可以独占 | 不存在 |
整体来看vdpa是性能最高的,还省cpu,但是价钱贵,性能其次是vhost-user,有点费cpu,vhost性能最差。
vhost-net用的最多,配套完善,稳定成熟。vhost-user据我所知在电信级别云中很常用,电信级别云只追求性能,不考虑超卖,vcpu强绑定,不跨numa,网元数据面passthrough,数据面和控制面通过vhost-user通信。如果数据面用了vhost-user能热迁移能横向扩展,这样更符合NFV的理念,但普通openstack环境vhost-user就不能用内核的功能了,内核qos,netfilter,connection tracking,三层转发和arp都不能用了,意味着openstack安全组,QOS和DVR都没法用了,只能用高性能三层专用网关。vpda个人没用过,没有这样的硬件,可能很多公司已经开始试用了,恐怕是一张很贵的卡,不仅要处理virtio还要处理vxlan,虚拟机能用,裸机也能用。
没有完美的开源技术,只有最佳实践经验,最佳适用场景,用起来,修改起来,回馈开源社区。
https://www.redhat.com/en/blog/deep-dive-virtio-networking-and-vhost-net
https://www.redhat.com/en/blog/journey-vhost-users-realm
https://www.redhat.com/en/blog/how-deep-does-vdpa-rabbit-hole-go