基于 Ubuntu 20.04 系统使用 KVM+CloudInit 创建虚拟机

看,是大盘鸡!

基于内核的虚拟机(Kernel-based Virtual Machine,KVM)是一项用于在受支持的硬件设备上执行硬件辅助完全虚拟化任务的解决方案。如果有一天你像我一样为如何充分利用一台支持虚拟化技术的独立服务器而烦恼,请考虑使用 KVM 技术拆分你的服务器。

本文以具有多 IPv4 地址的主机为例,使用基于 Cloud-init 配置文件的方法配置基于 KVM 虚拟化技术的 VMs。

虚拟化技术

在特别针对 KVM 技术进行讨论以前,回顾我们使用过的各种虚拟化技术对后续内容的理解是有意义的。计算机中的虚拟化技术是指将宿主计算机以多台或者一台完全不同的计算机的形式呈现出来。

本小节内容参考了 IBM Developerworks 的文章《虚拟 Linux》。

硬件虚拟化(Hardware virtualization)是一种隐藏真实计算机硬件,用于构建抽象计算平台的一项技术。在早期计算中,操作系统被称为 supervisor;能够在其他操作系统上运行的操作系统被称为 hypervisor。类似 VirtualBox 的虚拟机软件拥有虚拟出一台完整硬件计算机的能力。

同样地,QEMU 项目提供了一种被称为“硬件仿真”(Hardware emulation)的能力。这种技术甚至可以在一个 ARM 处理器主机上运行为 x86 架构设计的操作系统(或软件)。使用硬件仿真的主要问题是速度会非常慢。由于每条指令都必须在底层硬件上进行仿真,因此速度减慢 100 倍的情况也并不稀奇。若要实现高度保真的仿真,包括周期精度、所仿真的 CPU 管道以及缓存行为,实际速度差距甚至可能会达到 1000 倍之多。

在虚拟化的方式上,由于宿主机操作系统与计算机硬件的协调程度不同,又通常分为全虚拟化、半虚拟化与操作系统级虚拟化三种方式。

完全虚拟化(Full virtualization)在客户操作系统和原始硬件之间进行协调。特定受保护的指令必须被捕获下来并在 hypervisor 中进行处理,因为这些底层硬件并不由操作系统所拥有,而是由操作系统通过 hypervisor 共享。

半虚拟化(Paravirtualization)将与虚拟化有关的代码集成到了客户操作系统本身中。它需要为 hypervisor 修改客户操作系统。

操作系统虚拟化(OS-virtualization)在操作系统本身之上实现服务器的虚拟化。它支持单个操作系统,并可以将独立的服务器相互简单地隔离开来。Docker 属于操作系统级别的虚拟化。

服务器聚合或网格计算(Grid computing)是一种反向的形式,它将多个服务器聚合起来,使其看起来像一整个服务器一样。

基于内核的虚拟化(KVM)

我们充分认识到 KVM 技术的优点。基于硬件技术支持的 KVM 虚拟化技术将 Linux 内核转换为一个使用内核模块的 hypervisor。得益于对于操作系统内核提供的任务调度(Schedule)与内存管理(Memory management)等功能的充分运用,KVM 可以实现有限硬件资源上的超量共享操作。它还支持实时迁移,这提供了在物理宿主之间转移正在运行的虚拟机而不中断服务的能力。

内核中的 KVM 通过 /dev/kvm 字符设备来公开虚拟化后的硬件。客户操作系统使用为 PC 硬件仿真修改过的 QEMU 进程与 KVM 模块接口。KVM 模块向内核中引入了一个新的执行模块。普通内核支持内核模式和用户模式,而 KVM 则引入了一种客户模式。客户模式用来执行所有非 I/O 客户代码,而普通用户模式支持客户 I/O。

KVM 的代码位于内核仓库 https://git.kernel.org/pub/scm/virt/kvm/kvm.git/ 中,自主线版本 2.6.20 被引入内核。

配置宿主机操作系统

KVM 技术所需的 Intel VT/AMD-V 技术在大多数处理器中被支持。需要注意,从云服务商中购买的虚拟服务器(VPS)大多不支持再虚拟化(Embedded virtualization,又称嵌套虚拟化),也就是说,这些服务器无法再使用 KVM 技术进行虚拟化。

很容易在 Ubuntu 系统中使用 kvm-ok 指令判断主机操作系统是否支持 KVM 虚拟化。kvm-ok 指令包含于 cpu-checker 包中。

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

或者,对于一台不支持 KVM 虚拟化的主机来说:

# kvm-ok
INFO: Your CPU does not support KVM extensions
KVM acceleration can NOT be used

要充分使用 KVM 技术,需要安装以下包:

sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst cloud-image-utils virt-manager -y

qemu-kvm 包含 KVM 的用户空间(Userspace)态组件;libvirt* 打头的包内置用于管理虚拟化平台的工具;bridge-utils 包含用于配置宿主机与虚拟机之间网络连结的工具;virtinst 包含命令行版本的用于创建虚拟机的工具;cloud-image-utils 包含用于解析 cloud-init 格式文件的工具(稍后用到!);可选的 virt-manager 包内置用于创建和管理虚拟机的 GUI 工具。

我们有必要将当前用户添加到 libvirt 以及 kvm 组中,以允许其使用 KVM 相关的技术。

sudo adduser `id -un` libvirt
sudo adduser `id -un` kvm

如果安装成功,我们可以通过 virsh 指令查看目前运行的虚拟机情况:

$ virsh list --all
 Id Name                 State
----------------------------------

配置宿主机网络

通常来说,我们有三种配置网络的方式。对于独立服务器来说,如果需要生成的虚拟服务器(Virtual machine,VM)能够被公网访问,则可以考虑桥接网络(Bridged network),它允许 VM 绑定其公网 IPv4 以及 IPv6 地址;如果桥接网络不可用,则应考虑路由网络(Routed netword);如果服务器的 IPv4 地址有限多,则 NAT 网络(NAT-based network)或许是你的唯一选择。

对于使用 QEMU 启动的 VM 来说,NAT 网络是自动配置的选项。这特别适合于 VM 需要访问网络但无需作为服务器的情况。如果你选择 NAT 网络的接入方式,你可以直接略过本小节。但是,如果你的 VM 担当网络服务器等角色,需要通过公网 IP 地址进行访问的,则需要按照以下步骤进行设置网桥。

自 Ubuntu 18.04 LTS 以来,Ubuntu 系统默认的网络配置方式已变更为 netplan。以下示例假设你的宿主服务器使用 netplan ,并拥有(一个或多个)静态 IP 地址。

我们编辑 /etc/netplan 中的 YAML 格式文件,并添加 bridges 部分,完整的可能文件如下所示:

network:
  version: 2
  renderer: networkd
  ethernets:
    eno1:
      addresses: [192.161.60.114/29,192.161.60.115/29,192.161.60.116/29]
      gateway4: 192.161.60.113
      nameservers:
        addresses:
          - 1.1.1.1
          - 8.8.8.8
  bridges:
    br0:
      addresses: [192.161.60.117/29,192.161.60.118/29]
      gateway4: 192.161.60.113
      interfaces:
        - eno1
      parameters:
        stp: true

在该示例中,服务器拥有五个 IP 地址:192.161.60.114192.161.60.115192.161.60.116192.161.60.117192.161.60.118。我们搭建了使用 IPv4 地址 192.161.60.117/192.161.60.118 的虚拟网卡(网桥),这样,创建的所有虚拟机可以使用这一个虚拟网桥对应的两个 IPv4 地址。该网卡开启了 STP。使用 netplan apply 应用这些更改。需要注意,如果将 192.161.60.117/192.161.60.118 定义到 eno1 中的话,那么对这些 IP 地址的访问将由服务器本身抢先响应。

接下来,我们配置 libvirt 使用创建的网桥。我们首先删除默认的网络配置:

virsh net-undefine default

然后我们创建 host-bridge.xml 临时文件,内容为:

<network>
  <name>host-bridge</name>
  <forward mode="bridge"/>
  <bridge name="br0"/>
  <model type='virtio'/>
</network>

将网桥配置应用于 libvirt 中:

virsh net-define host-bridge.xml
virsh net-start host-bridge
virsh net-autostart host-bridge

可以通过 virsh net-list --all 命令验证这些配置是否正确。

出于安全和性能原因,我们建议在宿主系统中关闭桥接网络的 netfilter 功能。新建名为 /etc/sysctl.d/bridge.conf 的文件,并填充以下内容:

net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0

新建名为 /etc/udev/rules.d/99-bridge.rules 的文件,并填充以下内容:

ACTION=="add", SUBSYSTEM=="module", KERNEL=="br_netfilter", RUN+="/sbin/sysctl -p /etc/sysctl.d/bridge.conf"

重新启动系统以应用这些更改。

需要特别说明,如果因某些原因无法使用桥接(Bridge)的方式组网的话,则需考虑使用路由网络的方式,详见此处

创建 VM

截至目前,我们已经完成了对于虚拟化以及网络的全部配置。现在我们使用虚拟机前端管理程序 virt-install 创建 VM。virt-install自动选择合适的 hypervisor 程序:

virt-install --name vm1 --ram=2048 --disk size=10 --vcpus 1 --os-type linux --os-variant ubuntu20.04 --graphics none --location 'http://archive.ubuntu.com/ubuntu/dists/focal/main/installer-amd64/' --extra-args "console=tty0 console=ttyS0,115200n8" --network bridge=br0,model=virtio

--disk size 默认的单位是 GiB。通过 --location 指定的安装路径可以是本地 ISO 或 CDROM 文件,也可以是一个在线的发行版安装目录,如上例一样。--network bridge 指定了使用的网桥网卡的名称。适用于 VM 的 Ubuntu 发行版的各种镜像可以在 cloud-images.ubuntu.com 找到。如果服务器宿主机是多 IP 地址的(一般这种情况下不使用 DHCP 进行配置),则可能需要在安装配置的过程中手动指定虚拟机本身的 IP 地址和其他网络配置选项。

如果你愿意使用下载的 .img 格式的镜像启动 VM(像云服务商一样),则需要一些额外操作。首先,大多数下载的 .img 格式镜像实际上是 qcow2 格式,我们使用 qemu-img 命令验证这一点:

$ qemu-img info ubuntu-20.04-minimal-cloudimg-amd64.img
image: ubuntu-20.04-minimal-cloudimg-amd64.img
file format: qcow2
virtual size: 2.2 GiB (2361393152 bytes)
disk size: 199 MiB
cluster_size: 65536
Format specific information:
compat: 0.10
refcount bits: 16

默认的镜像空间很小,我们通过以下命令动态调整磁盘镜像的大小:

qemu-img resize ubuntu-20.04-minimal-cloudimg-amd64.img 25G

编辑 Cloud Config 文件

Cloud-init 是适用于多种发行版镜像初始化任务的工业级别标准。该文件描述了将发行版镜像初始化为 VM 实例中的一些行为。一个比较友好的教程由 Justin Ellingwood 贡献于 DigitalOcean 教程上。较简单的 cloud-init 文件如下:

#cloud-config
password: ubuntu
chpasswd: { expire: False }
ssh_pwauth: True
hostname: ubuntu

该示例为默认用户(ubuntu)创建了同名密码,并开启 SSH 密码登录。如果你对于该配置文件的格式感兴趣,请参阅上方链接。

cloud-localds 实用工具(位于 cloud-image-utils 包中)用于通过给定的 cloud-init 格式的文本文件生成对应的 .iso 初始化镜像。

cloud-localds /var/lib/libvirt/images/install.iso cloud.txt

其中,/var/lib/libvart/images/ 是 virsh 实用工具保存 VM 磁盘的默认路径。这样的话,我们的 virt-install 的命令为:

virt-install --name vm1 --ram=2048 --vcpus 1 --os-type linux --os-variant ubuntu20.04 --graphics none --disk /var/lib/libvirt/images/ubuntu.img,device=disk,bus=virtio --disk /var/lib/libvirt/images/install.iso,device=cdrom --network bridge=br0,model=virtio --import

首次通过 TTY 登录 VM 时,我们可以设置 VM 中的网络配置以及 SSH 配置,以支持远程登录。使用 ^] 键组合退出 TTY。如果配置正确,后续我们就可以通过 SSH 等方式管理我们的 VM 了。

为便于参考,我们给出客户操作系统使用 netplan 配置静态 IP 地址的配置示例:

network:
    ethernets:
        enp1s0:
            addresses: [x.x.x.x/x]
            gateway4: x.x.x.x
            nameservers:
                addresses: [1.1.1.1, 8.8.8.8]
            match:
                macaddress: xx:xx:xx:xx:xx:xx
            set-name: enp1s0
    version: 2

请注意,对于 Ubuntu 发行版的 Cloud Image 镜像来说,标注为 minimal 的镜像系列不支持 cloud-init 配置项。请使用适用于云计算的完整版本镜像。

后续维护操作

我们在此列出一些用于维护 VMs 的常用命令。完整的命令列表可以从 virsh(1) 中找到。

命令描述
virsh list -all列出所有 VMs
virsh dominfo <vm>查看 VM 基本信息
virsh start <vm>启动 VM
virsh console <vm>登录 VM 的控制台
virsh shutdown <vm>关闭 VM
virsh autostart <vm>开机自动启动 VM
virsh undefine <vm>删除 VM

由于之前没有做过类似的事情,这次花了几天的时间才把虚拟化这一套东西算是完完整整地折腾完。基本上文中涉及的每个技术都踩了一遍坑。踩坑的过程是痛苦并且需要兴趣才能支持下去的,不过当你最后成功的那一刻,就像又达成了一项人生成就一样开心。

– @DGideas