Vhost-net利用标准的virtio网络接口悄悄地成为了基于qemu-kvm的虚拟化环境的默认流量卸载机制。这个机制允许通过内核模块执行网络处理,解放了qemu进程,改进了网络性能。
在之前的文章里介绍了网络架构的组成:Post not found: introduction-virtio-networking-and-vhost-net [Introduction to virtio-networking and vhost-net] 同时提供了一个更加详细的解释:[Deep dive into Virtio-networking and vhost-net] 。这篇文章里我们将会提供一个详细的步骤实践性的设置一个架构。大建好之后我们能够检查主要的组件是如何工作的。
这篇文章主要面向研发人员,hackers和其他任何有兴趣学习真实的网络卸载是如何做到的。
在读完这篇文章之后(希望能够在你的PC上重建这个环境),你将会对虚拟化使用到的工具更加熟悉(比如virsh)。你将了解如何建立一个vhost-net环境并且了解到如何检查一个运行的云主机同时测试他的网络性能。
对那些需要快速建立环境并直接逆向工程的人,这里有特别的准备!https://github.com/redhat-virtio-net/virtio-hands-on 能够自动化部署环境的ansible脚本。
Setting things up 因为懒得准备原文中相同的环境,这里我用ZStack常用环境来做替代
Requirements 一个安装了CentOS Linux release 7.6.1810 (Core)的环境 使用root用户(或者有sudo权限的用户) home目录下有25G以上的空闲空间 至少8GB的RAM 首先安装一堆依赖,建议通过ZStack iso安装可以选择Host模式,如果是专家模式需要通过,如下命令安装
1 yum --disablerepo=* --enablerepo=zstack-mn,qemu-kvm-ev-mn install libguestfs-tools qemu-kvm libvirt kernel-tools iperf3 -y
另外根据OS版本下载一个rpm包 https://pkgs.org/download/netperf
对应的Centos 7的包是 https://centos.pkgs.org/7/lux/netperf-2.7.0-1.el7.lux.x86_64.rpm.html
安装virt-install
1 yum --disablerepo=* --enablerepo=ali* install virt-install -y
接下来确保当前用户被加入了libvirt的用户组,做一下修改
1 sudo usermod -a -G libvirt $(whoami)
然后重新登录,并重启libvirt
1 systemctl restart libvirtd
Creating VM 首先下载一个镜像,可以在内部 http://192.168.200.100/mirror/diskimages/ 找一个,这里直接用的一个c76的镜像
1 wget https://archive.fedoraproject.org/pub/archive/fedora/linux/releases/30/Cloud/x86_64/images/Fedora-Cloud-Base-30-1.2.x86_64.qcow2
这是一个封装好的镜像,我们把他作为一个copy,保证以后可以继续使用,执行如下命令,创建并查一下一下image的信息是否和预期一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@host ~]# qemu-img create -f qcow2 -b Fedora-Cloud-Base-30-1.2.x86_64.qcow2 virtio-test1.qcow2 20G Formatting 'virtio-test1.qcow2', fmt=qcow2 size=21474836480 backing_file=centos76.qcow2 cluster_size=65536 lazy_refcounts=off refcount_bits=16 [root@host ~]# qemu-img info virtio-test1.qcow2 image: virtio-test1.qcow2 file format: qcow2 virtual size: 20 GiB (21474836480 bytes) disk size: 196 KiB cluster_size: 65536 backing file: centos76.qcow2 Format specific information: compat: 1.1 lazy refcounts: false refcount bits: 16 corrupt: false
然后清理一下这个操作系统:
1 sudo virt-sysprep --root-password password:password123 --uninstall cloud-init --selinux-relabel -a virtio-test1.qcow2
这个命令会挂载文件系统,并自动做一些基础设置,让这个镜像准备好启动
我们需要把网络连接到虚拟机网络。Libvirt对网络的配置就和管理虚拟机一样,你可以通过xml文件定义一个网络,通过命令行控制他的启动和停止。
举个例子,我们使用一个libvirt提供的叫做‘default’的网络的便利设置用如下的命令定义default
网络启动并检测他已经运行
1 2 3 4 5 6 7 8 [root@host ~]# virsh net-define /usr/share/libvirt/networks/default.xml Network default defined from /usr/share/libvirt/networks/default.xml [root@host ~]# virsh net-start default Network default started [root@host ~]# virsh net-list Name State Autostart Persistent -------------------------------------------- default active no yes
最后我们能够使用virt-install创建虚拟机。这是一个命令行工具创建一堆操作系统需要的定义,并给出一个基础的我们可以自定义的配置:
1 virt-install --import --name virtio-test1 --ram=4096 --vcpus=2 --nographics --accelerate --network network:default,model=virtio --mac 02:ca:fe:fa:ce:01 --debug --wait 0 --console pty --disk /root/virtio-test1.qcow2,bus=virtio
这些命令用的选项特定了vCPUs的数量,还有我们虚拟机的RAM大小并且指定磁盘的路径和云主机要连接的网络。
除开虚拟机通过我们这些选项定义之外,virt-install命令也能够启动虚拟机,所以我们需要列出来:
1 2 3 4 [root@host ~]# virsh list Id Name State ------------------------------ 9 virtio-test1 running
我们的虚拟机在运行了
提醒一下,virsh是一个libvirt的命令行接口,你可以这样启动一个虚拟机:
1 virsh start virtio-test1
进入虚拟机的console:
1 virsh console virtio-test1
停止一个运行的虚拟机:
1 virsh shutdown virtio-test1
删除运行的虚拟机(不要做这个除非你想在创建一遍)
1 2 virsh undefine virtio-test1 Inspecting the guest
就像已经提到的,virt-install命令能够自动使用libvirt创建和启动云主机。每个虚拟机创建都是通过xml文件描述需要模拟的硬件设置并提交给libvirt。我们通过dump配置的内容可以看一下相关的文件。
1 2 3 4 5 6 7 8 9 10 11 12 <devices> ... <interface type='network'> <mac address='02:ca:fe:fa:ce:01'/> <source network='default' bridge='virbr0'/> <target dev='vnet0'/> <model type='virtio'/> <alias name='net0'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> </interface> ... </devices>
我们能够看到一个virtio设备被创建好了,并连接到网络,这堆配置里有virbr0。这个设备有PCI,bus和slot
然后通过console命令进入console
1 virsh console virtio-test1
进入guset安装一些测试依赖:
1 dnf install pciutils iperf3
然后在虚拟机里看一下,实际上虚拟PCI总线上挂了个网络设备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@localhost ~]# lspci -s 0000:00:02.0 -v 00:02.0 Ethernet controller: Red Hat, Inc. Virtio network device Subsystem: Red Hat, Inc. Device 0001 Physical Slot: 2 Flags: bus master, fast devsel, latency 0, IRQ 11 I/O ports at c040 [size=32] Memory at febc0000 (32-bit, non-prefetchable) [size=4K] Memory at febf4000 (64-bit, prefetchable) [size=16K] Expansion ROM at feb80000 [disabled] [size=256K] Capabilities: [98] MSI-X: Enable+ Count=3 Masked- Capabilities: [84] Vendor Specific Information: VirtIO: <unknown> Capabilities: [70] Vendor Specific Information: VirtIO: Notify Capabilities: [60] Vendor Specific Information: VirtIO: DeviceCfg Capabilities: [50] Vendor Specific Information: VirtIO: ISR Capabilities: [40] Vendor Specific Information: VirtIO: CommonCfg Kernel driver in use: virtio-pci
注意:lspci后面跟的地址是根据xml里面的source,bus,slot,function拼接起来的。
除了典型的PCI设备信息之外(比如内存阈和功能之外,驱动还实现了基于PCI的通用virtio功能吗,并创建了一个由virtio_net驱动的网络设备,比如我们可以深入的看一下这个设备
1 2 [root@localhost ~]# readlink /sys/devices/pci0000\:00/0000\:00\:02.0/virtio0/driver ../../../../bus/virtio/drivers/virtio_net
通过命令行readlink,可以看到这个pci设备使用的是virtio_net驱动。
是这个virtio_net驱动控制了操作系统使用的网络接口的创建:
1 2 3 4 5 6 [root@localhost ~]# ip link 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 02:ca:fe:fa:ce:01 brd ff:ff:ff:ff:ff:ff Inspecting the host
我们已经看过guset了,让我们再看看host。注意我们通过‘network’类型配置网络接口的默认行为是使用vhost-net
首先我们看一下vhost-net是否加载
1 2 3 4 5 [root@host ~]# lsmod | grep vhost vhost_net 22507 1 tun 31881 4 vhost_net vhost 48422 1 vhost_net macvtap 22796 1 vhost_net
我们能够检查到QEMU使用了tun,kvm和vhost-net设备,同时通过检查/proc文件系统也能发现这些文件描述符被分给qemu处理了。
1 2 3 4 5 6 7 [root@host ~]# ls -lh /proc/40888/fd | grep '/dev' lrwx------ 1 root root 64 Dec 27 22:55 0 -> /dev/null lrwx------ 1 root root 64 Dec 27 22:55 10 -> /dev/ptmx lrwx------ 1 root root 64 Dec 27 22:55 13 -> /dev/kvm lr-x------ 1 root root 64 Dec 27 22:55 3 -> /dev/urandom lrwx------ 1 root root 64 Dec 27 22:55 35 -> /dev/net/tun lrwx------ 1 root root 64 Dec 27 22:55 37 -> /dev/vhost-net
这意味着qemu进程,不仅打开了kvm设备执行虚拟化操作,同时也创建了一个tun/tap设备和一打开了一个vhost-net设备。当然我们也能够看到一个辅助qemu的vhost内核线程也被创建出来了
1 2 3 4 5 [root@host ~]# ps -ef | grep '\[vhost' root 40056 21741 0 09:53 pts/0 00:00:00 grep --color=auto \[vhost root 40894 2 0 Dec27 ? 00:00:03 [vhost-40888] [root@host ~]# pgrep qemu 40888
这个vhost内核线程的名字就是vhost-$qemu_pid
最后我们可以看到qemu进程创建的tun接口(上面通过/proc找到的)通过bridge把host和guest连在一起了。注意,虽然tap设备被挂在了qemu进程上,实际上进行tap设备读写的是vhost内核线程。
1 2 3 4 5 [root@host ~]# ip -d tuntap virbr0-nic: tap UNKNOWN_FLAGS:800 Attached to processes: vnet0: tap vnet_hdr Attached to processes: qemu-kvm(40888)
OK,所以vhost已经启动并且运行了,qemu也连接到了vhost上。现在我们可以制造一些流量来看看系统的表现。
Generating traffic 如果你已经完成之前的步骤的话,你已经可以尝试通过ip地址从host发送数据到guest或者反过来。举个例子,通过iperf3测试网络性能,注意这些测试方法不是正确的benchmarks,不同的输入比如软硬件版本,不同的网络协议栈参数,会显著影响测试结果。性能吞吐量,或者是特定的使用量基准不在本文的范围之内。
首先检查guest的ip地址,执行 iperf3 server (或者任意你打算用来测试连通性的工具)
1 2 3 4 5 6 7 8 [root@localhost ~]# ip addr ... 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 02:ca:fe:fa:ce:01 brd ff:ff:ff:ff:ff:ff inet 192.168.122.41/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0 valid_lft 2808sec preferred_lft 2808sec inet6 fe80::ca:feff:fefa:ce01/64 scope link valid_lft forever preferred_lft forever
然后再host上运行iperf3的client:
1 2 3 4 5 [root@host ~]# iperf3 -c 192.168.122.41 Connecting to host 192.168.122.41, port 5201 [ ID] Interval Transfer Bandwidth Retr [ 4] 0.00-10.00 sec 34.3 GBytes 29.5 Gbits/sec 1 sender [ 4] 0.00-10.00 sec 34.3 GBytes 29.5 Gbits/sec receiver
在iperf3的输出里我们能看到一个29.5 Gbits/sec 的传输速率(主机这个网络的带宽收到很多以来的影响,不要假定会和你的环境一致)。我们可以通过 -l 参数修改包的大小来测试更多数据平面。
如果我们在iperf3测试过程中运行top命令我们能够看到vhost-$pid内核线程使用了100%的core来做包转发,同时QEMU使用了几乎两倍的cores(可以多试几次,刚好中间观察到一个200% 一个100%,注意创建guest的时候指定的qemu的vcpu数量就是2)
1 2 3 4 5 6 7 8 9 top - 10:07:24 up 7 days, 17:30, 3 users, load average: 1.49, 0.55, 0.28 Tasks: 612 total, 2 running, 610 sleeping, 0 stopped, 0 zombie %Cpu(s): 2.9 us, 6.6 sy, 0.0 ni, 90.4 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st KiB Mem : 13174331+total, 10001235+free, 4672644 used, 27058324 buff/cache KiB Swap: 4194300 total, 4194300 free, 0 used. 12307848+avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 40888 root 20 0 6158388 1.2g 11436 S 200.0 0.9 3:38.41 qemu-kvm 40894 root 20 0 0 0 0 R 100.0 0.0 0:58.46 vhost-40888
要测试延迟,我们使用netperf命令启动一个netperf服务,然后测试延迟。(注:需要在guest里先启动一个netserver,如何在guest安装netperf 2021.12.18 fedora安装netperf)
1 2 3 4 5 6 7 8 9 [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t TCP_RR MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.122.41 () port 0 AF_INET : first burst 0 Local /Remote Socket Size Request Resp. Elapsed Trans. Send Recv Size Size Time Rate bytes Bytes bytes bytes secs. per sec 16384 87380 1 1 30.00 36481.26 16384 131072
计算打开vhost-net host → guest TCP_RR延迟为 1 / 36481.26 = 0.0000274s
就像之前说的,我们进行的不是benchmark或者是吞吐测试。我们只是熟悉一下这个方法。
就像我们看到的vhost-net是被作为默认行为的,因为带来了性能的提升。然而因为我们是来实践学习的,金庸vhost-net来看一下性能有什么不同,通过这个我们将知道qemu做了多“重”的包处理的操作,以及对性能造成了什么影响。
首先停止vm:
1 virsh shutdown virtio-test1
编辑云主机配置:
修改网卡配置为,增加了
1 2 3 4 5 6 7 8 9 10 11 <devices> ... <interface type='network'> <mac address='02:ca:fe:fa:ce:01'/> <source network='default'/> <model type='virtio'/> <driver name='qemu'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> </interface> ... </devices>
修改成功后llibvirt会显示
1 Domain virtio-test1 XML configuration not changed
然后启动云主机:
1 virsh start virtio-test1
我们可以检查一下指向/dev/vhost-net的文件描述符没了
1 2 3 4 5 6 7 [root@host ~]# ls -lh /proc/37518/fd | grep '/dev' lrwx------ 1 root root 64 Dec 28 11:22 0 -> /dev/null lrwx------ 1 root root 64 Dec 28 11:22 10 -> /dev/ptmx lrwx------ 1 root root 64 Dec 28 11:22 13 -> /dev/kvm lr-x------ 1 root root 64 Dec 28 11:22 3 -> /dev/urandom lrwx------ 1 root root 64 Dec 28 11:22 33 -> /dev/net/tun Analyzing the performance impact
如果我们在没有vhost-net的环境重复之前的测试,我们能看到vhost-net的线程没有在运行了
1 2 [root@host ~]# ps -ef | grep '\[vhost' root 9076 23993 0 11:24 pts/0 00:00:00 grep --color=auto \[vhost
我们获得的性能iperf3测试结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@host ~]# iperf3 -c 192.168.122.41 Connecting to host 192.168.122.41, port 5201 [ 4] local 192.168.122.1 port 58628 connected to 192.168.122.41 port 5201 [ ID] Interval Transfer Bandwidth Retr Cwnd [ 4] 0.00-1.00 sec 1.89 GBytes 16.2 Gbits/sec 0 2.08 MBytes [ 4] 1.00-2.00 sec 1.78 GBytes 15.3 Gbits/sec 0 2.19 MBytes [ 4] 2.00-3.00 sec 1.82 GBytes 15.6 Gbits/sec 0 2.37 MBytes [ 4] 3.00-4.00 sec 1.82 GBytes 15.7 Gbits/sec 0 2.47 MBytes [ 4] 4.00-5.00 sec 1.73 GBytes 14.8 Gbits/sec 0 2.61 MBytes [ 4] 5.00-6.00 sec 1.80 GBytes 15.4 Gbits/sec 0 2.64 MBytes [ 4] 6.00-7.00 sec 1.82 GBytes 15.6 Gbits/sec 0 2.64 MBytes [ 4] 7.00-8.00 sec 1.81 GBytes 15.6 Gbits/sec 0 2.69 MBytes [ 4] 8.00-9.00 sec 2.33 GBytes 20.0 Gbits/sec 0 2.81 MBytes [ 4] 9.00-10.00 sec 2.32 GBytes 19.9 Gbits/sec 0 2.93 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bandwidth Retr [ 4] 0.00-10.00 sec 19.1 GBytes 16.4 Gbits/sec 0 sender [ 4] 0.00-10.00 sec 19.1 GBytes 16.4 Gbits/sec receiver iperf Done.
可以看到传输速度从上面的29.5 Gbits/sec掉到了16.4 Gbits/sec
在通过top命令检查,我们可以发现qemu对CPU的使用,峰值会变得很高(这个需要多测试一下,是一个浮动的范围,关掉vhost-net之后大概是150% ~ 260%左右,之前开vhost-net的时候最高也就200%)
1 2 3 4 5 6 7 8 top - 11:27:10 up 7 days, 18:50, 3 users, load average: 0.57, 0.36, 0.29 Tasks: 617 total, 3 running, 614 sleeping, 0 stopped, 0 zombie %Cpu(s): 3.8 us, 4.0 sy, 0.0 ni, 91.7 id, 0.5 wa, 0.0 hi, 0.1 si, 0.0 st KiB Mem : 13174331+total, 10047221+free, 3931432 used, 27339668 buff/cache KiB Swap: 4194300 total, 4194300 free, 0 used. 12381579+avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 37518 root 20 0 6605056 514392 11372 R 242.9 0.4 1:02.22 qemu-kvm
如果我们再比较一下两种不同网络架构下的TCP和UDP的延迟,我们可以看到vhost-net对两种形式的性能都有一致的提升
记录一下关闭vhoset-net之后的测试结果
关闭vhost-net的host → guest TCP_RR测试
1 2 3 4 5 6 7 8 9 [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t TCP_RR MIGRATED TCP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.122.41 () port 0 AF_INET : first burst 0 Local /Remote Socket Size Request Resp. Elapsed Trans. Send Recv Size Size Time Rate bytes Bytes bytes bytes secs. per sec 16384 87380 1 1 30.00 27209.58 计算延迟为 1/27209.58 = 0.0000367s
关闭vhost-net的host → guest UDP_RR测试
1 2 3 4 5 6 7 8 [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t UDP_RR MIGRATED UDP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.122.41 () port 0 AF_INET : first burst 0 Local /Remote Socket Size Request Resp. Elapsed Trans. Send Recv Size Size Time Rate bytes Bytes bytes bytes secs. per sec 212992 212992 1 1 30.00 27516.04
计算延迟为 1/27516.04=0.0000363s
打开vhost-net的host → guest UDP_RR测试
1 2 3 4 5 6 7 8 [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t UDP_RR MIGRATED UDP REQUEST/RESPONSE TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 192.168.122.41 () port 0 AF_INET : first burst 0 Local /Remote Socket Size Request Resp. Elapsed Trans. Send Recv Size Size Time Rate bytes Bytes bytes bytes secs. per sec 212992 212992 1 1 30.00 37681.20
计算延迟为 1/37681.20 = 0.0000265s
同样的测试一下guest→host方向的流量延迟,这里不列代码只记录测试结果
打开vhost-net guest→host TCP_RR 1/36030.14 = 0.0000278s
打开vhost-net guest→host UDP_RR 1/37690.97 = 0.0000265s
关闭vhost-net guest→host TCP_RR 1/26697.53 = 0.0000375s
关闭vhost-net guest→host UDP_RR 1/25850.89 = 0.0000387s
结果如下表
使用strace统计系统调用,关闭vhost-net,测试iperf3的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@host ~]# strace -c -p 37518 # 进程的pid,统计结束后用ctrl+c结束 strace: Process 37518 attached ^Cstrace: Process 37518 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 41.63 1.137491 8 141188 2775 read 28.66 0.783060 6 135331 ioctl 27.16 0.742136 6 121757 writev 2.40 0.065491 8 8380 ppoll 0.13 0.003653 6 594 275 futex 0.01 0.000243 5 50 write 0.00 0.000020 20 1 clone 0.00 0.000012 3 4 sendmsg 0.00 0.000008 4 2 rt_sigprocmask ------ ----------- ----------- --------- --------- ---------------- 100.00 2.732114 407307 3050 total
打开vhost-net之后的iperf3测试时strace qemu的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@host ~]# strace -c -p 27346 strace: Process 27346 attached ^Cstrace: Process 27346 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 99.96 6.819794 131150 52 ppoll 0.02 0.001416 10 136 write 0.01 0.000862 13 66 futex 0.00 0.000341 9 39 read 0.00 0.000083 10 8 sendmsg 0.00 0.000022 22 1 clone 0.00 0.000016 8 2 rt_sigprocmask ------ ----------- ----------- --------- --------- ---------------- 100.00 6.822534 304 total
另外一个好方法就是看qmue发送了多少IOCTLs到KVM。因为每次I/O事件都需要qemu处理,qemu需要发送IOCTL给KVM来切换回VMX noroot的guest模式。我们可以通过strace分析qemu在每个syscall上花费的时间。根据上面两次strace的结果可以看到没打开vhost-net的情况,存在很多ioctl的调用,而打开vhost之后基本上只有ppoll。
Conclusions 在这篇文章里面我们完整了提供了一个创建一个QEMU + vhost-net的虚拟机,检查guest和host来理解这个架构的输入输出。我们也展示了性能是如何变化的。这个系列也到此为止。从 Introduction to virtio-networking and vhost-net 的总览到技术视角深入理解的 Deep dive into Virtio-networking and vhost-net 详细的解释了这些组件,现在展示完了如果配置,希望这些内容呢能够提供足够的资源给IT专家,架构师以及研发人员理解这个技术并开始和他一起工作。
下一个话题将会涉及 Userland networking and DPDK