文章目錄
- 存儲熱遷移流程
- 總體流程
- 代碼路徑
- QEMU Block layer
- 架構簡述
- Block Job
- 結構體設計
- 狀態轉換
- Mirror block job
- 拓撲結構
- 構建過程
- 數據結構
存儲熱遷移流程
總體流程
Libvirt migrate 命令提供 copy-storage-all 選項支持存儲熱遷移,相應地,Libvirt 熱遷移 API
(virDomainMigrateToURIX) 提供 VIR_MIGRATE_NON_SHARED_DISK flag 支持上層應用發起存儲熱遷移。下圖為存儲熱遷移的簡要流程圖:
步驟如下:
- Libvirt 解析熱遷移 API 的 flags 參數攜帶有 VIR_MIGRATE_NON_SHARED_DISK,首先通過 drive-mirror qmp 命令發起存儲熱遷移。
- QEMU 收到 drive-mirror 命令,內部按照 block job 的規程實現源端磁盤數據拷貝到目的端的工作。直到源端磁盤沒有臟數據且 in flght IO 也沒有,QEMU 發布一個 BLOCK_JOB_READY 事件,表示自己已經完成磁盤的數據遷移動作,隨時可以接受終止命令。 在此之前,QEMU 會將源端磁盤的增量 IO 同步到目的端。
- Libvirt 在發起存儲熱遷移后等待 QEMU 的 BLOCK_JOB_READY 事件,收到該事件后發起內存熱遷移。可以看到,內存熱遷移是在 block job 處于 ready 狀態后發起的。之后 Libvirt 監聽 switchover 事件等待熱遷移完成。
- QEMU 收到 migrate 命令后,發起內存熱遷移。熱遷移滿足進入最后一輪條件后,發布 migration pre-switchover 事件。
- Libvirt 收到 switchover 事件后,首先是發送 bllock-job-cancel 命令終止 drive-mirror 的 block job,等待 QEMU 的 BLOCK_JOB_COMPLETED 事件。
- QEMU 收到 bllock-job-cancel 命令后,將新產生的增量 IO 和 in flight IO 同步到目的磁盤,發布終止 BLOCK_JOB_COMPLETED 標志 drive-mirror 完成。
- Libvirt 收到 BLOCK_JOB_COMPLETED 事件后,發送 migrate-continue 命令通知 QEMU 進行 switchover。等待 QEMU 發布 migration completed 事件。
- QEMU 收到 migrate-continue 命令,發起熱遷移 switchover,完成后發布遷移完成事件。
- Libvirt 收到完成事件,遷移結束。
熱遷移的 switchover 的簡要邏輯參考下圖:
代碼路徑
- Libvirt 關鍵路徑與邏輯
client:
cmdMigrateif (vshCommandOptBool(cmd, "copy-storage-all"))flags |= VIR_MIGRATE_NON_SHARED_DISK;virDomainMigrateToURI3virDomainMigrateUnmanagedParams -> virDomainMigrateUnmanagedProto3
rpc:
qemuDomainMigratePerform3qemuMigrationSrcPerformqemuMigrationSrcPerformPhaseqemuMigrationSrcPerformNativeqemuMigrationSrcRun
關鍵函數:
qemuMigrationSrcRunif (flags & VIR_MIGRATE_NON_SHARED_DISK) {migrate_flags |= QEMU_MONITOR_MIGRATE_NON_SHARED_DISK;cookieFlags |= QEMU_MIGRATION_COOKIE_NBD;}if (migrate_flags & (QEMU_MONITOR_MIGRATE_NON_SHARED_DISK |QEMU_MONITOR_MIGRATE_NON_SHARED_INC)) {/* 調用 drive-mirror 發起存儲熱遷移,等待 QEMU BLOCK_JOB_READY 事件 */qemuMigrationSrcNBDStorageCopy...}/* 調用 migrate 發起內存熱遷移 */qemuMonitorMigrateToFd/* 等待 QEMU migrate pre-switchover 事件 */qemuMigrationSrcWaitForCompletion/* 內存熱遷移進入 switchover 前夕,終止存儲熱遷移 */if (mig->nbd) {qemuMigrationSrcNBDCopyCancel}/* 調用 migrate-continue 觸發熱遷移 switchover */qemuMigrationSrcContinue/* 等待 QEMU migrate completed 事件 */qemuMigrationSrcWaitForCompletion
- QEMU 關鍵路徑與邏輯
qmp_drive_mirrorbs = qmp_get_root_bsaio_context = bdrv_get_aio_context(bs)target_bs = bdrv_openbdrv_try_change_aio_context(target_bs, aio_context, NULL, errp)blockdev_mirror_commonmirror_start/* 傳入 mirror_job_driver,發起 mirror job */mirror_start_job
job 創建關鍵函數:
mirror_start_job/* 實例化 mirror filter driver,生成一個 BlockDriverState */mirror_top_bs = bdrv_new_open_driver(&bdrv_mirror_top, filter_node_name,BDRV_O_RDWR, errp)/* 清空源端磁盤上處于隊列中的待落盤 IO,等待其結束 */bdrv_drained_begin(bs)/* 將 mirror filter 加到源磁盤的 root bs 之上,接管源磁盤的 IO 回調處理 */ret = bdrv_append(mirror_top_bs, bs, errp)/* 允許源端磁盤上有 IO 排隊 */bdrv_drained_end(bs)/* 創建一個 job 并建立一個基于 job block 樹:/* 將 mirror filter 作為樹的第一個葉子節點,命名為 "main node" */block_job_create/* block job 已跟蹤 mirror filter,解引用 */bdrv_unref(mirror_top_bs)/* 新建一個目的端磁盤的 BlockBackend */s->target = blk_new/* 將新建的 BlockBackend 的 root bs 指向目的磁盤 bs */blk_insert_bs(s->target, target, errp)/* 在 job block 樹上添加第二個葉子節點,命令為 "source" */ret = block_job_add_bdrv(&s->common, "source", bs, 0,BLK_PERM_WRITE_UNCHANGED | BLK_PERM_WRITE |BLK_PERM_CONSISTENT_READ,errp)/* 在 job block 樹上添加第二個葉子節點,命令為 "target" */block_job_add_bdrv(&s->common, "target", target, 0, BLK_PERM_ALL,&error_abort)/* 調用 job 的 run 方法: mirror_run */job_start
QEMU Block layer
架構簡述
- Block layer 層次
Block layer 介于響應 Guest IO 的 block device 之下、主機存儲介質之上的一層 QEMU 組件,它負責處理 Guest IO 并最終將 IO 數據按照預期存儲到主機存儲介質上。如下圖所示:
直接將 Guest IO 透傳到主機存儲介質不行嗎?QEMU 為什么要引入 Block layer 軟件層增加處理邏輯?
前者的答案是可以;后者的答案是在保證修改影響最小的前提下引入對高級存儲特性的支持,比如磁盤級別的限速、加密、鏡像、備份等。 存儲的特性在 Block layer 內部提供的基礎軟件框架上實現并對外(Guest、host) 保持行為不變。 - Block layer 功能
基于 Block layer 的層次分析,我們可以推斷 Block layer 需要具備以下基本功能:
- 接收 Guest IO 數據流
- 定義 Guest IO 數據流在 Block layerr 的行為
- 將 Guest IO 數據流寫入主機存儲介質或從中讀出
基于當前 QEMU 的 Block layer 實現以及上面的推斷,Block layer 的具體功能包括: - 操縱 Guest IO 數據流實現存儲特性,比如:加密、限速和備份等
- 翻譯鏡像格式,即按照約定的鏡像格式將 IO 數據與 metadata 數據寫入主機存儲介質或從中讀出,這里的鏡像格式包括 qcow2、raw 等。
- 將 Block layer 層加工后的 IO 數據按照訪問協議寫入主機存儲介質或從中讀出,這里的協議包括 nbd、iscsi、file 等。
- Block layer tree
基于對 Block layer 層的功能分析,QEMU 將 Blocker layer 軟件基礎框架抽象為樹,其中樹的根離 Guest 最近,樹的葉子節點離 Host 最近,每個層次中節點角色不同,實現的功能也不同,如下:
Block Job
結構體設計
QEMU 設計了 Job 結構體,job 生命周期各階段通過狀態來描述, job 有如下狀態,參考官方文檔:
- undefined – Erroneous, default state. Should not ever be visible.
- created – The job has been created, but not yet started.
- running – The job is currently running.
- paused – The job is running, but paused. The pause may be requested by either the QMP user or by internal processes.
- ready – The job is running, but is ready for the user to signal completion. This is used for long-running jobs like mirror that are designed to run indefinitely.
- standby – The job is ready, but paused. This is nearly identical to paused. The job may return to ready or otherwise be canceled.
- waiting – The job is waiting for other jobs in the transaction to converge to the waiting state. This status will likely not be visible for the last job in a transaction.
- pending – The job has finished its work, but has finalization steps that it needs to make prior to completing. These changes will require manual intervention via job-finalize if auto-finalize was set to false. These pending changes may still fail.
- aborting – The job is in the process of being aborted, and will finish with an error. The job will afterwards report that it is concluded. This status may not be visible to the management process.
- concluded – The job has finished all work. If auto-dismiss was set to false, the job will remain in this state until it is dismissed via job-dismiss.
- null – The job is in the process of being dismantled. This state should not ever be visible externally.
- QEMU 設計了 JobDriver 結構體,Job 通過執行 JobDriver 中的方法將 Job 從一個狀態驅動到另一個狀態。
- BlockJob 繼承 Job 結構體,BlockJobDriver 繼承 JobDriver,用于實現塊設備的 Job。QEMU BlockJob 和 BlockJobDriver 結構體為基礎,實現了塊設備的高級特性,比如 mirror、duplicate 等。
- Job、JobDriver、BlockJob、BlockJobDriver 4 個結構體的定義和關系如下:
狀態轉換
- Job 的通用狀態轉換如下圖,其中 event 表示 QEMU 內部的狀態轉換:
有幾個點需要特別解釋:
- READY 狀態對于 drive-mirror 來說是一種完成的狀態,drive-mirror job 進入 READY 狀態時會發送同時發送 BLOCK_JOB_READY 事件。READY 狀態的 drive-mirror job 可以隨時可以接收 block-job-cancel 命令,這里的 block-job-cancel 命令并非字面意義上的取消任務,而是結束。QEMU 會在 block-job-cancel 命令的處理邏輯中發布 BLOCK_JOB_COMPLETED 事件,表示 drive-mirror job 結束。
- job-pause 停止任務時,處于 RUNNING 狀態的任務會變為 PAUSED;處于 READY 狀態的任務會變成 STANDBY。
- job-finalize 的作用是讓處于工作完成(PENDING)狀態的 job 通過用戶命令進入 QEMU 定義上的任務完成,設計這樣一個接口可以方便上層應用在 QEMU job 進入完成狀態之前可以執行一些準備命令。如果應用沒有需要,可以在發起 block job 時設置 auto-finalize 為 true 讓 QEMU 跳過等待用戶下發 job-finalize 的步驟,直接進入 QEMU 定義的完成狀態(CONCLUDED)。
- job-dismiss 的作用是清理 QEMU 內部維護的 Job 數據結構,沒有其它實質動作,job-dismiss 下發后 job 將不再能夠通過 qmp block-job-query 查詢到。
- Block job 狀態轉換如下圖所示,以 drive-mirror 功能為例:
Mirror block job
拓撲結構
處于 mirror 狀態的 Block layer tree 拓撲如上:
- MirrorBlockJOb 結構體繼承自 BlockJob
- MirrorBlockJOb 包含源、目的兩端的 BlockBackend
- 目的端 BlockBackend 通過 MirrorBlockJOb 結構體的 target 的跟蹤
- 源端 BlockBackend 通過 mirror node 間接引用 —— mirror node 作為 filter node 被插入到源端 BlockBackend 和 root bs 之間,代替 root bs 作為源端 BlockBackend 的子節點,Mirror node 子節點指向先前的 root bs
構建過程
drive-mirror 實現中涉及到的 mirror block layer tree 的構建邏輯分四個步驟,如下:
- 初始階段,打開源端和目的端磁盤的 bs (BlockDriverState)
- 根據參數中源磁盤的 node-name 打開源磁盤
BlockDriverState *bs = qmp_get_root_bs(arg->device, errp)
- 獲取包含源磁盤 bs 的 IO 回調上下文
AioContext *aio_context = bdrv_get_aio_context(bs)
- 根據參數中目標磁盤的 node-name 打開目標磁盤
target_bs = bdrv_open(arg->target, NULL, options, flags, errp)
- 將源磁盤的 IO 回調設置到目標磁盤上
bdrv_try_change_aio_context(target_bs, aio_context, NULL, errp)
- 插入 mirror filter node 到源磁盤的 block layer tree
- 打開一個基于 bdrv_mirror_top BlockDriver 驅動的 bs
BlockDriverState *mirror_top_bs = bdrv_new_open_driver(&bdrv_mirror_top, filter_node_name,BDRV_O_RDWR, errp)
- 將 mirror filter node 插入到源磁盤 BlockBackend 與 bs 之間,取代 bs 作為 BlockBackend 的子節點
bdrv_append(mirror_top_bs, bs, errp)
- 創建 BlockJob
MirrorBlockJob *s = block_job_create(job_id, driver, NULL, mirror_top_bs,BLK_PERM_CONSISTENT_READ,BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED |BLK_PERM_WRITE, speed,creation_flags, cb, opaque, errp)
- BlockJob 維護了 mirror filter node,因此刪除 qmp_drive_mirror 函數的引用
bdrv_unref(mirror_top_bs)
- 創建目標磁盤的 BlockBackend
- 基于源端磁盤的 IO 上下文新建一個 BlockBackend
s->target = blk_new(s->common.job.aio_context, target_perms, target_shared_perms)
- 將新建的 BlockBackend 指向目標磁盤
blk_insert_bs(s->target, target, errp)
- BlockJob 管理源端磁盤和目標端磁盤
- 將源端磁盤添加到 BlockJob 維護的 tree
block_job_add_bdrv(&s->common, "source", bs, 0,BLK_PERM_WRITE_UNCHANGED | BLK_PERM_WRITE |BLK_PERM_CONSISTENT_READ,errp)
- 將目標端磁盤添加到 BlockJob 維護的 tree
block_job_add_bdrv(&s->common, "target", target, 0, BLK_PERM_ALL, &error_abort)
數據結構
我們通過分析一個正在 drive-mirror 虛機的 QEMU 進程 core 文件進一步了解 Block layer 相關數據結構。
-
查詢當前虛機正在運行的所有 Job
-
查詢第一個 BlockJob 維護所有的 BdrvChild 節點
-
在源磁盤 Block layer 中查詢 mirror filter node 的葉子節點
可以看到,mirror filter node 下一級 node 屬性為 child_of_bds,而 BlockJob 維護的的 node 屬性為 child_job,BdrvChild klass 的不同決定了做添加和刪除節點操作時調用的回調函數(attach、detach、drain)不同
- 查看源端磁盤完整的 Block layer node
可以看到 mirror node 的 backing 指向了 format node,format node 的 file 指向了 protocol node