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 | [root@host ~]# qemu-img create -f qcow2 -b Fedora-Cloud-Base-30-1.2.x86_64.qcow2 virtio-test1.qcow2 20G |
然后清理一下这个操作系统:
1 | sudo virt-sysprep --root-password password:password123 --uninstall cloud-init --selinux-relabel -a virtio-test1.qcow2 |
这个命令会挂载文件系统,并自动做一些基础设置,让这个镜像准备好启动
我们需要把网络连接到虚拟机网络。Libvirt对网络的配置就和管理虚拟机一样,你可以通过xml文件定义一个网络,通过命令行控制他的启动和停止。
举个例子,我们使用一个libvirt提供的叫做‘default’的网络的便利设置用如下的命令定义default网络启动并检测他已经运行
1 | [root@host ~]# virsh net-define /usr/share/libvirt/networks/default.xml |
最后我们能够使用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 | [root@host ~]# virsh list |
我们的虚拟机在运行了
提醒一下,virsh是一个libvirt的命令行接口,你可以这样启动一个虚拟机:
1 | virsh start virtio-test1 |
进入虚拟机的console:
1 | virsh console virtio-test1 |
停止一个运行的虚拟机:
1 | virsh shutdown virtio-test1 |
删除运行的虚拟机(不要做这个除非你想在创建一遍)
1 | virsh undefine virtio-test1 |
就像已经提到的,virt-install命令能够自动使用libvirt创建和启动云主机。每个虚拟机创建都是通过xml文件描述需要模拟的硬件设置并提交给libvirt。我们通过dump配置的内容可以看一下相关的文件。
1 | <devices> |
我们能够看到一个virtio设备被创建好了,并连接到网络,这堆配置里有virbr0。这个设备有PCI,bus和slot
然后通过console命令进入console
1 | virsh console virtio-test1 |
进入guset安装一些测试依赖:
1 | dnf install pciutils iperf3 |
然后在虚拟机里看一下,实际上虚拟PCI总线上挂了个网络设备
1 | [root@localhost ~]# lspci -s 0000:00:02.0 -v |
注意:lspci后面跟的地址是根据xml里面的source,bus,slot,function拼接起来的。
除了典型的PCI设备信息之外(比如内存阈和功能之外,驱动还实现了基于PCI的通用virtio功能吗,并创建了一个由virtio_net驱动的网络设备,比如我们可以深入的看一下这个设备
1 | [root@localhost ~]# readlink /sys/devices/pci0000\:00/0000\:00\:02.0/virtio0/driver |
通过命令行readlink,可以看到这个pci设备使用的是virtio_net驱动。
是这个virtio_net驱动控制了操作系统使用的网络接口的创建:
1 | [root@localhost ~]# ip link |
我们已经看过guset了,让我们再看看host。注意我们通过‘network’类型配置网络接口的默认行为是使用vhost-net
首先我们看一下vhost-net是否加载
1 | [root@host ~]# lsmod | grep vhost |
我们能够检查到QEMU使用了tun,kvm和vhost-net设备,同时通过检查/proc文件系统也能发现这些文件描述符被分给qemu处理了。
1 | [root@host ~]# ls -lh /proc/40888/fd | grep '/dev' |
这意味着qemu进程,不仅打开了kvm设备执行虚拟化操作,同时也创建了一个tun/tap设备和一打开了一个vhost-net设备。当然我们也能够看到一个辅助qemu的vhost内核线程也被创建出来了
1 | [root@host ~]# ps -ef | grep '\[vhost' |
这个vhost内核线程的名字就是vhost-$qemu_pid
最后我们可以看到qemu进程创建的tun接口(上面通过/proc找到的)通过bridge把host和guest连在一起了。注意,虽然tap设备被挂在了qemu进程上,实际上进行tap设备读写的是vhost内核线程。
1 | [root@host ~]# ip -d tuntap |
OK,所以vhost已经启动并且运行了,qemu也连接到了vhost上。现在我们可以制造一些流量来看看系统的表现。
Generating traffic
如果你已经完成之前的步骤的话,你已经可以尝试通过ip地址从host发送数据到guest或者反过来。举个例子,通过iperf3测试网络性能,注意这些测试方法不是正确的benchmarks,不同的输入比如软硬件版本,不同的网络协议栈参数,会显著影响测试结果。性能吞吐量,或者是特定的使用量基准不在本文的范围之内。
首先检查guest的ip地址,执行 iperf3 server (或者任意你打算用来测试连通性的工具)
1 | [root@localhost ~]# ip addr |
然后再host上运行iperf3的client:
1 | [root@host ~]# iperf3 -c 192.168.122.41 |
在iperf3的输出里我们能看到一个29.5 Gbits/sec 的传输速率(主机这个网络的带宽收到很多以来的影响,不要假定会和你的环境一致)。我们可以通过 -l 参数修改包的大小来测试更多数据平面。
如果我们在iperf3测试过程中运行top命令我们能够看到vhost-$pid内核线程使用了100%的core来做包转发,同时QEMU使用了几乎两倍的cores(可以多试几次,刚好中间观察到一个200% 一个100%,注意创建guest的时候指定的qemu的vcpu数量就是2)
1 | top - 10:07:24 up 7 days, 17:30, 3 users, load average: 1.49, 0.55, 0.28 |
要测试延迟,我们使用netperf命令启动一个netperf服务,然后测试延迟。(注:需要在guest里先启动一个netserver,如何在guest安装netperf 2021.12.18 fedora安装netperf)
1 | [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t TCP_RR |
计算打开vhost-net host → guest TCP_RR延迟为 1 / 36481.26 = 0.0000274s
就像之前说的,我们进行的不是benchmark或者是吞吐测试。我们只是熟悉一下这个方法。
Extra: Disable vhost-net
就像我们看到的vhost-net是被作为默认行为的,因为带来了性能的提升。然而因为我们是来实践学习的,金庸vhost-net来看一下性能有什么不同,通过这个我们将知道qemu做了多“重”的包处理的操作,以及对性能造成了什么影响。
首先停止vm:
1 | virsh shutdown virtio-test1 |
编辑云主机配置:
1 | virsh edit virtio-test1 |
修改网卡配置为,增加了
1 | <devices> |
修改成功后llibvirt会显示
1 | Domain virtio-test1 XML configuration not changed |
然后启动云主机:
1 | virsh start virtio-test1 |
我们可以检查一下指向/dev/vhost-net的文件描述符没了
1 | [root@host ~]# ls -lh /proc/37518/fd | grep '/dev' |
如果我们在没有vhost-net的环境重复之前的测试,我们能看到vhost-net的线程没有在运行了
1 | [root@host ~]# ps -ef | grep '\[vhost' |
我们获得的性能iperf3测试结果为:
1 | [root@host ~]# iperf3 -c 192.168.122.41 |
可以看到传输速度从上面的29.5 Gbits/sec掉到了16.4 Gbits/sec
在通过top命令检查,我们可以发现qemu对CPU的使用,峰值会变得很高(这个需要多测试一下,是一个浮动的范围,关掉vhost-net之后大概是150% ~ 260%左右,之前开vhost-net的时候最高也就200%)
1 | top - 11:27:10 up 7 days, 18:50, 3 users, load average: 0.57, 0.36, 0.29 |
如果我们再比较一下两种不同网络架构下的TCP和UDP的延迟,我们可以看到vhost-net对两种形式的性能都有一致的提升
记录一下关闭vhoset-net之后的测试结果
关闭vhost-net的host → guest TCP_RR测试
1 | [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t TCP_RR |
关闭vhost-net的host → guest UDP_RR测试
1 | [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t UDP_RR |
计算延迟为 1/27516.04=0.0000363s
打开vhost-net的host → guest UDP_RR测试
1 | [root@host ~]# netperf -l 30 -H 192.168.122.41 -p 16604 -t UDP_RR |
计算延迟为 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 | [root@host ~]# strace -c -p 37518 # 进程的pid,统计结束后用ctrl+c结束 |
打开vhost-net之后的iperf3测试时strace qemu的结果
1 | [root@host ~]# strace -c -p 27346 |
另外一个好方法就是看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