Hands on vhost-net: Do. Or do not. There is no try

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或者是吞吐测试。我们只是熟悉一下这个方法。

Extra: Disable vhost-net

就像我们看到的vhost-net是被作为默认行为的,因为带来了性能的提升。然而因为我们是来实践学习的,金庸vhost-net来看一下性能有什么不同,通过这个我们将知道qemu做了多“重”的包处理的操作,以及对性能造成了什么影响。

首先停止vm:

1
virsh shutdown virtio-test1

编辑云主机配置:

1
virsh edit 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

deep dive virtio networking and vhost net

在这篇文章里我们将会解释在 Post not found: introduction-virtio-networking-and-vhost-net [introduction] 里描述的vhost-net架构,并通过技术视角来弄清楚所有东西是怎么协同工作的。这系列博客里的这部分内容是为了让你们更好的理解virtio-networking领域是如何将虚拟化和网络连接在一起的

本文主要主要是面向对有兴趣理解上一篇blog提到的vhost-net/virtio-net底层原理的架构师和研发人员

我们将从描述不同的virtio规范的标准组件和共享内存在hypervisor里如何组织的,QEMU如何模拟一个virtio网络设备以及一个guest使用根据virtio规范来实现一个虚拟化驱动来管理并和设备通讯的

在展示过QEMU virtio架构,我们将会分析I/O瓶颈和限制,同时我们将会用host的kernel来解决这个问题,同时最后给出一个宏观的vhost-net的架构

最后,我们将会展示如何通过在它所运行的host上使用OVS(一个开源的虚拟化,SDN,分布式交换机)连接虚拟机到外部网络

希望在读完这篇文章后,你能够理解vhost-net/virtio-net架构是如何工作的,同时能够理解这架构中每一个组件的目标和作用以及数据包是如何被发送和接收的

Previous Concepts

在这个部分我们将会简短的介绍一些帮助你完全理解这篇文章需要知道的概念。对于精通这个问题的人是比较基础的内容,但主要是为了提供一个共同的基础

Networking

让我们从最基础的开始。一个物理网卡(Nic,Network Interface Card)是一个真实的硬件组件,允许物理机连接到外部网络。网卡可以承担一些offload,比如代替CPU执行checksum计算,Segmentation Offload(把一个很大片的数据转换成很多切片,切片大小就是以太网MTU的大小)或者是Large Receive Offload(从CPU角度看,就是把很多数据包合成一个数据包)

另外我们还有tun/tap设备,虚拟化的用户态用来交换数据包的点对点网络设备。交换二层(ethernet frames)数据的叫做tap设备,如果交换 (IP packets)三层数据的就是tun设备。

当tun的kernel模块被加载之后会创建一个特殊的/dev/net/tun设备。进程可以创建tap设备,并发打开这个设备发送一些特殊的ioctl命令给这个设备。新创建的tap设备在。dev文件系统里存在一个名字,并且其他的进程可以打开,并发送接收Ethernet frames

IPC,System programming

Unix sockets是一个在同一台物理机器上做进程间通信的有效方法。在这篇文章涉及的范围内,通讯的服务端监听了文件系统上一个路径下的Unix socket,因此一个客户端(client)可以连接到这个路径使用它。这样,进程间就可以交换消息了。注意,unix sockets也可以用来在进程间交换文件描述符。

eventfd是一个轻量级IPC的实现。虽然Unix sockets允许发送和接收各种消息,eventfd只是一个生产者可以修改,消费者可以读取的整型值。这个使得eventfd更适合用于等待通知机制,而不是传输信息的场景。

这两个IPC系统都为通信中的每个进程公开一个文件描述符。 fcntl调用对该文件描述符执行不同的操作,使它们成为非阻塞的(因此,如果没有可读取的内容,则读取操作会立即返回)。 ioctl调用遵循相同的模式,但实现特定于设备的操作,例如发送命令。

共享内存是我们要介绍的最后一个IPC方法。不同于提供一个进程间通讯的channel,共享内存使用进程的内存区域指向相同的内存页面,因此一个进程覆盖了这部分内存的修改也会影响其他进程之后的读操作。

QEMU and device emulation

QEMU是一个host层的虚拟机模拟器,给guest提供了一系列不同的硬件和设备模型。对host来说,qemu是一个标准Linux可调度的标准进程,有自己的进程内存。在进程里QEMU分配了内存区域来给guest当作物理内存么,同时QEMU还要执行虚拟机的CPU指令

为了在裸机设备上执行I/O操作么,比如存储和网络,CPU必须给物理设备下发特殊的指令并访问特殊的内存区域,比如这个设备被映射到的内存区域

当guest访问这些内存区域的时候,控制面就返回到了执行设备透明模拟的guest的QEMU里

KVM

Kernel-based Virtual Machine(KVM)是一个Linux内置的开源虚拟化技术。它为虚拟化软件提供硬件辅助,利用内置CPU虚拟化技术减少虚拟化开销(缓存、I/O、内存),提高安全性。

使用KVM,QEMU可以创建一个虚拟机,该虚拟机具有处理器识别的虚拟 CPU (vCPU),运行native-speed指令。当特殊的比如需要和设备或者特殊内存交互的命令到达KVM的时候,vCPU会停下来,然后通知QEMU暂停的原因,然后hypervisor就会对这个事件作出反应了。

在常规的KVM操作里,hyervisor会打开/dev/kvm这个设备,然后通过ioctl和他通讯调用它创建虚拟机,增加CPU,增加内存(qemu分配,但是虚拟机看来是物理内存),发送CPU中断(外部设备引发的),等等。举个例子,某一个ioctl的命令运行了KVM vCPU,阻塞了QEMU而且vCPU需要等到它找到了需要硬件辅助的命令。那时ioctl就会返回(这个叫做vmexit)同时QEMU也能知道这个exit的原因(比如offending instruction)。

对一些特别的内存区域,KVM有类似的访问方式,把内存区域标记为只读或者完全不映射,通过KVM_EXIT_MMIO造成一个vmexit。

The virtio specification

Virtio specification: devices and drivers

Virtio是一个虚拟机数据I/O的一个开放规范,提供了简单、有效、标准并且可拓展的虚拟设备机制,而不是固定在在每个环境或每个操作系统的机制。它主要基于guest可以和host共享内存以进行I/O来实现。

virtio规范基于两个元素:设备和驱动。在最经典的实现里,hypervisor通过一系列传输方法试将virtio设备暴露给guest。设计上他们在虚拟机内看起来是物理设备。

嘴常见的传输方法就是PCI或者PCIe总线。然而,这些设备在一些预定义好的guest内存地址是可用的(MMIO transport)。这些设备可以完全在没有物理设备或者是物理的兼容性接口的情况下被虚拟出来。

最典型最简单的暴露virtio设备的方法是通过PCI端口因为我们可以借用PCI已经是一个成熟并且在QEMU和Linux驱动自持的很好的协议。实际的PCI硬件通过特殊的物理内存地址范围和/或特殊的处理器指令暴露配置空间(比如,驱动可以通过这些内存范围读或者写设备的寄存器)。在虚拟机世界里,hypervisor能捕获访问这些内存范围并且执行设备模拟,暴露和真实设备相同的内存布局,并提供相同的返回。virtio标准也定义了PCI配置空间的布局,因此实现它是很简单的。

当guest驱动并使用PCI/PCI自动发现机制的时候,virtio设备通过PCI vendor ID和他们的PCI device ID标识自己。guest kernel通过使用这些标记来知道使用哪些驱动来处理这些设备。特别是,linux内核已经包含了virtio驱动程序。

virtio驱动必须能够分配给hypervisor和设备都能读写的内存区域,比如通过共享内存。我们把数据平面作为使用内存区域进行数据通讯的一个部分,同时控制平面主要是来配置他们。我们会在后续的文章里提供一个更深层次的virtio协议的实现细节以及内存布局。

virtio内核驱动共享了一个通用的传输专用接口(virtio-pci),并被用于实际的传输以及设备实现(比如virtio-net,virtio-scsi)

Virtio specification: virtqueues

Virtqueues是virtio设备的批量数据传输机制。每个设备可以有0个或者多个virtqueue link。它由guest分配的host可以访问并且可以读或者写的缓存队列组成。补充一下,virtio标准也定义了双向的通知:

  • Available Buffer Notification:使用驱动来通知buffer就绪并可以被设备处理
  • Used Buffer Notification:被设备使用来通知已经处理完了一些buffers

在PCI的场景,guest通过写一个特殊的内存地址发送available buffer notification,然后设备(这个场景里是QEMU)使用vCPU中断来发送used buffer notification。

virtio规范也允许notifications动态的启用或者停用。这种情况下设备和驱动可以批量的缓存通知或者主动的向virtqueues请求新的缓存。这个方法更适合高流量的场景。

总结一下,virtio驱动接口暴露了一下内容:

  • Device’s feature bits(设备和guest需要协商的部分)
  • Status bits(状态位)
  • Configuration space(包含设备特殊信息,比如MAC地址)
  • Notification system(配置变更,缓存可用,缓存使用)
  • Zero or more virtqueues
  • Transport specific interface to the device

Networking with virtio: qemu implementation

Figure 1: virtio-net on qemu

virtio网络设备是一个虚拟以太网卡,支持TX/RX(发送,接收)多队列。空的缓存被放在N virtqueues里用来接收数据包,往外送的数据包责备放到另外一个N virtqueues里面等待发送。另一个virtqueue被用于数据面之外的驱动和设备的通讯,比如控制高级过滤特性,设置mac地址,或者是一堆活跃的队列。像物理网卡一样,virtio设备支持很多特性比如offloading,能够让真实的host设备来处理。

为了发送数据包,驱动会发送给设备一个缓存包括metadata信息比如在要发送的packet frame上带有的数据包期望的offloading。驱动能够将这个缓存拆分成不同的条目,比如可以把这个metadata的header从packet frame上分离出来。

这些缓存被驱动管理,被映射给设备。因此这个情况下我们可以说这个设备实际上在hypervisor里(结合前面提到hypervisor)因为qemu能够访问所有的guest内存,所以有能力知道缓存的位置并能够对他们进行读写。

下面的流程图表示了virtio-net设备配置和使用virtio-net驱动发送数据包通过PCI和virtio-net设备通讯。在组装好要发送的数据包之后,出发了一个available buffer notification,把控制返回给QEMU然后它就能够把包通过TAP设备送出去

QEMU然后通知guest这些缓存操作执行完成了(读或者写)并且它通过把这些数据放到virtqueue然后发送一个used notification event来触发guest的vCPU中断。

接收数据包的过程和发送类似。唯一不同的是在接收数据包的场合,空缓存会提前被guest分配出来然后提供给设备一个可用缓存保证它能够把即将收到的数据写进去。

Figure 2: Qemu virtio sending buffer flow diagram

Vhost-net

vhost-net是一个内核驱动,实现了vhost协议的处理侧,用来实现一个高效的数据面。比如数据包转发,在这个实现里,qemu和vhost-net内核驱动(handler)使用ioctls来交换vhost消息和一大批叫做irqfd的类似eventfd的文件描述符然后ioeventfs被用来和guest交换通知

当vhost-net驱动被加载的时候,它会在/dev/vhost-net暴露一个字符设备。当qemu启用了vhost-net并启动后,它会打开这个设备并且一些ioctl调用初始化vhost-net实例。这是把vhost-net和hypervisor联系起来必不可少的步骤,准备virtio特性检查,然后给guest提供映射到vhost-net驱动的内存。

在初始化的过程里vhost-net驱动创建了一个内核线程叫做vhost-$pid这个$pid就是hypervisor(也就是qemu进程)的pid。这个线程也被叫做“vhost worker thread”

tag设备仍然被用于VM和host的通讯但是现在这个worker thread处理了这些I/O事件,比如它会不断poll驱动的通知或者是tap的事件并且做数据的转发。

Qemu分配了一个eventfd然后注册了到了vhost和KVM来实现通知的传递。vhost-pid内核线程poll这个eventfd,当guest写某一个特殊地址的时候KVM则会写这个eventfd。这个机制被叫做ioeventfd。用这个方法,对一个特别的guest内存地址简单的读写操作不需要再穿过钢轨的QEMU进程唤醒同时能够被直接路由到vhost worker thread。这也提供了异步的优势,也不再需要vCPU停下来(因此没必要立刻做上下文切换)

另一方面,qemu分配了另外的eventfd并再次注册他们到KVM和vhost来处理vCPU的直接中断注入。这个机制又叫irqfd,他们允许host通过写irqfd来注入vCPU中断到guest。同时也具有异步特性,不需要立刻做上下文切换

注意这些改动对virtio包处理后端对guest来说是完全透明的,guest仍然使用的是标准的virtio接口

下面的图展示了qemu数据路径的offloading到vhost-net内核驱动:

Figure 3: vhost-net block diagram

Figure 4: vhost-net sending buffer diagram flow

Communication with the outside world

guest能够和host通过tap设备通讯,然而还有一个遗留的问题就是guest如何和同一个host伤的其他vm或者是其他host上的vm通讯(使用internet通讯)

我们可以内核网络协议栈提供的转发和路由的机制,比如标准的Linux bridges。然而一个更加高级的解决方式就是一个全虚拟化的分发,管理交换机比如Open Virtual Switch

就像在总览篇说的一样,OVS的数据路径在这个场景里是作为内核模块运行的,ovs-switchd是一个用户态的控制管理守护进程然后ovsdb-server是一个转发数据库。

就像图上画的一样,OVS的数据路径在kernel运行然后在物理网卡和虚拟TAP设备间做包的forward:

Figure 5: Introduce OVS

总结

在这篇文章里,我们展示了virtio-net的架构是如何工作的,我们对每一个步骤做了详细的解剖并解释了每个组件的功能。

我们开始解释了默认的qemu IO设备如何通过提供给guest一个开放virtio标准的实现来运作的。我们接下来分析了guest如何和这些设备通过virtio驱动能够发送和接收数据包,发送和接收通知的

然后我们评价了在数据路径中有qemu的情况下需要切换上下文,然后展示了如何在host上使用vhost协议通过vhost-net内核驱动offload这些任务。我们也能够覆盖virtio通知在这个新设计下是如何工作的

最后我们展示了如何将VM连接到外部的非自己所在的host的世界。

在接下来的文章里我们将会提供一个使用之前学习到的解决方案里的不同组件完成关于vhost-net/virtio-net的架构实现。

如果你因为什么原因跳过了那篇文章,我们将在下一篇文章介绍一个新的用户态的使用DPDK的vhost处理协议。我们会列举他的优点,使用DPDK和用户态的观点,我们将建立第二个符合这些概念的架构。

Introduction to virtio-networking and vhost-net

背景

因为最近在看vdpa-blk,查资料的时候发现了红帽在19年写了一系列文章来介绍virtio网络入门的感觉和存储的加速思路差不多就拿来整理顺便做一下翻译。原文链接已附在文尾。

Introduction

virtio被开发作为云主机简化访问类似块设备和网络适配器的标准开放api。其中的virtio-net是一个虚拟以太网卡,是目前virtio支持过的最复杂的设备。本文会从virtio-networking的架构提供一个高级的基于建立host kernel和VM guest kernel的接口的解决方案。将介绍包括 KVM、qemu 和 libvirt 在内的基本组件,同时结合virtio spec和vhost协议以及Open vSwitch来讲云主机连接到外部世界。接下来描述的基于vhost-net/virtio-net的架构是许多virtio-networking架构中的第一个,这些架构将在一系列帖子中介绍,这些帖子因其性能、应用程序的易用性和实际部署而异。

读完这篇文章之后你应该能够清楚的了解提到的名词然后能够知道云主机中的应用如何发送数据包到其他云主机运行的应用以及如何发送到外部网络。这些术语也会作为下一片文章的基础。

VIrtio basic building blocks

guest VM或者说guest是被安装,执行并托管在物理机器上的虚拟机。这个提供托管guest VM并提供资源功能的机器就叫做host。guest通过hypervisor拥有分离的运行在host操作系统之上的操作系统。举个例子,host会提供虚拟网卡给虚拟机,guest机器则把它当作一个真实的网卡使用,虽然事实上它只是个虚拟网卡。

下列的组件创建了接下来virtio需要提供链接的虚拟环境

  1. KVM - kernel based virtiual machine允许Linux提供hypervisor的功能支持运行多个隔离的虚拟环境又叫做guests。KVM提供了Linux作为hypervisor的基本功能。比如内存管理,调度,网络栈等都包含在了Linux kernel里。虚拟机通常是通过标准Linux调度去调度的进程,同时上面加在了一些专用的虚拟硬件比如网络适配器。
  2. QEMU - A hosted virtual machine monitor。通过模拟给虚拟机提供一系列硬件和设备模型。QEMU 可以与 KVM 一起使用,利用硬件扩展以接近本机的速度运行虚拟机。guest通过qemu command line interface(CLI)运行。CLI 供了为QEMU指定所有必要配置选项的能力
  3. Libvirt - 一个翻译xml格式配置到qemu CLI调用的接口。它提供了一个admin守护进程来配置子进程比如qemu,因此qemu不需要root特权。例如,当Openstack Nova想要启动一个VM时,它使用libvirt通过为每个VM调用一个qemu进程来为每个VM启动一个qemu进程。

下图显示了这三个组件如何结合在一起:

host和guest都包含了kernel space和user space。可以根据图看到,KVM运行在host的内核态,而libvirt运行在host的用户态

guest虚拟机运行在qemu进程里,只是一个简单的运行在host用户态的继承并且和libvirt(用户态应用)以及KVM(内核态)通信

每一个虚拟机都会创建一个qemu进程,所以如果你创建N个虚拟机,你就会有N个虚拟机qemu进程,并且libvirt会和每一个进程通信

Virtio spec and the vhost protocol

当讨论vritio-networking的时候我们可以分开两层讨论

  1. 控制平面 - 用于host和guest之间能力的协商,用于建立和终止数据平面
  2. 数据平面 - 用于host和guest之间传输实际的数据(packets)

区分清楚这两个层级是很重要的,因为这些层级需要实现的要求不一样(比如性能)并且本文和后续文章也会展示很多不同的实现。

不过在以后的架构里最基础的要求,数据平面要求有尽可能好的性能来快速移动数据包,同时控制平面要求尽可能的有很好的拓展性来支持不同的设备和厂商

就像在开头提到的,virtio是被开发出来作为guest访问host设备的接口的。我们可以把virtio分成两部分:

  1. virtio spec - The virtio spec根据OASIS提到的。定义了如何创建一个介于host和guest之间的控制平面和数据平面。举个例子数据平面需要由缓存和环状布局组成,设置在spec里面详细说明的
  2. vhost protocol - 一个为了提升性能允许virtio数据平面的实现被下放到其他元素(用户态进程或者内核模块)

qemu进程中实现的virtio的控制平面是基于virtio spec的,然而数据平面不是。因此问题转变为了为什么数据平面没有像类似virtio spec中定义的那样实现在qemu进程里的?

答案是:性能问题

如果简单的按照virtio spec在qemu里实现了数据平面,我们就需要为每个从kernel进入guest的包切换上下文了反之亦然。这是一个代价很大的操作,增加了延迟同时也占用了更多的进程时间(提醒一下当前qemu只是一个Linux进程),所以我们想要尽可能的避免这个问题。

这就是为什么vhost protocol出现了,让我们能够去实现一个数据平面直接从host kernel到guest而不经过qemu进程

不过vhost协议本身仅描述了如何建立数据平面。任何实现被要求实现环状布局来描述数据缓存(包括host和guest的)以及真实发送/接受的数据包

就像之后的章节要介绍的内容那样,vhost协议可以被实现成内核态(vhost-net)或者是用户态(vhost-user)。本文描述的vhost-net/virtio-net架构专注于内核态的又被叫做vhost-net的实现

The vhost-net/virtio-net architecture

当我们谈论关于virtio接口的时候我们有一个后端组件和一个前端组件

  • 后端组件是host侧的virtio接口
  • 前端组件是guest侧的virtio接口

在vhost-net/virtio-net架构的组件里是这样的

  • vhost-net是运行在host内核空间
  • virtio-net运行在guest的内核空间

下面的图展示了宏观上virtio的前端和后端是如何对应的:

有一些点需要说明:

  • 因为vhost-net和virtio-net都运行在host和guest的内核空间,我们也叫他们驱动(drivers)所以如果有人“vhost-net-driver”不太要感觉奇怪指的是同一个东西
  • 在后端和前端之间有分离的控制平面和数据平面。就像前面解释过的,控制平面简单给vhost-net内核模块实现了virtio spec同时qemu进程进入guest的通讯最终到了virtio-net。Vhost-net使用了vhost协议建立了框架实现了使用共享内存的数据平面的host-guest内核包的直接转发的功能

实际上数据面通讯 接收 receive(RX)和发送 transmit(TX)是通过vCPU专用队列完成的

每一个guest都能根据vCPU的数量关联起一堆RX/TX队列,这个队列对应每一个CPU。举一个更精确的例子比如有4 vCPUs会像这样(去掉控制平面简化一下这个图):

Virtio-networking and OVS

目前我们已经描述了如何guest能够通过virtio-networking接口把包送到host内核。为了转发这些包到其他运行在同一台host或者其他host的guest上,我们使用OVS

OVS是一个软交换机,能够给提供内核内包转发。它由用户态和内核态两个部分组成

  • 用户空间 - 包括一个数据库(ovsdb-server)和一个OVS deamon来管理和控制交换机(ovs-switched)
  • 内核空间 - 包括ovs内核模块主要负责数据通路和转发平面

OVS controller同时和数据库服务以及内核转发面通讯。为了使得包能够出入OVS我们使用了Linux端口。在我们的场景里,我们使用了一个端口连接OVS内核转发平面到一个物理网卡同时另外一个端口连接到vhost-net的后端。

注意我们是在描述简化过的场景。实际上可能会有多个网卡连接到OVS多个端口和虚拟机运行,。因此也需要很多端口连接到vhost-net的后端。

接着讲virtio-networking下面的图展示了OVS如何连接到virtio:

请注意所提到的用于将OVS连接到主机外部和vhost-net以及从vhost-net连接到virtio-ne 和在VM中运行的应用程序的端口。

这个总结了vhost-net/virio-net的基于host内核,guest内核和内核态OVS的架构的大概内容

Summary

在这篇文章中,我们已经触及了 virtio-networking 生态系统的表面,向您介绍了 virtio-networking 使用的虚拟化和网络的基本构建组件。我们简短的涵盖了virtio spec和vhost protocol,回顾了用于实现 virtio 接口的前端和后端架构,并带您了解了 vhost-net(host内核)与 virtio-net(guest内核)通信的 vhost-net/virtio-net 架构。

我们在尝试解释事物时遇到的一个基本挑战是术语历史问题。例如,virtio-net既指virtio规范中的virtio网络设备实现,也指vhost-net/virtio-net架构中描述的guest内核前端。我们试图通过在上下文解释术语并使用virtio-net仅描述guest内核前端来解决这个问题。

正如将在后面的文章中解释的那样,基于使用DPDK和不同硬件offload技术的virtio规范网络设备还有其他实现,这些都在virtio-networking的保护伞下。

接下来的两篇文章会提供一个更加深入的关于vhost-net/virtio-net架构的理解。一篇文章将面向架构师,提供对vhost-net/virtio-net的技术深入研究,并解释数据平面和控制平面在实践中是如何实现的。面向开发人员的另一篇文章将是一个动手部分,包括Ansible脚本,以实现对vhost-net/virtio-net架构的试验。

附录

原文链接: introduction-virtio-networking-and-vhost-net