⚠️ 本文經由 AI 整理並撰寫。
假設你正在替一個虛擬化平台設計網路資料路徑。起初,讓 QEMU 處理虛擬網卡的 I/O 很直覺:裝置由軟體管理,狀態也容易掌握。但當封包率提高,Host CPU 花在描述符處理、通知與封包搬運上的成本,就會逐漸變得明顯。
把這些工作交給核心、專用的使用者空間程式,甚至網卡硬體,似乎是自然的下一步。然而,雲端平台不只需要「傳得快」,還需要在主機維護、資源調度時,把正在執行的虛擬機搬到另一台主機。當裝置可以繞過 QEMU,直接改寫 Guest 記憶體,QEMU 又該如何知道哪些資料必須重新傳送?到了交接那一刻,又由誰確認封包處理到哪裡?
這正是 VirtIO、vhost、vDPA 與 Shadow Virtqueue(SVQ)串在一起的原因:它們不只是不同的加速技術,更是在重新分配資料處理工作,以及對執行狀態的控制權。
本文以 Linux Host 上的 QEMU/KVM 與 virtio-net 為例,先從一個封包如何離開 Guest 談起,再沿著硬體卸載、髒頁追蹤與熱遷移,理解這些技術各自解決了什麼問題。具體功能與限制仍須對照實際的核心、QEMU、驅動及韌體版本。
一、先看一個封包:VirtIO 統一的是介面,不是後端
Guest 裡的應用程式送出封包後,作業系統最終會把工作交給網卡驅動。在 VirtIO 架構中,這個驅動不必理解某一家實體網卡的私有佇列格式,而是使用標準的 VirtIO 介面,透過 Virtqueue(VQ) 與裝置交換工作。
這裡容易產生第一個誤會:Virtqueue 並不是一個裝滿封包內容的環形儲存區。以 Split Virtqueue 為例,它主要包含三種結構:
| 結構 | 用途 |
|---|---|
| Descriptor Table | 描述 buffer 的位址、長度、存取方向與鏈結關係 |
| Available Ring | 告訴裝置有哪些描述符鏈可以處理 |
| Used Ring | 由裝置回報哪些描述符鏈已完成,以及完成長度 |
真正的封包內容稱為 payload,通常放在描述符指向的另一塊 Guest buffer。換句話說,Virtqueue 傳遞的是「去哪裡讀寫資料」以及「工作完成了沒有」,而不等同於資料本身。
具體而言,一個 Split descriptor 的欄位配置如下;這裡列的是欄位寬度,實際讀寫還必須遵守規範的位元組序與對齊要求:
| 欄位 | 大小 | 意義 |
|---|---|---|
addr | 64 bits | buffer 位址 |
len | 32 bits | buffer 長度 |
flags | 16 bits | 描述符方向、是否還有下一節,以及是否為間接描述符表 |
next | 16 bits | 描述符鏈中下一節的索引 |
四個欄位合計 16 bytes。VRING_DESC_F_WRITE 表示裝置可以寫入 buffer,而不是 Guest 可以寫入;TX payload 通常供裝置讀取,RX buffer 則供裝置寫入。VRING_DESC_F_NEXT 用於鏈結多個描述符,VRING_DESC_F_INDIRECT 則讓描述符指向另一張描述符表。一個封包可能跨多個 buffers,不能把「一個封包」等同於「一個 16 bytes 描述符」。
Guest 在 available ring 放入描述符鏈的 head index,再推進 avail->idx;裝置消費後,在 used ring 回報 head ID 與寫入長度,再推進 used->idx。這些索引描述的是發布與完成進度,並不是 payload 的位址。像 last_avail_idx、last_used_idx 這類軟體或後端狀態,則用來記住各自已處理到哪裡;它們的語意不能只靠名稱推斷,更不能在兩組不同佇列之間直接照抄。
發送時,Guest 驅動準備好 payload 與描述符,依 VirtIO 的記憶體排序規則發布工作,必要時再通知後端有新工作,這個通知稱為 Kick。後端處理完後更新完成資訊,必要時通知 Guest,這個方向的通知通常稱為 Call。通知可以被抑制或批次處理,因此不能把每個封包都想成固定經歷一次 VM-exit 與一次中斷。
這裡的記憶體排序並非枝節:必須先讓描述符與資料可見,再發布新的 available index;讀取完成資訊時,也必須確保裝置先前寫入的內容已可見。即使 Guest、QEMU 與裝置共享同一份 RAM,也不代表可以省略 memory barrier。Packed Virtqueue 會把描述符及 available / used 狀態放進不同於 Split Ring 的配置,後續談到 SVQ 時,必須另外核對它支援的 ring 格式。
如果後端由 QEMU 實作,那麼裝置模型與主要的 VirtIO 後端資料處理都在 QEMU 使用者空間。但 VirtIO 並沒有要求後端一定是 QEMU:只要維持 Guest 所看到的介面與語意,後端就能換成另一種實作。
這個前後端分離的特性,讓我們不必先改 Guest 驅動,就能開始調整 Host 的 I/O 路徑。
二、把工作移出 QEMU:vhost 解決了什麼?
要理解 vhost,先把虛擬裝置的工作分成兩類。**控制面(Control Plane)**負責裝置建立、功能協商、佇列設定、記憶體映射,以及停止、重設與恢復;**資料面(Data Plane)**則持續讀取描述符、處理 payload、更新完成狀態並發送通知。
前者關心「這個裝置如何運作」,後者關心「這一批工作如何完成」。vhost 的核心價值,就是讓後者不必都由 QEMU 執行。
最常見的例子是 vhost-net。QEMU 仍然向 Guest 呈現 VirtIO 網卡、完成必要設定,但主要的 VirtIO 資料處理移到 Host 核心,由核心後端對接 TAP 等網路路徑。這減少了資料路徑對 QEMU 使用者空間的依賴,不代表整條核心網路路徑從此沒有 CPU 或資料複製成本。
在 Linux 中,這條路徑由 vhost_net 模組與 vhost worker 等核心機制支援,相關實作可從 drivers/vhost/net.c 追蹤。Guest 的通知可以經核心註冊的 eventfd 路徑交給後端,不必每次先喚醒 QEMU。這仍是軟體資料面:核心要執行描述符處理及相應網路工作,並非網卡自動理解了 VirtIO。
另一種選擇是 vhost-user:QEMU 透過 Unix domain socket 與獨立的使用者空間後端交換控制訊息,設定共享記憶體、佇列及通知通道。真正的資料處理則由外部程式執行,例如 OVS-DPDK;儲存場景也有 SPDK 等應用。
Socket 在這裡主要傳遞控制訊息與檔案描述符,不是逐包傳送 payload。後端透過可共享的記憶體 backing,例如適當配置的 memfd 或檔案映射,存取 Guest RAM,並使用協定設定的 kick / call file descriptors 協調工作。控制通道與資料所在的共享記憶體分開,才是它能避免讓 QEMU 成為逐包中繼站的原因。
兩者可以用同一張圖理解:Guest 端不變,改變的是誰消費 Virtqueue。
DPDK 很快,為什麼還需要硬體卸載?
vhost-user 經常與 DPDK 一起出現,但兩者不是同義詞。vhost-user 是後端整合協定;DPDK 是使用者空間資料面開發套件,其 Poll Mode Driver(PMD)讓應用程式直接管理網卡資料面佇列,通常繞過一般核心網路協定棧,以輪詢與批次處理降低開銷。vhost-user 本身不強制使用 DPDK,也不強制輪詢。
這種設計把 CPU 使用方式從「等待通知後處理」轉成「主動檢查工作」。Busy polling 能在高負載下提供良好延遲與吞吐量,但通常也需要保留專用核心。DPDK 另有中斷、節能與混合模式;反過來,核心網路路徑也有 NAPI、批次處理與 offload,不能只用「輪詢對中斷」判斷兩者效能。
此外,繞過一般核心網路協定棧不等於全鏈路零複製。實體網卡的 DPDK buffer 與 Guest buffer 可能是兩套記憶體,後端仍可能需要搬運封包;是否能避免複製,要看 buffer 管理、offload 與實際資料路徑。
以 OVS-DPDK 的接收路徑為例,可以把工作拆成以下幾段:
其中「PMD buffer → Guest RX buffer」是否需要 memcpy,是獨立於「網卡能不能 DMA」的另一個問題。PMD 也不一定完全排除核心參與:有些裝置透過 VFIO 綁定,有些採核心與使用者空間協作的驅動模式。Hugepages 常用來配置資料面記憶體,但實際需求仍取決於 PMD 與記憶體模式。
因此,即使軟體方案已經足夠快,平台仍可能希望把資料處理交給硬體,以回收 Host CPU。假設一台 64 核心主機保留 8 核心給軟體資料面,就占了核心數的 12.5%。這只是容量計算示例,卻指出一個重要問題:達到相同吞吐量,需要付出多少可供工作負載使用的 CPU?
vDPA 就是在這個問題上,往硬體卸載再走一步。
DPU 的價值不只在吞吐量
網路從 25G 提升到 100G、200G 或 400G 後,Host 面臨的可能不只是頻寬,還有每秒封包數、快取污染、記憶體頻寬及每包處理成本。相同頻寬下,小封包通常需要更多次描述符與轉送處理;因此不能只用鏈路速度推算需要保留多少 CPU,也不能替軟體資料面設定一個通用的吞吐量上限。
DPU / SmartNIC 可以把支援的網路、儲存、安全或管理工作移到獨立的處理核心與加速引擎。這除了回收 Host CPU,也可能讓基礎設施控制與租戶工作負載位於不同的執行域,並支援裸金屬服務的管理架構。但實際隔離邊界取決於平台設計,不能只因為用了 DPU,就認定所有功能都已硬體化或安全問題都已消失。
因此,DPU 是較廣的基礎設施卸載平台,vDPA 則是本文用來接上 VirtIO 相容資料面的機制。兩者可以搭配,卻不能畫上等號。
三、vDPA:保留 VirtIO 介面,把資料面交給相容裝置
**vDPA(vhost Data Path Acceleration)**提供管理 VirtIO 相容資料面裝置的抽象框架。在硬體 vDPA 的情境下,裝置能處理 VirtIO 相容佇列,並透過 DMA 直接存取 Guest buffer,不必由 Host 軟體逐一搬運 payload。
不過,vDPA 與 vhost-vdpa 必須分開看。前者是裝置框架,後者是 QEMU 等使用者空間程式接入該框架的一種方式。典型 Linux 路徑如下:
這個分層把廠商差異留在 Host。QEMU 使用通用後端介面,廠商驅動再把設定轉換成硬體所需的操作。Linux 的 drivers/vhost/vdpa.c 實作 vhost-vdpa 接口,vDPA 框架與廠商驅動則另位於 drivers/vdpa/,所以不宜單純把 vDPA 當成 vhost 的子集。
廠商驅動要銜接的並不只有封包收發,還包括取得及設定 feature bits、queue size、ring 位址、queue ready 狀態、通知與記憶體映射,以及後端所支援的佇列狀態保存 / 恢復。硬體資料面可由 ASIC、FPGA 或可程式化引擎實作;判斷重點是它能否提供所需的 VirtIO 語意,而不是產品是否被冠上 SmartNIC 或 DPU 的名稱。
這也不是唯一的整合方式:vDPA 裝置可以透過 virtio-vdpa 接入 Host 的 VirtIO 子系統;DPDK 也有使用者空間 vDPA 驅動,可經 vhost-user 與 QEMU 整合。Linux 甚至提供軟體 vDPA 模擬裝置。因此,下文所說的「硬體直接處理」,特指硬體 vDPA,而非 vDPA 框架必然具有的屬性。
封包不經 QEMU,為什麼還需要 QEMU?
因為「處理封包」與「管理虛擬裝置」是兩件事。
在本文的 QEMU/KVM 架構中,Guest 看到的仍是 QEMU 呈現的標準 VirtIO 裝置,例如 virtio-net-pci。QEMU 協調功能協商、佇列數量、Guest 記憶體註冊、DMA 映射與通知通道,也負責 reset、停止及遷移等生命週期事件。部分控制操作由核心、後端或硬體執行,但仍需要有人把它們協調成一個完整的虛擬裝置。
所以,vDPA 省去的是穩態資料路徑中的部分軟體工作,不是把裝置管理一併拿掉。概念上也不一定要由 QEMU 扮演這個角色,其他 VMM 可以使用相應介面。
這與 SR-IOV VF 直通有什麼不同?
SR-IOV 是把 PCIe 裝置資源劃分為 Physical Function(PF)與多個 Virtual Function(VF)的硬體能力,不是遷移協定。傳統 VF 經 VFIO 指派給 Guest 時,Guest 通常看到廠商特定的 PCIe 裝置介面,因此需要相應的 VF 驅動;vDPA 則把廠商特定控制留在 Host,Guest 使用 VirtIO 驅動。
| 比較項目 | 傳統 VF 經 VFIO 直通 | QEMU + 硬體 vDPA |
|---|---|---|
| Guest 看到的介面 | 通常是廠商特定 VF 介面 | 標準 VirtIO 介面 |
| Guest 驅動 | 對應 VF 的驅動 | 例如 virtio-net |
| Host 端支援 | PF 管理驅動、VFIO 與平台支援 | vDPA 廠商驅動及相應後端 |
| 遷移條件 | VFIO migration、裝置與驅動能力 | vDPA、QEMU 與裝置狀態恢復能力 |
這裡的重點是驅動與介面的邊界,不是開源與私有之分:不少廠商 VF 驅動已在 Linux 核心中。vDPA 的廠商驅動,例如 mlx5_vdpa,裝在 Host;Guest 仍須具備對應的 VirtIO 驅動,Windows 映像也不一定預裝 NetKVM。
兩種技術並不互斥,有些 vDPA 實作本身就使用 VF 提供資源。現代 VFIO 裝置也可能支援遷移。VirtIO 的標準介面有助於降低 Guest 與硬體的耦合,卻不代表兩張都支援 vDPA 的網卡,就能任意互相遷移。
要知道這個限制從何而來,接下來必須看看硬體究竟如何碰到 Guest 記憶體。
四、硬體如何直接存取 Guest 記憶體?
Guest RAM 實際上由 Host 記憶體承載。同一塊 buffer 在不同角色眼中,會有不同的位址:Guest 使用 Guest Physical Address(GPA),QEMU 透過 Host Virtual Address(HVA)存取它,底層對應 Host Physical Address(HPA);裝置執行 DMA 時,則使用 I/O Virtual Address(IOVA)。
這些名稱描述的是不同位址空間,而不是一條必然固定的轉換鏈。沒有 Guest vIOMMU 等額外轉換時,IOVA 可能採用 GPA;加入不同的記憶體與後端配置後,IOVA 就不一定等於 GPA。
建立這條路徑時,QEMU 提供記憶體區域資訊,核心與驅動依所選後端建立、維護映射及存取權限。映射必須涵蓋 ring 與必要的 payload buffers,而不只是 Virtqueue metadata。核心 vhost-vdpa 與 VFIO / IOMMUFD 是不同的整合路徑,不能把一套介面的能力直接套用到另一套。
在 QEMU 的 vhost-vdpa 實作中,可以從 vhost_vdpa_dma_map() 看見這個交接:QEMU 透過 vhost-vdpa file descriptor 傳送 IOTLB 更新訊息,內容包含 IOVA、對應的 HVA、長度及讀寫權限。這不是把 HVA 直接放上 PCIe 匯流排,而是讓核心與裝置驅動建立正確的 DMA 映射。撤銷時則要送出 invalidation,並確保裝置不會再使用舊映射。
QEMU 透過 MemoryListener 的 region_add / region_del 等回呼掌握位址空間變更;若涉及 Guest vIOMMU,還需要處理對應的映射通知。因此,映射不是只在 VM 啟動時「一次寫好就永遠不變」。另外,QEMU 中的軟體 IOTLB 訊息、裝置的位址轉換快取與平台 IOMMU 雖然相關,並不是同一個物件。
映射建立後,硬體就能讀取描述符,直接讀出 TX buffer,或把收到的封包寫入 RX buffer。相關頁面的生命週期通常必須依後端要求固定,避免裝置繼續使用失效的映射,這就是 memory pinning 所要解決的問題。Hugepages 能降低頁表與 IOTLB(I/O 位址轉換快取)壓力,但不是所有 vDPA 實作的強制要求;ulimit -l 設定的是記憶體鎖定資源限制,不是 QEMU 用來執行 pinning 的系統呼叫。
以 64 GiB 記憶體為例,若分別用 4 KiB、2 MiB、1 GiB 的粒度表示,數量依序是 16,777,216、32,768 與 64。這說明大頁為什麼可能減少轉換壓力,但實際能否使用大粒度 DMA 映射,仍受實體配置、對齊、IOMMU 與驅動支援影響;使用 Hugepages 不保證所有映射都能合併,也不保證 IOTLB 能容納整台 VM 的工作集合。
DMA 之外,通知仍有成本
資料不經 CPU 逐位元組搬運,並不表示所有通知也自動消失。Guest 的 Kick、裝置的 Call,以及 Host 排程與管理路徑,仍可能需要軟體參與。
這也是 MSI / MSI-X 與 Interrupt Posting 容易被混淆的地方。**MSI / MSI-X 是 PCIe 裝置發出中斷的機制,Interrupt Posting 則是虛擬化平台加速遞送中斷的機制。**以 Intel 平台為例,裝置發出 MSI / MSI-X 記憶體寫入訊息後,中斷重映射硬體可依路由設定更新目標 vCPU 的 Posted-Interrupt Descriptor,再配合 CPU 虛擬中斷能力,減少軟體介入。
把這段再往下拆,可以看到三種不同層次的工作:
- 裝置發出訊息。 MSI / MSI-X 使用已設定的 address / data 發出 PCIe 記憶體寫入。MSI-X 提供多個可個別設定及遮蔽的向量,適合多佇列裝置,但不代表一個向量必然對應一個 queue。
- 平台決定路由。 中斷重映射硬體依裝置身分與路由表判斷投遞目標;在 posted-interrupt 路徑中,可以更新記憶體中 Posted-Interrupt Descriptor 的 PIR(Pending Interrupt Requests)位元,並在必要時產生通知事件。
- CPU 遞送虛擬中斷。 配合 APIC virtualization / virtual interrupt delivery 等能力,在受支援的執行條件下,CPU 可以把 pending interrupt 遞送給 Guest,減少傳統軟體注入所需的 VM-exit。
裝置仍使用設定好的 MSI / MSI-X 訊息,不必自行理解 vCPU 的 PIR,也不能把這描述成網卡必須產生另一種通用的「Posted MSI」封包格式。上述是 Intel 平台的說明,不能直接套用到所有 CPU 架構。
而在軟體通知路徑中,ioeventfd 讓 KVM 在匹配到 Guest 的 I/O 寫入時通知 eventfd,後端可據此得知有新工作;irqfd 則讓 eventfd 事件觸發 Guest 中斷注入。它們能減少 QEMU 逐次處理通知的需求,但 eventfd 不是一條必然直通硬體 doorbell 的實體線,也不等於 posted interrupt。
但這需要整條平台與後端路徑支援;vCPU 未執行、需要被喚醒,或通知走 eventfd / irqfd 路徑時,仍可能需要 Host 軟體。Kick 也不是裝置向 Guest 發出的硬體中斷,不能與 MSI-X 畫上等號。
因此,硬體 vDPA 比較準確的效益是「降低 Host 資料面 CPU 成本」,而不是「Host CPU 使用率歸零」;Guest vCPU 本身也仍然使用實體 CPU。
到這裡,穩態傳輸的架構已經完整:軟體先設定,硬體再自主處理。但這種自主性,也正是遷移時的新問題。
五、熱遷移的難題:誰改寫了正在搬運的記憶體?
如果 VM 已經停止,複製 RAM 相對單純。Pre-copy 則希望 VM 在大部分搬運期間繼續執行:先複製一輪 RAM,再把這段期間重新被修改的頁面補送,反覆進行,最後才停止來源端,傳送剩餘資料與 CPU / 裝置狀態。
因此,遷移系統不只要知道「哪些頁面已送過」,還要知道「送過之後,哪些頁面又變了」。這就是 Dirty Page Tracking(髒頁追蹤)。
關鍵不在於只選一種資料結構,而是先找出所有寫入者:
| 寫入來源 | 典型追蹤路徑 |
|---|---|
| Guest vCPU | KVM dirty logging,例如 bitmap 或 dirty ring |
| QEMU 裝置模型 | QEMU 記憶體 API 與顯式 dirty marking |
| 外部軟體後端或裝置 DMA | 後端 dirty log、SVQ,或受支援的裝置 / IOMMU dirty tracking |
這三條路徑最後必須整合成遷移所需的記憶體變更資訊。Guest vCPU 的髒頁追蹤,不會自動涵蓋 QEMU 或裝置對 Guest RAM 的寫入。
Bitmap 與 dirty ring:記錄格式不等於收集機制
最直覺的記錄方式是 dirty bitmap:一個 bit 對應一個追蹤粒度的頁面,設為 1 表示需要重送。以 64 GiB RAM、4 KiB 頁面為例,共有 16,777,216 頁,bitmap 大約只需要 2 MiB。
KVM 可以透過 KVM_MEM_LOG_DIRTY_PAGES 啟用追蹤,再由 KVM_GET_DIRTY_LOG 等介面同步。底層如何發現寫入,則可能依靠寫保護 fault、硬體 dirty bit 或 Intel PML(Page Modification Logging)等機制。Bitmap 是回報格式,並不代表底層只能採用某一種攔截方式。
例如,以寫保護為基礎的追蹤,可以理解成以下循環:
- QEMU 為對應的 KVM memory slot 啟用 dirty logging。
- KVM 在適當的粒度與時機,讓尚未記錄的頁面以寫保護方式接受追蹤;在 x86 上,這涉及 EPT / NPT 等第二階段位址轉換機制。
- Guest vCPU 嘗試寫入時觸發 fault,由 KVM 標記對應頁面為 dirty,並按追蹤策略允許後續寫入繼續。
- QEMU 收集 dirty bitmap,把這些頁面加入自己的遷移待送集合。
- KVM 依所使用的 API 與能力清除記錄、重新建立保護,開始下一輪追蹤。
Bitmap 通常可用 unsigned long 陣列等形式儲存,但 API 是以 memory slot 為單位回報,不應把它想成所有 vCPU 每次寫入都必須持有同一把全域鎖。KVM_GET_DIRTY_LOG 的清除行為也會受 manual dirty-log protect 能力影響;使用相應模式時,需要配合 KVM_CLEAR_DIRTY_LOG,不能一律說「讀取 bitmap 就完成全部重新保護」。
如果使用 Intel PML,硬體會把相關 EPT dirty 狀態變化所對應的 Guest 實體頁位址,記錄到每個 vCPU 使用的 PML buffer。這個硬體 buffer 可容納 512 筆位址,滿時會觸發 VM-exit,由 KVM 收集紀錄。因此,PML 能降低逐頁寫保護攔截的頻率,卻不是整個追蹤期間完全沒有 VM-exit;PML buffer 也不等於 QEMU 可直接讀取的 KVM dirty ring。
另一種介面是 KVM dirty ring。它以每個 vCPU 的 struct kvm_dirty_gfn 陣列回報髒頁,其中 slot 識別記憶體 slot,offset 是相對該 slot 的頁面偏移,flags 則表示 DIRTY、RESET 等條目狀態。KVM 產生條目,使用者空間收集後依協定標記,再透過 KVM_RESET_DIRTY_RINGS 回收並重新建立追蹤。
其 UAPI 結構如下,單筆為 16 bytes:
在這裡,slot 按 API 編碼包含 address-space / slot 識別;offset 不是任意全域 GPA,而是相對該 slot 起點的頁面偏移。例如,某個 slot 起於 GPA 0x40000000,頁面大小為 4 KiB,offset = 3 就對應該 slot 中 GPA 0x40003000 的頁面。
使用者空間先查詢 KVM_CAP_DIRTY_LOG_RING 或 KVM_CAP_DIRTY_LOG_RING_ACQ_REL 等能力,再於建立 vCPU 之前透過 KVM_ENABLE_CAP 啟用。Ring 大小以 bytes 指定,必須符合 API 要求;例如 64 KiB 可以容納 4096 筆條目。建立 vCPU 後,使用者空間透過 mmap 存取對應的 per-vCPU ring,並為需要追蹤的 memory slots 設定 KVM_MEM_LOG_DIRTY_PAGES。
條目的生命週期可以畫成:
「收集」是把頁面編號納入待遷移集合,不是等 RAM 傳完才設 RESET。依 KVM API,必須先呼叫 KVM_RESET_DIRTY_RINGS 重新建立追蹤,再讀取相關頁面內容;否則頁面在複製期間再次被寫入時,可能漏掉下一輪變更。條目也必須依序收集,不能跳過中間一筆直接處理後面一筆。
Per-vCPU 設計可降低共享資料競爭,但生產者與消費者仍需遵守原子操作及記憶體排序規則。Ring 滿時,KVM 可用 KVM_EXIT_DIRTY_RING_FULL 要求使用者空間收集與回收;這不等於自動替遷移限速並保證收斂。收集一筆條目,也不代表 QEMU 當下就把那一頁傳出去。
尤其在弱記憶體排序架構上,讀取及更新 flags 必須遵守 acquire / release 或對應 memory barrier 規則。要取得最後完整的髒頁集合,還必須確保 vCPU 已退出 KVM_RUN,讓尚留在處理器 dirty buffers 裡的記錄同步到 ring,而不是只掃一次 mmap 區域就認定追蹤已結束。
同樣地,dirty ring 與 PML 分屬回報介面及硬體收集機制,兩者不是同一件事,也不互為必要條件。
| 比較項目 | Dirty bitmap | KVM dirty ring |
|---|---|---|
| 主要記錄單位 | 每頁一個 bit,依 memory slot 組織 | 每筆一個 slot / offset 與狀態 |
| 使用者空間收集 | 取得並處理 bitmap | 依序收集已產生的條目 |
| 空間與工作量 | 隨追蹤頁數增加,掃描成本與收集方式有關 | Ring 容量隨 vCPU 數與設定大小增加,處理成本也受髒化事件數影響 |
| 壓力來源 | 大記憶體範圍的同步與掃描 | Ring 滿造成的退出、收集與重設 |
| 是否保證遷移收斂 | 否 | 否 |
兩者不必然完全互斥。部分架構可透過 KVM_CAP_DIRTY_LOG_RING_WITH_BITMAP,另外用 bitmap 補足不在 vCPU / ring context 中產生的 KVM 髒頁;這仍不是自動攔截任意 QEMU 寫入或外部裝置 DMA 的機制。
從髒頁回報到 QEMU 遷移
KVM 的紀錄只是來源之一。QEMU 還要把 KVM、裝置模型與後端回報整合到自己的 RAM 遷移追蹤。MemoryListener 可用來協調記憶體區域及 dirty logging 的啟停、同步等事件,相關回呼包含 log_start、log_stop、log_sync;但它不是對每次 Host 記憶體寫入都自動觸發的監聽器。
對 QEMU 裝置模型而言,必須透過正確的記憶體 API 或顯式 dirty marking 回報寫入;外部 vhost 後端也需要使用約定的 dirty-log 機制。這些紀錄同步後,遷移執行緒才知道哪些 RAM 頁需要排入下一輪傳輸。
Pre-copy 能否收斂,則取決於髒化速度與有效傳輸速度。如果 VM 持續修改記憶體的速度接近或超過遷移通道能補送的速度,換成 dirty ring 也不會自動解決;平台仍需調整頻寬、停頓目標、節流或遷移策略。
DMA 留下的缺口
當 vDPA 網卡直接把封包寫入 Guest RX buffer,寫入者不是 Guest vCPU,不能期待 KVM 的 vCPU dirty logging 自動捕捉它。Host CPU 由 QEMU 寫入 Guest RAM,也不會因此自動進入 Guest 的 PML 或寫保護追蹤路徑。
裝置若能回報 DMA 修改過的頁面,或者整條軟硬體路徑支援 IOMMU dirty tracking,就能補上這個缺口。但「有 IOMMU」、「使用 VFIO」或「使用 IOMMUFD」都不是 DMA dirty tracking 已可用的保證,更不能推論 vhost-vdpa 能直接使用其他後端的能力。
如果沒有適用的硬體追蹤能力,還有一個思路:既然 QEMU 不必搬運封包本身,那麼能不能只讓它掌握「哪些 buffer 交給裝置,以及何時完成」?
這就是 SVQ 的切入點。
六、Shadow Virtqueue:接回描述符,不必接回整個封包
**Shadow Virtqueue(SVQ)**是 QEMU 管理的影子佇列。啟用後,硬體不再直接以 Guest VQ 作為 ring metadata 的處理對象,而改為處理 QEMU 提供的 SVQ。QEMU 則負責解析 Guest 提交的工作、建立後端描述符,並把完成結果對應回 Guest。
乍看之下,這像是把整條資料路徑拉回 QEMU。實際上,必須把描述符與 payload 分開畫:
**硬體改讀寫 SVQ 的 ring metadata,不等於封包先寫進 SVQ,再由 QEMU 複製到 Guest。**一般資料佇列中,SVQ 的描述符仍可指向 Guest 原本的 payload buffer,硬體依然直接 DMA 讀寫那塊記憶體。
用一次 TX 與 RX 看清楚兩條路徑
假設 Guest 要送出一個 1500 bytes 的封包。Guest 驅動把封包放在自己的 TX buffer,再把描述符提交到 Guest VQ。QEMU 解析這個工作,為 SVQ 建立對應描述符,必要時轉換成後端使用的 IOVA,並記錄 shadow descriptor 與 Guest element 的對照。硬體從 SVQ 得知工作位置後,直接 DMA 讀取 Guest TX buffer;完成時更新 SVQ,再由 QEMU 回報給 Guest。
接收方向的原理相同,只是 buffer 的方向相反:Guest 先提供可供裝置寫入的 RX buffer,QEMU 將對應描述符提交給 SVQ,硬體收到封包後直接 DMA 寫入 Guest RX buffer,再更新 SVQ 的完成資訊。
| 階段 | TX:Guest → 網路 | RX:網路 → Guest |
|---|---|---|
| Guest 提交 | 已有內容、供裝置讀取的 buffer | 供裝置寫入的 buffer |
| QEMU 中介 | 建立 shadow 描述符與對照 | 建立 shadow 描述符與對照 |
| 硬體處理 payload | DMA 讀取 Guest buffer | DMA 寫入 Guest buffer |
| QEMU 收尾 | 回報 Guest 完成狀態 | 標記可寫入 buffer 的髒頁,再回報完成 |
所以,TX 與 RX 都可以避免 QEMU 額外的 payload memcpy。這項性質限定在這條資料路徑,並不是宣稱 Guest 網路協定棧或整條網路從未複製資料;控制佇列及特殊配置也可能採取不同處理。
SVQ 的工作也不只是「複製一個指針」。單個 Split descriptor 雖然是 16 bytes,但一個封包可能跨多個描述符、間接表及 buffers;QEMU 還要處理描述符重建、位址轉換與完成狀態。Packed Ring 的結構不同,其 SVQ 相容性須按版本確認。因此,即使不複製 payload,中介仍有 CPU 與延遲成本。
描述符轉換與完成回收,在程式裡如何對應?
以 QEMU hw/virtio/vhost-shadow-virtqueue.c 的 Split Ring 路徑為例,可以把提交過程拆成四個動作:
- 取得 Guest 工作。 QEMU 透過
virtqueue_pop()解析描述符鏈,建立VirtQueueElement。其中out_sg是裝置可讀的 scatter-gather buffers,in_sg是裝置可寫的 buffers;方向以裝置為準。 - 轉換位址。
vhost_svq_add()及其內部路徑查找VhostIOVATree維護的映射,把 Guest backing 或 QEMU buffer 對應成裝置可用的 IOVA,並檢查範圍是否合法。 - 重建 shadow 描述符。 QEMU 在 SVQ 配置自己的描述符鏈,記錄
desc_state[head].elem與佔用的描述符數量,再以正確的 memory barrier 發布 available index,必要時 Kick 裝置。 - 依完成 ID 找回原工作。 裝置更新 SVQ used ring 後,
vhost_svq_get_buf()讀取完成 ID 與長度,透過desc_state找回 Guest element,回收 shadow 描述符;後續由virtqueue_fill()/virtqueue_flush()等路徑更新 Guest 完成狀態。
因此,Guest 的 descriptor head ID 與 SVQ 的 head ID 不必相同,兩側的 available / used index 也不必有相同數值。QEMU 靠明確的對照與各自的進度管理連接兩者,而不是假設兩張表可以逐格原樣複製。
例如,Guest RX buffer 位於 GPA 0x12345000,SVQ 為它配置的 IOVA 可能是 0x40000000。硬體讀到的是後者,再經映射抵達同一塊 Guest backing memory;完成時回報的則是 shadow 描述符 ID,由 QEMU 轉回原本的 Guest element。這三種資訊——Guest 位址、裝置 DMA 位址與描述符 ID——各有用途,不能混為同一個「指針」。
此外,GPA 連續不保證 QEMU 的 HVA scatter-gather 區段也連續,同一個 Guest 工作可能需要較多 shadow 描述符。SVQ 若暫時沒有足夠空間,vhost_svq_add() 可以回報 -ENOSPC,QEMU 保留待提交工作,待裝置釋放描述符後再繼續。這也是不複製 payload 仍可能遇到背壓與排程成本的原因。
通知路徑則由原本的 Guest / 後端兩端互動,改成四個邏輯端點:
這些名稱對應 QEMU 中的 event notifiers,不代表每個工作都會產生四次系統呼叫;通知仍可批次或抑制。SVQ 也不必然使用專屬的 busy-poll worker:程式可透過事件處理與完成回收運作,不能直接把它等同於 DPDK 的輪詢模型。
不碰 payload,怎麼知道哪一頁髒了?
答案在 QEMU 維護的對照關係。它知道哪些 Guest 記憶體範圍以可寫入 buffer 的形式交給後端;當硬體回報 SVQ 工作完成,QEMU 就能找回對應範圍,透過 VirtIO / 記憶體處理路徑顯式標記髒頁,再更新 Guest used 狀態。
這不是 DMA 觸發了 KVM dirty ring,也不是 QEMU 把 DMA 寫入「變成」vCPU 寫入,而是軟體根據工作語意與完成資訊,補上遷移系統原本看不到的變更。
這種追蹤可能依 buffer 或頁面範圍保守標記,不保證精確到每個實際寫入的 byte。另外,TX payload 的 DMA read 雖然不會弄髒記憶體,但 TX 完成時更新 Guest used ring 仍然是寫入,同樣必須追蹤。
例如,Guest 提供一塊 4 KiB RX buffer,裝置只寫入其中約 1500 bytes,遷移仍以頁面為單位記錄;若可寫範圍跨頁,則可能涉及不只一頁。完成長度與實作的 unmap / dirty-marking 語意會影響標記範圍,不能由封包長度直接推論「恰好只髒一頁」。
更重要的是,尚未完成的 RX 工作可能已開始 DMA,但 QEMU 尚未收到 used 回報。最終切換時,必須先讓裝置停止在受支援的邊界、處理剩餘工作與必要的 dirty marking,才能宣告最後的 RAM 狀態完整;只收集已收到的 Call,並不足以證明沒有未結算的 DMA。
SVQ 因而取得一個有用的平衡:讓硬體繼續搬運大量 payload,讓軟體重新掌握工作與記憶體變更的關係。然而,「看得到完成」還不夠;真正交接時,尚未完成的工作也必須有明確歸屬。
七、從追蹤到交接:熱遷移如何安全切換?
把前面的機制放回 pre-copy 流程,就能看到 SVQ 所在的位置:
SVQ 若用來補足遷移期間的 DMA 髒頁追蹤,就必須在需要追蹤的期間發揮作用,而不是等到最後幾毫秒才介入。實際上,SVQ 可能動態啟用,也可能常駐;目的端是否先使用 SVQ、何時切回直接資料面,同樣取決於版本、後端能力與配置。
切換不是只改一個地址
不論從直接資料面切到 SVQ,或反向切回,都需要讓舊路徑停止在可理解的邊界,再讓新路徑接續。概念上必須滿足四個條件:
- 穩定舊路徑。 停止提交新工作,依後端能力 suspend / stop,確認裝置不會再存取即將撤銷的映射。
- 結算既有工作。 收集已完成結果,釐清未消費與 in-flight 描述符的所有權及進度,避免遺漏或重複處理。
- 建立新路徑。 配置 ring 位址、佇列狀態、DMA 映射,以及 Kick / Call 通道。
- 驗證後恢復。 確認設定成功,再恢復後端,處理切換期間累積的工作與通知。
相關介面可能包括 VHOST_GET_VRING_BASE、VHOST_SET_VRING_ADDR、VHOST_SET_VRING_BASE 與受支援的 VHOST_VDPA_SUSPEND。但這不是所有版本通用的固定 ioctl 腳本,vring_base 也不是涵蓋完整裝置狀態的單一計數器。
把各介面的責任列開,就能看見為什麼「重新映射」本身不足以完成切換:
| 介面或狀態 | 主要用途 | 不能由此推論的事 |
|---|---|---|
VHOST_VDPA_SUSPEND | 在後端支援時,讓裝置資料面進入可停止的狀態 | 不代表所有已發布工作都已完成 |
VHOST_GET_VRING_BASE | 依後端契約取得佇列處理進度 | 不等於取得全部裝置狀態,也不能盲目信任未穩定的進度 |
VHOST_SET_VRING_ADDR | 設定 descriptor / available / used 區域的位址 | 不會自動建立所有 DMA 映射或同步佇列進度 |
VHOST_SET_VRING_BASE | 設定後端使用的佇列起始進度 | 不是把整條 ring 或 payload 寫入晶片 |
VHOST_SET_VRING_KICK / VHOST_SET_VRING_CALL | 綁定通知 file descriptors | 不等於啟用端到端硬體中斷直通 |
VHOST_VDPA_SET_STATUS、queue enable 等設定 | 依裝置狀態機完成重設、啟動等生命週期操作 | 不是任意順序設定後都能安全恢復 |
在 QEMU 的 SVQ 啟動路徑中,vhost_svq_start() 建立影子 ring 及描述符對照,vhost_vdpa_svq_map_rings() 再替這些記憶體配置裝置可用的 IOVA。裝置需要讀取的 descriptor / available 區域可以用唯讀映射,裝置要更新的 used 區域則需要可寫映射;Guest payload 的合法 DMA 映射仍然保留。最後設定到後端的 ring 位址,才會指向這組 shadow metadata。
這裡的 VhostIOVATree 是 QEMU 用來管理與查找映射的軟體資料結構,不是直接替代硬體 IOMMU 的頁表。SVQ 自己的 ring 可從零開始計數,Guest 的工作進度則由 QEMU 的 VirtQueue 狀態維持;因此不必把硬體先前的所有 index 原樣複製到新 SVQ。
反向停用時,程式需要把通知重新接回直接資料面。在支援動態切換的實作中,shadow_vqs_enabled 與 SVQ_TSTATE_DISABLING 等狀態可協助區分過渡期,例如在旗標尚未完全清除前,先把 Call 的目的地切回 Guest notifier。但這些是版本相關的內部狀態,不應被當成穩定的使用者 API。
尤其要注意,停止裝置後,不能假設所有未完成工作還會自行完成,所以安全收尾不等於「掛起後一直等到 ring 全空」。同樣地,遮蔽 Kick 只是改變通知,不會自動阻止 Guest 修改記憶體,也不一定阻止後端消費已發布的工作;只改 IOMMU 映射,也無法取代佇列進度、描述符對照與通知狀態的同步。
例如,QEMU 的網路 SVQ 停止路徑可先轉交已完成的 used entries,再對尚未完成的 Guest elements 執行 virtqueue_unpop() 等處理,把工作退回 Guest 佇列的處理邊界,而不是要求每個 RX buffer 都必須先收到封包。這類處理依賴裝置已安全停止及網路後端的語意,不能直接套用到任意儲存或其他 VirtIO 裝置。
可以用下面三種狀態檢查切換是否完整:
| 工作所在狀態 | 切換時需要釐清的事 |
|---|---|
| 已發布在 Guest VQ,尚未交給後端 | 保留為待處理工作,避免新路徑漏掉已發布但未通知的項目 |
| 已交給後端,尚未回報完成 | 確認是否已消費、是否可能有 DMA 或外部副作用,按契約保存、退回或接續 |
| 後端已完成,Guest 尚未看到 used 回報 | 完成 dirty marking 與 used 狀態同步,避免新路徑重做 |
這說明「新 SVQ 是空的,所以不用處理舊工作」也不成立:要切進一組空佇列,仍然必須結算舊資料面的所有權。
來源端與目的端不是鏡像操作
來源端原本正在執行,必須主動停止並穩定狀態;目的端通常已在 incoming 準備狀態,尚未執行 Guest,工作是接收、載入與恢復。跨主機遷移傳遞的是可恢復的裝置狀態,不是把來源端的本地 DMA 映射原封不動套到另一台主機。
也因此,恢復資料面不應想成「把整條 SVQ 塞回晶片」。Ring 與 payload 通常在 RAM 中,硬體透過 DMA 存取;真正需要設定或恢復的,是裝置的佇列位址、進度、功能、映射與通知等狀態。目的端必須讓本地硬體設定與載入的 Guest 記憶體一致,才有條件接管。
| 階段 | 來源端 | 目的端 |
|---|---|---|
| 背景複製 | VM 持續執行,回報髒頁 | 建立 incoming VM,接收 RAM 與可提前準備的資源 |
| Stop-and-copy | 停止 vCPU 及相關資料面,收集最後的變更與裝置狀態 | 保持 Guest 尚未執行,載入最終狀態 |
| 準備接管 | 保持停止,依協定等待或完成交接 | 建立本地 DMA 映射、佇列與通知,確認可恢復 |
| 接管後 | 不再作為可直接恢復的最新執行個體 | 開始執行 Guest,承擔後續狀態與外部副作用 |
這個最後交接的狀態不只包含網卡,還有 CPU 暫存器、虛擬中斷控制器、其他裝置及 RAM。因此,在 QEMU 的生命週期中可能看到 vm_stop()、vm_start() 或 vhost 後端 stop / start 等操作,但它們是不同層次的協調,不代表一個函式呼叫就能原子地凍結整台主機。
安全交接,不等於零停頓或零丟包
傳統 QEMU/KVM live migration 通常仍有最後的 stop-and-copy 與接管窗口;SVQ 模式切換也可能另外造成短暫 I/O pause。但以下幾種觀測值不是同一件事:
| 指標 | 觀察的是什麼 |
|---|---|
| VM downtime | Guest vCPU 暫停執行的時間 |
| I/O pause | 裝置暫停處理工作的時間 |
| Network jitter | 封包延遲的變動 |
| Packet loss | 因緩衝區不足、路徑收斂或故障等原因遺失的封包 |
裝置切換、VM 交接與網路重導向可能出現在不同時間點,不能把一次遷移固定描述成「恰好兩次卡頓」。停頓長度也取決於 RAM 髒化速度、頻寬、佇列數、裝置狀態、映射與網路收斂,不能只憑 vDPA 或 SVQ 名稱給出通用的毫秒數。硬體能降低穩態 CPU 成本,也不代表遷移切換必然比軟體後端更快。
緩衝區可以吸收部分停頓,但空間有限,因此不能保證零丟包。TCP 遇到短暫停頓時,RTO 到期通常先觸發重傳與退避,不是立即斷線;持續沒有進展,才可能依重試策略或 TCP_USER_TIMEOUT 終止連線。TCP keepalive 通常也需要明確啟用,並非每條閒置連線都會自動探測。
更重要的是,應用程式的 RPC deadline、心跳或連線池 timeout,可能遠短於 TCP 自己的放棄時間。即使 TCP 連線仍存活,應用請求也可能早已失敗;RST、程序退出或 NAT / 防火牆狀態變化,也可能直接中斷連線。因此,評估遷移品質時,必須把「VM 沒重開機」、「TCP 還活著」與「服務沒有違反延遲目標」分開。
TCP 的幾種 timeout,分別在等什麼?
對一條已建立且仍有資料待確認的 TCP 連線,收不到 ACK 可能觸發 RTO。重傳間隔會依協定與實作進行退避,並受到上限等條件影響,不是每條連線都固定依 1、2、4、8 秒開始計算。初始 RTO 與已取得 RTT 樣本後的 RTO 也不同,不能拿某個最小值當成所有連線的安全停頓時間。
Linux 常見的控制項如下;實際值可能因核心版本、namespace、發行版或應用程式設定而改變:
| 參數或選項 | 控制內容 | 常見預設與注意事項 |
|---|---|---|
net.ipv4.tcp_retries2 | 已建立連線持續重傳時的放棄策略 | 常見預設為 15,不是固定「15 秒」或所有情境下精確重傳 15 次就斷線 |
TCP_USER_TIMEOUT | 對 socket 設定資料未獲確認或因零視窗而無法送出時可容忍的最長時間 | 單位為毫秒,不是控制每次 RTO 的間隔 |
SO_KEEPALIVE | 是否啟用該 socket 的 TCP keepalive | 只改系統 keepalive 參數,不會替所有 socket 自動啟用 |
net.ipv4.tcp_keepalive_time | 閒置多久後開始探測 | 常見預設為 7200 秒 |
net.ipv4.tcp_keepalive_intvl | keepalive 探測間隔 | 常見預設為 75 秒 |
net.ipv4.tcp_keepalive_probes | 放棄前的未回應探測次數 | 常見預設為 9,socket 選項可另行調整 |
可以用唯讀命令檢查目前 namespace 的系統設定:
| |
這些系統值不會顯示所有 per-socket 覆寫或應用層 deadline。例如,一次 80 ms 的停頓就可能讓 50 ms deadline 的 RPC 失敗,即使 TCP 仍保留連線並稍後完成重傳。這只是說明兩種時間尺度的差異,不是某種遷移後端的實測 downtime。
八、遷移失敗的退路:先確認狀態與執行權在哪一端
能否回復,核心問題不是「用了哪種加速技術」,而是哪一端仍擁有可繼續執行的一致狀態,以及執行權是否已經交出去。
Pre-copy:保留來源端,不代表所有失敗都能自動回滾
在 pre-copy 中,如果目的端尚未接管、來源端保有一致狀態,而且來源端裝置仍可恢復,通常有機會取消遷移或恢復來源端。但實際結果取決於失敗階段、遷移協定、確認機制與管理平台,不是任意硬體錯誤都能觸發通用的 all-or-nothing 自動回滾。
來源端後端可能已在 stop-and-copy 階段停止,因此恢復時仍可能需要重新設定或啟動裝置,不能假設硬體從未停過。網路也可能需要透過交換機學習、RARP / GARP、IPv6 鄰居通告或 SDN 控制面恢復轉送,而不是 QEMU 必定發一個 GARP 就完成。
假設目的端在載入 queue state 或建立 DMA 映射時失敗,合理的錯誤處理目標是:
- 限制目的端的副作用。 讓尚未完成載入的 Guest 保持未執行,停止或清理已建立的後端資源,避免殘留 DMA。
- 確認失敗與接管狀態。 由遷移通道、已配置的確認機制及管理平台判斷是否可放棄目的端;不能只看到一個 ioctl 錯誤就假設另一端已安全停止。
- 恢復來源端可執行狀態。 重新建立必要的裝置設定、通知與映射,並按遷移狀態停用不再需要的 dirty logging 或調整 SVQ 模式。
- 恢復執行與網路轉送。 確認只有來源端繼續對外服務,再由相應網路機制恢復封包路徑。
這是需要滿足的安全條件,而不是宣稱所有 QEMU 部署都有同一套跨主機交易協定。若管理平台使用額外的交接確認機制,也要把它納入故障測試。
回復期間是否繼續使用 SVQ,或重新建立直接資料面,取決於當時配置。來源端 RAM 中的 ring 與 payload 可以仍然存在,但裝置可能已被 suspend 或 reset,所以仍需檢查硬體設定是否一致。不必搬移整條佇列進晶片,不等於完全不必恢復硬體狀態。
如果目的端已經執行並產生外部副作用,就不能直接把舊來源端再啟動,否則會形成 split-brain。這個接管邊界,也解釋了 postcopy 為什麼沒有同樣簡單的退路。
Postcopy:可以恢復傳輸,不等於可以回到舊快照
Postcopy 在 RAM 尚未全部傳完時,就先讓目的端執行;缺少的頁面再透過背景傳輸或缺頁處理取得,常涉及 userfaultfd。目的端開始修改 CPU、RAM 與裝置狀態後,來源端的舊狀態就不再是可直接恢復的最新快照。
在支援的 postcopy 路徑中,QEMU 處理目的端對缺頁的需求,向來源端取得頁面並補入記憶體,讓等待的存取繼續。這與 pre-copy 「先把可恢復的 RAM 狀態備齊,再讓目的端執行」的次序不同;postcopy 也可以先經過 pre-copy,並沒有固定「90% RAM 必然還在來源端」的比例。
若目的端已把某頁改成新內容,直接恢復來源端就會丟掉那次修改;若目的端還缺另一頁而來源端永久失效,缺頁又可能無法補齊。問題是最新執行狀態與尚未傳送的必要頁面分別依賴兩端,而不是 RAM 位於兩台主機本身就必然代表資料已損毀。
Virtqueue metadata 與 payload 也是 Guest RAM 的一部分,因此還要解決裝置能否存取未到達的頁面。CPU 的缺頁可以透過受支援的 postcopy 機制處理,但一般 DMA 不能被假設會自動觸發同樣的 userfaultfd 補頁流程;硬體能 DMA,也不代表支援可恢復的 DMA page fault。這正是必須檢查裝置與後端 postcopy 能力,而不能只看 VirtIO 介面的原因。
若兩端仍存活、只是遷移通道中斷,QEMU 的 postcopy recovery 可以在重新連線後繼續遷移,但這不是回滾。若任一端永久失去必要狀態,就可能無法恢復。裝置 DMA 存取尚未到達的頁面另有要求,使用 vDPA、SVQ 或 VFIO 並不代表配置支援 postcopy,必須先確認後端限制。
| 比較項目 | Pre-copy 接管前 | 已進入目的端執行的 postcopy |
|---|---|---|
| 正在推進 VM 狀態的一端 | 來源端,或已為交接暫停的來源端 | 目的端 |
| 來源端舊狀態可否直接繼續 | 滿足一致性及裝置恢復條件時有機會 | 通常不行,已不包含目的端的新修改 |
| 通道中斷時的方向 | 可能取消並恢復來源端,依階段與協定決定 | 在支援條件下重新連線並繼續傳輸 |
| 主要風險 | 目的端啟動失敗、來源端裝置無法恢復、接管確認不明 | 任一端遺失必要狀態、缺頁無法補齊、DMA 能力不匹配 |
這裡真正不可忽略的,是「先停止舊執行個體,再確認誰能繼續」的所有權管理。單機上的 SVQ 模式切換,即使有嚴密的本地狀態機,也不能自動提供跨主機的 split-brain 防護。
九、回到選型:先確認需要什麼,再確認裝置真的支援什麼
沿著資料路徑走到這裡,可以看到各種方案並非單純的快慢排名,而是在 CPU 成本、可程式性、硬體依賴與遷移能力之間取捨。
| 主要需求 | 可優先評估的方向 |
|---|---|
| 一般 VM,優先考慮部署與維護便利 | VirtIO + vhost-net |
| 可程式化的高效能軟體交換 | vhost-user + DPDK / OVS-DPDK |
| 降低 Host 資料面 CPU 成本,保留 VirtIO Guest 介面 | 硬體 vDPA |
| 直接使用 VF 專屬能力 | SR-IOV + VFIO,另確認遷移支援 |
| 驗證 vDPA 框架,不要求硬體效能 | 軟體 vDPA 模擬裝置 |
硬體 vDPA 的前提,是裝置與驅動真的能提供所需的 VirtIO 相容資料面及控制能力。支援 DPDK,不等於支援 vDPA:PMD 能操作廠商格式的 NIC queue,並不能證明裝置能處理 VirtIO queue。DPU / SmartNIC 也只是較廣的硬體類別,不等於 vDPA,更不保證交換、安全與控制面都已卸載。
例如,不能由 Intel E810 具有 ice 驅動、DPDK PMD、SR-IOV 或 DDP package,就推導它具備原生硬體 vDPA 能力;DPDK IFCVF 文件描述的是 Intel FPGA 100G VF,也不能把 ifcvf 的支援對象直接等同於 E810。相同原則適用於其他廠牌:應核對精確型號、PCI ID、韌體、驅動及功能矩陣,而不是只看產品家族名稱。
DDP(Dynamic Device Personalization)處理的可程式化封包解析等能力,與裝置是否能消費 VirtIO queue 是不同問題;同理,switchdev 模式、RDMA / RoCE / iWARP 與 vDPA 也各有自己的功能矩陣及資源限制。不能由其中一項能力,推論其他能力已具備,也不應把某一型號的互斥限制擴大成所有 vDPA 裝置的共同規則。
即使裝置已能建立 vDPA queue,遷移仍可能受 feature bits、queue 規模、控制佇列、ring 格式及 Guest vIOMMU 等條件限制。例如,本文參考的 QEMU hw/virtio/vhost-vdpa.c 版本包含對 SVQ 與 Guest vIOMMU 組合的檢查,不能因為 Host 本身有 IOMMU,就認定 Guest vIOMMU 與 SVQ 可任意搭配。實際部署應查對應版本的檢查條件與 migration blocker。
可以先用下列唯讀命令收集部署資訊;部分環境可能需要額外權限:
沒有列出管理裝置,可能是驅動、韌體或配置問題;成功列出裝置,也只代表評估的起點,不代表遷移能力已經齊備。真正部署前,至少要回答以下問題:
- 介面相容嗎? 核心、QEMU、後端、驅動與韌體的版本是否匹配?Guest feature bits、Split / Packed Ring、佇列數與大小是否受支援?
- 記憶體路徑完整嗎? DMA 映射與隔離、pinning 限制、Hugepages / NUMA 是否配置妥當?vCPU、QEMU 與 DMA 的寫入是否全被納入髒頁追蹤?
- 狀態能接續嗎? 來源端能否安全停止與保存,目的端能否恢復?兩端硬體與功能是否相容?所選 pre-copy / postcopy、SVQ 模式是否存在 migration blocker?
- 失敗與服務影響測過嗎? 除了吞吐量與 CPU,也應量測 downtime、延遲分位數、丟包、重傳與收斂情形,並測試高髒化率、滿載流量、取消遷移及後端 / 目的端失敗。
最後再把這些結果放回總持有成本:回收多少 Host 核心,是否值得新增硬體、功耗、維運與相容性成本?硬體卸載的價值應由工作負載與量測證明,而不是用「CPU 歸零」或固定收益比例替代評估。
把架構、驅動與遷移放在同一張表裡,可以作為最後的交叉檢查:
| 維度 | vhost-net | vhost-user + DPDK | 硬體 vDPA | 傳統 VFIO VF 直通 |
|---|---|---|---|---|
| 主要 VirtIO 資料處理位置 | Host 核心 | 外部使用者空間後端 | VirtIO 相容裝置,必要時由 SVQ 中介 | Guest 使用的通常是廠商特定硬體資料面 |
| Guest 驅動 | virtio-net | virtio-net | virtio-net | 對應 VF 驅動 |
| Host CPU 主要成本 | 核心後端、網路與通知 | PMD、軟體交換與後端處理 | 通知、管理、例外及可能的 SVQ 處理 | VFIO 管理、通知與平台路徑 |
| Payload 是否必然零複製 | 否 | 否 | 可避免 QEMU 額外複製,不代表全鏈路皆無複製 | 可直接 DMA,仍須看完整工作負載 |
| 髒頁與狀態責任 | KVM、QEMU 與核心後端協作 | KVM、QEMU 與外部後端協作 | 額外確認 DMA tracking、SVQ 與裝置可恢復性 | 額外確認 VFIO migration、DMA tracking 與硬體能力 |
| 可否只憑方案名稱保證熱遷移 | 否 | 否 | 否 | 否 |
結語:卸載的是工作,不能丟掉的是狀態
VirtIO 先統一 Guest 與裝置之間的語言,vhost 讓資料處理離開 QEMU,硬體 vDPA 再把相容資料面交給裝置。這條演進路徑的目的,是在不迫使 Guest 跟著底層實作改變的前提下,降低 Host 的 I/O 成本。
熱遷移則把另一面問題帶回來:當硬體可以自主工作,軟體仍必須知道哪些記憶體被修改、哪些描述符已完成,以及誰有權繼續執行。SVQ 的價值,正是在保留 payload 直接 DMA 的同時,讓 QEMU 重新掌握描述符與完成回報,把硬體工作接回遷移所需的狀態管理。
因此,評估這套架構最重要的,不只是問「封包有沒有繞過 QEMU」,而是接著問:繞過之後,誰還能完整說明系統現在的狀態?,能回答這個問題,才有機會同時做好效能、可攜性與可靠的熱遷移。