主题
04 · 部署实战 B:容器化 KubeVirt 装进既有 K8s
形态:不换底座,只叠加能力。在已运行的 RKE2 集群上用 Helm 部署 Harvester v1.8.2 (
promote模式),把 KubeVirt + Longhorn + CDI + harvester 控制器作为一组子 chart 装进去。实测结果:控制面全绿、VM 端到端(创建 → 冷迁移 → 快照 → 恢复 → 清理)全流程通过, 平台巡检 PASS=30 / WARN=6 / FAIL=0。 本章命令均可直接复制执行;标注 ⚠️/❌ 的是已定位的阻塞点与坑。
与 03 ISO 一体机 的取舍见 10 五方对比。
1. 选型与架构
1.1 两种形态:promote 到底是什么
| 形态 | 说明 | 适用 | 网络参数来源 |
|---|---|---|---|
| Harvester OS(ISO) | 节点即 Harvester,内置 RKE2,全托管 | 全新裸金属虚拟化集群 | 装机时自己建(config.yaml) |
| Helm 装进既有 RKE2(本章) | 把 Harvester 作为一组 chart 叠加到现有集群 | 已有 K8s,想加虚拟化能力 | ★ 必须用 promote 告知既有集群的真实 CIDR |
promote 的三个键(clusterDNS / clusterPodCIDR / clusterServiceCIDR)之所以是硬性要求: Harvester 的 kube-vip、harvester-network-controller、harvester-load-balancer 会按这些值计算路由与 LB 后端。ISO 形态下集群是它自己建的,参数天然一致; 而叠加部署时若沿用默认值,这三个组件会算错路由——症状是 LB/VIP 不通、网络配置下发失败, 且报错点离根因很远。
⚠️ 官方支持矩阵以 Harvester OS 为主线,Helm 叠加部署属「可行但需自行兜底」的路径: 本章所有固化动作(§5)与验收(§6)都是为此而设。
1.2 组件拓扑(实测)
┌───────────────────────────────────────────────┐
浏览器 / kubectl ───► │ Service harvester(NodePort 32735, https) │
(★ 必须带 SNI) └──────────────────┬────────────────────────────┘
│
┌──────────────────────┴──────────────────────┐
│ harvester / harvester-webhook (Deployment) │ ← Steve 聚合 API + Admission
└──────────────────────┬──────────────────────┘
│ APIService (harvesterhci.io …)
┌────────────────────────────────────────┴─────────────────────────────────────┐
│ kube-apiserver(RKE2 提供,★ 不是 Harvester 自带) │
└──┬──────────────┬──────────────┬──────────────┬──────────────┬───────────────┘
│ │ │ │ │
kubevirt CR cdi CR longhorn CR harvesterhci.io snapshot.storage.k8s.io
│ │ │ │ │
virt-operator cdi-operator longhorn-mgr harvester 控制器 rke2-snapshot-controller
virt-api cdi-deploy longhorn-csi (20+ 个) (RKE2 自带 ✓)
virt-controller cdi-upload instance-mgr
virt-handler(DS)
│
virt-launcher Pod(每 VMI 一个,内含 qemu-kvm)与 ISO 形态的关键差别:K8s 控制面、CNI、snapshot-controller 都由既有集群提供, Harvester 只带来「虚拟化 + 存储 + 网络管理」这一层。因此冲突预判(§2.3)比装机更重要。
1.3 版本矩阵与子 chart 清单(实测)
| 组件 | 版本 / 值 | 来源 |
|---|---|---|
| Harvester chart | 1.8.2(appVersion v1.8.2) | deploy/charts/harvester |
| Helm release | harvester @ harvester-system,REVISION 1→5 | helm get metadata |
| KubeVirt | 1.7.4-150700.3.24.2 | registry.suse.com/suse/sles/15.7/virt-* |
| Longhorn | 1.11.2 | 子 chart longhorn-1.11.2.tgz |
| CDI | 子 chart 0.3.0 | charts/cdi |
| 集群 | RKE2 v1.34.4-rc11+rke2r1 / containerd;3 节点,各 8C / ~32G | kubectl get node |
| 网络 | Pod 10.42.0.0/16,Svc 10.43.0.0/16,DNS 10.43.0.10,Calico VXLAN | 实测 |
| 入口 | NodePort 32735 + SNI harvester.local | 实测 |
10 个子 chart 必须齐备(缺一个就会在运行期以「某功能不可用」的形式暴露):
cdi, harvester-load-balancer, harvester-network-controller, harvester-networkfs-manager,
harvester-node-manager, kube-vip, kubevirt, kubevirt-operator, longhorn, whereabouts2. 前置条件与冲突预判
2.1 硬性前置
- [ ] RKE2 集群健康:
kubectl get node全部Ready - [ ] 节点数 ≥ 3:Longhorn 默认
numberOfReplicas=3,节点不足会让卷永久Pending - [ ] 每节点 Longhorn 数据盘空间充足(实测 max ≈127 GB/节点,路径
/var/lib/longhorn) - [ ]
helmv3 /kubectl/curl可用,执行账号具备 cluster-admin - [ ] 放行 NodePort(本环境
32735);时钟同步(Longhorn 与 TLS 均敏感) - [ ] ★ 节点支持硬件虚拟化:
egrep -c '(vmx|svm)' /proc/cpuinfo> 0 且/dev/kvm存在 (在 VM 里跑则需宿主开嵌套虚拟化,见 02 §1.2)
2.2 ★ CIDR 必须实测探测,不要抄任何文档
bash
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
# clusterDNS:CoreDNS Service 的 ClusterIP
kubectl -n kube-system get svc rke2-coredns-rke2-coredns -o jsonpath='{.spec.clusterIP}'; echo
# clusterPodCIDR:节点级 podCIDR 通常是 /24,★ 必须归一化为集群级 /16
kubectl get node -o jsonpath='{.items[0].spec.podCIDR}'; echo # 10.42.6.0/24 → 10.42.0.0/16
# clusterServiceCIDR:由 kubernetes Service 的 ClusterIP 反推
kubectl -n default get svc kubernetes -o jsonpath='{.spec.clusterIP}'; echo # 10.43.0.1 → 10.43.0.0/16| 探测项 | 探测方式 | 常见错误 |
|---|---|---|
clusterDNS | CoreDNS Service 的 ClusterIP | 抄成节点 /etc/resolv.conf 里的宿主 DNS |
clusterPodCIDR | node.spec.podCIDR 归一化到 /16 | 直接填 /24 → 跨节点 Pod 路由算错 |
clusterServiceCIDR | kubernetes Service ClusterIP 反推 | 与 CNI 实际配置不一致 |
💡
deploy-harvester.sh把这三项做成自动探测 + 渲染 values,探测不到直接die, 从机制上消灭「手填 CIDR 填错」这一类故障。
2.3 与既有组件的冲突预判(实测结论)
| 既有组件 | 是否冲突 | 处置 |
|---|---|---|
| Calico(VXLAN) | ❌ 不冲突 | Harvester 不替换 CNI;VM 走 masquerade + Pod 网络,实测跨节点可达 |
RKE2 rke2-snapshot-controller | ⚠️ 会冲突 | csi-snapshotter.enabled=false,复用 RKE2 自带;代价见 §4.3 |
| ingress-nginx / traefik | ⚠️ 需注意 | 用 NodePort 暴露 harvester Service,避开既有 ingress 的 80/443 |
| 既有默认 StorageClass | ⚠️ 会冲突 | longhorn.persistence.defaultClass=false,保证 harvester-longhorn 是唯一默认类 |
| 既有 Longhorn | ⚠️ 版本需核对 | 子 chart 会装 1.11.2;若集群已有 Longhorn,先核对版本与 defaultDataPath |
| 既有 KubeVirt | ⚠️ 不要共存 | 一套集群只应有一个 virt-operator,否则 KubeVirt CR 互相覆盖 |
| Rancher(嵌入模式) | ❌ 本方案关闭 | rancherEmbedded: false,避免引入 Rancher/Steve 隧道与 harvester-aggregation 拨号错误 |
2.4 官方安装模式 vs promote 模式(对照)
| 维度 | 官方模式(ISO / Harvester OS) | promote 模式(本章) |
|---|---|---|
| K8s 控制面 | Harvester 自建 RKE2 | 复用既有 RKE2 |
| CIDR | 装机时确定,天然一致 | ★ 必须探测并显式声明 |
| snapshot-controller | 自带 csi-snapshotter | 复用 RKE2 的,子 chart 要关 |
| 默认 StorageClass | 装机即唯一 | 需手工校正(§5 A) |
| VolumeSnapshotClass | chart 自动建 | ★ 子 chart 关掉后必须手工补建(§5 B) |
| 入口 | VIP(kube-vip)+ Dashboard | NodePort + SNI 主机名 |
| 升级 | Harvester ISO 升级链路 | helm upgrade(★ 之后必须重跑 post-install) |
| 支持强度 | 官方主线 | 可行但需自行兜底 |
这张表就是「为什么本章有 §5 这一节」的答案:promote 模式下有四件事不会自动发生, 必须部署后立刻固化,否则会在几天后以「快照不可用 / PVC 绑错类」的形式爆出来。
3. 部署步骤
3.1 一键部署(推荐)
bash
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
bash scripts/deploy-harvester.sh --chart /path/to/harvester --nodeport 32735| 参数 / 环境变量 | 默认 | 说明 |
|---|---|---|
--chart | 无(则 git clone 对应 tag 取 deploy/charts/harvester) | 目录或 .tgz;离线环境必须显式指定 |
--version | v1.8.2 | 仅在需要 clone 时生效 |
--namespace / HV_NAMESPACE | harvester-system | |
--release / HV_RELEASE | harvester | |
--nodeport / HV_NODEPORT | 留空=chart 自动分配 | ★ 生产建议固定,便于防火墙放行 |
--sni-host / HV_SNI_HOST | harvester.local | API/UI 必须带该主机名访问 |
--skip-post / SKIP_POST=1 | 0 | 只装 chart,不做安装后修复(不建议) |
RUN_HOOKS=1 | 0 | 跨版本升级时才需要跑 Helm hooks |
3.2 脚本的六个阶段(每一步都可手工复现)
| 阶段 | 做什么 | 失败即停的关键判据 |
|---|---|---|
| 1/6 前置检查 | 工具、KUBECONFIG、节点数、containerd、K8s 版本 | 节点 ❤️ 只 warn(Longhorn 副本无法调度);连不上集群 die |
| 1.1 探测网络 | 自动取 clusterDNS / clusterPodCIDR(归一化 /16)/ clusterServiceCIDR | 三项任一探测不到 → die,提示手工在 values 指定 |
| 2/6 准备 chart | .tgz 自动解压;定位 Chart.yaml;核对子 chart 齐备 | 缺 charts/ → die(提示 helm dependency build) |
| 3/6 渲染 values | 把探测值写进 values;按集群现状决定两个开关 | — |
| 3.1 dry-run 预检 | helm … --dry-run 后 grep 渲染结果 | ①未渲染出 - Snapshot → die(values 路径写错)②默认 SC 注解数 ≠1 → warn |
| 4/6 安装 / 升级 | helm upgrade --install … --no-hooks --wait --timeout 20m | --wait 超时不退出,转逐项就绪检查 |
| 4.1 / 4.2 等就绪 | 10 个 Deployment + 3 个 DaemonSet 逐个 rollout status | 未就绪项汇总打印,并附 Pod/Events 摘要 |
| 5/6 安装后修复 | 调 post-install.sh(§5) | 返回非 0 只 warn,要求人工复核 |
| 6/6 验收 | 12 项 chk(§6.4)+ 异常 Pod + SNI 连通性 | 任一失败 RC=1 |
两个自动决策开关(脚本按集群现状推导,不要手填):
bash
# ① RKE2 已自带 snapshot-controller → 禁用 Harvester 的 csi-snapshotter(避免双控制器抢锁)
kubectl -n kube-system get deploy rke2-snapshot-controller && CSI_SNAPSHOTTER_ENABLED=false
# ② 集群已有别的默认 StorageClass → Longhorn 不设为默认(避免双默认类)
kubectl get sc -o json | grep -c 'is-default-class": "true"'3.3 手工等价命令(排障 / 无脚本环境)
bash
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
# ① 探测三项 CIDR(见 §2.2),填入 values-harvester.yaml
# ② dry-run 预检(★ 强烈建议:能在不落盘的情况下发现 values 路径写错)
helm upgrade --install harvester <chart> -n harvester-system --create-namespace \
-f values-harvester.yaml --no-hooks --dry-run > /tmp/dryrun.txt
grep -q '^\s*- Snapshot$' /tmp/dryrun.txt && echo "gate OK"
grep -c 'is-default-class: "true"' /tmp/dryrun.txt # 期望 1
# ③ 安装
helm upgrade --install harvester <chart> -n harvester-system --create-namespace \
-f values-harvester.yaml --no-hooks --wait --timeout 20m
# ④ 部署后固化(★ 必做,见 §5)
bash scripts/post-install.sh
# ⑤ 验收
bash scripts/healthcheck.sh
bash scripts/vm-e2e-test.sh⚠️
--no-hooks的理由:Longhorn 的 pre/post-upgrade 作业在版本不变的配置变更时并无必要, 且可能对存量卷做多余操作;harvester-cdi-namespace-modify作业首次安装后会自动完成。 跨版本升级时才设RUN_HOOKS=1(去掉该 flag)。
3.4 就绪观察
bash
# 10 个 Deployment
for dp in harvester harvester-webhook harvester-load-balancer harvester-network-controller \
virt-operator virt-api virt-controller cdi-operator cdi-deployment cdi-uploadproxy; do
kubectl -n harvester-system rollout status deploy/$dp --timeout=300s
done
# 3 个 DaemonSet(注意 longhorn-manager 可能在 longhorn-system)
for ds in virt-handler longhorn-manager harvester-node-manager; do
kubectl -n harvester-system rollout status ds/$ds --timeout=300s \
|| kubectl -n longhorn-system rollout status ds/$ds --timeout=300s
done💡 组件未就绪时先看「上次崩溃」而不是当前日志:
kubectl logs <pod> --previous --tail=12,配合lastState.terminated.{reason,exitCode,finishedAt}。exitCode=1 + level=fatal是应用级致命错误,137才是 OOM——两者处置完全不同。
4. values 关键项逐条解释
4.1 完整 values(实测可用)
yaml
# --- 1. 部署形态 -------------------------------------------------------------
# false = 独立 Harvester(不内嵌 Rancher)。内嵌 Rancher 会引入 Rancher/Steve 隧道,
# 产生 harvester-aggregation 连接错误(非致命但噪声大)。
rancherEmbedded: false
# --- 2. 复用既有 RKE2 集群网络(promote 模式关键项)--------------------------
# 必须与 RKE2 实际 CIDR 完全一致,否则 kube-vip / network-controller 会算错路由。
promote:
clusterDNS: 10.43.0.10 # kubectl -n kube-system get svc rke2-coredns-rke2-coredns
clusterPodCIDR: 10.42.0.0/16 # ★ 集群级 /16,不是节点级 /24
clusterServiceCIDR: 10.43.0.0/16
# --- 3. API/UI 暴露方式 ------------------------------------------------------
# NodePort:无 LB/Ingress 环境下最稳妥,配合 SNI 主机名访问(见 §6.1)。
service:
harvester:
type: NodePort # 本环境实际端口:32735
# nodePorts:
# harvester: 32735 # 固定端口时取消注释
# --- 4. 禁用 Harvester 自带 csi-snapshotter 子 chart -------------------------
# 原因:RKE2 已自带 kube-system/rke2-snapshot-controller(含 CRD 安装作业),
# 再装一份会造成 snapshot-controller 双实例抢锁与 CRD 冲突。
# ★ 代价:该子 chart 同时负责创建名为 longhorn 的 VolumeSnapshotClass,
# 禁用后必须手工补建(§5 B)。
csi-snapshotter:
enabled: false
# --- 5. Longhorn 存储 --------------------------------------------------------
longhorn:
persistence:
# 关键:不要让用户态 longhorn SC 成为默认 StorageClass。
# Harvester 依赖 harvester-longhorn 为【唯一】默认类;双默认类会让
# 未指定 storageClassName 的 PVC 绑到错误的类。
defaultClass: false
defaultSettings:
defaultDataPath: /var/lib/longhorn # 需为独立磁盘/分区挂载点,预留足够容量
# --- 6. KubeVirt 特性门 ------------------------------------------------------
# ★ 路径务必是 kubevirt.spec.configuration...(子 chart 模板渲染的是 .Values.spec)
kubevirt:
spec:
configuration:
developerConfiguration:
featureGates:
- CPUManager
- DeclarativeHotplugVolumes
- ExpandDisks
- EnableVirtioFsConfigVolumes
- HostDevices
- Snapshot # ← VM 快照必需;单数!(Beta since v1.3.0)4.2 ★ 六个「写错就静默失效」的点
| # | 错误写法 | 为什么静默 | 正确写法 / 验证方法 |
|---|---|---|---|
| 1 | kubevirt.configuration.… | 生成一个无人读取的键,Helm 不报错 | kubevirt.spec.configuration.…;helm --dry-run 后 grep -q '^\s*- Snapshot$' |
| 2 | gate 写成 Snapshots(复数) | KubeVirt 当未知 gate 静默忽略 | Snapshot(单数);webhook 不再报 snapshot feature gate not enabled |
| 3 | clusterPodCIDR 填节点级 /24 | 路由计算错,症状出现在 LB/网络控制器,离根因很远 | 归一化为集群级 /16 |
| 4 | 忘了 csi-snapshotter.enabled=false | 双 snapshot-controller 抢 leader lock,快照时好时坏 | 关掉子 chart + 手工补建 VSC |
| 5 | 忘了 longhorn.persistence.defaultClass=false | 出现两个默认 SC,PVC 绑错类 | kubectl get sc -o json | grep -c 'is-default-class": "true"' → 必须 1 |
| 6 | 用 kubectl edit kubevirt 改配置 | 下次 helm upgrade 被回滚(CR 带 meta.helm.sh/release-name 归属注解) | 配置一律落在 values(§7) |
💡 判据:凡是「改了没报错、但行为没变」,先怀疑键路径与归属注解, 再怀疑组件本身。
helm get values+helm get manifest | grep是最快的对照手段。
4.3 ★ VolumeSnapshotClass 的 type 选型(决定快照能不能恢复)
type | 语义 | 依赖 | 恢复可靠性 |
|---|---|---|---|
snap | Longhorn 卷内快照 | 无(不需要 backup target) | ⚠️ 恢复需从源卷克隆;源卷被删则 cloneStatus=failed,VM 起不来 |
bak | Longhorn 备份 | ★ 必须配置 backup target(NFS/S3) | ✅ 数据在集群外,恢复不依赖源卷存活(生产推荐) |
yaml
kind: VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
metadata:
name: longhorn # ★ 名字必须是 longhorn,harvester 启动时按名引用
driver: driver.longhorn.io
deletionPolicy: Delete
parameters:
type: snap # 生产改 bak,并先配置 backup target为什么名字必须是 longhorn:csi-snapshotter 子 chart 被禁用后,harvester server 启动时 仍会引用名为 longhorn 的 VolumeSnapshotClass;不存在时它自己的 webhook (validator.harvesterhci.io)会拒绝该请求并把错误当 fatal:
level=fatal msg="failed to create harvester server: admission webhook \"validator.harvesterhci.io\"
denied the request: volumesnapshotclasses.snapshot.storage.k8s.io \"longhorn\" not found"实测后果:三个 harvester 副本各重启 10 次(exitCode=1,非 OOM), 直到 VSC 被补建才停止增长——这是一个部署窗口内的一次性引导问题,不是持续故障。
配 type: bak 的前置检查:
bash
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 default is not available⚠️
type: snap的致命陷阱:CSI 层的VolumeSnapshot.readyToUse=true只表示 「快照曾创建成功」,不会在 Longhorn 后端快照被 GC 后回写为false。 于是会出现「控制面全绿、数据面全红」:VirtualMachineRestoreComplete成功 → 源 PV 被删(VolumeFailedDelete)→ 新卷cloneStatus卡在initiated(attemptCount递增)→ VMIReady=False / GuestNotRunning。 必须查 Longhorn 层四元组:status.state+status.robustness+status.replicas[*].mode+status.cloneStatus.state。 详见 06 存储实战。
5. ★ 部署后必做的 4 项固化(post-install.sh 全自动)
promote 模式下有 4 件事不会自动发生,不做就会在几天后爆出来:
| # | 项目 | 不做的后果 | 脚本段落 |
|---|---|---|---|
| A | 默认 StorageClass 校正为唯一的 harvester-longhorn | 未指定 SC 的 PVC 绑错类;VM 磁盘行为异常 | A. |
| B | 补建 VolumeSnapshotClass longhorn 并确认 type | VM 快照无类可用 / harvester server 启动 fatal 反复重启 | B. |
| C | KubeVirt featureGates 增加 Snapshot(走 values 持久化) | 快照被 webhook 拒绝:snapshot feature gate not enabled | C. |
| D | 滚动重启 virt-api(逐个删 Pod,不断流) | CR 已改但 gate 不生效,快照仍被拒 | D. |
| E | 服务端 dry-run 探测 webhook 是否真的放行 | 「以为改好了」——无法证明 gate 生效 | E. |
bash
bash scripts/post-install.sh # 默认 type=snap
VSC_TYPE=bak bash scripts/post-install.sh # 备份型(需先配置 backup target)
CHART_PATH=/opt/harvester-chart/harvester bash scripts/post-install.sh # ★ 用 helm 持久化 gate(推荐)5.1 A · 默认 StorageClass
bash
kubectl get sc # 期望只有 harvester-longhorn 带 (default)
kubectl annotate sc longhorn storageclass.kubernetes.io/is-default-class-
kubectl annotate sc harvester-longhorn storageclass.kubernetes.io/is-default-class=true --overwrite⚠️ 现场校正不持久:下次
helm upgrade若未带longhorn.persistence.defaultClass=false会被改回。 根治必须在 values(§4.1 第 5 段)。
5.2 B · VolumeSnapshotClass
见 §4.3。要点:名字必须是 longhorn,parameters.type 决定快照语义,生产用 bak。
5.3 C · Snapshot feature gate(三层叠加,需逐层排除)
| 层 | 检查 | 命令 |
|---|---|---|
| ① gate 名 | 必须是 Snapshot(单数,Alpha v0.30.0 / Beta v1.3.0) | kubectl get kubevirt -n harvester-system kubevirt -o jsonpath='{.spec.configuration.developerConfiguration.featureGates}' |
| ② values 路径 | 必须是 kubevirt.spec.configuration.…(漏 spec 静默失效) | helm get values harvester -n harvester-system |
| ③ virt-api 是否重启 | gate 仅在启动时加载 | kubectl -n harvester-system get pod -l kubevirt.io=virt-api 看 AGE |
持久化写法(推荐,CHART_PATH 已设时脚本自动走这条):
bash
helm upgrade harvester <chart> -n harvester-system --reuse-values --no-hooks \
--set 'kubevirt.spec.configuration.developerConfiguration.featureGates={CPUManager,DeclarativeHotplugVolumes,ExpandDisks,EnableVirtioFsConfigVolumes,HostDevices,Snapshot}'改完等 virt-operator 调和:status.observedGeneration == metadata.generation,且 phase=Deployed。
5.4 D · virt-api 滚动重启(★ 最容易漏的一步)
实测:即使 KubeVirt CR 已含 Snapshot、phase=Deployed,virt-api 不重启, webhook 仍持续返回 snapshot feature gate not enabled。
做法上的两个讲究:
| 讲究 | 理由 |
|---|---|
逐个删 Pod,而不是 rollout restart | 不改 Deployment 模板,避免与 virt-operator 的 install-strategy 注解冲突 |
| 等全部就绪再删下一个 | webhook 不断流;期间只有 VM 创建/删除等写操作可能短暂被拒,存量 VM 不受影响 |
bash
for p in $(kubectl -n harvester-system get pod -l kubevirt.io=virt-api -o jsonpath='{.items[*].metadata.name}'); do
kubectl -n harvester-system delete pod "$p" --wait=false
kubectl -n harvester-system rollout status deploy/virt-api --timeout=120s
done5.5 E · 用服务端 dry-run 证明 gate 真的生效
bash
cat > /tmp/vmsnap-probe.yaml <<'EOF'
apiVersion: snapshot.kubevirt.io/v1beta1
kind: VirtualMachineSnapshot
metadata: {name: gate-probe, namespace: default}
spec:
source: {apiGroup: kubevirt.io, kind: VirtualMachine, name: __nonexistent_vm__}
EOF
kubectl apply --dry-run=server -f /tmp/vmsnap-probe.yaml判读(★ 关键:故意用一个不存在的源 VM):
| 返回 | 结论 |
|---|---|
snapshot feature gate not enabled | ❌ gate 未生效 → 回查 §5.3 三层 |
… "__nonexistent_vm__" not found / denied | ✅ gate 已生效(请求已越过 gate 校验,仅因源 VM 不存在而失败) |
💡
--dry-run=server会完整经过 admission 链但不落盘,是验证 webhook/gate 类配置 最快、最安全的手段。凡是「改了配置想确认生效」,优先想它。
6. 访问与验证
6.1 ★ API/UI 必须带 SNI 主机名
Harvester 的 API/UI 校验 SNI/Host,直接用 IP 访问会失败:
bash
NODE_IP=$(kubectl get node -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
PORT=$(kubectl -n harvester-system get svc harvester \
-o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
curl -k --resolve harvester.local:${PORT}:${NODE_IP} \
https://harvester.local:${PORT}/dashboard/ # → 200 ✓
curl -k --resolve harvester.local:${PORT}:${NODE_IP} \
https://harvester.local:${PORT}/v1/harvester # → schema 列表 ✓浏览器访问:本机 hosts 加 <NODE_IP> harvester.local,再打开 https://harvester.local:32735/。
聚合层是否正常,用「能枚举到哪些资源」来判断:
snapshot.kubevirt.io.virtualmachinesnapshots / virtualmachinesnapshotcontents / virtualmachinerestores
harvesterhci.io.virtualmachinerestores / volumeremoterestores
groupsnapshot.storage.k8s.io.volumegroupsnapshot{classes,contents,snapshots}
k3s.cattle.io.etcdsnapshotfiles结论(实测):Harvester v1.8 的 VM 快照走 KubeVirt 原生
snapshot.kubevirt.ioAPI, 不存在 Harvester 自有的VirtualMachineSnapshotCRD。找错 API group 会白排查半天。
6.2 平台健康巡检:healthcheck.sh
约 20 s、只读、不创建任何 VM;非零退出码可直接接告警。实测 PASS=30 / WARN=6 / FAIL=0。
| 段 | 检查内容 |
|---|---|
| 1 | 节点与运行时(Ready、containerd、/dev/kvm、虚拟化扩展) |
| 2 | Harvester 控制面 Pod(含重启计数分级:近 1 小时内 → FAIL,历史 → WARN) |
| 3 | Helm release(STATUS=deployed、REVISION) |
| 4 | KubeVirt / CDI 自定义资源(Available=True、phase=Deployed、featureGates 含 Snapshot) |
| 5 | 存储:StorageClass(默认类唯一)/ VolumeSnapshotClass / Longhorn |
| 6 | Webhook / APIService |
| 7 | 虚拟机与卷 |
| 8 | 已知非致命日志噪声甄别 + API/UI 连通性 |
bash
bash scripts/healthcheck.sh # 完整报告
BRIEF=1 bash scripts/healthcheck.sh # 只出结论行
VERBOSE=1 bash scripts/healthcheck.sh # 附带证据明细已甄别为非致命的噪声(降级 WARN,不计入 FAIL):
| 噪声 | 为什么非致命 |
|---|---|
harvester-aggregation 反复拨号 /v3/connect 失败 | rancherEmbedded=false,本来就没有 Rancher 隧道 |
ssl-certificates 控制器反复 requeue | 未配置自定义证书时的预期行为 |
longhorn-manager Failed to sync Longhorn VolumeAttachment | 卷生命周期瞬态,自愈 |
6.3 能力端到端验证:vm-e2e-test.sh
自建自清,6 个阶段(实测全流程通过):
| 阶段 | 内容 | 实测基线 |
|---|---|---|
| 0 | 前置条件检查(SC/VSC/gate/节点数) | — |
| 1 | 创建 VM(RWO + Filesystem + blank 空盘) | DV Succeeded → VMI Running |
| 2 | 冷迁移(停机 → 换节点 → 启动) | 落点节点变化 ✓ |
| 3 | 创建 VM 快照(snapshot.kubevirt.io/v1beta1) | ~5 s |
| 4 | 从快照恢复(停机 → 恢复 → 数据面核对) | 卷四元组核对通过 |
| 5 | 清理测试对象并核查无残留 | 卷数归 0 |
bash
bash scripts/vm-e2e-test.sh # 全流程
bash scripts/vm-e2e-test.sh --skip-migrate # 跳过冷迁移
bash scripts/vm-e2e-test.sh --keep # 结束后保留测试 VM 便于人工检查
# 其他:--vm NAME / --ns NS / --size SIZE / --skip-snapshot★ 第 4 步内置「克隆 120 s 未收敛 → 自动反查
spec.dataSource源卷是否还存在 → 直接输出根因与整改建议」的判定,不会只给一句模糊 WARN(对应 §4.3 的snap陷阱)。
6.4 验收清单(deploy-harvester.sh 第 6 阶段的 12 项)
- [ ]
helm get metadata→STATUS: deployed - [ ]
KubeVirtCRAvailable=True - [ ]
featureGates含Snapshot - [ ]
CDICRAvailable=True - [ ] 默认 StorageClass 是
harvester-longhorn - [ ] 默认类唯一(
grep -c 'is-default-class": "true"'→1) - [ ] VolumeSnapshotClass
longhorn存在且driver=driver.longhorn.io - [ ] Harvester APIService 数量 ≥1
- [ ] KubeVirt 校验 webhook 存在
- [ ]
harvester-system无异常 Pod(非 Running/Completed) - [ ] 无高频重启 Pod(
RESTARTS > 5告警) - [ ]
curl --resolve …/dashboard/→ 200
7. 升级与回滚
7.1 升级前留档(强烈建议)
bash
helm get values harvester -n harvester-system > values-backup-$(date +%F).yaml
kubectl get kubevirt -n harvester-system kubevirt -o yaml > kubevirt-cr-$(date +%F).yaml
kubectl get sc,volumesnapshotclasses.snapshot.storage.k8s.io -o yaml > storage-backup-$(date +%F).yaml7.2 变更配置 / 升级
bash
# 变更配置(示例:加 feature gate)——★ 走 values,不要 kubectl edit
helm upgrade harvester <chart> -n harvester-system --reuse-values --no-hooks \
--set 'kubevirt.spec.configuration.developerConfiguration.featureGates={CPUManager,DeclarativeHotplugVolumes,ExpandDisks,EnableVirtioFsConfigVolumes,HostDevices,Snapshot}'
# 跨版本升级:需要跑 hooks(Longhorn pre/post-upgrade 作业)
RUN_HOOKS=1 bash scripts/deploy-harvester.sh --chart <new-chart> --nodeport 327357.3 回滚
bash
helm history harvester -n harvester-system
helm rollback harvester <REV> -n harvester-system --no-hooks7.4 ★★ 每次 helm upgrade 之后必须重跑 post-install.sh
bash
bash scripts/post-install.sh && bash scripts/healthcheck.sh原因:upgrade 会用 chart 内的 CR 覆盖集群中的 KubeVirt CR,导致后加的 Snapshot gate 静默失效(本环境实际发生过)。同理,现场用 annotate 改的默认 SC 也会被改回。
| 会被 upgrade 冲掉的现场修改 | 持久做法 |
|---|---|
kubectl patch kubevirt 加的 gate | 写进 values 的 kubevirt.spec.configuration.… |
kubectl annotate sc 改的默认类 | values 的 longhorn.persistence.defaultClass=false |
| 手工建的 VolumeSnapshotClass | 保留清单文件,upgrade 后 apply 一次(post-install.sh 已含) |
| 手工重启过的 virt-api | upgrade 后再重启一次(gate 仅启动时加载) |
💡 判断某个修改会不会被冲掉:看资源上有没有
meta.helm.sh/release-name/app.kubernetes.io/managed-by: Helm归属注解。 有 → 归 Helm 管,必须改 values;没有 → 是你自己的资源,upgrade 不会动它。
8. 本形态特有的坑(按「会不会丢数据」排序)
| # | 坑 | 症状 | 根因 | 处置 |
|---|---|---|---|---|
| 1 | ★★ type: snap 恢复不可靠 | 恢复「成功」但 VM 卡 Scheduling / GuestNotRunning | KubeVirt 恢复完成后删除原 DV/PVC;snap 恢复卷的 dataSource=snap://<源卷>/<快照> 需从源卷克隆,源卷被 GC 后 cloneStatus 永不收敛(initiated + attemptCount 递增) | 生产用 type: bak + backup target;snap 只验证到 readyToUse=true 为止 |
| 2 | ★ 热迁移被拒 | DisksNotLiveMigratable:"PVC xxx is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)" | 磁盘 PVC 是 ReadWriteOnce | 迁移用途的 VM 磁盘用 RWX + Filesystem;本环境 RWX 数据面另有阻塞(见 3) |
| 3 | ★ RWX 数据面不可用 | blockdev: cannot open /dev/cdi-block-volume: Permission denied(Block);改 Filesystem 后 qemu-img execution failed: exit status 1、importer 重启 3~5 次 | RWX 卷虽 state=attached/robustness=healthy,但 shareEndpoint 与 shareState 均为空、集群内无任何 share-manager Pod → NFS 导出端点从未建立,数据面不完整 | 短期用 RWO + Filesystem + 冷迁移(实测通过);中期按「节点 NFS 客户端能力 → share-manager 镜像可拉取 → Longhorn 是否尝试创建 share-manager → 普通 Pod 挂载 RWX 复测」四步排查;热迁移路线见 08 高可用与迁移 |
| 4 | harvester Pod 重启 10 次 | 三副本 RESTARTS=10,exitCode=1(非 137/OOM) | 启动期顺序依赖:VSC longhorn 不存在 → 自己的 webhook 拒绝 → level=fatal | 部署后立即 post-install.sh 补建 VSC;判读重启用 lastState.terminated.* + logs --previous |
| 5 | gate 静默失效 | 快照报 snapshot feature gate not enabled | ①写成复数 Snapshots ②values 路径漏 spec ③virt-api 未重启 ④upgrade 覆盖 | §5.3 三层排除 + §5.5 服务端 dry-run 证明 |
| 6 | 双默认 StorageClass | 未指定 SC 的 PVC 绑到 longhorn 而非 harvester-longhorn | Longhorn 子 chart 默认 persistence.defaultClass=true | values 置 false;现场 annotate 校正(不持久) |
| 7 | 镜像导入卡 99.62% 后失败 | DV 进度回退 99.62% → 2.5% 重头再来 | 公网镜像源不稳定,CDI(nbdkit/curl)重试并从头计时 | 换内网镜像源;先用 blank/pvc 验证平台链路 |
| 8 | kubectl get vsc 查错对象 | 看到的是 volumesnapshotcontents 而非 classes | 短名冲突 | 用全名 volumesnapshotclasses.snapshot.storage.k8s.io |
第 1、3 条是唯一会真正丢数据/丢可用性的两类,必须在上线前用
vm-e2e-test.sh+ Longhorn 卷四元组核对实测验证,不能只看控制面状态。
9. 快速参考卡
bash
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
# ── 部署 ────────────────────────────────────────────────
bash scripts/deploy-harvester.sh --chart <chart> --nodeport 32735 # 六阶段一键
bash scripts/post-install.sh # ★ 必做(4 项固化 + dry-run 验证)
# ── 验收 ────────────────────────────────────────────────
bash scripts/healthcheck.sh # 巡检 ~20s,只读;期望 FAIL=0
bash scripts/vm-e2e-test.sh # 端到端:建机→冷迁移→快照→恢复→清理(自建自清)
# ── 访问(★ 必须带 SNI)────────────────────────────────
NODE_IP=$(kubectl get node -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
PORT=$(kubectl -n harvester-system get svc harvester -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
curl -k --resolve harvester.local:$PORT:$NODE_IP https://harvester.local:$PORT/dashboard/
# ── 关键状态 ────────────────────────────────────────────
helm get metadata harvester -n harvester-system # STATUS / REVISION
kubectl get kubevirt -n harvester-system kubevirt \
-o jsonpath='{.status.phase} {.spec.configuration.developerConfiguration.featureGates}'
kubectl get sc # 默认类必须唯一 = harvester-longhorn
kubectl get volumesnapshotclasses.snapshot.storage.k8s.io longhorn \
-o jsonpath='{.driver} {.parameters.type}'
kubectl -n harvester-system get pod # 期望全 Running、RESTARTS 不增长
# ── 升级 ────────────────────────────────────────────────
helm get values harvester -n harvester-system > values-backup.yaml # 先留档
helm upgrade harvester <chart> -n harvester-system --reuse-values --no-hooks -f values.yaml
bash scripts/post-install.sh && bash scripts/healthcheck.sh # ★ upgrade 后必重跑
helm rollback harvester <REV> -n harvester-system --no-hooks # 回滚三条速记:
- CIDR 靠探测,不靠抄(
promote三项写错 → 路由算错,症状离根因很远)。 - 配置改 values,不改 CR(
meta.helm.sh/release-name归属注解决定谁说了算)。 - 改了 gate 必须重启 virt-api,并用服务端 dry-run 证明(否则永远是「以为生效了」)。
10. 小结与下一步
| 本章要点 | 一句话 |
|---|---|
| 形态 | Helm 把 Harvester 叠加进既有 RKE2,K8s/CNI/snapshot-controller 都复用 |
promote | 三项 CIDR 必须实测探测并显式声明,否则 kube-vip / network-controller / LB 算错路由 |
| 冲突预判 | csi-snapshotter 要关(复用 RKE2)、longhorn.defaultClass=false(避免双默认类)、rancherEmbedded=false |
| 必做固化 | 默认 SC / VSC longhorn / Snapshot gate / virt-api 滚动重启,最后用服务端 dry-run 证明 |
| 升级铁律 | 每次 helm upgrade 之后重跑 post-install.sh,否则 gate 静默失效 |
| 数据面 | type: snap 恢复不可靠 → 生产用 bak + backup target;RWX 阻塞 → 暂用冷迁移 |
接下来:
- 建机/镜像/cloud-init/快照/清理的日常操作 → 05 虚拟机生命周期实战
- 磁盘规格、
snapvsbak、恢复后数据面核对 → 06 存储实战 - 热迁移阻塞的完整修复路线、维护模式、HA → 08 高可用与迁移实战
- 两种形态怎么选(含成本模型与决策树)→ 10 五方对比
- 本形态 18 个实测案例原始记录 →
worker/harvester-container/02-故障排查与修复记录.md