本文验证:虚拟机关机后,删除历史 RWO PVC 和 PV 对象,保留 NFS 后端数据,再重建为 RWX,能否继续使用 KubeVirt 热迁移。
PVC 的 accessModes 不能直接修改。转换前要先确认 PV 和 PVC 都是 ReadWriteOnce,再关机并确认 VMI、virt-launcher 已经退出。本实验删除 PVC/PV 对象,但保留 NFS 服务和导出路径中的数据,然后用相同路径重建 ReadWriteMany PV 和 PVC。系统盘由 CDI DataVolume 管理时,还要用 allowClaimAdoption 让新 DV 接管已有 PVC。
实验使用三节点 kind、KubeVirt、Kube-OVN 和 NFS,结果如下。
- RWO PVC 的 VMI
LiveMigratable=False,热迁移被 KubeVirt admission webhook 拒绝。 - 停机后删除 PVC 和 PV 对象,NFS 后端路径不变,重建 RWX PV/PVC 后仍能读到同一个哨兵文件。
- 新 DataVolume 成功接管已有 RWX PVC,PVC 的 ownerReference 指向该 DV,DV 进入
Succeeded。 - VM 改用 DV 后,VMI
LiveMigratable=True;VMIM 从 rwo-rwx-lab-worker 迁移到 rwo-rwx-lab-worker2,IP 10.251.0.19 保持不变。
实验使用的是 NFS Filesystem,只用于验证对象重建、后端路径复用和 KubeVirt 共享 PVC 热迁移。它不能替代生产 Ceph CSI/RBD 验证。
KubeVirt 要求使用 PVC 的热迁移具备共享的 RWX 访问模式;VMI 的 LiveMigratable 条件也会反映卷是否满足要求。KubeVirt Live Migration
完整清单和验证结果已经提交到 k8sdev cce508d0:
1
2
3
4
5
6
7
8
9
10
11
12
| rwo-to-rwx-kind/
├── kind-config.yaml
├── 01-vpc-subnet.yaml
├── 02-nfs.yaml
├── 03-rwo-pv-pvc.yaml
├── 04-seed-pod.yaml
├── 05-vm-rwo.yaml
├── 06-rwx-pvc.yaml
├── 07-dv-adoption.yaml
├── 08-vm-rwx.yaml
├── 09-rwx-reader-pod.yaml
└── evidence/
|
本次实际版本如下:
| 组件 | 版本或配置 |
|---|
| kind | v0.31.0 |
| Kubernetes | v1.35.0,1 个 control-plane、2 个 worker |
| Kube-OVN | v1.14.3 |
| KubeVirt | v1.9.0 |
| CDI | v1.66.0 |
| 存储 | 静态 NFS PV,Retain,Filesystem;数据位于 NFS 导出路径 |
| 网络 | Kube-OVN VPC rwo-rwx-vpc、Subnet rwo-rwx-subnet |
kind 集群配置:
Kube-OVN VPC 和 Subnet:
NFS 服务:
kind 集群关闭默认 CNI。先下载固定提交中的实验目录,以下命令均在该目录执行。
1
2
3
4
5
| export EXPERIMENT_ROOT=/tmp/rwo-to-rwx-kind
git clone https://github.com/jimyag/k8sdev.git "$EXPERIMENT_ROOT"
git -C "$EXPERIMENT_ROOT" checkout cce508d0bc28d88d01c71b1378ee176f449b5f5c
cd "$EXPERIMENT_ROOT/rwo-to-rwx-kind"
kind create cluster --config kind-config.yaml
|
安装 Kube-OVN 时使用和清单匹配的版本,并显式打开热迁移优化:
1
2
3
4
| cd "$EXPERIMENT_ROOT/rwo-to-rwx-kind"
curl -fsSL https://raw.githubusercontent.com/kubeovn/kube-ovn/v1.14.3/dist/images/install.sh \
-o kube-ovn-install.sh
ENABLE_LIVE_MIGRATION_OPTIMIZE=true bash kube-ovn-install.sh
|
安装脚本可能因无法写入 /usr/local/bin 返回非零;应检查 ovs-ovn、kube-ovn-cni、kube-ovn-controller 和节点状态。VM 还需要设置 bridge 网络热迁移 annotation。Kube-OVN Live Migration
安装 KubeVirt 和 CDI:
1
2
3
4
5
6
7
8
9
10
11
| export KUBEVIRT_VERSION=v1.9.0
kubectl --context kind-rwo-rwx-lab apply -f \
https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml
kubectl --context kind-rwo-rwx-lab apply -f \
https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml
export CDI_VERSION=v1.66.0
kubectl --context kind-rwo-rwx-lab apply -f \
https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-operator.yaml
kubectl --context kind-rwo-rwx-lab apply -f \
https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-cr.yaml
|
CDI 最终为 Deployed。KubeVirt 的 VM 和 VMIM 主链路正常,但 virt-exportproxy 未启动,KubeVirt CR 仍为 Deploying;本文不把它当作完整安装成功的证据。
创建 Kube-OVN VPC/Subnet、NFS 服务、静态 PV 和 RWO PVC。
1
2
3
4
5
| kubectl --context kind-rwo-rwx-lab apply -f 01-vpc-subnet.yaml
kubectl --context kind-rwo-rwx-lab apply -f 02-nfs.yaml
kubectl --context kind-rwo-rwx-lab apply -f 03-rwo-pv-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pvc/rwo-rwx-disk \
--for=jsonpath="{.status.phase}"=Bound --timeout=5m
|
实验 PV 使用 Retain,NFS 的 / 作为固定后端路径。完整配置见固定提交中的 03-rwo-pv-pvc.yaml。
RWO PV/PVC:
用临时 Pod 写入固定内容,然后删除这个 Pod,避免它继续占用 RWO PVC:
写入哨兵文件的 Pod:
1
2
3
4
5
| kubectl --context kind-rwo-rwx-lab apply -f 04-seed-pod.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pod/seed-rwo-disk \
--for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test logs pod/seed-rwo-disk
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pod seed-rwo-disk --wait=true
|
写入结果为:
1
2
| rwo-rwx-sentinel-20260919
8aa6c4c3f25107038ce031ffcd90a83071388e315b2d3049666e900fcc975b70 /data/sentinel.txt
|
然后创建引用 RWO PVC 的 VM。VM 使用 Kube-OVN bridge 网络,并带上允许 Pod bridge 网络热迁移的 annotation:
RWO 阶段的 VM:
1
2
3
| kubectl --context kind-rwo-rwx-lab apply -f 05-vm-rwo.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
--for=condition=Ready --timeout=10m
|
此时 VMI 为 Running,但 LiveMigratable=False:
1
| LiveMigratable=False reason=DisksNotLiveMigratable message=cannot migrate VMI: PVC rwo-rwx-disk is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
|
执行迁移也会在 admission 阶段失败:
1
2
| Error migrating VirtualMachine Internal error occurred: admission webhook "migration-create-validator.kubevirt.io" denied the request: Cannot migrate VMI, Reason: DisksNotLiveMigratable, Message: cannot migrate VMI: PVC rwo-rwx-disk is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)
virtctl_rc=1
|
请求在创建 VMIM 前就被 admission webhook 拒绝。
不能只看 VM 的 runStrategy=Halted,还要等待 VMI 删除并检查 Pod 和卷挂载:
1
2
3
4
5
6
| kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test patch vm rwo-rwx-vm \
--type=merge -p='{"spec":{"runStrategy":"Halted"}}'
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
--for=delete --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get vm,vmi,pod,pvc -o wide
|
删除前先验证 PV 和 PVC 都还是 RWO,并记录 PV 与 PVC 的绑定关系:
1
2
3
4
| kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ACCESS:.spec.accessModes[*],VOLUME_MODE:.spec.volumeMode,RECLAIM:.spec.persistentVolumeReclaimPolicy'
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get pvc rwo-rwx-disk \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ACCESS:.spec.accessModes[*],VOLUME_MODE:.spec.volumeMode,VOLUME:.spec.volumeName'
|
本次实验的预期结果是 PV、PVC 均为 Bound、ReadWriteOnce,PVC 的
volumeName 指向 rwo-rwx-disk-pv。生产静态 Ceph Block PV 还要额外确认
volumeMode=Block,而不是把 Filesystem 卷当成 Block 卷处理。
生产实现还应确认 virt-launcher、VolumeAttachment、备份任务和 CDI worker 都不再占用该卷,并确认 guest 已 flush 文件系统。
这里同时删除 PVC 和 PV 对象。NFS 服务和导出路径不删除,因此数据仍然保留。重新创建的 PV 不是原来的 Kubernetes 对象,但它指向同一个 NFS 后端路径。
这里的“分离”是指 PV/PVC 对象和 NFS 数据路径分开管理。实验中的 NFS 服务使用 emptyDir,删除 NFS 服务或整个 kind 集群仍会删除实验数据;本实验只验证删除 PV/PVC 不会删除 NFS 路径中的数据。
1
2
3
| kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pvc rwo-rwx-disk --wait=true
kubectl --context kind-rwo-rwx-lab delete pv rwo-rwx-disk-pv --wait=true
kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv --ignore-not-found
|
删除后旧 PV 对象不存在:
接着用相同的 NFS server/path 创建新的 RWX PV 和 PVC:
RWX PV/PVC:
1
2
3
4
5
| kubectl --context kind-rwo-rwx-lab apply -f 06-rwx-pvc.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pvc/rwo-rwx-disk \
--for=jsonpath="{.status.phase}"=Bound --timeout=5m
kubectl --context kind-rwo-rwx-lab get pv rwo-rwx-disk-pv -o wide
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get pvc rwo-rwx-disk -o wide
|
结果:
1
2
| PV phase=Bound modes=ReadWriteMany claim=rwo-rwx-disk
PVC phase=Bound modes=ReadWriteMany volume=rwo-rwx-disk-pv
|
在第二个 worker 挂载 PVC,读取哨兵文件:
RWX 读盘 Pod:
1
2
3
4
5
| kubectl --context kind-rwo-rwx-lab apply -f 09-rwx-reader-pod.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait pod/check-rwx-disk \
--for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test logs pod/check-rwx-disk
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test delete pod check-rwx-disk --wait=true
|
读取到的内容和转换前完全一致:
1
2
| rwo-rwx-sentinel-20260919
8aa6c4c3f25107038ce031ffcd90a83071388e315b2d3049666e900fcc975b70 /data/sentinel.txt
|
内容和 SHA-256 与转换前一致,说明重建对象没有覆盖后端数据。
PV 只是 Kubernetes 中描述 Ceph image 的对象,真正的 block 数据在 Ceph image
中。删除 PV/PVC 前,先确认这是静态 PV,回收策略为 Retain,并保存下面这些
信息:
- PV 和 PVC 当前都是
ReadWriteOnce,并且都处于 Bound; - PVC 的
spec.volumeName 指向目标 PV; volumeMode=Block、容量和 storageClassName;- CSI driver、
volumeHandle、clusterID、pool、imageName、镜像特性和格式; - Node Stage、Controller Expand 等 Secret 引用,以及其他节点阶段参数。
停止虚拟机并确认没有挂载后,删除 PVC 和 PV,分别等待两个对象真正消失。然后
使用同一个 volumeHandle 和同一组 CSI 字段重建 PV,只把访问模式改成 RWX:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # 使用原来的 Ceph image 重建静态 PV,只修改访问模式,不创建新镜像。
apiVersion: v1
kind: PersistentVolume
metadata:
# PV 名称需要与后续 PVC 的 volumeName 对应。
name: <pv-name>
spec:
capacity:
# 容量必须和原 PV 保持一致。
storage: <same-capacity>
accessModes:
# 重建后允许多个节点同时读写。
- ReadWriteMany
# Ceph RBD 以原始块设备提供给虚拟机。
volumeMode: Block
# 静态卷删除 Kubernetes 对象时保留 Ceph image。
persistentVolumeReclaimPolicy: Retain
# 静态 PV 不使用动态 StorageClass。
storageClassName: ""
csi:
# CSI driver 和后端标识必须与原 PV 一致。
driver: rbd.csi.ceph.com
volumeHandle: <same-volume-handle>
volumeAttributes:
# 下面的 Ceph 参数必须指向原来的 image。
clusterID: <same-cluster-id>
pool: <same-pool>
imageName: <same-image-name>
imageFormat: "2"
imageFeatures: <same-image-features>
staticVolume: "true"
# 节点挂载阶段使用的 Secret 必须继续有效。
nodeStageSecretRef:
name: <same-node-stage-secret>
namespace: <same-secret-namespace>
# 如果支持扩容,扩容阶段的 Secret 也要保留。
controllerExpandSecretRef:
name: <same-controller-expand-secret>
namespace: <same-secret-namespace>
|
再创建同名 RWX PVC,并通过 volumeName 绑定刚重建的 PV。不能只看到 PVC
进入 Bound 就结束,还要检查 PV/PVC 都是 ReadWriteMany、volumeMode=Block、
volumeHandle 没变,并实际挂载读取数据。任何 CSI 字段缺失,都可能出现 PVC
显示 Bound、实际挂载失败的情况。
系统盘通常由 CDI DataVolume 管理。先创建同名 RWX PVC,再创建同名 DV,并在 DV 的 metadata 上设置 annotation:
DataVolume 和 Claim Adoption 配置:
本次执行:
1
2
3
4
| kubectl --context kind-rwo-rwx-lab apply -f 07-dv-adoption.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait dv/rwo-rwx-disk \
--for=jsonpath="{.status.phase}"=Succeeded --timeout=5m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get dv,pvc -o wide
|
结果:
1
2
3
| DataVolume phase=Succeeded
PVC phase=Bound accessModes=[ReadWriteMany] volumeName=rwo-rwx-disk-pv
PVC ownerReference=DataVolume/rwo-rwx-disk
|
Claim Adoption 只恢复 DataVolume -> PVC 的管理关系,不会修改访问模式、复制数据或格式化磁盘。DataVolume Claim Adoption
目标 PVC 已经存在时,annotation 必须随 DV 创建请求一起发送;没有 annotation 时,CDI admission webhook 会拒绝请求,DV 不会创建。
将 VM 置为 Halted,应用引用 DataVolume 的最终规格,再启动 VM:
RWX 阶段的 VM:
1
2
3
4
5
6
7
| kubectl --context kind-rwo-rwx-lab apply -f 08-vm-rwx.yaml
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test patch vm rwo-rwx-vm \
--type=merge -p='{"spec":{"runStrategy":"Always"}}'
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test wait vmi/rwo-rwx-vm \
--for=condition=Ready --timeout=10m
kubectl --context kind-rwo-rwx-lab -n rwo-rwx-test get vm,vmi,pvc,dv -o wide
|
此时 VMI 的 LiveMigratable=True,执行迁移:
1
2
| virtctl \
--context kind-rwo-rwx-lab -n rwo-rwx-test migrate rwo-rwx-vm
|
迁移成功后,同时检查 VMIM、VMI、launcher、PVC 和 Kube-OVN IP。
| 检查项 | 结果 |
|---|
| VMIM | kubevirt-migrate-vm-jcrtl, Succeeded, completed=true |
| VMIM 源/目标 | rwo-rwx-lab-worker -> rwo-rwx-lab-worker2 |
| VMIM 阶段 | Pending -> Scheduling -> Scheduled -> PreparingTarget -> TargetReady -> Running -> Succeeded |
| VMI | Running, Ready=True, LiveMigratable=True |
| IP | 迁移前后均为 10.251.0.19 |
| Kube-OVN IP | 归属目标节点 rwo-rwx-lab-worker2 和 rwo-rwx-subnet |
| PVC | Bound, ReadWriteMany, volumeName=rwo-rwx-disk-pv |
| DV | Succeeded |
| 数据 | 哨兵文件内容和 SHA-256 不变 |
源 launcher 可能短暂处于 Completed。应等 VMIM 有 completed=true 和 endTimestamp,VMI 切到目标节点,旧 launcher 被清理,PVC/DV 状态稳定后再返回成功。
只允许复用后端数据仍然存在的卷,例如静态 Ceph RBD PV。转换前核对 CSI driver、volumeHandle、RBD image、pool、clusterID、volumeMode=Block、节点阶段参数、Secret 引用和 persistentVolumeReclaimPolicy: Retain。节点本地盘、临时盘、local-path 和依赖原节点目录的 RWO 卷不在范围内。
本地 RWO 不能靠重建 Kubernetes 对象变成共享存储,需要使用 KubeVirt Volume Migration 或应用层复制。KubeVirt Volume Migration
转换期间要阻止启动、挂卸盘、快照、恢复、扩容、删除和再次迁移。任务应保存原 PV/PVC/DV UID、后端句柄、旧新规格和当前阶段;删除对象时使用 UID precondition。PVC Bound 只能说明绑定完成,不能单独作为任务成功条件。
系统盘要同时检查 DV Succeeded、PVC Bound + RWX、PVC ownerReference、新 DV 的后端句柄,再恢复 VM。只重建 PVC 可能让 CDI 按旧 DV 规格重建 PVC,或触发错误的镜像导入。
PV/PVC 显示 RWX 只代表对象匹配成功,不代表 CSI、块设备和 KubeVirt 设备组合都支持多节点读写。本文 kind fixture 使用 NFS Filesystem 验证对象重建和热迁移链路;生产静态 Ceph RBD Block 还必须按上一节保留完整 CSI 信息,并做真实挂载验证。
共享盘满足后,CPU、资源、VMI 网络、OVN 端口或目标节点状态仍可能导致迁移失败。Kube-OVN 需要:
1
2
3
| # 允许 KubeVirt bridge Pod 网络在热迁移中切换节点端口。
annotations:
kubevirt.io/allow-pod-bridge-network-live-migration: "true"
|
验收要检查 VMIM Succeeded + completed=true + completedAt/endTimestamp、VMI 目标节点、目标 launcher、Kube-OVN IP、PVC/DV、旧资源清理和 guest 探针。HTTP 202、VMIM 创建、PVC Bound 或 Pod Running 都不是终态。
实验结束后清理 kind 集群:
1
| kind delete cluster --name rwo-rwx-lab
|
实验清单、原始结果和固定提交仍保留在 k8sdev cce508d0。