jimyag's Blog

KubeVirt 停机把 RWO 静态 PV 重建为 RWX:kind、Kube-OVN 实验

KubeVirt 停机把 RWO 静态 PV 重建为 RWX:kind、Kube-OVN 实验

本文验证:虚拟机关机后,删除历史 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,结果如下。

  1. RWO PVC 的 VMI LiveMigratable=False,热迁移被 KubeVirt admission webhook 拒绝。
  2. 停机后删除 PVC 和 PV 对象,NFS 后端路径不变,重建 RWX PV/PVC 后仍能读到同一个哨兵文件。
  3. 新 DataVolume 成功接管已有 RWX PVC,PVC 的 ownerReference 指向该 DV,DV 进入 Succeeded
  4. 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 验证。

1. 资源和实验范围

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/

本次实际版本如下:

组件版本或配置
kindv0.31.0
Kubernetesv1.35.0,1 个 control-plane、2 个 worker
Kube-OVNv1.14.3
KubeVirtv1.9.0
CDIv1.66.0
存储静态 NFS PV,RetainFilesystem;数据位于 NFS 导出路径
网络Kube-OVN VPC rwo-rwx-vpc、Subnet rwo-rwx-subnet

kind 集群配置:

Loading GitHub file…

Kube-OVN VPC 和 Subnet:

Loading GitHub file…

NFS 服务:

Loading GitHub file…

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-ovnkube-ovn-cnikube-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;本文不把它当作完整安装成功的证据。

2. 转换前:RWO PVC 不能直接热迁移

创建 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:

Loading GitHub file…

用临时 Pod 写入固定内容,然后删除这个 Pod,避免它继续占用 RWO PVC:

写入哨兵文件的 Pod:

Loading GitHub file…
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:

Loading GitHub file…
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 拒绝。

3. 用户关机后转换 PVC/PV

3.1 先确认实例真的停干净

不能只看 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 均为 BoundReadWriteOnce,PVC 的 volumeName 指向 rwo-rwx-disk-pv。生产静态 Ceph Block PV 还要额外确认 volumeMode=Block,而不是把 Filesystem 卷当成 Block 卷处理。

生产实现还应确认 virt-launcherVolumeAttachment、备份任务和 CDI worker 都不再占用该卷,并确认 guest 已 flush 文件系统。

3.2 删除 PVC/PV,保留后端数据并重建 RWX 对象

这里同时删除 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 对象不存在:

1
没有输出

接着用相同的 NFS server/path 创建新的 RWX PV 和 PVC:

RWX PV/PVC:

Loading GitHub file…
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:

Loading GitHub file…
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 与转换前一致,说明重建对象没有覆盖后端数据。

3.3 生产静态 Ceph Block PV 的删建流程

PV 只是 Kubernetes 中描述 Ceph image 的对象,真正的 block 数据在 Ceph image 中。删除 PV/PVC 前,先确认这是静态 PV,回收策略为 Retain,并保存下面这些 信息:

  • PV 和 PVC 当前都是 ReadWriteOnce,并且都处于 Bound
  • PVC 的 spec.volumeName 指向目标 PV;
  • volumeMode=Block、容量和 storageClassName
  • CSI driver、volumeHandleclusterIDpoolimageName、镜像特性和格式;
  • 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 都是 ReadWriteManyvolumeMode=BlockvolumeHandle 没变,并实际挂载读取数据。任何 CSI 字段缺失,都可能出现 PVC 显示 Bound、实际挂载失败的情况。

4. 系统盘的 DataVolume 与 Claim Adoption

系统盘通常由 CDI DataVolume 管理。先创建同名 RWX PVC,再创建同名 DV,并在 DV 的 metadata 上设置 annotation:

DataVolume 和 Claim Adoption 配置:

Loading GitHub file…

本次执行:

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

Loading GitHub file…

目标 PVC 已经存在时,annotation 必须随 DV 创建请求一起发送;没有 annotation 时,CDI admission webhook 会拒绝请求,DV 不会创建。

5. 改成 RWX 后执行 KubeVirt/Kube-OVN 热迁移

将 VM 置为 Halted,应用引用 DataVolume 的最终规格,再启动 VM:

RWX 阶段的 VM:

Loading GitHub file…
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。

检查项结果
VMIMkubevirt-migrate-vm-jcrtl, Succeeded, completed=true
VMIM 源/目标rwo-rwx-lab-worker -> rwo-rwx-lab-worker2
VMIM 阶段Pending -> Scheduling -> Scheduled -> PreparingTarget -> TargetReady -> Running -> Succeeded
VMIRunning, Ready=True, LiveMigratable=True
IP迁移前后均为 10.251.0.19
Kube-OVN IP归属目标节点 rwo-rwx-lab-worker2rwo-rwx-subnet
PVCBound, ReadWriteMany, volumeName=rwo-rwx-disk-pv
DVSucceeded
数据哨兵文件内容和 SHA-256 不变

源 launcher 可能短暂处于 Completed。应等 VMIM 有 completed=trueendTimestamp,VMI 切到目标节点,旧 launcher 被清理,PVC/DV 状态稳定后再返回成功。

6. 生产注意事项

后端和访问模式

只允许复用后端数据仍然存在的卷,例如静态 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 只能说明绑定完成,不能单独作为任务成功条件。

DataVolume 和热迁移验收

系统盘要同时检查 DV Succeeded、PVC Bound + RWX、PVC ownerReference、新 DV 的后端句柄,再恢复 VM。只重建 PVC 可能让 CDI 按旧 DV 规格重建 PVC,或触发错误的镜像导入。

RWX 不是存储能力证明

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 都不是终态。

7. 清理

实验结束后清理 kind 集群:

1
kind delete cluster --name rwo-rwx-lab

实验清单、原始结果和固定提交仍保留在 k8sdev cce508d0

#Kubernetes #KubeVirt #Kube-Ovn #Storage #Kind