
# KubeVirt RWO 云盘 Volume Migration 实测

本文验证 KubeVirt 的 Volume Migration：VM 运行时，把数据盘从一个静态 RWO PVC 迁移到另一个静态 RWO PVC。

这里没有把 NFS 伪装成 Ceph。测试集群没有 Ceph CSI/RBD，因此用两个 NFS 导出目录模拟源、目标后端，重点验证 KubeVirt 的卷迁移流程，以及迁移后数据和 PVC 状态是否正确。

结论先放在前面：迁移成功，目标 PVC 仍然是 RWO。Volume Migration 复制数据并切换 VM 的卷引用，不会把 RWO 自动变成 RWX。迁移完成后，VM 仍不能进行普通的共享存储热迁移。

<!--more-->

## Volume Migration 做什么

KubeVirt 的 [Volume Migration](https://kubevirt.io/user-guide/storage/volume_migration/) 用于在 VM 运行时更换存储。源卷和目标卷需要提前准备好，KubeVirt 负责复制数据并更新 VM 的卷引用，不负责创建目标 PVC 或目标存储。

本次实验的关系是：

```mermaid
flowchart LR
    A["VM 运行<br/>source PVC: RWO"] --> B["更新 datadisk<br/>claimName"]
    B --> C["KubeVirt Volume Migration<br/>VMIM Succeeded"]
    C --> D["target PVC: RWO"]
    D --> E["disk.img 哈希一致"]
    D -.-> F["普通 Live Migration 仍受限"]
```

## 两种 RWO Volume Migration 方式

RWO 卷迁移有两种资源模型，区别不在 KubeVirt 是否创建 VMIM，而在 VM 的卷引用和 PVC 的管理者：

| 对比项 | 直接使用 PVC | 使用 DataVolume |
| --- | --- | --- |
| VM 的卷引用 | `persistentVolumeClaim.claimName` | `dataVolume.name` |
| 目标资源 | 预先创建并绑定目标 PVC | 预先绑定目标 PVC，再创建同名 DV 接管 |
| PVC 管理者 | 没有 DV OwnerReference | PVC 的 controller OwnerReference 指向同名 DV |
| 数据复制者 | KubeVirt Volume Migration | 仍然是 KubeVirt Volume Migration |
| CDI 的作用 | 不参与 | 管理 DV/PVC 的归属和就绪状态 |
| 迁移前检查 | PVC `Bound`、PV/PVC 是 RWO | PVC `Bound`、DV `Succeeded`、OwnerReference 正确、没有 importer Pod |

两种方式最后都不会把目标 RWO 变成 RWX。DataVolume 不是另一种数据复制引擎，它解决的是 PVC 的资源生命周期和归属问题。

## 测试环境

只记录本次用到的软件版本：

| 软件 | 版本 |
| --- | --- |
| kind | v0.31.0 |
| Kubernetes | v1.35.0 |
| KubeVirt | v1.9.0 |
| CDI | v1.66.0 |
| Kube-OVN | v1.14.3 |

源、目标 PV 都是静态 PV，配置如下：

- `accessModes: ReadWriteOnce`
- `volumeMode: Filesystem`
- `persistentVolumeReclaimPolicy: Retain`
- 源、目标 PVC 分别绑定固定的 PV
- 源、目标 PV 使用不同的 NFS 导出路径

NFS Deployment 使用 `emptyDir` 保存后端数据，只适合实验。它证明的是 KubeVirt 能否完成这条 Volume Migration 控制链，不证明 Ceph CSI/RBD 的 attach、detach 或 RBD image 语义。真实云盘环境还需要用实际 CSI 驱动单独验证。

实验文件已经提交到 [k8sdev `0337aacd`](https://github.com/jimyag/k8sdev/tree/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs)。

DataVolume 版本使用独立的实验目录，提交为 [`5aea5d4`](https://github.com/jimyag/k8sdev/tree/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs)。

## 前置配置

Volume Migration 依赖 KubeVirt 的 `LiveUpdate` 和 `LiveMigrate` 配置。实验配置如下：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/00-kubevirt-config.yaml" >}}

这个配置是 KubeVirt CR 的集群级配置，只应在专用实验集群或确认影响范围后应用。

相对于没有启用卷在线更新的 KubeVirt CR，关键新增项如下：

```diff
diff --git a/KubeVirt CR b/KubeVirt CR
@@
+spec:
+  configuration:
+    vmRolloutStrategy: LiveUpdate
+  workloadUpdateStrategy:
+    workloadUpdateMethods:
+    - LiveMigrate
```

这是迁移前置条件，不是源 PVC 到目标 PVC 的数据切换本身。

先准备 NFS 模拟后端：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/01-nfs.yaml" >}}

准备静态 RWO PV/PVC：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/02-static-rwo-pv-pvc.yaml" >}}

在目标集群中执行：

```bash
export EXPERIMENT_ROOT=/tmp/rwo-volume-migration-nfs
git clone https://github.com/jimyag/k8sdev.git "$EXPERIMENT_ROOT"
git -C "$EXPERIMENT_ROOT" checkout 0337aacd803045fa21080702057a14894d328e9f
cd "$EXPERIMENT_ROOT/rwo-volume-migration-nfs"

kubectl --context kind-rwo-rwx-lab apply -f 00-kubevirt-config.yaml
kubectl --context kind-rwo-rwx-lab apply -f 01-nfs.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  rollout status deployment/nfs-server --timeout=180s
kubectl --context kind-rwo-rwx-lab apply -f 02-static-rwo-pv-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  wait --for=jsonpath='{.status.phase}'=Bound \
  pvc/volume-migration-source pvc/volume-migration-target --timeout=180s
```

确认两组对象都是 `Bound`、`ReadWriteOnce`、`Filesystem` 后再启动 VM。不要只看 PVC 是不是 `Bound`，还要核对 PV/PVC 的 access mode、volume mode 和 reclaim policy。

## 方式一：KubeVirt 直接切换 PVC

这一种方式里，VM 直接引用源 PVC 和目标 PVC。目标 PVC 必须提前创建并绑定，KubeVirt 发现 VM 的 PVC 引用变化后，负责执行 Volume Migration。

### 真正改了什么

这次不是修改 PVC 的 `accessModes`，也不是修改 PV。真正触发 Volume Migration 的改动只有 VM 数据盘的 PVC 引用：

```diff
diff --git a/rwo-volume-migration-nfs/04-vm-source.yaml b/rwo-volume-migration-nfs/05-vm-target.yaml
--- a/rwo-volume-migration-nfs/04-vm-source.yaml
+++ b/rwo-volume-migration-nfs/05-vm-target.yaml
@@ -44,7 +44,7 @@ spec:
       - name: datadisk
         persistentVolumeClaim:
-          claimName: volume-migration-source
+          claimName: volume-migration-target
```

这段差异里，`-` 是迁移前的源 PVC，`+` 是迁移后的目标 PVC。`updateVolumesStrategy: Migration` 在两个 VM 文件中都存在，并不是这次切换时才新增的字段。

可以按下面三层理解：

| 配置位置 | 作用 | 是否是本次切换的差异 |
| --- | --- | --- |
| KubeVirt CR | 开启 `LiveUpdate` 和 `LiveMigrate` | 前置配置 |
| VM `updateVolumesStrategy` | 指定卷引用变化使用 Volume Migration | 源、目标文件都必须保留 |
| VM `datadisk.claimName` | 从源 PVC 改为目标 PVC | **真正触发迁移的改动** |

因此，`04-vm-source.yaml` 和 `05-vm-target.yaml` 大部分内容相同，重点只看 `datadisk` 对应的 `claimName`。目标 PVC 必须提前创建并处于 `Bound`，KubeVirt 不会因为这个字段变化自动创建目标卷。

## 先验证源 PVC

源 PVC 先由一次性 Pod 挂载并写入一个文件：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/03-seed-source.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 03-seed-source.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  wait --for=jsonpath='{.status.phase}'=Succeeded pod/seed-source --timeout=180s
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration logs pod/seed-source
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  delete pod/seed-source --wait=true
```

这个 Pod 的作用是提前确认静态 PVC 能被节点挂载和写入，并不是给 VM 生成业务数据。RWO PVC 不能长期被这个 Pod 占用，所以检查完成后要删除它。

还有一个容易误判的地方：这里的 PVC 是 `Filesystem`，KubeVirt 会在挂载目录中使用 `disk.img` 作为 VM 数据盘。上面写入的 `sentinel.txt` 位于挂载目录根部，不等于 guest 文件系统里的文件，因此不能直接拿它作为迁移后的 VM 数据证明。最终校验比较 `disk.img` 的哈希。

## 启动 VM 并迁移数据盘

源卷版本的 VM：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/04-vm-source.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 04-vm-source.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  wait --for=condition=Ready vmi/volume-migration-vm --timeout=180s
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration get vmi volume-migration-vm -o wide
```

此时 VM 使用 `volume-migration-source`。把 VM 清单中的数据盘改为目标 PVC：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/05-vm-target.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 05-vm-target.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  get vmim -l kubevirt.io/volume-update-migration=volume-migration-vm -w
```

需要看到 VMIM 进入 `Succeeded`，并检查 `VM.status.volumeUpdateState`：

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  get vmim -l kubevirt.io/volume-update-migration=volume-migration-vm -o yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  get vm volume-migration-vm -o yaml
```

本次实测结果：

- VMIM `kubevirt-workload-update-xbp7h` 进入 `Succeeded`。
- VM 仍在运行，VMI 从源节点切换到目标节点。
- `VM.status.volumeUpdateState.volumeMigrationState.migratedVolumes` 记录了 `volume-migration-source` 到 `volume-migration-target` 的切换。
- 源、目标 PVC 的 `accessModes` 都是 `ReadWriteOnce`，`volumeMode` 都是 `Filesystem`。

不要把“生成了 VMIM”当成完成证据。VMIM 还要等到 `Succeeded`，并且要继续检查目标卷中的数据。

## 校验目标数据

先停止 VM，让目标 RWO PVC 可以被检查 Pod 单独挂载：

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  patch vm volume-migration-vm --type=merge \
  -p '{"spec":{"runStrategy":"Halted"}}'
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  wait --for=delete vmi/volume-migration-vm --timeout=180s
```

目标校验 Pod：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/0337aacd803045fa21080702057a14894d328e9f/rwo-volume-migration-nfs/06-check-target.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 06-check-target.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration \
  wait --for=jsonpath='{.status.phase}'=Succeeded pod/check-target --timeout=180s
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration logs pod/check-target
```

输出中的目标文件为 `disk.img`，本次 SHA-256 是：

```text
49bc20df15e412a64472421e13fe86ff1c5165e18b2afccf160d4dc19fe68a14
```

源、目标 `disk.img` 的哈希一致，说明这次迁移复制出的数据与源端一致。

## 方式二：DataVolume 管理 PVC

如果业务侧不是直接管理 PVC，而是通过 CDI 的 DataVolume 管理磁盘，就不能只把 VM 的 `persistentVolumeClaim.claimName` 换成另一个 PVC。VM 应该引用 DataVolume，PVC 的归属也要先交给对应的 DataVolume。

这里使用 CDI 的 [DataVolume Claim Adoption](https://github.com/kubevirt/containerized-data-importer/blob/main/doc/datavolume-claim-adoption.md)：先创建并绑定静态目标 PVC，再创建同名 DataVolume，并加上 `cdi.kubevirt.io/allowClaimAdoption: "true"`。CDI 接管这个 PVC 后，会给 PVC 增加指向 DataVolume 的 controller OwnerReference，并把 DataVolume 标记为 `Succeeded`。这个过程不会创建 importer Pod，也不会初始化或覆盖已有数据。

因此，这种方式仍然分成两步：

1. CDI 负责把已经存在的 PVC 纳入 DataVolume 的生命周期。
2. KubeVirt 发现 VM 的 `dataVolume.name` 从源 DV 变成目标 DV 后，负责执行 Volume Migration。

### 先准备静态 PVC

源、目标 PV/PVC 的配置与前面的实验相同，都是 `ReadWriteOnce`、`Filesystem`、`Retain`。区别是这里的 PVC 会被同名 DataVolume 接管：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/01-nfs.yaml" >}}

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/02-static-rwo-pv-pvc.yaml" >}}

执行准备命令：

```bash
export DV_EXPERIMENT_ROOT=/tmp/rwo-volume-migration-dv-nfs
git clone https://github.com/jimyag/k8sdev.git "$DV_EXPERIMENT_ROOT"
git -C "$DV_EXPERIMENT_ROOT" checkout 5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea
cd "$DV_EXPERIMENT_ROOT/rwo-volume-migration-dv-nfs"

kubectl --context kind-rwo-rwx-lab apply -f 00-kubevirt-config.yaml
kubectl --context kind-rwo-rwx-lab apply -f 01-nfs.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  rollout status deployment/nfs-server --timeout=180s
kubectl --context kind-rwo-rwx-lab apply -f 02-static-rwo-pv-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=jsonpath='{.status.phase}'=Bound \
  pvc/volume-migration-dv-source pvc/volume-migration-dv-target --timeout=180s
```

先确认两个 PVC 都是 `Bound`，并且 PV/PVC 仍然是 `ReadWriteOnce` 和 `Filesystem`。

### 让 CDI 接管 PVC

源、目标 DataVolume 都必须与对应 PVC 使用相同的 namespace 和 name。`source.blank: {}` 在这里不是“清空磁盘”，而是让 DataVolume 在 Claim Adoption 场景下不执行导入；真正的数据仍然来自已经绑定的 PVC。

源 DataVolume：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/04-source-dv.yaml" >}}

目标 DataVolume：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/05-target-dv.yaml" >}}

先让 CDI 接管源 PVC，再启动 VM：

```bash
kubectl --context kind-rwo-rwx-lab apply -f 04-source-dv.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=jsonpath='{.status.phase}'=Succeeded \
  dv/volume-migration-dv-source --timeout=180s

kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  get pvc volume-migration-dv-source -o yaml
```

输出中应该能看到 PVC 的 controller OwnerReference 指向 `DataVolume/volume-migration-dv-source`。再检查 CDI 没有启动 importer：

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  get pod -l cdi.kubevirt.io/dataVolume=volume-migration-dv-source
```

这个查询没有 Pod 是预期结果。Claim Adoption 只建立资源管理关系，不负责复制数据。

### 真正触发迁移的改动

启动引用源 DataVolume 的 VM：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/06-vm-source.yaml" >}}

```bash
kubectl --context kind-rwo-rwx-lab apply -f 06-vm-source.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=condition=Ready vmi/volume-migration-dv-vm --timeout=180s
```

接着创建目标 DataVolume。目标 PVC 必须已经是 `Bound`，并且没有被其他对象控制：

```bash
kubectl --context kind-rwo-rwx-lab apply -f 05-target-dv.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=jsonpath='{.status.phase}'=Succeeded \
  dv/volume-migration-dv-target --timeout=180s
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  get pvc volume-migration-dv-target -o yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  get pod -l cdi.kubevirt.io/dataVolume=volume-migration-dv-target
```

目标 PVC 的 OwnerReference 指向目标 DataVolume，且没有 importer Pod 后，再把 VM 的数据盘从源 DV 改为目标 DV：

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/07-vm-target.yaml" >}}

这里的实际 diff 只有 DataVolume 引用变化：

```diff
diff --git a/rwo-volume-migration-dv-nfs/06-vm-source.yaml b/rwo-volume-migration-dv-nfs/07-vm-target.yaml
--- a/rwo-volume-migration-dv-nfs/06-vm-source.yaml
+++ b/rwo-volume-migration-dv-nfs/07-vm-target.yaml
@@
       - name: datadisk
         dataVolume:
-          name: volume-migration-dv-source
+          name: volume-migration-dv-target
```

应用目标 VM 并等待 VMIM 完成：

```bash
kubectl --context kind-rwo-rwx-lab apply -f 07-vm-target.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  get vmim -l kubevirt.io/volume-update-migration=volume-migration-dv-vm -w
```

### DataVolume 方式的验证结果

本次实测结果如下：

- 源、目标 DataVolume 都进入 `Succeeded`。
- 目标 PVC 的 controller OwnerReference 指向同名目标 DataVolume。
- 两个 DataVolume 都没有启动 importer Pod。
- VMIM 进入 `Succeeded`，VM 的 `migratedVolumes` 记录了源 PVC 到目标 PVC 的切换。
- 停止 VM 后挂载目标 PVC，`disk.img` 的 SHA-256 仍为 `49bc20df15e412a64472421e13fe86ff1c5165e18b2afccf160d4dc19fe68a14`。
- 目标 PV/PVC 仍然是 `ReadWriteOnce`，并没有因为使用 DataVolume 变成 `ReadWriteMany`。

停止 VM 后可以用下面的清单检查目标磁盘：

```bash
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  patch vm volume-migration-dv-vm --type=merge \
  -p '{"spec":{"runStrategy":"Halted"}}'
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=delete vmi/volume-migration-dv-vm --timeout=180s
kubectl --context kind-rwo-rwx-lab apply -f 08-check-target.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv \
  wait --for=jsonpath='{.status.phase}'=Succeeded \
  pod/check-target --timeout=180s
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration-dv logs pod/check-target
```

{{< github-file url="https://github.com/jimyag/k8sdev/blob/5aea5d4276ad4d38e344ad1fc7ae0bdb822723ea/rwo-volume-migration-dv-nfs/08-check-target.yaml" >}}

### 两种方式怎么选

直接使用 PVC 的方式更简单，适合控制器本身就管理 PV/PVC 的场景；DataVolume 方式适合已经把磁盘生命周期交给 CDI 的场景。两者的迁移主体都是 KubeVirt，区别只是 VM 引用的对象和 PVC 的管理者不同。

DataVolume 方式需要特别注意：

- DataVolume 和要接管的 PVC 必须同 namespace、同 name。
- PVC 必须先绑定，并且不能已经被其他对象控制。
- DataVolume 的存储参数要与 PVC/PV 对齐，至少要核对 access mode、volume mode、storage class 和容量。
- Claim Adoption 只接管 PVC，不复制数据；数据复制仍由 KubeVirt Volume Migration 完成。
- 生产控制器不能只看到 DataVolume `Succeeded` 或 VMIM 创建就认为完成，还要等 VMIM `Succeeded`、VM 状态收敛，并在目标卷上做数据校验。
- 失败处理时不要直接删除目标 PVC。先保留 PV、PVC、DataVolume 和 VMIM 的状态，确认目标卷是否已经写入，再决定回滚或清理。

## 迁移后的访问模式

最后再次检查 PV/PVC：

```bash
kubectl --context kind-rwo-rwx-lab get pv \
  volume-migration-source-pv volume-migration-target-pv
kubectl --context kind-rwo-rwx-lab -n rwo-volume-migration get pvc \
  volume-migration-source volume-migration-target
```

结果仍然是：

```text
source PV/PVC   Bound   ReadWriteOnce   Filesystem   Retain
target PV/PVC   Bound   ReadWriteOnce   Filesystem   Retain
```

VM 的 `LiveMigratable` 条件为 `False`，原因是目标 PVC 仍然不是共享卷：

```text
reason: DisksNotLiveMigratable
message: PVC volume-migration-target is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
```

所以，Volume Migration 和 RWO 转 RWX 是两件事：

- Volume Migration：运行中的 VM 从旧 PVC 迁移到已准备好的新 PVC。
- RWO 转 RWX：需要后端数据复用和 PV/PVC 对象重建，访问模式由 `ReadWriteOnce` 改为 `ReadWriteMany`。

如果目标是让 VM 后续支持普通热迁移，目标存储必须真正支持共享访问，并且目标 PV/PVC 要按 RWX 创建。只修改 PVC 的 `accessModes` 不会改变后端存储能力。

## 这次实验的注意事项

1. 目标 PVC 必须先存在。Volume Migration 不会替你创建目标卷，也不会替你把 RWO 改成 RWX。
2. `LiveUpdate` 和 `LiveMigrate` 是必要前置条件。缺少时，更新 VM 的卷引用可能只停留在 `VolumesChange=True`，看不到有效的 VMIM。
3. 如果一次更新没有生成 VMIM，先把 VM 恢复到源 PVC，等待 `VolumesChangeCancellation` 完成，再重新更新到目标 PVC。不要在状态未收敛时连续覆盖 VM 清单。
4. NFS 后端在这个实验中可以被多个节点访问，但 PV 仍声明为 RWO。因此它不能验证真实云盘的独占挂载、CSI attach/detach 或 RBD image 行为。
5. 生产环境删除 PV 前，要确认 `Retain` 和后端资产的关系，并保存 `volumeHandle`、存储池、集群标识、`volumeMode`、挂载参数及 Secret 引用。重建对象时漏掉 CSI 字段，PVC 可能仍是 `Bound`，实际挂载却会失败。
6. 验证结果至少要同时包含 VMIM 终态、VMI/VM 的卷迁移状态、目标卷数据和 PV/PVC 状态。`PVC Bound`、HTTP 成功或 VMIM 创建都只是中间状态。

