Skip to content

04 · 运维手册:虚拟机迁移与高可用

环境:Harvester v1.8.2(Helm on RKE2)、KubeVirt 1.7.4-150700.3.24.2、Longhorn 1.11.2、3 节点。 结论:本环境当前只支持冷迁移(已实测通过),热迁移被 Longhorn RWX 数据面阻塞(已定位)。


1. 能力矩阵(本环境实测)

能力状态前置条件实测证据
冷迁移(停机 → 换节点启动)可用RWO+Filesystem 卷、目标节点可调度sza122101 → sza122102,VMI Running,Longhorn 卷 attached/healthy,新 virt-launcher
热迁移(不停机换节点)不可用所有 PVC 为 RWX + Longhorn RWX 数据面可用KubeVirt DisksNotLiveMigratable;Longhorn RWX 卷无 shareEndpoint、无 share-manager Pod,CDI qemu-img 写盘失败
节点维护模式⚠️ 需改策略默认策略是 Migrate,依赖热迁移见 §4,本环境应改为 ShutdownAndRestartAfterEnable
快照创建✅ 可用gate=Snapshot(单数);实测 5~11s readyToUse=true03-运维手册-虚拟机生命周期.md §6
快照恢复⚠️ 不可靠type: snap 下源卷被 GC 与 full-copy 克隆竞态(4 次实测 2 失败、2 成功但卷 degraded);须 type: bak + backup target02 案例 17、03 §6.4
存储高可用(多副本)✅ 可用numberOfReplicas ≤ 可调度节点数harvester-longhorn = 3;Longhorn 3 节点均 schedulable、available≈110GB

2. 冷迁移(★ 当前标准做法)

2.1 原理

VM 停机 → VMI 与 virt-launcher Pod 被回收 → 卷 detach → 修改调度约束 → 启动 → KubeVirt 在满足约束的节点重建 Pod,Longhorn 把卷 attach 到新节点。

2.2 操作流程(已实测)

bash
VM=default/vm-demo
FROM=sza122101.local
TO=sza122102.local

# 1) 停机(注意:是 Halted,不是 Stopped;不要用 spec.running)
kubectl patch vm -n ${VM%%/*} ${VM##*/} --type=merge -p '{"spec":{"runStrategy":"Halted"}}'

# 2) 等待 VMI 完全回收(无 OS 的空盘 VM 需等 terminationGracePeriodSeconds,约 30s)
kubectl -n ${VM%%/*} wait vmi/${VM##*/} --for=delete --timeout=180s

# 3) 绑定目标节点
kubectl patch vm -n ${VM%%/*} ${VM##*/} --type=merge \
  -p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"kubernetes.io/hostname\":\"${TO}\"}}}}}"

# 4) 启动
kubectl patch vm -n ${VM%%/*} ${VM##*/} --type=merge -p '{"spec":{"runStrategy":"Always"}}'

# 5) 验证
kubectl get vmi -n ${VM%%/*} ${VM##*/} -o wide          # NODE 应为 TO
kubectl get pod -n ${VM%%/*} -l kubevirt.io/domain=${VM##*/} -o wide
PV=$(kubectl get pvc -n ${VM%%/*} ${VM##*/}-disk-0 -o jsonpath='{.spec.volumeName}')
kubectl get volumes.longhorn.io -n longhorn-system "$PV" \
  -o jsonpath='state={.status.state} robustness={.status.robustness} node={.status.currentNodeID}{"\n"}'

2.3 实测结果

迁移前  node=sza122101.local  pod=virt-launcher-…-6l2x6   卷 state=attached robustness=healthy
停机    runStrategy=Halted → VMI 回收(空盘 VM 因无 ACPI 响应,等待约 48s,符合 grace period)
迁移后  node=sza122102.local  pod=virt-launcher-…-5zqpv   卷 state=attached robustness=healthy ✓
        VMI phase=Running     VM printableStatus=Running  nodeSelector=sza122102.local ✓

耗时:停机约 48s(受 terminationGracePeriodSeconds=30 + 回收时间影响)+ 启动约 20s。

2.4 注意事项

  • 停机慢是空盘/无 guest agent 的特性;有正常 OS 与 qemu-guest-agent 时会快得多;
  • 迁移后 nodeSelector永久生效,若希望恢复自由调度需移除:
bash
kubectl patch vm -n default <vm> --type=json \
  -p '[{"op":"remove","path":"/spec/template/spec/nodeSelector/kubernetes.io~1hostname"}]'
  • 冷迁移会造成业务中断,需在变更窗口执行;关键 VM 建议先打快照(见 03 §6)。

3. 热迁移(当前不可用,含修复路线)

3.1 硬性前提

前提说明本环境状态
LiveMigration feature gateGA 且无法关闭(不在可禁用列表)✓ 无需处理
migration 网络未显式配置时自动使用 Pod 网络✓ 无需处理
所有 PVC 为 ReadWriteManyKubeVirt 迁移前置检查强制要求✗ 当前用 RWO
RWX 卷数据面可用Longhorn RWX 依赖 share-manager/NFS阻塞点

3.2 实测报错链

① RWO 卷直接迁移:
   reason=DisksNotLiveMigratable
   "Some disks don't support live migration, please make sure all PVCs have access mode: ReadWriteMany"

② 改用 RWX(Filesystem)后 —— CDI 导入阶段就失败:
   could not create raw image with size 2147483648 in qemu-img format.
   qemu-img: Could not open '/mnt/…/disk.img': Could not open '…': No such file or directory
   qemu-img execution failed: exit status 1

③ 改用 RWX(Block):
   blockdev: cannot open /dev/cdi-block-volume: Permission denied
   (Longhorn RWX 走 NFS 共享文件系统,不提供块设备)

④ 根因(Longhorn 数据面):
   kubectl get volumes.longhorn.io -n longhorn-system <rwx-vol> -o wide
     state=detached  shareEndpoint=<空>  shareState=<空>
   kubectl get pod -n longhorn-system | grep share-manager
     <无输出>
   kubectl get engines.longhorn.io -n longhorn-system <vol> -o wide
     mode=""  currentState=""

3.3 修复路线(若业务确需热迁移)

  1. 确认节点镜像具备 NFS 客户端能力: Harvester OS 内建 share-manager 组件,但本环境是 Helm 部署在通用 RKE2 节点上, 节点镜像未必包含 nfs-utils / NFS 内核模块:
    bash
    # 在每个节点执行
    rpm -q nfs-utils || dpkg -l | grep nfs-common
    lsmod | grep -E '^nfs|nfsd'
    mount -t nfs -V
  2. 检查 Longhorn RWX 相关组件与配置
    bash
    kubectl -n longhorn-system get pod | grep -iE 'share-manager|csi-plugin'
    kubectl -n longhorn-system get ds longhorn-csi-plugin -o wide    # 期望 3/3
    kubectl -n longhorn-system get settings.longhorn.io | grep -iE 'share-manager|nfs'
  3. 手工验证 share-manager 能否拉起:创建一个独立 RWX PVC(不挂 VM)观察是否出现 share-manager-<vol> Pod 且 shareEndpoint 被填充;据此定位是 NFS 客户端问题还是 Longhorn 控制器问题。
  4. 修好后复测顺序:RWX PVC 独立挂载 → CDI 导入 RWX 卷成功 → VM 以 RWX 运行 → 发起热迁移 → 确认 VirtualMachineInstanceMigration 进入 Succeeded 且 VMI nodeName 变更。

3.4 源码侧参考(避免误判)

go
// vendor/kubevirt.io/kubevirt/pkg/util/hardware/hardware.go:131
LiveMigration = featuregate.FeatureGate{
    Name: LiveMigrationGate, FeatureSpec:
    featuregate.FeatureSpec{Default: true, PreRelease: featuregate.GA},   // GA,不可禁用
}

// pkg/util/virtualmachine/virtualmachine.go:169
func volumeSupportRWXForVM(accessModes []corev1.PersistentVolumeAccessMode, provisioner string) bool {
    if provisioner == util.CSIProvisionerLonghorn {
        // Longhorn provisioner does not support RWX volume for VM
        return false
    }
    return slices.Contains(accessModes, corev1.ReadWriteMany)
}

说明:Harvester 自身在 Longhorn 场景不认可 VM 使用 RWX,这与 KubeVirt 热迁移要求 RWX 存在 张力 —— 即在 Longhorn 形态下,官方路径本就是"冷迁移/维护模式关机重启",热迁移并非默认支持能力。


4. 节点维护模式(★ 本环境必须调整策略)

4.1 v1.8 的维护模式模型(与旧版本不同)

v1.8.2 不是通过 Harvester Node CR 的 spec.maintenanceMode 字段,而是 节点 action + 注解/标签 模型(源码:pkg/api/node/formatter.gopkg/controller/master/node/maintain_controller.go):

状态注解harvesterhci.io/maintain-status = running / completed
节点 actionenableMaintenanceModedisableMaintenanceModecordonuncordonlistUnhealthyVMmaintenancePossiblepowerActionshutdown/poweron/reboot)、enableCPUManager/disableCPUManager
驱逐请求注解harvesterhci.io/drain-requestedharvesterhci.io/drain-forced
KubeVirt 驱逐键kubevirt.io/drain
可行性预检drainhelper.DrainPossible() → 不可行时报 draining of this node is not possible
策略标签harvesterhci.io/maintain-mode-strategy
策略归属注解harvesterhci.io/maintain-mode-strategy-node-name

4.2 维护模式策略(决定 VM 的命运)

源码 pkg/util/constants.go

go
MaintainModeStrategyMigrate                        = "Migrate"
MaintainModeStrategyShutdownAndRestartAfterEnable  = "ShutdownAndRestartAfterEnable"
MaintainModeStrategyShutdownAndRestartAfterDisable = "ShutdownAndRestartAfterDisable"
MaintainModeStrategyShutdown                       = "Shutdown"
MaintainModeStrategyDefault                        = MaintainModeStrategyMigrate   // ★ 默认 Migrate
策略行为本环境可用性
Migrate默认热迁移 VM 到其他节点不可用(热迁移被 RWX 阻塞)
ShutdownAndRestartAfterEnable进入维护时关机,维护结束后自动重启推荐
ShutdownAndRestartAfterDisable进入维护时关机,退出维护时重启✅ 可用
Shutdown仅关机,不自动重启✅ 可用(需人工启动)

关键运维结论:本环境沿用默认 Migrate 会让维护模式卡在"无法迁移 VM", 因此必须显式指定 ShutdownAndRestartAfterEnable(或 Shutdown)。 修复 Longhorn RWX 后才可回到 Migrate 实现无中断维护。

4.3 操作流程

bash
NODE=sza122102.local
NS=default

# 1) 预检:该节点能否进入维护(会检查 VM 可迁移性/可关闭性)
kubectl annotate node $NODE harvesterhci.io/drain-requested="true" --overwrite   # 触发 drain 预检/流程
kubectl get node $NODE -o jsonpath='{.metadata.annotations}{"\n"}' | tr ',' '\n' | grep -i maintain

# 2) 指定 VM 的维护策略(避免默认 Migrate)
for vm in $(kubectl get vm -n $NS -o name); do
  kubectl -n $NS label $vm harvesterhci.io/maintain-mode-strategy=ShutdownAndRestartAfterEnable --overwrite
done

# 3) 观察维护进度:注解由 running → completed
kubectl get node $NODE -o jsonpath='{.metadata.annotations.harvesterhci\.io/maintain-status}{"\n"}'

# 4) 维护完成后退出(清除注解 / 调 disableMaintenanceMode action / uncordon)
kubectl annotate node $NODE harvesterhci.io/maintain-status- 
kubectl uncordon $NODE
kubectl get node $NODE -o jsonpath='{.spec.unschedulable}{"\n"}'   # 应为空/false

Dashboard 操作:Hosts → 选中节点 → 右上 Enable Maintenance Mode; 退出时同一菜单 Disable Maintenance Mode。UI 会调用上述 action 并展示 maintenancePossible 结果。

4.4 手工 drain(不使用维护模式时)

bash
# Harvester 的 drain 是 Longhorn/KubeVirt 感知的(drainhelper 基于 k8s.io/kubectl/pkg/drain + 过滤器)
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --force
# 完成后
kubectl uncordon $NODE

直接 kubectl drain 不会处理 VM 的维护策略;对含 VM 的节点,优先使用维护模式


5. 高可用与容量规划

5.1 存储副本与调度

bash
kubectl get sc harvester-longhorn -o yaml | grep -E 'numberOfReplicas|dataLocality|migratable|fromBackup'
#   numberOfReplicas: "3"     ← 3 节点集群的上限;节点 < 3 时必须下调,否则卷无法调度
#   dataLocality: disabled
#   migratable: "true"
#   fromBackup: ""
kubectl get nodes.longhorn.io -n longhorn-system -o wide    # 关注 schedulable / available
集群节点数建议 numberOfReplicas
11
22(或 1,视可用性要求)
≥33

5.2 备份与容灾

bash
# 备份目标(生产必配;否则 type: bak 快照会失败)
kubectl get settings.longhorn.io -n longhorn-system backup-target -o jsonpath='{.value}'
kubectl get settings.longhorn.io -n longhorn-system backup-target-credential-secret -o jsonpath='{.value}'
  • 未配置 backup target 时,Longhorn 快照只能走 type: snap(卷内快照), 既不满足容灾要求(数据仍在集群内),也无法可靠地用于 VM 恢复: restore 会替换 PVC → 源卷被删 → Longhorn Snapshot CR 因 ownerReference 被 GC, 与 full-copy 克隆形成竞态;4 次实测 2 次失败(卷卡 initiated/failed、VM 起不来), 2 次成功但卷遗留 robustness=degraded(3 副本只建出 1 个)。 取证与机制见 02-故障排查与修复记录.md 案例 1703 手册 §6.4;
  • 因此 配置 backup target 不是"锦上添花",而是 VM 快照可靠恢复的前提: 生产环境应配置 NFS/S3 backup target,并用 type: bak 语义做 VM 快照;
  • 建议演练:定期做一次"快照 → 恢复 → 启动 → 数据校验",确认恢复链路真实可用 (scripts/vm-e2e-test.sh 已自动化,且会在克隆不收敛时反查源卷给出根因)。

5.3 计算层高可用

组件副本说明
harvester3Deployment,滚动更新(maxSurge=1/maxUnavailable=1
harvester-webhook3校验/默认化 webhook
kubevirt 控制器(virt-controller/virt-api/virt-handler)kube-systemvirt-handler 为 DaemonSet,每节点一个
vm-import-controller2Harvester 的 VirtualMachineImport(v2v)控制器
bash
kubectl get deploy,ds -n harvester-system -o wide
kubectl get deploy,ds -n kube-system | grep -E 'virt-|cdi|snapshot'

5.4 巡检与告警建议

bash
bash scripts/healthcheck.sh          # 平台健康度(约 20s,非零退出码可用于告警)
bash scripts/vm-e2e-test.sh          # VM 端到端能力验证(创建→冷迁移→快照→清理)

建议纳入监控的指标/信号:

  • Longhorn 各节点 available(磁盘水位)、卷 robustness != healthy 数量;
  • csi-snapshotter 是否出现 backup target is not available(配置漂移);
  • kubevirt CR spec.configuration.developerConfiguration.featureGates 是否仍含 Snapshothelm upgrade 会覆盖 CR,导致 gate 静默失效 —— 本环境实际发生过);
  • 默认 StorageClass 是否仍为 harvester-longhorn(可能被其他 chart 抢占);
  • VM printableStatus 长期停留在 Provisioning/Scheduling/DataVolumeError