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数智技术]公众号