KVM 是一种让 Linux 变成虚拟机监控程序的技术,它可以让多个虚拟机在同一台物理机上运行。为了让虚拟机正常运行,KVM 需要一些操作系统的组件,如内存管理器、进程调度程序、输入/输出堆栈、设备驱动程序、安全管理器和网络堆栈等,这些组件都包含在 KVM 中。每个虚拟机都像一个 Linux 进程一样运行,使用虚拟的硬件设备,如网卡、图形适配器、CPU、内存和磁盘等。

初始化和安装软件依赖

由于 KVM 和 Linux 内核紧密相关,所以为了保持文档的一致性,我们这里使用 5.15 版本的 Ubuntu 内核

$ uname -a
Linux sxueck-server 5.15.0-1034-realtime #37-Ubuntu SMP PREEMPT_RT Wed Mar 1 20:50:08 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
$ sudo apt update
$ sudo apt upgrade

$ sudo apt -y install bridge-utils cpu-checker libvirt-clients libvirt-daemon qemu qemu-kvm genisoimage libguestfs-tools linux-tools-common
$ echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 启用hugepages

在进行任务之前,我们需要对前置条件进行检查

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

上面代表了当前的系统能良好支持 KVM 环境

启动一个虚拟系统

在进行了最简单的安装依赖后,我们已经具备了简单运行虚拟系统的能力。

在这里,我们并不直接使用普通的 Ubuntu Server 版本,而是选择更合适的 Ubuntu Cloud Server 版本,它是专门为云环境设计和优化的 Ubuntu 镜像,具备了快速启动和扩展的能力,不仅预装了 Cloud-init 等工具,同时也使用特别版本内核以适应云环境的特殊要求,如虚拟化性能、内存管理、网络性能和安全性等。

在启动之前,我们需要使用管理工具进行规划依赖资源,例如网络和磁盘等,注意这里的目录所在磁盘如果不够大,需要提前进行分配重挂载。

$ sudo apt install virt-manager
$ sudo mkdir -p /var/lib/libvirt/images
$ sudo virsh pool-define-as --name vm-images --type dir --target /var/lib/libvirt/images
Pool vm-images defined
$ sudo virsh pool-autostart vm-images
Pool vm-images marked as autostarted
$ sudo virsh pool-start vm-images
Pool vm-images started

启动 virt-manager 后,可以通过它来创建、启动、关闭、暂停和管理虚拟机,以及对虚拟机进行监控和管理。

这将创建一个名为 vm-images 的存储池,将虚拟机镜像存储在 /var/lib/libvirt/images 目录中,并自动启动和挂载该存储池。

有了存储池后就可以创建硬盘了

$ sudo qemu-img create -f qcow2 /var/lib/libvirt/images/ubuntu.qcow2 30G
Formatting '/var/lib/libvirt/images/ubuntucloud.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=32212254720 lazy_refcounts=off refcount_bits=16

这里其实有两种选择:raw 和 qcow2 格式

raw 格式是一个裸的块设备,和普通的块设备操作一样。qcow2 是虚拟的块设备,存在结构的定义。raw 设备读写性能会比 qcow2 性能好,但 qcow2 也有减少存储空间,快照,加密等拓展功能。两者都支持空洞特性,即虽然声明了例如 100G,但物理占用还是看实际的内容大小。

下面先试试普通 UbuntuServer 的创建,以了解一下基本的步骤

$ sudo virt-install \
--name ubuntu \
--ram 1024 \
--disk path=/var/lib/libvirt/images/ubuntu.qcow2,format=qcow2,bus=virtio \
--vcpus 1 \
--os-variant ubuntu-lts-latest \
--network bridge=br0 \
--graphics none \
--console pty,target_type=serial \
--location 'https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/kinetic/ubuntu-22.10-live-server-amd64.iso' \
--extra-args 'console=ttyS0'

WARNING  Requested memory 1024 MiB is less than the recommended 2048 MiB for OS ubuntu22.04
Starting install...
ERROR    Couldn't find kernel for install tree.
Domain installation does not appear to have been successful.
If it was, you can restart your domain by running:
  virsh --connect qemu:///system start ubuntucloud
otherwise, please restart your installation.

上面参数中,我们挑几个较为难理解的来理解

  • os-variant:该参数是用于指定虚拟机操作系统的类型和版本的选项,它告诉 KVM 使用何种方式去支持和优化虚拟机操作系统,以优化虚拟机性能和兼容性。如果没有指定该选项,KVM 会尝试自动检测虚拟机操作系统类型和版本,但这可能会导致一些性能和兼容性问题。详细的操作系统版本可以通过 virt-install --osinfo list 命令进行查看。
  • console pty,target_type=serial:选项指定了虚拟机的控制台连接方式和类型。其中,pty表示使用伪终端设备作为虚拟机的控制台设备,target_type=serial 表示将该伪终端设备配置为串行设备,作为虚拟机的串行控制台。通过这种方式,虚拟机的控制台可以通过一个串行端口与 KVM 主机进行连接。在虚拟机中,可以通过串口接收和发送控制台输出和输入。这种方式可以方便地进行虚拟机的操作和管理,也可以方便地进行虚拟机的自动化部署和配置。

这种对于含桌面版本的宿主机最实用,因为可以通过 VNC 进行容器配置

下面是 Cloud 版本的创建

$ wget https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img

# 验证镜像信息
$ qemu-img info ubuntu-22.04-server-cloudimg-amd64.img
image: ubuntu-22.04-server-cloudimg-amd64.img
file format: qcow2
virtual size: 2.2 GiB (2361393152 bytes)
disk size: 657 MiB
cluster_size: 65536
Format specific information:
    compat: 0.10
    compression type: zlib
    refcount bits: 16

$ sudo mkdir /var/lib/libvirt/images/base/ -p
$ sudo cp ubuntu-22.04-server-cloudimg-amd64.img /var/lib/libvirt/images/base/ubuntu_cloudimages.qcow2
$ sudo mkdir /var/lib/libvirt/images/ubuntu_cloud_instance1
$ sudo qemu-img create -f qcow2 -F qcow2 -o backing_file=/var/lib/libvirt/images/base/ubuntu_cloudimages.qcow2 /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2
Formatting '/var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2', 
fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=2361393152 
backing_file=/var/lib/libvirt/images/base/ubuntu_cloudimages.qcow2 backing_fmt=qcow2 lazy_refcounts=off refcount_bits=16

# 再次进行新磁盘的验证
$ sudo qemu-img info /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2
file format: qcow2
virtual size: 2.2 GiB (2361393152 bytes)
disk size: 196 KiB
cluster_size: 65536
backing file: /var/lib/libvirt/images/base/ubuntu_cloudimages.qcow2
backing file format: qcow2
Format specific information:
    compat: 1.1
    compression type: zlib
    lazy refcounts: false
    refcount bits: 16
    corrupt: false
    extended l2: false

# 目前的磁盘仅为 2G,我们将其扩容到20G
$ sudo qemu-img resize /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2 20G
Image resized.
$ sudo qemu-img info /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2
image: /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2
file format: qcow2
virtual size: 20 GiB (21474836480 bytes)
disk size: 200 KiB
cluster_size: 65536
backing file: /var/lib/libvirt/images/base/ubuntu_cloudimages.qcow2
backing file format: qcow2
Format specific information:
    compat: 1.1
    compression type: zlib
    lazy refcounts: false
    refcount bits: 16
    corrupt: false
    extended l2: false

完成了系统磁盘的创建后,我们需要配置 Cloud-init 功能。Cloud-init 是一种用于自定义 Linux 虚拟机的方法,它可以在虚拟机启动时自动运行脚本以完成一些预配置的工作,例如配置 SSH 登录等一系列信息。在配置 Cloud-init 时,我们需要编辑 cloud.cfg 文件,该文件包含各种配置选项,例如用户、密码、公钥、主机名、时区、DNS、NTP 和网络等。除了 cloud.cfg 文件,还可以通过 user-data 文件和 meta-data 文件来配置 Cloud-init 功能,它们可以包含更为详细的自定义脚本和元数据信息,以满足更加复杂的需求。

# 创建 Meta-data
$ cat >meta-data <<EOF
local-hostname: instance-1
EOF

$ export PUB_KEY=$(cat ~/.ssh/id_rsa.pub)
$ cat >user-data <<EOF
#cloud-config
users:
  - name: ubuntu
    ssh-authorized-keys:
      - $PUB_KEY
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    groups: sudo
    shell: /bin/bash
runcmd:
  - echo "AllowUsers ubuntu" >> /etc/ssh/sshd_config
  - systemctl restart sshd
EOF

$ sudo genisoimage -output /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1-cidata.iso -volid cidata -joliet -rock user-data meta-data
Total translation table size: 0
Total rockridge attributes bytes: 331
Total directory bytes: 0
Path table size(bytes): 10
Max brk space used 0
183 extents written (0 MB)

现在使用两个附加的磁盘启动虚拟机:instance-1.qcow2 作为根磁盘,instance-1-cidata.iso 作为包含 Cloud-Init 配置的磁盘。

$ virt-install --connect qemu:///system \
	--virt-type kvm \
	--name instance-1 \
	--ram 1024 \
  --vcpus=1 \
	--os-variant ubuntu-lts-latest \
  --disk path=/var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2,format=qcow2 \
	--disk /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1-cidata.iso,device=cdrom \
	--import --network network=default --noautoconsole \
  --mem-path /dev/hugepages --mem-prealloc

WARNING  Requested memory 1024 MiB is less than the recommended 2048 MiB for OS ubuntu22.04

Starting install...
Creating domain...                                                                                                                           |    0 B  00:00:00
Domain creation completed.

借用Virsh进行容器管理

对于 KVM 来说,它有几种可用于管理虚拟机和 Libvirt 的实用程序。例如使用最广泛的使用在命令行中的 Virsh 程序

在上面的创建过程结束后,我们现在进入管理模式

$ sudo virsh list
 Id   Name         State
----------------------------
 1    instance-1   running

# 查看虚拟机配置
$ sudo virsh dumpxml instance-1

# 查看虚拟机网络地址
$ sudo virsh domifaddr instance-1
 Name       MAC address          Protocol     Address
-------------------------------------------------------------------------------
 vnet0      52:54:00:8c:a1:4e    ipv4         192.168.122.6/24

如果虚拟机无法访问,可以挂载实例镜像并在本地检查:

$ sudo virsh shutdown instance-1
$ sudo guestmount -a /var/lib/libvirt/images/ubuntu_cloud_instance1/instance-1.qcow2 -m /dev/sda1 /mnt/cloudimg/
# 进行调试操作后取消挂载
$ sudo umount /mnt/cloudimg
  • 如果忘记了密码怎么办

    使用命令计算出密码掩码

    openssl passwd -1 12345678

    并将其写入到 etc/passwd 即可

    testuser:$1$0aMJjMIh$1XPT6uYZI.LppKOWuzo6L/:1001:1001:Test User,,,:/home/testuser:/bin/bash

现在我们就可以进入到虚拟机中进行操作,下面可以看到虚拟机完美符合我们给的资源配置

$ ssh ubuntu@192.168.122.73

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           969Mi       161Mi       438Mi       2.0Mi       369Mi       661Mi
Swap:             0B          0B          0B
$ cat /proc/cpuinfo  | grep processor | wc -l
1

对于KVM进行监控和调优

要确定什么消耗了最多的 VM 资源,以及虚拟机性能的哪方面需要优化,可以使用性能诊断工具,包括通用的和特定于虚拟机的。

当然这里只是简单的进行一个思路引导,具体更深入的调优操作还是需要下很大功夫的。

在进行监控之前,需要安装特定的依赖,例如内核工具

	$ sudo apt install linux-tools-5.15.0-1034-realtime # 请根据自己内核版本进行安装

简单的 vCPU 监控

使用 perf kvm stat 命令之一来显示虚拟化主机的 perf 统计信息,若是要记录一段时间内 hypervisor 的 perf 数据,可以使用 perf kvm stat record 命令激活日志记录。命令被取消或中断后,数据保存在 perf.data.guest 文件中,可以使用 perf kvm stat report 命令进行分析

现在我们需要:

  1. 开启监控
  2. 进行一些简单的基本操作,例如安装服务等
  3. 分析瓶颈
$ sudo perf kvm stat record

# 另外一个终端
$ sudo apt update -y && sudo apt upgrade -y
$ sudo apt install nginx

# 返回分析终端
[ perf record: Captured and wrote 63.364 MB perf.data.guest (587162 samples) ]

$ sudo perf kvm stat report

Analyze events for all VMs, all VCPUs:

             VM-EXIT    Samples  Samples%     Time%    Min Time    Max Time         Avg time

       EPT_VIOLATION     119855    43.81%     0.37%      1.46us    179.79us      6.46us ( +-   0.17% )
           MSR_WRITE      38757    14.17%     0.06%      1.21us     50.79us      3.24us ( +-   0.21% )
  EXTERNAL_INTERRUPT      37339    13.65%     0.20%      0.95us   1079.84us     11.15us ( +-   0.55% )
                 HLT      23530     8.60%    99.25%      1.12us 447991.01us   8821.80us ( +-   2.09% )
               CPUID      21262     7.77%     0.01%      0.62us     29.66us      0.97us ( +-   0.58% )
       EPT_MISCONFIG      20493     7.49%     0.10%      1.48us    326.41us      9.72us ( +-   0.56% )
    PREEMPTION_TIMER       7086     2.59%     0.01%      1.36us     25.84us      4.24us ( +-   0.39% )
    INTERRUPT_WINDOW       4956     1.81%     0.00%      1.01us     66.70us      2.06us ( +-   1.04% )
            MSR_READ        241     0.09%     0.00%      2.14us     29.33us      5.70us ( +-   2.67% )
       EXCEPTION_NMI         30     0.01%     0.00%      5.82us     11.91us      8.36us ( +-   2.78% )

Total Samples:273549, Total events handled time:209154505.85us.

我们可以分析 VM-EXIT 事件类型及其分发的 perf 输出,其代表发生在虚拟机执行一条指令时需要离开虚拟机环境进入虚拟化层的情况,从这个输出结果来看,虚拟机的 CPU 使用率主要被 HLT 操作占用了,占用了 99.25% 的时间,这表明虚拟机的CPU处于空闲状态,可能是因为没有足够的工作负载,或者在虚拟机中没有足够的进程在运行。

另外,EPT_VIOLATION、MSR_WRITE 和 EXTERNAL_INTERRUPT 也是比较高的,这些事件通常是由于虚拟机访问非法地址或者尝试写入只读的寄存器等错误引起的。如果这些事件的数量过高,可能需要调整虚拟机的内存分配或者配置,以减少对虚拟化层的访问。

  • MSR_WRITE 指标表示虚拟机中发生的 MSR 写操作次数。如果 MSR 写操作非常频繁,可能表示虚拟机中的某些进程正在频繁地更改 MSR 寄存器
  • EXTERNAL_INTERRUPT 指标表示虚拟机中外部中断发生的次数。如果外部中断频繁发生,可能会导致虚拟机的性能下降。
  • EPT_VIOLATION 指标表示在虚拟机中执行期间出现 EPT 页表违规的次数。这个指标在使用虚拟化技术时经常会出现,但是如果它的数量非常高,则可能表示存在某些性能问题

简单的vCPU调优

简单粗暴地提高配置

将当前的虚拟机 CPU 核心增加为 2

$ sudo virsh shutdown instance-1
# 修改下面字段
$ sudo virsh edit instance-1
<vcpu placement='static' current='2'>2</vcpu>

hugepages内存管理

KVM使用的内存管理机制与主机的物理内存管理机制不同。在KVM中,每个虚拟机都有自己的虚拟内存空间,这个虚拟内存空间被划分成多个虚拟页面,每个页面映射到主机的物理内存上。对于KVM虚拟机来说,内存管理效率的提高可以通过一些参数的调整来实现。而其中一个常用的优化手段是 hugepages。hugepages 是一种特殊的内存页,它的大小通常为 2MB 或 1GB,相比普通的 4KB 大小的内存页,hugepages 的大小更大,减少了内存页表的条目数,提高了内存访问效率。

关于 hugepages 的优化手段,其实我们在上文中已经实现了,例如启动参数中的 mem-path /dev/hugepages 表示使用 /dev/hugepages 目录下的 hugepages, mem-prealloc 表示预先分配内存,可以提高内存管理效率。

KSM内存同页合并

当 Linux 启用了 KSM 之后,KSM 会检查多个正在运行的进程,并比对它们的内存。如果有任何区域或页面相同,KSM 就会毫不犹豫地将它们合并成一个页面。这样,新的页面也会被标记为写时复制。如果虚拟机要修改内存,Linux 就会为该虚拟机分配新的内存。

KSM 技术可以让多个虚拟机共享相同的内存页,从而降低了虚拟机的总体内存使用率,提高了资源利用率和密度。同时,它也存在一些局限性,如内存超用可能会导致虚拟机性能下降,并且使用边通道可能存在泄露客户信息的风险。因此,在生产环境下应慎用,而在测试环境和桌面虚拟化环境下建议使用,但要注意内存使用情况。

内核相同页面合并 (KSM) 提高了内存密度,但它会增加 CPU 的使用率,并且可能会对总体性能产生不利影响,具体取决于工作负载。在这种情况下,我们还可以通过启停 KSM 来提高虚拟机的性能(即有利也有弊)。

$ sudo apt install ksmtuned

NUMA访问技术

NUMA(Non-Uniform Memory Access)是一种计算机系统体系结构,其中多个处理器或处理器核心可以访问不同的物理内存区域。在 NUMA 系统中,不同的 CPU 有自己的本地内存,也可以访问共享内存,访问本地内存速度比访问远程内存快。因此,NUMA 系统可以提高大规模计算机系统的内存访问效率。

在 KVM 中,使用 NUMA 可以将虚拟机的内存划分到不同的物理节点上,保证虚拟机的内存访问效率。KVM 支持将 NUMA 节点直接映射到虚拟机的虚拟 NUMA 节点,使虚拟机在访问内存时可以更快地访问到本地内存,从而提高虚拟机的性能。同时,使用 NUMA 还可以避免虚拟机内部的 NUMA 效应,确保虚拟机内部的内存访问效率。

但同时这个技术也是需要一定兼容的,请使用 virsh nodeinfo 命令,并查看 NUMA cell (s) 行,如果行的值为 2 或更高,则代表主机与 NUMA 兼容。

$ virsh nodeinfo
CPU model:           x86_64
CPU(s):              24
CPU frequency:       3066 MHz
CPU socket(s):       1
Core(s) per socket:  6
Thread(s) per core:  2
NUMA cell(s):        2
Memory size:         32910100 KiB

这里我们使用工具进行 NUMA 自均衡

$ sudo apt install numad
$ virt-xml instance-1 --edit --vcpus placement=auto
$ virt-xml instance-1 --edit --numatune mode=preferred
$ echo "numa=on" | sudo tee -a /etc/default/grub
$ echo "numa_balancing=on" | sudo tee -a /etc/default/grub
$ update-grub
Sourcing file `/etc/default/grub'
Sourcing file `/etc/default/grub.d/99-realtime.cfg'
Sourcing file `/etc/default/grub.d/init-select.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-5.15.0-1037-realtime
Found initrd image: /boot/initrd.img-5.15.0-1037-realtime
Found linux image: /boot/vmlinuz-5.15.0-1034-realtime
Found initrd image: /boot/initrd.img-5.15.0-1034-realtime
Found linux image: /boot/vmlinuz-5.15.0-71-generic
Found initrd image: /boot/initrd.img-5.15.0-71-generic
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
done

# 此时已经启用成功了
$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 1 2 4 6 8 10 12 14 16 18 20 22
node 0 size: 24079 MB
node 0 free: 11354 MB
node 1 cpus: 0 3 5 7 9 11 13 15 17 19 21 23
node 1 size: 8059 MB
node 1 free: 561 MB
node distances:
node   0   1
  0:  10  20
  1:  20  10