KubeVirt
KubeVirt 以 CRD 形式将 VM 管理接口接入到 kubernetes,通过一个 pod 去使用 libvirtd 管理 VM 方式,实现 pod 与 VM 的一对一对应,做到如同容器一般去管理虚拟机,并且做到与容器一样的资源管理、调度规划。本文中所有涉及的代码可以从我的 Github 中找到。
背景介绍
CRD 设计
Kubevirt 主要实现了下面几种资源,以实现对虚拟机的管理:
VirtualMachineInstance(VMI): 类似于 kubernetes Pod,是管理虚拟机的最小资源。一个VirtualMachineInstance对象即表示一台正在运行的虚拟机实例,包含一个虚拟机所需要的各种配置。通常情况下用户不会去直接创建 VMI 对象,而是创建更高层级的对象,即 VM 和 VMRS。VirtualMachine(VM): 为集群内的VirtualMachineInstance提供管理功能,例如开机/关机/重启虚拟机,确保虚拟机实例的启动状态,与虚拟机实例是 1:1 的关系,类似与spec.replica为 1 的 StatefulSet。VirtualMachineInstanceReplicaSet: 类似ReplicaSet,可以启动指定数量的VirtualMachineInstance,并且保证指定数量的VirtualMachineInstance运行,可以配置 HPA。
架构设计
为什么 kube-virt 可以做到让虚拟机无缝的接入到 K8S?首先,先给大家介绍一下它的整体架构。
virt-api
- kubevirt 是以 CRD 形式去管理 vm pod, virt-api 就是所有虚拟化操作的入口,包括常规的 CRD 更新验证以及 vm start、stop
virt-controlller
- Virt-controller 会根据 vmi CRD,生成对应的 virt-lancher pod,并维护 CRD 的状态
virt-handler
virt-handler会以 Daemonset 形式部署在每个节点上,负责监控节点上每个虚拟机实例状态变化,一旦检测到状态变化,会进行响应并确保相应操作能达到所需(理想)状态。virt-handler保持集群级 VMI Spec 与相应 libvirt 域之间的同步;报告 Libvirt 域状态和集群 Spec 的变化;调用以节点为中心的插件以满足 VMI Spec 定义的网络和存储要求。
virt-launcher
- 每个
virt-lanuncher pod对应着一个 VMI, kubelet 只是负责virt-lanuncher pod运行状态,不会去关心 VMI 创建情况。 virt-handler会根据 CRD 参数配置去通知 virt-lanuncher 去使用本地 libvirtd 实例来启动 VMI, virt-lanuncher 就会通过 pid 去管理 VMI,如果 pod 生命周期结束,virt-lanuncher 也会去通知 VMI 去终止。- 每个 virt-lanuncher pod 对应一个 libvirtd,virt-lanuncher 通过 libvirtd 去管理 VM 的生命周期,这样做到去中心化,不再是以前虚拟机那套做法,一个 libvirtd 去管理多个 VM。
libvirtd
An instance of libvirtd is present in every VMI pod. virt-launcher uses libvirtd to manage the life-cycle of the VMI process.
virtctl
- virctl 是 kubevirt 自带类似 kubectl 命令,它是越过 virt-lancher pod 这层去直接管理 vm,可以控制 vm 的 start、stop、restart。
VM 流程
上述架构里其实已经部分简述了 VM 的创建流程,以下进行流程梳理:
- K8S API 创建 VMI CRD 对象
virt-controller监听到VMI创建时,会根据 VMI 配置生成 pod spec 文件,创建virt-launcher podsvirt-controller发现 virt-launcher pod 创建完毕后,更新 VMI CRD 状态virt-handler监听到 VMI 状态变更,通信virt-launcher去创建虚拟机,并负责虚拟机生命周期管理
如下图所示:
Client K8s API VMI CRD Virt Controller VMI Handler
-------------------------- ----------- ------- ----------------------- ----------
listen <----------- WATCH /virtualmachines
listen <----------------------------------- WATCH /virtualmachines
| |
POST /virtualmachines ---> validate | |
create ---> VMI ---> observe --------------> observe
| | v v
validate <--------- POST /pods defineVMI
create | | |
| | | |
schedPod ---------> observe |
| | v |
validate <--------- PUT /virtualmachines |
update ---> VMI ---------------------------> observe
| | | launchVMI
| | | |
: : : :
| | | |
DELETE /virtualmachines -> validate | | |
delete ----> * ---------------------------> observe
| | shutdownVMI
| | |
: : :
部署流程
本实验在腾讯云上进行,创建一个包含黑石机型的 TKE 集群。
节点初始化
节点上需要安装 libvirt 和 qemu 软件包:
|
|
查看节点是否支持 KVM 硬件虚拟化
|
|
检查此时节点上已经加载了 kvm
|
|
安装 kubevirt
|
|
备注:如果之前节点不支持硬件虚拟化,可以通过修改 kubevirt-cr 来开启软件模拟模式,参考 software emulation fallback
部署结果
|
|
部署 Containerized Data Importer
Containerized Data Importer(CDI)项目提供了用于使 PVC 作为 KubeVirt VM 磁盘的功能。参考 Github: Containerized Data Importer :
|
|
部署 HostPath Provisioner
在本次实验中,采用 PVC 作为持久化存储,但是在腾讯云黑石服务器集群使用 CBS 作为 PVC 时,发现存在 PVC 挂载不到 Pod 的问题,于是选择使用了 kubevirt 提供的 hostpath-provisioner 作为 PVC 的 provisioner。
参考 Github: kubevirt.io.hostpath-provisioner,hostpath-provisioner 作为 DaemonSet 运行在每个节点上,可以通过 hostpath-provisioner-operator 来部署,部署方式如下:
|
|
接下来创建 CR 作为后端存储,这里指定了 Node 上的 /var/hpvolumes 作为实际数据存放位置:
|
|
真正创建 PVC 之后,可以在对应目录看到存放的 image
|
|
接下来需要创建对应的 storageclass,注意这里的 storagePool 即是刚才创建 CR 里面的 local:
|
|
配置 HardDisk
参考 Kubevirt: Activating feature gates,打开 kubevirt 的 HardDisk 模式:
|
|
客户端准备
Kubevirt 提供了一个命令行工具 virtctl,可以直接下载:
|
|
也可以通过 krew 安装为 kubectl 的插件:
|
|
创建 Linux 虚拟机
以下是一个创建 Linux 虚拟机的 VMI 示例,基于此 VMI 会自动创建一个 VM。在这个 CR 里面,指定了一个虚拟机需要的几个关键元素:
- Domain:domain 是一个虚拟机都需要的根元素,指定了虚拟机需要的所有资源。kubevirt 会根据这个 domain spec 转换成 libvirt 的 XML 文件,创建虚拟机。
- 存储:
spec.volumes表示真正的存储后端,spec.domain.devices.disks表示这个 VM 要使用什么存储。具体参考存储一节。 - 网络:
spec.networks表示真正的网络后端,spec.domain.devices.interfaces表示这个 VM 使用什么类型网卡设备
|
|
创建以下 VirtualMachineInstance CR 之后,可以看到集群中启动了 virt-launcher-testvmi-nocloud2-jbbhs 这个 Pod。查看 Pod 和 虚拟机:
|
|
登陆虚拟机,账号和密码都是 fedora:
|
|
创建 Windows 虚拟机
上传镜像
CDI 提供了使用 PVC 作为虚拟机磁盘的方案,CDI 支持以下几种模式导入镜像到 PVC:
- 通过 URL 导入虚拟机镜像到 PVC,URL 可以是 http 链接,s3 链接
- Clone 一个已经存在的 PVC
- 通过 container registry 导入虚拟机磁盘到 PVC,需要结合
ContainerDisk使用 - 通过客户端上传本地镜像到 PVC
这里使用第四种方式,通过 virtctl 命令行工具,结合 CDI 项目上传本地镜像到 PVC:
|
|
参数解释:
- –image-path : 操作系统镜像的本地地址
- –pvc-name : 指定存储操作系统镜像的 PVC,这个 PVC 不需要提前准备好,镜像上传过程中会自动创建。
- –pvc-size : PVC 大小,根据操作系统镜像大小来设定,一般略大一个 G 就行
- –uploadproxy-url : cdi-uploadproxy 的 Service IP,可以通过命令
kubectl -n cdi get svc -l cdi.kubevirt.io=cdi-uploadproxy来查看。
创建虚拟机
创建 VirtualMachine CR 之后,可以看到集群中创建了对应的 Pod 和虚拟机:
|
|
这里用到了 3 个 Volume:
- harddrive:虚拟机使用的磁盘,即操作系统就会安装在该磁盘上。这里选择
hostDisk直接挂载到宿主机以提升性能,如果使用分布式存储则体验非常不好。 - cdromiso : 提供操作系统安装镜像,即上文上传镜像后生成的 PVC
iso-win10。 - virtiocontainerdisk : 由于 Windows 默认无法识别 raw 格式的磁盘,所以需要安装 virtio 驱动。 containerDisk 可以将打包好 virtio 驱动的容器镜像挂载到虚拟机中。
启动虚拟机实例:
|
|
查看启动的 VM 实例,Windows 虚拟机已经可以正常运行了。
|
|
配置 VNC 访问
参考 Access Virtual Machines' graphic console using noVNC 可以部署 virtVNC 来访问启动的 Windows 服务器。这里主要是暴露出了一个 NodePort 的服务,可以通过
|
|
通过访问这个 NodePort 服务,既可以
接下来就可以像 kubernetes 使用 kubevirt 创建 windows10 虚拟机 里面指导的一样,安装 Windows 操作系统了。
配置远程连接
尽管 VNC 可以远程访问 Windows 图形界面,但是操作体验比较难受。当系统安装完成后,就可以使用 Windows 的远程连接协议 RDP。选择开始 >设置 >系统>远程桌面,打开启用远程桌面就好了。
现在可以通过 telnet 来测试 RDP 端口 3389 的连通性:
|
|
如果你的本地电脑能够直连 Pod IP 和 SVC IP,现在就可以直接通过 RDP 客户端来远程连接 Windows 了。如果你的本地电脑不能直连 Pod IP 和 SVC IP,但可以直连 Kubernetes 集群的 Node IP,可以通过 NodePort 来暴露 RDP 端口。具体操作是创建一个 Service,类型为 NodePort:
|
|
然后就可以通过 Node IP 来远程连接 Windows 了。如果你的本地操作系统是 Windows 10,可以在任务栏的搜索框中,键入“远程桌面连接”,然后选择“远程桌面连接”。在“远程桌面连接”中,键入你想要连接的电脑的名称(从步骤 1),然后选择“连接”。如果你的本地操作系统是 macOS,需要在 App Store 中安装 Microsoft Remote Desktop。
对于以上使用了
windows 访问外网
外网访问 windows
在 Windows 虚拟机中安装 nginx 服务,可以在 Windows 中访问:
这个时候直接访问 Windows 虚拟机对应的 IP,既可以在集群内访问 Nginx 服务:
|
|
为了能够将这个 Windows 虚拟机的服务暴露到外网,创建以下 Service:
|
|
可以看到创建了 NodePort 的服务:
|
|
这时候访问 NodePort 的服务就可以访问 Windows 虚拟机提供的服务:
存储
虚拟机镜像(磁盘)是启动虚拟机必不可少的部分,KubeVirt 中提供多种方式的虚拟机磁盘,虚拟机镜像(磁盘)使用方式非常灵活。这里列出几种比较常用的:
- PersistentVolumeClaim : 使用 PVC 做为后端存储,适用于数据持久化,即在虚拟机重启或者重建后数据依旧存在。使用的 PV 类型可以是 block 和 filesystem
- 使用 filesystem 时,会使用 PVC 上的 /disk.img,格式为 RAW 格式的文件作为硬盘。
- block 模式时,使用 block volume 直接作为原始块设备提供给虚拟机。
- ephemeral : 基于后端存储在本地做一个写时复制(COW)镜像层,所有的写入都在本地存储的镜像中,VM 实例停止时写入层就被删除,后端存储上的镜像不变化。
- containerDisk : 基于 scratch 构建的一个 docker image,镜像中包含虚拟机启动所需要的虚拟机镜像,可以将该 docker image push 到 registry,使用时从 registry 拉取镜像,直接使用 containerDisk 作为 VMI 磁盘,数据是无法持久化的。
- hostDisk : 使用节点上的磁盘镜像,类似于
hostpath,也可以在初始化时创建空的镜像。 - dataVolume : 提供在虚拟机启动流程中自动将虚拟机磁盘导入 pvc 的功能,在不使用 DataVolume 的情况下,用户必须先准备带有磁盘映像的 pvc,然后再将其分配给 VM 或 VMI。dataVolume 拉取镜像的来源可以时 http,对象存储,另一块 PVC 等。
更多参考 Kubevirt: Disks and Volumes
bridge 网络原理
虚拟机网络就是 pod 网络,virt-launcher pod 网络的网卡不再挂有 pod ip,而是作为虚拟机的虚拟网卡的与外部网络通信的交接物理网卡,virt-launcher 实现了简单的单 ip dhcp server,就是需要虚拟机中启动 dhclient,virt-launcher 服务会分配给虚拟机。
出向:目的地为集群外地址
在虚拟机中查看路由:
|
|
- 虚拟机 Node1 上所有 Pod 属于同一个 IP 子网 172.20.0.0/26,这些 Pod 都连接到了虚拟网桥 cbr0 上。如上面路由表的第二条路由条目所示,目地地为子网 172.20.0.0/26 的流量将通过虚拟机的 eth0 发出去,eth0 的 Veth pair 对端网卡处于网桥上,因此网桥会收到该数据包。网桥收到数据包后,通过二层转发将该数据包从网桥上连接到目的 Pod 的端口发送出去,数据将到达该端的 Veth pair 对端,即该数据包的目的 Pod 上。
- 如果是发往外界的 IP,则默认发给网关 172.16.0.1。这里的网关是宿主机上面 cbr0 的地址。
其实这里虚拟机的路由都来自于 launcher Pod 中的路由信息。此时查看 launcher Pod 中的路由信息,已经为空:
|
|
查看虚拟机 IP 信息,可以看到虚拟机 IP 为 172.16.0.24,MAC 为 5e:04:61:17:c1:c9,这也是从 launcher Pod 拿来的。
|
|
查看 launcher Pod 中现有的接口,
eth0-nic为 Pod 原来 eth0 的 interfacetap0为虚拟机 eth0 对应的 tap 设备k6t-eth0为 launcher Pod 中的网桥
|
|
可以看到 eth0-nic 和 tap0 都被挂载到网桥 k6t-eth0 上:
|
|
对于宿主机上 cbr0 的信息:
|
|
从虚拟机访问外部网段,比如 8.8.8.8,在 launcher Pod 中抓包,这里在 tap 设备上抓包:
- 源 IP:虚拟机 eth0 的 IP
- 源 Mac:虚拟机 eth0 的 MAC
- 目的 IP:虚拟机网关的 IP,也就是宿主机上的 cbr0 的 IP
- 目的 Mac: 宿主机上 cbr0 的 MAC
|
|
Tap 设备接到了网桥上,在网桥 k6t-eth0 设备上抓包,源和目的地址都不变
|
|
接下来会在 Pod 原来的网口 eth0-nic 上抓到包,源和目的地址都不变:
|
|
网桥 k6t-eth0 是怎么知道应该把包给到 eth0-nic 呢?这里是在 k6t-eth0 网桥这里做了 flood,所有挂接到网桥上的接口都可以抓到访问 8.8.8.8。
虚拟机是怎么知道 172.16.0.1 对应的 MAC 呢?在虚拟机查看 ARP
|
|
这里即是 cbr0 的 MAC,干掉这个表项之后,可以抓到对应的 arp 包
到达 eth0-nic 之后,就可以到达 Pod 对端的 veth 网口上了,按照全局路由的模式从节点出去。
入向:从节点上访问虚拟机
在全局路由模式中,对于本节点全局路由网段的 IP,默认走节点上的网桥 cbr0
|
|
cbr0 网桥挂载了 Pod 的 veth,对于访问虚拟机的 IP,默认通过网桥二层转发到 Pod 对应的 veth pair。到达 Pod 之后,走 Pod 里面的网桥,给到 tap 设备,最后到达虚拟机。
网络代码阅读
参考 Kubevirt: VMI Networking,Kubevirt 中 VM 的 Interface 的配置划分为两个 Phase:
- privileged networking configuration: 在 virt-handler 进程中实现
- unprivileged networking configuration: occurs in the virt-launcher process
Phase1
Phase1 的第一步是决定改使用哪种 BindMechanism,每个 BindMechanism 实现了以下方法:
|
|
一旦 BindMechanism 确定,则依次调用以下方法:
- discoverPodNetworkInterface: 获取 Pod 网卡设备相关的信息,包括 IP 地址,路由,网关等信息
- preparePodNetworkInterfaces:基于前面获取的信息,配置网络
- setCachedInterface:缓存接口信息在内存中
- setCachedVIF:在文件系统中持久化 VIF 对象,地址
/proc/<virt-launcher-pid>/root/var/run/kubevirt-private/vif-cache-<iface_name>.json.
|
|
Phase2
Phase2 运行在 virt-launcher 中,相比于 phase1 所具有的权限会很多,网络中只具有 CAP_NET_ADMIN 权限。Phase2 也会选择正确的 BindMechanism,然后取得 Phase1 的配置信息(by loading cached VIF object)。基于 VIF 信息, proceed to decorate the domain xml configuration of the VM it will encapsulate
loadCachedInterfaceloadCachedVIFdecorateConfig
|
|
BindMechanism
DiscoverPodNetworkInterface
|
|
PreparePodNetworkInterface
|
|
热迁移
热迁移是指将虚拟机从一台物理机的 Hypervisor 迁移到另一台物理机的 Hypervisor 上运行。虚拟机迁移的目的在于在于增强整个计算系统的负载均衡能力,提升系统错误容忍能力,同时简化整个系统的维护管理,优化整个系统的电源管理。虚拟机迁移可以说是实现弹性计算的关键技术,主流的虚拟化方案都提供迁移工具。
Kubevirt 支持开启热迁移,需要打开 LiveMigration 的 feature gate。
但是存在以下限制:
- 使用 PVC 虚拟机 的 PVC 需要使用
a shared ReadWriteMay (RWX)模式 - 热迁移不支持使用 Bridge 网络模式的 Pod
- 热迁移需要在
virt-launcherPod 里面开启 49152, 49153 端口。如果这个端口在 masquarade interface 指定了,则热迁移不会工作
参考 Kubevirt Live Migration 发起热迁移,可以看到 使用 bridge 模式的 Pod 不支持热迁移:
|
|
参考资料
-
No backlinks found.