虚拟机与容器的混合管理实践
1. 背景
当前容器已经成为企业上云的主流选择,经过 2019 年下半年的深度研发和推广,2020 年 OPPO 基本实现了基于 kubernetes 的容器的大规模使用和全业务上云。容器的优势是敏捷和高性能,然而由于需要共享宿主机内核,隔离不彻底等原因,当用户需要修改很多定制的内核参数或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,或者只是需要隔离性更高时,在容器上都是难以实现的。而由于历史原因,公司内部也仍然有一些业务需要使用强隔离的虚拟机,因此提供虚拟机服务,势在必行。
经过调研,我们发现对于已经建设有容器平台的公司,虚拟机的管理方案大部分是维护一套 OpenStack 或者类似的系统。然而 OpenStack 庞大且繁重,维护成本较高,且底层资源不能够统一管理,将会为混合调度带来很多不便。因此我们将统一的控制面管理,实现容器与虚拟机的统一调度和管理,作为选型的主要方向。
2. 方案选型 Kubevirt or Virtlet
虚拟机与容器通过 k8s 平台进行混合管理,业界比较好的项目有 kubevirt 和 virtlet 等。
Kubevirt 是 Redhat 开源的以容器方式运行虚拟机的项目,以 k8s add-on 方式,利用 k8s CRD 增加资源类型 VirtualMachineInstance(VMI), 使用容器的 image registry 去创建虚拟机并提供 VM 生命周期管理。
Virtlet 是一个 Kubernetes(Container Runtime Interface)的实现,能够在 Kubernetes 上运行基于虚机的 Pods。(CRI 能够令 Kubernetes 运行非 docker 的容器,例如 Rkt)。
下面这张图是我们 2020 年初做选型时做的一个 Kubevirt 和 Virtlet 的对比图的部分。可以看到,Virtlet 使用同一种资源类型 Pod 描述容器和虚拟机,因此如果使用原生的方式,虚拟机也只能有 Running 和删除两种状态,无法支持 pause/unpause, start/stop 等虚拟机专属状态,显然这是无法满足用户需求的。如果想要支持这些状态,又得深度定制 kubelet,会导致虚拟机管理和容器管理耦合太重;另外考虑到当时 virtlet 社区不如 kubevirt 社区活跃,因此我们最终选择的方案是 Kubevirt。
3. Kubevirt 介绍
3.1 VmiCRD/Pod/Domain 对应关系
3.2 组件介绍
kubevirt 的各组件服务是部署在 k8s 上的,其中 virt-api 和 virt-controller 是 deployment,可以多副本高可用部署,virt-api 是无状态的,可以任意扩展;virt-controller 是通过选举的方式选出一个主台提供服务;virt-handler 以 daemonset 的方式部署,每一台虚拟机节点都运行一个 virt-handler;而一个 virt-launcher 服务则对应一个虚拟机,每当创建一个虚拟机,都会创建一个对应的 virt-launcher pod。
virt-api
kubevirt API 服务,kubevirt 是以 CRD 的方式工作的,virt-api 提供了自定义的 api 请求处理,可以通过 virtctl 命令执行同步命令 virtctl vnc/pause/unpause/stop/start vm 等。
virt-controller
与 k8s api-server 通讯监控 VMI 资源创建删除等事件,并触发相应操作
根据 VMI 定义创建 virt-launcher pod,该 pod 中将会运行虚拟机
监控 pod 状态,并随之更新 VMI 状态
virt-handler
运行在 kubelet 的 node 上,定期更新 heartbeat,并标记”kubevirt.io/schedulable”
监听在 k8s apiserver 当发现 VMI 被标记的 nodeName 与自身 node 匹配时,负责虚拟机的生命周期管理
virt-launcher
以 pod 形式运行
根据 VMI 定义生成虚拟机模板,通过 libvirt API 创建虚拟机
每个虚拟机会对应独立的 libvirtd
与 libvirt 通讯提供虚拟机生命周期管理
4. Kubevirt 架构改造
4.1 原生架构
原生架构中管理面与数据面耦合。在 virt-launcher pod 中运行虚拟机,当由于不确定原因(比如说 docker 的原因或物理机原因或者 virt-launcher 本身的挂掉升级等原因),造成 virt-launcher 容器退出后,会导致虚拟机也退出,从而会影响用户使用,增加了虚拟机的稳定性风险。因此我们在原有架构的基础上做了改造。
改造点
将数据面 kvm 及 libvirtd 等进程移出管理面的 virt-laucher 容器,物理机上的 libvirtd 进程管理此物理机上的所有虚拟机。
新增 virt-start-hook 组件用以对接网络组件、存储组件及 xml 的路径变动等。
重构虚拟机镜像制作和分发方式,借助于 OCS 的对象存储管理,实现镜像的快速分发。
除了实现管理面与数据面的分离,我们还在稳定性增强等方面做了很多工作。比如实现了 kubevirt 的每个组件不管在任何时间任何情况下失效、故障、异常了,都不会影响到正常虚拟机的运行,并且要求测试覆盖到这些组件异常情况下的测试;物理机重启后虚拟机可以正常恢复生命周期管理等生产级要求,进一步保障了整个虚拟机管理系统的稳定性。
4.2 改造后架构
4.3 架构改造后创建虚拟机流程
用户创建 vmi crd,kubectl create -f vmi.yaml
virt-controller watch 到新的 vmi 对象,为 vmi 创建对应的 virt-launcher pod
virt-launcher pod 创建好后,k8s 的调度器 kube-scheduler 会将其调度到符合条件的 kubevirt node 节点上
然后 virt-controller 会将 virt-launcher pod 的 nodeName 更新到 vmi 对象上
kubevirt node 节点 watch 到 vmi 调度到本节点后,会将虚拟机的基础镜像 mount 到指定位置,然后调用 virt-launcher 的 syncVMI 接口创建 domain
virt-launcher 接受到创建请求后,将 vmi 对象转变为 domain 对象,然后调用 virt-start-hook,根据 backingFile 创建 qcow2 虚拟机增量镜像磁盘,将 domain xml 中的相关路径转变为物理机上路径,请求网络,配置 xml,然后将最终配置好的 xml 返回 virt-launcher
virt-launcher 收到 virt-start-hook 的返回后,调用物理机上的 libvirtd 来 define domain xml 和 create domain
4.4 架构改造后删除虚拟机流程
用户执行删除 vmi 命令,kubectl delete -f vmi.yaml
virt-handler watch 到 vmi 的 update 事件,并且 vmi 的 deletionTimeStamp 不为空,调用 virt-launcher shutdownDomain,virt-launcher 调用 virt-start-hook 释放网络然后调用 libvirtd 关机
domain shutdown 的消息由 virt-launcher watch 到并发送给 virt-handler,virt-handler 根据 vmi 和 domain 已停机的状态调用 virt-launcher deleteDomain,virt-launcher 调用 virt-start-hook 删除网络然后调用 libvirtd undefineDomain
domain undefine 的消息由 virt-launcher watch 到并发送给 virt-handler,virt-handler 根据 vmi 和 domain 已删除的状态更新 vmi 添加 domain 已删除的 condition,然后清理该 domain 的垃圾文件及路径
virt-controller watch 到 vmi 状态 deleteTimeStamp 不为空,并且 vmi 的 condition DomainDeleted 为 True,则删除 virt-launcher pod,然后等 pod 删除后,清理 vmi 的 finalizer,使 vmi 自动删除
5. 存储方案
5.1 原生镜像存储方案
kubevirt 中虚拟机的原始镜像文件会 ADD 到 docker 基础镜像的/disk 路径下,并推送到镜像中心,供创建虚拟机时使用。
创建虚拟机时,会创建一个 vmi crd,vmi 中会记录需要使用的虚拟机镜像名称,vmi 创建好后 virt-controller 会为 vmi 创建对应的 virt-launcher pod,virt-launcher pod 中有两个 container,一个是运行 virt-launcher 进程的容器 compute,另一个是负责存放虚拟机镜像的容器 container-disk,container-disk 容器的 imageName 就是 vmi 中记录的虚拟机镜像名称。virt-launcher pod 创建后,kubelet 会下载 container-disk 的镜像,然后启动 container-disk 容器。container-disk 启动好后会一直监听在—copy-path 下的 disk_0.sock 文件,而 sock 文件会通过 hostPath 的方式映射到物理机上的路径/var/run/kubevirt/container-disk/vmiUUID/ 中。
virt-handler pod 会使用 HostPid,这样 virt-handler 容器内就可以看到物理机的 pid 和挂载信息。在创建虚拟机时,virt-handler 会根据 vmi 的 disk_0.sock 文件找到 container-disk 进程的 pid,标记为 Cpid,然后根据/proc/Cpid/mountInfo 找到 container-disk 容器根盘的磁盘号,然后根据 container-disk 根盘的磁盘号和物理机的挂载信息(/proc/1/mountInfo)找到 container-disk 根盘在物理机上的位置,再拼装上虚拟机镜像文件的路径/disk/xxx.qcow2,拿到虚拟机原始镜像在物理机上的实际存储位置 sourceFile,然后将 sourceFile mount 到 targetFile 上,供后面创建虚拟机时作为 backingFile 使用。
5.2 本地盘存储
原生 kubevirt 中根据基础镜像 backingFile 创建的增量镜像文件 xxx.qcow2 只支持放到 emptydir 中,而我们的容器的数据盘一般使用的是 lvm 的方式,如果保存两种使用方式的话,在虚拟机容器混合部署的场景中,不利于物理机磁盘的统一规划统一调度,因此我们在原生的基础上也支持了虚拟机增量镜像文件存放到由 virt-launcher 容器申请的 lvm 盘中,从而保持了虚拟机与容器磁盘使用方式的一致性。此外我们还支持了为虚拟机单独创建一个 qcow2 空盘挂载为数据盘使用,也存放在 virt-launcher 容器申请的另外的 lvm 盘中。
5.3 云盘存储
我们为虚拟机的系统盘和数据盘对接了云存储,方便用户在迁移或者某些其他场景下使用。
5.3.1 系统盘接入云盘
系统盘对接云存储,首先需要将虚拟机的基础镜像上传到 basic ns 下的 pvc 中,然后根据此 pvc 创建 volumesnapshot。而在某个 namespace 下创建虚拟机时,需要从 basic ns 下拷贝基础镜像的 volumesnapshot 到自己的 namespace 下,然后依据拷贝的 volumesnapshot 创建出新的 pvc 给虚拟机使用。其中上传虚拟机基础镜像到 basic namespace 下的 pvc 及做 snapshot 的步骤,我们做了一个上传镜像的工具来统一管理;而创建虚拟机时需要的系统盘 pvc 及将 pvc 挂载到 vmi 中的一系列操作,我们则是通过一个新定义的 crd,及新的 crd controller 来实现统一的自动化管理。
5.3.2 数据盘接入云盘
数据盘对接云存储,则是先在虚拟机所在 namespace 下创建 pvc,然后将 pvc 配置到 vmi 的 yaml 中,virt-controller 在创建 vmi 对应的 virt-launcher pod 时,会根据 vmi 中 pvc 的配置,将 pvc volume 配置到 virt-launcher pod 中,然后存储组件会挂载一个带有 pvc 信息的目录给 pod,之后 virt-start-hook 会根据 virt-launcher pod 中 pvc 目录中的信息将云盘配置到 domain 的 xml,供虚拟机使用。
6. 扩展功能
6.1 支持虚拟机停机/启动/重启
原生 kubevirt 提供了一些同步接口,比如 pause 和 unpause,分别的作用是将虚拟机挂起和唤醒。原生的 stop 和 start 需要操作 vm crd 会导致虚拟机销毁再重建,这无法满足我们的需求。另外由于原本的架构不支持虚拟机的 shutdown 和 start,因此也并未提供直接 stop 和 start 及 reboot 本虚拟机的接口(stop 即对应 shutdown)。而我们的用户有这个需求,由于经过架构改造后的 kubevirt 支持了虚拟机的 shutdown 和 start,因此我们也在 pause/unpause vmi 的基础上定义开发了虚拟机的 stop/start/reboot 等接口,并增加了 stopping,starting,rebooting 等中间状态,方便用户查看使用。
6.2 支持虚拟机静态扩容缩容 CPU/内存/本地盘
停机扩容缩容 CPU/Mem/本地盘,提供的也是同步接口。此功能在扩容时,最终修改虚拟机的 xml 配置之前,需要先动态扩容 virt-launcher pod 的相关资源以便于检查虚拟机所在节点上是否有足够的资源进行扩容,如果所在节点资源不足需要拦截本次扩容请求,并回滚对于 vmi 及 pod 等相关配置的相关修改。而动态扩容 pod 配置,原生 kubernetes 是不支持的,这是我们在内部的 k8s 中提供的另外一套解决方案。
6.3 支持虚拟机 Cpu 绑核及大页内存
cpu 绑核功能主要是结合了 kubelet 的 cpuset 功能来实现的,需要 kubelet 配置—cpu-manager-policy=static 开启容器的绑核功能。流程大概是这样的,vmi 配置上 cpu 的相关绑核配置 dedicatedCpuPlacement=”true”等,然后创建 guarantee 的 virt-launcher pod,virt-launcher pod 调度到开启了绑核配置的 kubelet 节点上,kubelet 为 virt-launcher pod 分配指定的 cpu 核,然后 virt-launcher 进程从自己的 container 中查看自己有哪些核,再将这些核配置到虚拟机 xml 中,从而通过 kubelet 管理 cpu 的方式实现了虚拟机和容器的 cpuquota 和 cpuset 的分配方式的统一管理。而虚拟机大页内存也是与 k8s 资源管理结合的思路,即通过使用 k8s 中已存在的大页内存资源,通过 pod 占用再分配给虚拟机的方式实现的。
6.4 其他功能
除了以上介绍的扩展功能外,我们还实现了支持虚拟机静态和动态增加或减少云盘、重置密码、查看虚拟机 xml、支持云盘只读限制、支持直通 GPU、直通物理机磁盘、virtionet 支持多队列、IP 显示优化等其他需求,供用户使用。
总结
当前我们已在多个集群中,同时提供了虚拟机和容器服务,实现了混合集群管理。基于此方案生产的虚拟机在我们的私有云领域已经提供给了众多业务使用,在稳定性及性能等方面都提供了强有力的保障。下一步主要的工作在于将容器和虚拟机在节点上能够实现混合部署,这样不仅在控制面上能够进行统一调度,在数据面上也能够进行混合管理。
另外除了本文介绍的工作之外,我们也实现了虚拟机快照,镜像制作和分发,静态迁移等方案,后续我们团队也会继续发文分享。
作者简介
Weiwei OPPO 高级后端工程师
主要从事调度、容器化、混合云等相关方向的工作。
获取更多精彩内容,搜索关注[OPPO 数智技术]公众号
版权声明: 本文为 InfoQ 作者【OPPO数智技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/cb1fd723ddf0643525607f35e】。文章转载请联系作者。
评论