虛擬機器與容器的混合管理實踐

OPPO數智技術發表於2021-11-10

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:
1)kubevirt API服務,kubevirt是以CRD的方式工作的,virt-api提供了自定義的api請求處理,可以通過virtctl命令執行同步命令 virtctl vnc/pause/unpause/stop/start vm等。
virt-controller:
1)與k8s api-server通訊監控VMI資源建立刪除等事件,並觸發相應操作
2)根據VMI定義建立virt-launcher pod,該pod中將會執行虛擬機器
3)監控pod狀態,並隨之更新VMI狀態
virt-handler:
1)執行在kubelet的node上,定期更新heartbeat,並標記”kubevirt.io/schedulable”
2)監聽在k8s apiserver當發現VMI被標記的nodeName與自身node匹配時,負責虛擬機器的生命週期管理
virt-launcher:
1)以pod形式執行
2)根據VMI定義生成虛擬機器模板,通過libvirt API建立虛擬機器
3)每個虛擬機器會對應獨立的libvirtd
4)與libvirt通訊提供虛擬機器生命週期管理

4. Kubevirt架構改造

4.1 原生架構


原生架構中管理面與資料面耦合。在virt-launcher pod中執行虛擬機器,當由於不確定原因(比如說docker的原因或物理機原因或者virt-launcher本身的掛掉升級等原因),造成virt-launcher容器退出後,會導致虛擬機器也退出,從而會影響使用者使用,增加了虛擬機器的穩定性風險。因此我們在原有架構的基礎上做了改造。

改造點:
1)將資料面kvm及libvirtd等程式移出管理面的virt-laucher容器,物理機上的libvirtd程式管理此物理機上的所有虛擬機器。
2)新增virt-start-hook元件用以對接網路元件、儲存元件及xml的路徑變動等。
3)重構虛擬機器映象製作和分發方式,藉助於OCS的物件儲存管理,實現映象的快速分發。

除了實現管理面與資料面的分離,我們還在穩定性增強等方面做了很多工作。比如實現了kubevirt的每個元件不管在任何時間任何情況下失效、故障、異常了,都不會影響到正常虛擬機器的執行,並且要求測試覆蓋到這些元件異常情況下的測試;物理機重啟後虛擬機器可以正常恢復生命週期管理等生產級要求,進一步保障了整個虛擬機器管理系統的穩定性。

4.2 改造後架構

4.3 架構改造後建立虛擬機器流程

1)使用者建立vmi crd,kubectl create -f vmi.yaml
2)virt-controller watch到新的vmi物件,為vmi建立對應的virt-launcher pod
3)virt-launcher pod建立好後,k8s的排程器kube-scheduler會將其排程到符合條件的kubevirt node節點上
4)然後virt-controller會將virt-launcher pod的nodeName更新到vmi物件上
5)kubevirt node節點watch到vmi排程到本節點後,會將虛擬機器的基礎映象mount到指定位置,然後呼叫virt-launcher的syncVMI介面建立domain
6)virt-launcher接受到建立請求後,將vmi物件轉變為domain物件,然後呼叫virt-start-hook,根據backingFile建立qcow2虛擬機器增量映象磁碟,將domain xml中的相關路徑轉變為物理機上路徑,請求網路,配置xml,然後將最終配置好的xml返回virt-launcher
7)virt-launcher收到virt-start-hook的返回後,呼叫物理機上的libvirtd來define domain xml和create domain

4.4 架構改造後刪除虛擬機器流程

1)使用者執行刪除vmi命令,kubectl delete -f vmi.yaml
2)virt-handler watch到vmi的update事件,並且vmi的deletionTimeStamp不為空,呼叫virt-launcher shutdownDomain,virt-launcher呼叫virt-start-hook釋放網路然後呼叫libvirtd關機
3)domain shutdown的訊息由virt-launcher watch到併傳送給virt-handler,virt-handler根據vmi和domain已停機的狀態呼叫virt-launcher deleteDomain,virt-launcher呼叫virt-start-hook刪除網路然後呼叫libvirtd undefineDomain
4)domain undefine的訊息由virt-launcher watch到併傳送給virt-handler,virt-handler根據vmi和domain已刪除的狀態更新vmi新增domain已刪除的condition,然後清理該domain的垃圾檔案及路徑
5)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數智技術]公眾號

相關文章