将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2021-10-23 15:50:47
最近在看DPDK和容器直接的对接方案,我们知道容器(如Docker)网络一般都采用tap+bridge方案,也就是基于内核的网络通信,即使使用ovs一般也是kernel ovs,而不是dpdk-ovs。那么当我们使用的vswitch是基于DPDK时如何和容器对接呢?下面我们重点分析一下。
首先,我们先确定目标——容器内部无感知且方案通用,所以排除SR-IOV和SmartNIC方案。有了这个前提,那就说明容器首先还是需要用tap作为网卡(无感知),要将tap的数据流量连接到DPDK,我们自然就想到DPDK和kernel的通信。所以我们先重点回顾一下DPDK和kernel的常用方式。
DPDK和kernel直接的通信路径也叫做exception path,DPDK支持几种方式让用户空间的报文重新进入内核协议栈。
可以使用内核提供的TAP/TUN设备,这种设备的使用需要使用系统调用,并涉及到copy_to_user()和copy_from_user()的开销。DPDK也支持了tun/tap pmd(详见:),可以通过vdev参数指定创建tap设备。对应命令行如下:
--vdev=net_tap0,iface=foo0 --vdev=net_tap1,iface=foo1, ...
但是是有tap设备有个缺陷是用户态部分(tap pmd)每次收发包都需要调用系统调用,我们看DPDK的tap pmd发送函数tap_write_mbufs最终就是调用writev,这样必然导致频繁的上下文切换,影响转发性能。
点击(此处)折叠或打开
DPDK提供了KNI接口用于提高用户态和内核态之间报文的处理效率。KNI是通过内核模块构造了一个虚拟网络接口,并且通过FIFO和用户态的DPDK应用交换报文。
正如DPDK官方所讲,使用DPDK KNI的好处是:
l 比现有的Linux TUN / TAP(通过消除系统调用和copy_to_user()/ copy_from_user()操作)性能更高效。
l 允许使用标准Linux网络工具(如ethtool,ifconfig和tcpdump)管理DPDK端口。
l 允许与内核网络协议栈的交互。
但是KNI从内核收到报文后最后是通过netif_rx函数进入协议栈的,它并不能直接将流量和某个tap设备关联,从这点看也不符合我们和容器通信的需要。此外,该方案的缺点是KNI内核模块无法upstream,维护代价较大。
virtio-user最初是为了支持容器内部和DPDK通信的,如下图所示,如果vswitch使用的是vhost-user,那么如何和容器对接呢?我们知道在虚拟机场景时,vhost-user是通过mmap整个qemu进程的内存,进而进行共享内存,然后操作virtio ring进行通信。但在容器场景vhost-user怎么共享内存呢?或者说共享谁的内存。virtio-user就是解决这个问题,它的角色类似于虚拟机场景下的virtio-net+qemu,即承担网络驱动的角色(virtio-net),又承担和后端协商的作用(qemu)。这样后端vhost-user就仅需要共享容器中DPDK进程的内存,即可实现通过共享ring实现网络通信。
通过上面我们发现virtio-user充当了qemu+virtio-net的角色,另外我们知道qemu+virtio-net可以和vhost-net组合作为内核场景下的前后端,所以virtio-user一样可以和vhost-net组合构成一个前后端通信模型,这也是virtio-user的第二个作用——使用它与现有的vhost-kernel方案配合来实现exception path。需要内核中vhost.ko和vhost-net.ko两个模块。如下图所示
为了更加理解这个通信架构和虚拟机qemu场景的异同,我画了如下一个流程对比图。关键的一点,在虚拟机qemu场景中虚拟机中的网卡是qemu虚拟化出来的,而不是tap设备,甚至tap设备也不是虚拟网卡的后端,vhost-net才是后端,而tap设备是由qemu设置作为vhost-net的backend的;在virtio-user容器场景中tap设备是作为容器的网卡的(容器场景没有前后端之说),容器发出的流量时直接通过协议栈进入tap设备的。
可能还有一点容易让人有歧义,如下图,在没有vhost-net时(qemu直接作为后端对接tap的场景),qemu对接tap和virtio-user场景容器对接tap设备有什么不同。虽然都是经过tap设备从用户态发送数据流量,但两者的流量路径完全不同。前者是qemu作为一个普通的用户进程打开tap设备,得到一个fd,通过读写文件fd的方式发送/接收数据,后者是将tap设备作为一个网络设备,应用程序不直接操作感知tap,对于应用程序只是把数据包发给协议栈,由协议栈最终调用tap设备的xmit函数。
当然也可以采用如下图左侧方式使用virtio-user,即通过tap设备桥接到bridge(或直接使用veth-pair),在同另一个tap设备接入容器。这种方案相对右侧来说路径较长,不过它的好处是容器中的网卡(tap)设备不受外部DPDK-ovs的启停影响。这个我们后面再详述。
总之,virtio-user是virtio PMD的虚拟设备,启动DPDK virtio-user,系统就会创建一个内核态的虚拟设备tap, tap通过vhost-kthread和virtio-user进行数据的发送接收;此外vhost-net的内核模块也是virtio-user的控制面,发送接收一些控制消息。这样一来,从DPDK收到的包进入到virtio-user,通过vhost-kthread进入到tap设备,tap设备支持内核协议栈,很好地实现了exception path的包处理。
从维护角度来看,本方案所依赖的vhost.ko和vhost-net.ko都是早已upsteam的内核模块,不需要维护out-of-tree的内核模块;从灵活性来看,本方案不依赖任何硬件功能;从线程模型来看,和KNI相似,本方案只需将数据包放到virtio ring上,数据拷贝操作由vhost kthread来完成。从网络功能来看,vhost-net本来就是为网络而生的,能通过checksum计算和验证、数据包分片offload到物理网卡来进行。由于对Multi-seg数据包的支持,和KNI的方案相比, 本方案将iperf的性能提升了2倍以上。
首先看一下virtio-user的初始化流程如下:
经过以上初始化流程得到以下数据结构关系:
其中在virtio_user_dev_setup中有如下语句:
点击(此处)折叠或打开
我们这里只将kernel(vhost-net)场景。我们还注意到后端是vhost-user和vhost-net的一个区别是,在vhost-user场景virtio-user-dev设备字需要一个vhostfd,而在vhost-net场景却需要一个vhostfds数组。这是因为当tap设备支持多队列时,需要设置IFF_MULTI_QUEUE,这个时候就需要open多次,并且每次设置一次TUNSETIFF,tap name相同,每次open返回的fd对应一个tap设备队列,而vhostfds就是用来记录这些队列对应的fd的。
(关于tap设备的使用细节可以参考:参考:https://www.kernel.org/doc/Documentation/networking/tuntap.txt )
但在上面流程中我们没有看到virtio-user和tap设备直接的关系,这个要从上面的vtpci_set_status函数说起。
点击(此处)折叠或打开
而在前面virtio_user_eth_dev_alloc的函数中有以下初始化语句:
virtio_hw_internal[hw->port_id].vtpci_ops = &virtio_user_ops;
所以vtpci_set_status就是调用virtio_user_ops的set_status,由于virtio_user_ops定义如下,
点击(此处)折叠或打开
所以其实是调用virtio_user_set_status
点击(此处)折叠或打开
其中关键是VIRTIO_CONFIG_STATUS_DRIVER_OK和 VIRTIO_CONFIG_STATUS_RESET,我们重点看下前者,也就是virtio_user_start_device
点击(此处)折叠或打开
这里注意一下VHOST_USER_SET_MEM_TABLE的设置好像没有传递参数,其实是在对应的ops中组装的参数。如当后端是vhost-user时,对应的send_request为vhost_user_sock。
点击(此处)折叠或打开
可以看到,VHOST_USER_SET_MEM_TABLE发送前进行了参数的准备,我们看一下 prepare_vhost_memory_user是如何得到一个个映射的memregion的。
点击(此处)折叠或打开
通过以上分析可以看出virtio-user进行前后端内存共享就是共享本端的DPDK进程的hugepage。对于后端是vhost-net情况也是类似的,我们看下其对应的mem region是如何产生的。
点击(此处)折叠或打开
其实就是映射DPDK进程的memseg,也就是DPDK进程的所有hugepage。但这里注意memseg的个数不能大于 max_regions(64),所以要保证DPDK大页内存的碎片不能超过64。
如果使用vhost-net的时候关键是需要创建tap设备,以及设置tap设备和vhost-net的关系,而这一切都是在 virtio_user_start_device的最后一步,enable_qp。以vhost-net为例,这里dev->ops->enable_qp(dev, 0, 1)就是vhost_kernel_enable_queue_pair。
点击(此处)折叠或打开
这样就将tap设备和vhost-net建立了关联,而由于前面virtio_user_dev_init等函数已经将vhost-user设备和内核的vhost-net建立了关系,所以这样就建立了virtio-user,vhost-net,tap设备三者的关系。
以上就是控制面的相关流程和数据结构关系,至于数据面流程和相对比较简单,很多都是和qemu vhost-net处理流程相似的。以容器发送方向为例子,首先容器发送数据包,最终经过协议栈调用tap设备的发送函数tun_net_xmit。
tun_net_xmit-->vhost_poll_wakeup-->vhost_poll_queue-->vhost_work_queue-->wake_up_process会唤醒vhost_worker。
vhost_worker-->handle_rx_kick-->handle_rx-->tun_recvmsg-->tun_do_read会进行报文的收取,然后通过virtio ring发送到用户态,而用户态的DPDK virtio-user处会轮询收取报文,调用rte_eth_rx_burst-->virtio_recv_pkts_vec会从相应的队列中读取数据。
我们可以使用l2fwd做一个测试,如下图所示,两个tap设备分别各两个namespace,然后使用virtio-user接入l2fwd,主要给两个tap设备配置上ip后就可以相互通信了。
具体命令行参数如下:
sudo ./build/app/l2fwd -m 1024 -c 0xc -n 2 --vdev=virtio_user0,path=/dev/vhost-net,iface=tap0 --vdev=virtio_user1,path=/dev/vhost-net,iface=tap1 --huge-dir=/hugepage -- -q 1 -p 0x3 --no-mac-updating
注意要加上参数no-mac-updating,否则l2fwd会修改mac,导致网络不通。
关于使用vhost-net+virtio-user做容器和DPDK对接需要注意以下几个问题:
1. DPDK重启后tap设备会丢失
解决方案:使用TUNSETPERSIST ioctl命令
diff --git a/drivers/net/virtio/virtio_user/vhost_kernel_tap.c b/drivers/net/virtio/virtio_user/vhost_kernel_tap.c
index a3faf1d..bf013ab 100644
--- a/drivers/net/virtio/virtio_user/vhost_kernel_tap.c
+++ b/drivers/net/virtio/virtio_user/vhost_kernel_tap.c
@@ -112,6 +112,11 @@ vhost_kernel_open_tap(char **p_ifname, int hdr_size, int req_mq,
goto error;
}
+ if(ioctl(tapfd, TUNSETPERSIST, 1) < 0) {
+ PMD_DRV_LOG(ERR, "TUNSETPERSIST failed: %s", strerror(errno));
+ goto error;
+ }
+
fcntl(tapfd, F_SETFL, O_NONBLOCK);
if (ioctl(tapfd, TUNSETVNETHDRSZ, &hdr_size) < 0) {
diff --git a/drivers/net/virtio/virtio_user/vhost_kernel_tap.h b/drivers/net/virtio/virtio_user/vhost_kernel_tap.h
index e0e95b4..4926b97 100644
--- a/drivers/net/virtio/virtio_user/vhost_kernel_tap.h
+++ b/drivers/net/virtio/virtio_user/vhost_kernel_tap.h
@@ -6,6 +6,7 @@
/* TUN ioctls */
#define TUNSETIFF _IOW('T', 202, int)
+#define TUNSETPERSIST _IOW('T', 203, int)
#define TUNGETFEATURES _IOR('T', 207, unsigned int)
#define TUNSETOFFLOAD _IOW('T', 208, unsigned int)
#define TUNGETIFF _IOR('T', 210, unsigned int)
2. 如果TAP设备加入到namespace的话,重启virtio-user就因为在当前的宿主机找不到TAP设备,会生成一个新的TAP设备,从这个角度看(可能使用brdige或veth-pair更合适。
点击(此处)折叠或打开
3. virtio-user创建的tap设备的mac地址不能指定;
解决方案:通过SIOCSIFHWADDR ioctl命令设置(设置前网卡必须是down的)
4. tap设备重新生成后,是down状态
解决方案:使用SIOCGIFFLAGS ioctl命令
点击(此处)折叠或打开
5. Disable queue导致tap设备close的问题,DPDK高版本已修复
commit 47ac9661b68275321fae0876cce743b9d17671fe
Author: Tiwei Bie
Date: Mon Nov 25 16:14:40 2019 +0800
net/virtio-user: do not close tap when disabling queue pairs
Do not close the tap fds when disabling queue pairs, instead,
we just need to unbind the backend. Otherwise, tap port can be
destroyed unexpectedly.
Fixes: e3b434818bbb ("net/virtio-user: support kernel vhost")
Cc: stable@dpdk.org
Reported-by: Stephen Hemminger
Signed-off-by: Tiwei Bie
Reviewed-by: Maxime Coquelin