議實(shí)現(xiàn)虛擬機(jī)熱遷移的完整原理(含回滾機(jī)制))
RancherVM 源碼剖析③基于 QEMU QMP 協(xié)議實(shí)現(xiàn)虛擬機(jī)熱遷移的完整原理含回滾機(jī)制【免費(fèi)下載鏈接】old-vm(OBSOLETE) Package and Run Virtual Machines as Docker Containers項(xiàng)目地址: https://gitcode.com/gh_mirrors/ol/old-vmRancherVM一個(gè)將虛擬機(jī)打包并以 Docker 容器形式運(yùn)行在 Kubernetes 上的開(kāi)源項(xiàng)目Package and Run Virtual Machines as Docker Containers不僅能讓虛擬機(jī)像 Pod 一樣被調(diào)度還實(shí)現(xiàn)了真正的虛擬機(jī)熱遷移在不停機(jī)的情況下把正在運(yùn)行的虛擬機(jī)從一個(gè)節(jié)點(diǎn)搬到另一個(gè)節(jié)點(diǎn)。本文從源碼出發(fā)帶你完整理解它是如何利用QEMU QMP 協(xié)議完成遷移的以及遷移中途取消時(shí)回滾機(jī)制是如何兜底的。什么是虛擬機(jī)熱遷移熱遷移Live Migration指在虛擬機(jī)持續(xù)運(yùn)行的狀態(tài)下將其內(nèi)存、CPU 狀態(tài)、磁盤等遷移到目標(biāo)主機(jī)全程業(yè)務(wù)幾乎無(wú)感知。在 RancherVM 中觸發(fā)方式極其簡(jiǎn)單把 VirtualMachine 資源里的nodeName字段改成另一個(gè)節(jié)點(diǎn)控制器就會(huì)自動(dòng)開(kāi)始遷移。源碼中的狀態(tài)機(jī)為此新增了migrating狀態(tài)定義見(jiàn) pkg/apis/ranchervm/v1alpha1/types.gorunning→ 用戶修改nodeName后 →migrating→ 遷移完成回到running但落在新節(jié)點(diǎn)遷移中途用戶把nodeName改回原節(jié)點(diǎn) →觸發(fā)回滾虛擬機(jī)原地恢復(fù)運(yùn)行整體架構(gòu)一次熱遷移的 4 步走熱遷移涉及三個(gè)角色源節(jié)點(diǎn)上正在跑的 VM Pod、目標(biāo)節(jié)點(diǎn)上新建的 VM Pod以及一個(gè)專門執(zhí)行遷移指令的遷移 Job Pod。步驟動(dòng)作關(guān)鍵源碼① 狀態(tài)切換檢測(cè)到nodeName變化狀態(tài)置為migratingpkg/controller/vm/machine.go② 準(zhǔn)備目標(biāo)在目標(biāo)節(jié)點(diǎn)創(chuàng)建第二個(gè) VM Pod等待兩邊就緒pkg/controller/vm/migrate.go③ 執(zhí)行遷移創(chuàng)建 Job通過(guò) QMP 向源 QEMU 下發(fā)migrate命令pkg/qemu/job.go、pkg/qemu/client.go④ 清理收尾刪除舊 Pod 和 Job切換 VirtualMachine 歸屬pkg/controller/vm/migrate.go入口邏輯在pkg/controller/vm/machine.go的start()中當(dāng)發(fā)現(xiàn) VM 處于運(yùn)行狀態(tài)、但期望節(jié)點(diǎn)與 Pod 實(shí)際所在節(jié)點(diǎn)不一致時(shí)就調(diào)用migrateMachine()。核心原理如何與 QEMU 對(duì)話QMP 協(xié)議QMPQEMU Machine Protocol是 QEMU 提供的JSON over Socket管理接口。每個(gè) VM Pod 內(nèi)都會(huì)暴露一個(gè) Unix Socket形如vm名稱_monitor.sockRancherVM 的所有 QEMU 操作都通過(guò)它完成。1?? 建立連接三步握手客戶端實(shí)現(xiàn)非常精巧見(jiàn) pkg/qemu/client.goNewMonitorClient()通過(guò) Unix Socket 連接到 QEMU 監(jiān)控端讀取 QEMU 主動(dòng)發(fā)來(lái)的greeting 消息聲明 QMP 能力立即發(fā)送qmp_capabilities命令完成能力協(xié)商——這一步是 QMP 協(xié)議的硬性要求之后才能執(zhí)行真正的操作命令。連接命令的拼裝邏輯在 pkg/qemu/commands.go所有命令都是統(tǒng)一的 JSON 結(jié)構(gòu){execute: 命令名, arguments: { ... 參數(shù) ... }}2?? 下發(fā)遷移指令migrate(uri)命令把源 VM 的全部狀態(tài)通過(guò)目標(biāo)端提供的TCP 地址tcp:目標(biāo)PodIP:遷移端口推送過(guò)去{execute: migrate, arguments: {uri: tcp:10.42.0.7:43501, detach: true}}detach: true表示 QEMU 異步執(zhí)行遷移客戶端立刻返回之后靠輪詢查進(jìn)度。值得一提的是readReply()的實(shí)現(xiàn)細(xì)節(jié)它從 512 字節(jié)緩沖區(qū)起步每讀滿一次就翻倍512 → 1024 → …直到某次讀取不滿從而優(yōu)雅地應(yīng)對(duì)任意長(zhǎng)度的 JSON 應(yīng)答。3?? 輪詢遷移進(jìn)度query-migrate真正的遷移引擎在 pkg/qemu/migrate.go 的Migrate()方法它每秒輪詢一次邏輯清晰得像一段狀態(tài)機(jī)每秒循環(huán) ├─ readSilently() // 靜默吞掉 QEMU 的事件推送如 STOP 事件避免污染應(yīng)答流 ├─ query-migrate // 查詢遷移狀態(tài) ├─ status active → 記錄已傳輸內(nèi)存 / 總內(nèi)存 / 速率估算剩余時(shí)間 ├─ status completed → 遷移成功返回 └─ status failed → 遷移失敗報(bào)錯(cuò)兩個(gè)值得學(xué)習(xí)的細(xì)節(jié)readSilently()給連接設(shè)一個(gè) 100ms 的讀超時(shí)把 QEMU 主動(dòng)推送的事件悄悄讀走否則事件消息會(huì)混進(jìn)query-migrate的應(yīng)答導(dǎo)致 JSON 解析錯(cuò)亂——這是用長(zhǎng)連接驅(qū)動(dòng) QEMU 時(shí)最容易踩的坑剩余時(shí)間估算用已耗時(shí) × 總量 / 已傳輸量 - 已耗時(shí)做瞬時(shí)速率推算簡(jiǎn)單實(shí)用。遷移 Job把 QMP 客戶端打包成 K8s Job控制器本身跑在集群里、夠不到節(jié)點(diǎn)上的 QEMU Socket所以它采用了一個(gè)巧妙的設(shè)計(jì)創(chuàng)建一個(gè)一次性 Kubernetes Job通過(guò) Pod 親和性把它調(diào)度到源 VM 所在的節(jié)點(diǎn)上詳見(jiàn) pkg/qemu/job.go 的NewMigrationJob()。這個(gè) Job Pod 有 3 個(gè)關(guān)鍵配置Pod 親和性TopologyKey: kubernetes.io/hostname 源 Pod 標(biāo)簽保證遷移 Pod 和源 VM 落在同一臺(tái)機(jī)器掛載 hostPath把節(jié)點(diǎn)上 VM 狀態(tài)目錄掛到/vm這樣就能摸到 QEMU 的_monitor.sock執(zhí)行參數(shù)/ranchervm -migrate -sock-path /vm/pod_monitor.sock -target-uri tcp:目標(biāo)IP:端口對(duì)應(yīng) cmd/main.go 中的migrate分支——它會(huì)創(chuàng)建MonitorClient并直接調(diào)用Migrate()。目標(biāo)端口哪來(lái)的新建的目標(biāo) VM Pod 在創(chuàng)建時(shí)addMigratePort()見(jiàn) pkg/controller/vm/util.go會(huì)隨機(jī)分配一個(gè) 32768~65535 之間的端口寫入環(huán)境變量MIGRATE_PORT和注解migrate_port。源 QEMU 遷移時(shí)就把內(nèi)存狀態(tài)流推送到這個(gè)端口目標(biāo)端的 QEMU 早已在該端口監(jiān)聽(tīng)并接收最終完成 CPU/設(shè)備狀態(tài)的切換?;貪L機(jī)制遷移失敗或用戶取消怎么辦這是本設(shè)計(jì)最有工程味的部分核心在 pkg/controller/vm/migrate.go 的migrateMachine()// Check if the user canceled mid-migration if oldPod ! nil machine.Spec.NodeName oldPod.Spec.NodeName machine.Status.State api.StateMigrating { return ctrl.migrateRollback(machine, newPod) }只要用戶把nodeName改回原節(jié)點(diǎn)控制器在下一次調(diào)和循環(huán)就會(huì)發(fā)現(xiàn)期望節(jié)點(diǎn) 舊 Pod 所在節(jié)點(diǎn)判定為用戶取消隨即執(zhí)行migrateRollback()做三件事刪除還在跑的遷移 Job源端 QEMU 停止向目標(biāo)推流遷移自然終止刪除目標(biāo)節(jié)點(diǎn)上已創(chuàng)建的影子 Pod把 VirtualMachine 狀態(tài)從migrating恢復(fù)為running。這個(gè)機(jī)制之所以零數(shù)據(jù)風(fēng)險(xiǎn)是因?yàn)?QEMU 熱遷移本身的語(yǔ)義就保證了遷移未成功完成時(shí)源端 VM 一直是主執(zhí)行者目標(biāo)端只是接收副本任何時(shí)刻掐掉遷移業(yè)務(wù)都還活在源節(jié)點(diǎn)。RancherVM 的回滾只是把這個(gè)天然安全的狀態(tài)顯式地整理干凈。遷移成功后的清理migrationCleanup()則負(fù)責(zé)收尾更新 VirtualMachine 關(guān)聯(lián)新 Pod、刪除源 Pod、刪除遷移 Job最后還會(huì)刪掉noVNC 控制臺(tái) Pod——因?yàn)榭刂婆_(tái)也是通過(guò)節(jié)點(diǎn)上的 Unix Socket 連接 QEMU 的VM 換了節(jié)點(diǎn)后必須重建。小結(jié)這套設(shè)計(jì)值得借鑒的 3 個(gè)地方協(xié)議層極薄pkg/qemu/包不到 300 行就把 QMP 連接、命令封裝、進(jìn)度輪詢寫得干凈利落是學(xué)習(xí)用長(zhǎng)連接驅(qū)動(dòng)外部守護(hù)進(jìn)程的好樣例編排層復(fù)用 K8s 原語(yǔ)親和性保證同機(jī)執(zhí)行、Job 承載一次性任務(wù)、Informers 的事件驅(qū)動(dòng)調(diào)和——遷移全程沒(méi)有一行自定義調(diào)度代碼冪等 可取消控制器每一步建目標(biāo) Pod、查 Job 是否存在、狀態(tài)比對(duì)都是冪等檢查任意時(shí)刻被中斷都能從狀態(tài)機(jī)里自愈用戶改回nodeName即回滾體驗(yàn)上遷移和回退完全對(duì)稱。至此RancherVM 熱遷移的完整鏈路——nodeName 變更 → 狀態(tài)機(jī)流轉(zhuǎn) → 目標(biāo) Pod 準(zhǔn)備 → QMP 下發(fā) migrate → 輪詢 query-migrate → 清理 / 回滾——就全部講完了。【免費(fèi)下載鏈接】old-vm(OBSOLETE) Package and Run Virtual Machines as Docker Containers項(xiàng)目地址: https://gitcode.com/gh_mirrors/ol/old-vm創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考