ᕕ( ᐛ )ᕗ Jimyag's Blog

KubeVirt local-path 卷迁移实测:进程、I/O 与网络连续性

KubeVirt local-path 卷迁移实测:进程、I/O 与网络连续性

KubeVirt 普通热迁移要求源、目标节点同时访问 PVC,所以绑定 local-path RWO PVC 的 VM 不能直接执行 virtctl migrate。本文先用 CDI 创建 DataVolume,再让 VM 引用 DataVolume;迁移时创建另一个 DataVolume,由 KubeVirt 复制磁盘并切换卷引用。

上一篇实验 测了共享存储和 Kube-OVN 网络。这次只测 local-path,并且不只看 VMIM 是否成功:guest 内持续写盘,外部同时请求 HTTP 服务并保持 TCP 长连接。

VM 没有重启,进程 PID 不变,6,689 次 fdatasync 没有丢序号,最大写入间隔为 114.776ms。网络侧没有这么平滑:TCP 探针超时后重新连接,外部相邻成功响应的最大间隔约为 2.06s

1. 普通热迁移与 Volume Migration

普通 Live Migration 假设持久盘已经是共享存储。KubeVirt 根据 PVC 的 Access Mode 判断 VMI 是否可迁移,普通迁移遇到 RWO 本地盘会直接拒绝。KubeVirt Live Migration 文档中说明:LiveMigration 只复制实例内存;BlockMigration 表示还有磁盘需要从源端复制到目标端。

Volume Migration 最终仍在两个 PVC 之间复制数据。本文不直接创建 PVC,而是先创建源、目标 DataVolume,由 CDI 生成同名 PVC。VM 需要设置:

1
2
spec:
  updateVolumesStrategy: Migration

VM 通过 volumes[].dataVolume.name 引用源 DataVolume。把这个字段改成目标 DataVolume 后,KubeVirt 创建 VirtualMachineInstanceMigration,复制卷并迁移 VM。目标卷可以使用不同的存储类型,但容量不能小于源卷。其他限制见 Volume Migration 文档

local-path PVC 仍然只属于一个节点。迁移发生在两个 PVC 之间:

  flowchart LR
    A["worker<br/>local-source DV/PVC"] -->|"复制磁盘块"| B["worker2<br/>local-target DV/PVC"]
    C["源 virt-launcher<br/>guest 继续运行"] -->|"迁移内存与设备状态"| D["目标 virt-launcher<br/>恢复执行"]

2. 实验环境

实验在一台 Linux 主机上创建临时三节点 kind 集群:

组件 版本或配置
kind v0.32.0
Kubernetes v1.35.0,1 个 control-plane、2 个 worker
KubeVirt v1.9.0
CDI v1.66.0
宿主机内核 6.8.0-138-generic
虚拟化 /dev/kvm
数据盘 两个 DataVolume,各生成一个 ReadWriteOnceFilesystemstandard PVC
VM 网络 kind 默认 CNI、KubeVirt masquerade、NodePort Service

2.1 kind.yaml

三个 kind 节点都挂载宿主机的 /dev/kvm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建一个 control-plane 和两个 worker,用于验证跨节点迁移。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: kv-local-continuity
nodes:
- role: control-plane
  image: kindest/node:v1.35.0
  extraMounts:
  # 把宿主机的 KVM 设备挂进 kind 节点。
  - hostPath: /dev/kvm
    containerPath: /dev/kvm
- role: worker
  image: kindest/node:v1.35.0
  extraMounts:
  - hostPath: /dev/kvm
    containerPath: /dev/kvm
- role: worker
  image: kindest/node:v1.35.0
  extraMounts:
  - hostPath: /dev/kvm
    containerPath: /dev/kvm

创建集群:

1
kind create cluster --config kind.yaml

2.2 kubevirt-config.yaml

安装 KubeVirt v1.9.0 和 CDI v1.66.0:

1
2
3
4
5
6
7
8
9
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/v1.9.0/kubevirt-operator.yaml
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/v1.9.0/kubevirt-cr.yaml
kubectl -n kubevirt wait kubevirt kubevirt \
  --for=condition=Available --timeout=20m

kubectl apply -f https://github.com/kubevirt/containerized-data-importer/releases/download/v1.66.0/cdi-operator.yaml
kubectl apply -f https://github.com/kubevirt/containerized-data-importer/releases/download/v1.66.0/cdi-cr.yaml
kubectl -n cdi wait cdi cdi \
  --for=condition=Available --timeout=15m

下面是本次实验合并到 KubeVirt CR 的配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 这个文件通过 kubectl patch 合并到已安装的 KubeVirt CR。
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  configuration:
    # 允许运行中的 VM 通过热更新应用配置。
    vmRolloutStrategy: LiveUpdate
  workloadUpdateStrategy:
    # Volume Migration 依赖 LiveMigrate 更新方式。
    workloadUpdateMethods:
    - LiveMigrate
1
2
3
4
kubectl -n kubevirt patch kubevirt kubevirt \
  --type=merge --patch-file kubevirt-config.yaml
kubectl -n kubevirt wait kubevirt kubevirt \
  --for=condition=Available --timeout=5m

2.3 业务资源

每个文件只放一个资源。把下面的代码块按标题保存到当前目录即可。CDI 根据 DataVolume 创建同名 PVC;两个临时 Pod 只用于让 standard StorageClass 分别在两个 worker 上创建本地 PV。

DataVolume 既可以作为系统盘,也可以作为数据盘。本文保留 containerDisk 作为启动盘,只迁移承载写入探针的数据盘,避免镜像导入过程影响连续性数据。VM 关联 DataVolume 的字段和迁移方式相同,都是 volumes[].dataVolume.name

namespace.yaml

1
2
3
4
5
# 实验资源统一放在独立命名空间,便于清理。
apiVersion: v1
kind: Namespace
metadata:
  name: vm-local-migration-test

source-dv.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 源 DataVolume 由 CDI 创建同名 PVC,并生成空白数据盘。
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: local-source
  namespace: vm-local-migration-test
spec:
  source:
    blank: {}
  pvc:
    accessModes:
    - ReadWriteOnce
    volumeMode: Filesystem
    # kind 默认的 standard StorageClass 使用本地存储。
    storageClassName: standard
    resources:
      requests:
        storage: 1Gi

target-dv.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 目标 DataVolume 使用相同容量,稍后固定到第二个 worker。
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: local-target
  namespace: vm-local-migration-test
spec:
  source:
    blank: {}
  pvc:
    accessModes:
    - ReadWriteOnce
    volumeMode: Filesystem
    storageClassName: standard
    resources:
      requests:
        storage: 1Gi

prepare-source.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# kind 没有业务侧的调度逻辑,用临时 Pod 把源 DV 固定到第一个 worker。
apiVersion: v1
kind: Pod
metadata:
  name: prepare-source
  namespace: vm-local-migration-test
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: kv-local-continuity-worker
  containers:
  - name: prepare
    image: quay.io/quay/busybox:latest
    command:
    - /bin/true
    volumeMounts:
    - name: disk
      mountPath: /data
  volumes:
  - name: disk
    persistentVolumeClaim:
      claimName: local-source

prepare-target.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 用临时 Pod 把目标 DV 固定到第二个 worker,确保迁移跨节点发生。
apiVersion: v1
kind: Pod
metadata:
  name: prepare-target
  namespace: vm-local-migration-test
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: kv-local-continuity-worker2
  containers:
  - name: prepare
    image: quay.io/quay/busybox:latest
    command:
    - /bin/true
    volumeMounts:
    - name: disk
      mountPath: /data
  volumes:
  - name: disk
    persistentVolumeClaim:
      claimName: local-target

vm.yaml

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
# VM 的数据盘通过 DataVolume 名称关联,而不是直接引用 PVC。
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: local-continuity-vm
  namespace: vm-local-migration-test
spec:
  runStrategy: Always
  # 修改 DataVolume 引用时,要求 KubeVirt 执行 Volume Migration。
  updateVolumesStrategy: Migration
  template:
    metadata:
      labels:
        kubevirt.io/domain: local-continuity-vm
    spec:
      evictionStrategy: LiveMigrate
      domain:
        resources:
          requests:
            memory: 256Mi
        devices:
          disks:
          - name: rootdisk
            disk:
              bus: virtio
          - name: datadisk
            disk:
              bus: virtio
          - name: cloudinitdisk
            disk:
              bus: virtio
          interfaces:
          - name: default
            masquerade: {}
      networks:
      - name: default
        pod: {}
      volumes:
      - name: rootdisk
        containerDisk:
          image: quay.io/kubevirt/cirros-container-disk-demo:latest
      - name: datadisk
        dataVolume:
          name: local-source
      - name: cloudinitdisk
        cloudInitNoCloud:
          # 仅供临时实验登录,不要在生产环境使用明文密码。
          userData: |
            #cloud-config
            password: gocubsgo
            chpasswd: { expire: false }
            ssh_pwauth: true

service.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 通过 kind 节点的固定端口访问 VM 内的 SSH、HTTP 和 TCP 探针。
apiVersion: v1
kind: Service
metadata:
  name: local-continuity-vm
  namespace: vm-local-migration-test
spec:
  type: NodePort
  selector:
    kubevirt.io/domain: local-continuity-vm
  ports:
  - name: ssh
    port: 22
    targetPort: 22
    nodePort: 30022
  - name: http
    port: 8080
    targetPort: 8080
    nodePort: 30080
  - name: tcp-echo
    port: 9000
    targetPort: 9000
    nodePort: 30090

先创建源 DataVolume。等待它和临时 Pod 完成后,再创建引用源 DV 的 VM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
kubectl apply -f namespace.yaml
kubectl apply -f source-dv.yaml
kubectl apply -f prepare-source.yaml

kubectl -n vm-local-migration-test wait dv/local-source \
  --for=condition=Ready --timeout=5m
kubectl -n vm-local-migration-test wait pod/prepare-source \
  --for=jsonpath='{.status.phase}'=Succeeded --timeout=5m

kubectl -n vm-local-migration-test delete pod prepare-source --wait=true

kubectl apply -f vm.yaml
kubectl apply -f service.yaml

kubectl -n vm-local-migration-test wait \
  --for=create vmi/local-continuity-vm --timeout=2m
kubectl -n vm-local-migration-test wait vmi/local-continuity-vm \
  --for=condition=Ready --timeout=5m

VM 运行后,再创建目标 DataVolume,并把它固定到第二个 worker:

1
2
3
4
5
6
7
8
9
kubectl apply -f target-dv.yaml
kubectl apply -f prepare-target.yaml

kubectl -n vm-local-migration-test wait dv/local-target \
  --for=condition=Ready --timeout=5m
kubectl -n vm-local-migration-test wait pod/prepare-target \
  --for=jsonpath='{.status.phase}'=Succeeded --timeout=5m

kubectl -n vm-local-migration-test delete pod prepare-target --wait=true

此时两个 DataVolume 都是 Succeeded,对应 PVC 的节点关系为:

1
2
local-source  -> kv-local-continuity-worker
local-target  -> kv-local-continuity-worker2

CDI 为 blank DataVolume 创建的空白盘包含大量未分配块,所以本文不比较磁盘吞吐量。

3. 探针设计

只比较文件内容无法排除 VM 重启,因此还要记录 boot ID、进程 PID 和外部连接。

3.1 确认 guest 没有重启

系统启动时会生成 /proc/sys/kernel/random/boot_id。迁移前后值相同,说明 guest 没有重启。

VM 内同时运行三个长期进程:

  • 每 10ms 写盘并调用 fdatasync 的探针。
  • TCP echo server。
  • 每次请求返回 probe-ok 的 HTTP server。

迁移前记录 PID,迁移后检查对应的 /proc/<pid>

3.2 观察 fdatasync 间隔

写盘探针使用静态链接的 C 程序。每次写入包括递增序号和 CLOCK_REALTIME 纳秒时间戳,写完立即调用 fdatasync

1
2
3
4
5
6
7
8
while (running) {
    clock_gettime(CLOCK_REALTIME, &now);
    snprintf(line, sizeof(line), "%llu,%lld%09ld\n",
             ++sequence, now.tv_sec, now.tv_nsec);
    write_all(fd, line, strlen(line));
    fdatasync(fd);
    nanosleep(&interval_10ms, NULL);
}

迁移后检查序号和相邻时间戳。间隔还包含 guest 调度和 fdatasync 延迟,只能视为 guest 可见停顿的上界,不能当作 QEMU 的精确 downtime。

3.3 观察服务访问

VM 通过 NodePort Service 暴露两个端口:

  • HTTP 探针每 10ms 建立一个新连接并发起请求,单次超时 500ms。
  • TCP 探针保持一条 echo 长连接,每 10ms 发送一次序号;发生错误后才重新连接。

探针经过完整的服务访问路径:

1
m6 -> kind NodePort -> Kubernetes Service -> virt-launcher -> masquerade -> guest

这组数据用于区分 guest 停顿和入口网络切换。

4. 触发迁移

先尝试普通迁移:

1
virtctl -n vm-local-migration-test migrate local-continuity-vm

准入检查按预期拒绝:

1
2
3
Cannot migrate VMI, Reason: DisksNotLiveMigratable,
Message: cannot migrate VMI: PVC local-source is not shared,
live migration requires that all PVCs must be shared

然后把 VM 数据卷改为目标 DataVolume:

1
2
3
kubectl -n vm-local-migration-test patch vm local-continuity-vm \
  --type=json \
  -p='[{"op":"replace","path":"/spec/template/spec/volumes/1/dataVolume/name","value":"local-target"}]'

提交 PATCH 后,KubeVirt 自动创建 VMIM。最终状态如下:

1
2
3
4
phase=Succeeded
pre_node=kv-local-continuity-worker
post_node=kv-local-continuity-worker2
method=BlockMigration completed=true

libvirt 使用 PreCopy,没有启用自动收敛、post-copy 或额外的工作负载中断:

1
2
3
4
5
mode=PreCopy
allowAutoConverge=false
allowPostCopy=false
allowWorkloadDisruption=false
maxDowntimeMs=900

Pre-copy 先创建目标 VM,guest 继续在源端运行;状态复制完成后,guest 才切到目标端。KubeVirt Live Migration 策略说明建议大多数场景使用这种方式。

5. 实验结果

5.1 boot ID 和 PID

迁移前后读取结果一致:

信号 迁移前 迁移后
boot ID ef3b014e-1546-4de0-862c-fb37168c0c0d 相同
fdatasync 探针 PID 665 665,存活
TCP server PID 666 666,存活
HTTP server PID 667 667,存活

5.2 fdatasync 序列

指标 结果
总落盘记录 6,689
首尾序号 16689
丢失序号 0
最大相邻落盘间隔 114.776ms

最大间隔出现在迁移开始后的约 13.94s,接近最终切换阶段:

1
2
5501,1788687461340871853
5502,1788687461455647847

迁移后还能继续创建并同步新文件:

1
post-migration-write-ok

这组顺序追加写没有丢数据,但它不能替代数据库事务和故障恢复测试,也不覆盖宿主机掉电。

5.3 HTTP 与 TCP

探针 总次数 成功 超时 相邻成功响应最大间隔 连接数
HTTP 新连接 5,502 5,494 8 2059.871ms 每次新建
TCP 长连接 5,890 5,886 4 2060.876ms 2

切换附近,连接 1 上的一次请求在 500ms 内没有响应,探针随即关闭它。三次重连超时后,连接 2 建立成功。由于探针主动关闭了原 socket,本轮无法判断它是否会自行恢复,只能确认客户端没有无感通过切换。

HTTP 超时也集中在切换附近,但不是连续失败。失败时段与源 virt-launcher、目标 virt-launcher 和 EndpointSlice 的交接重合;迁移完成后观察到:

1
2
source virt-launcher: Completed, endpoint ready=false
target virt-launcher: Running,   endpoint ready=true

外部路径在切换阶段抖动约 2 秒,明显长于 guest 内约 115ms 的最大写入间隔。问题可能出在 NodePort、Service endpoint、virt-launcher 或 masquerade 的交接;由于没有抓包,本文不再细分。

  sequenceDiagram
    participant Client as 外部探针
    participant Source as 源 virt-launcher
    participant VM as guest
    participant Target as 目标 virt-launcher

    Client->>Source: HTTP 和 TCP 流量
    Source->>VM: guest 继续运行并 fdatasync
    Source-->>Target: 复制磁盘、内存和设备状态
    Note over VM: 最大写入间隔约 114.8ms
    Source--xClient: 原连接未按时响应
    Target->>VM: 在目标节点继续执行
    Client->>Target: 重连后恢复
    Note over Client,Target: 相邻成功响应最大间隔约 2.06s

6. 使用边界

本轮只说明:在 KubeVirt v1.9.0、Pre-copy 和低写入压力下,local-path Volume Migration 能保留 guest 与进程状态,持续同步写盘也没有丢序号。网络连续性要单独评估,不能用 VMIM Succeeded 代替业务可用性测试。

测试盘是稀疏文件,写入量也很小。高内存 dirty rate 或高磁盘写入速率可能让 Pre-copy 无法收敛。当前 allowWorkloadDisruption=false,超时后迁移应失败或取消;启用 post-copy 或允许暂停后,需要重新评估故障风险和停顿时间。

上线前至少测试:

  1. 接近真实容量和已分配数据量的磁盘。
  2. 数据库或实际业务的峰值写入速率。
  3. 业务自己的 P99/P999 延迟和超时设置。
  4. 生产 CNI、Service、LB 和客户端重试策略。
  5. 迁移失败、取消以及 ManualRecoveryRequired 的恢复流程。

7. 清理

实验完成后删除临时 kind 集群:

1
kind delete cluster --name kv-local-continuity

参考资料

#Kubevirt #Kind #Kubernetes #Storage #Virtualization