主题
07 · 日常运维手册:巡检、备份、迁移、升级、凭据
适用版本:Harvester v1.8 | 基线:12 节点 ISO 集群,管理网
10.181.0.0/24(VIP10.181.0.100),VM 直通网10.181.91.0/24(VLAN 91)本章是"值班手册":每一节都能照着敲命令。可执行脚本副本见
appendix/ops/。
7.1 巡检:每天 5 分钟,每周 30 分钟
7.1.1 每日巡检
bash
# 1) 节点是否都 Ready、有没有被 cordon
kubectl get nodes -o wide
kubectl get nodes | grep -i SchedulingDisabled || echo "无节点被禁止调度"
# 2) Harvester 核心组件是否健康
for ns in harvester-system cattle-system longhorn-system kube-system harvester-public; do
BAD=$(kubectl get pods -n $ns --no-headers 2>/dev/null | grep -Ev 'Running|Completed')
[[ -z "$BAD" ]] && echo "$ns 正常" || { echo "== $ns 异常 =="; echo "$BAD"; }
done
# 3) 重启次数异常的 Pod(>3 次就要看日志)
kubectl get pods -A -o jsonpath='{range .items[?(@.status.containerStatuses[0].restartCount>3)]}{.metadata.namespace}/{.metadata.name}{" restarts="}{.status.containerStatuses[0].restartCount}{"\n"}{end}'
# 4) VM 状态:非 Running 的一律要看
kubectl get vm -A | grep -v Running || echo "所有 VM 处于 Running"
# 5) 直通网络状态(本项目最该每天看的一条)
kubectl get clusternetwork cn-vm
kubectl get vlanstatus -A
# 期望:cn-vm Ready=True,每个节点一条 vlanstatus 且 Ready=True
# 6) Longhorn 卷健康
kubectl get volumes.longhorn.io -n longhorn-system --no-headers | awk '$2!="attached" && $2!="detached"'
kubectl get nodes.longhorn.io -n longhorn-system \
-o custom-columns='NODE:.metadata.name,SCHED:.spec.allowScheduling'
# 7) 节点磁盘水位(系统分区 + 数据盘)
for ip in $(seq 11 22); do
printf '10.181.0.%-4s ' "$ip"
ssh -o ConnectTimeout=3 -o BatchMode=yes "rancher@10.181.0.$ip" \
"df -h /usr/local | tail -1" 2>/dev/null || echo "(SSH 失败)"
done7.1.2 每周巡检
| 检查项 | 命令 | 期望 |
|---|---|---|
| 备份目标可达 | kubectl get setting backup-target -n harvester-system -o yaml | 已配置且能列出备份 |
| 定时备份成功 | kubectl get vmschedule -A | Ready,最近备份 Ready |
| API 证书有效期 | openssl s_client -connect 10.181.0.100:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -enddate | 剩余 > 30 天 |
| 时间同步 | 各节点 timedatectl status | System clock synchronized: yes |
| 当前版本 | kubectl get setting server-version -n harvester-system -o jsonpath='{.value}' | 在支持矩阵内 |
| 升级空间 | 各节点 df -h /usr/local/ | 空闲 ≥ 30 GiB,否则升级被拒 |
| 网络冗余 | 节点 cat /proc/net/bonding/*;交换机侧 LACP | 成员链路都 up,LACP 已协商 |
| IP 台账 | 对照 appendix/vm/vms.csv 与实际 VM | 无未登记 IP、无冲突 |
| 告警链路 | 主动触发一条测试告警 | 邮件/IM 能收到 |
| 支持包 | 试生成一次 support bundle | 能下载 |
7.1.3 一键巡检脚本
appendix/ops/daily-check.sh(可直接 cron,输出发值班群):
bash
#!/usr/bin/env bash
# appendix/ops/daily-check.sh —— Harvester 集群每日巡检
set -uo pipefail
OK=$'[ OK ]'; WARN=$'[WARN]'; FAIL=$'[FAIL]'
p(){ printf '%s %s\n' "$1" "$2"; }
count(){ wc -l | tr -dc '0-9'; }
NOT_READY=$(kubectl get nodes --no-headers | awk '$2!="Ready"' | count)
[[ "$NOT_READY" == 0 ]] && p "$OK" "所有节点 Ready" || p "$FAIL" "$NOT_READY 个节点非 Ready"
CORDON=$(kubectl get nodes --no-headers | grep -c SchedulingDisabled || true)
[[ "$CORDON" == 0 ]] && p "$OK" "无节点被 cordon" || p "$WARN" "$CORDON 个节点禁止调度"
BAD_PODS=$(kubectl get pods -A --no-headers | grep -Ev 'Running|Completed' | count)
[[ "$BAD_PODS" == 0 ]] && p "$OK" "系统 Pod 全部 Running" || { p "$WARN" "$BAD_PODS 个 Pod 异常:"; \
kubectl get pods -A --no-headers | grep -Ev 'Running|Completed' | head -10; }
VM_TOTAL=$(kubectl get vm -A --no-headers 2>/dev/null | count)
VM_BAD=$(kubectl get vm -A --no-headers 2>/dev/null | grep -v Running | count)
if [[ "$VM_BAD" == 0 ]]; then
p "$OK" "VM 总数 $VM_TOTAL,全部 Running"
else
p "$WARN" "VM 总数 $VM_TOTAL,非 Running $VM_BAD:"
kubectl get vm -A --no-headers 2>/dev/null | grep -v Running | head -10
fi
CN_READY=$(kubectl get clusternetwork cn-vm \
-o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status}{end}' 2>/dev/null)
[[ "$CN_READY" == True ]] && p "$OK" "ClusterNetwork cn-vm Ready" || p "$FAIL" "cn-vm Ready=${CN_READY:-N/A}"
VS_BAD=$(kubectl get vlanstatus -A --no-headers 2>/dev/null | grep -v True | count)
[[ "$VS_BAD" == 0 ]] && p "$OK" "所有 vlanstatus Ready" || p "$WARN" "$VS_BAD 条 vlanstatus 异常"
LH_BAD=$(kubectl get volumes.longhorn.io -n longhorn-system --no-headers 2>/dev/null \
| awk '$2!="attached" && $2!="detached"' | count)
[[ "$LH_BAD" == 0 ]] && p "$OK" "Longhorn 卷状态正常" || p "$FAIL" "$LH_BAD 个卷异常"
BT=$(kubectl get setting backup-target -n harvester-system -o jsonpath='{.value}' 2>/dev/null)
[[ -n "$BT" && "$BT" != "{}" ]] && p "$OK" "backup-target 已配置" || p "$WARN" "backup-target 未配置"
SCHED=$(kubectl get vmschedule -A --no-headers 2>/dev/null | count)
SCHED_SUS=$(kubectl get vmschedule -A --no-headers 2>/dev/null | grep -ci suspend || true)
p "$OK" "VM Schedule 共 $SCHED 个,挂起 $SCHED_SUS 个"
# 升级前置:系统分区空闲 ≥30GiB
MIN_FREE=$(kubectl get nodes -o name | cut -d/ -f2 | while read -r n; do
ip=$(kubectl get node "$n" -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
ssh -o ConnectTimeout=3 -o BatchMode=yes "rancher@${ip:-$n}" \
"df --output=avail -BG /usr/local | tail -1 | tr -dc '0-9'" 2>/dev/null
done | sort -n | head -1)
if [[ -n "${MIN_FREE:-}" ]]; then
(( MIN_FREE >= 30 )) && p "$OK" "最小 /usr/local 空闲 ${MIN_FREE}G" \
|| p "$WARN" "最小 /usr/local 空闲 ${MIN_FREE}G(升级要求 ≥30G)"
else
p "$WARN" "无法免密 SSH 检查节点磁盘,请手工确认 df -h /usr/local"
fi
echo "巡检完成:$(date '+%F %T')"bash
chmod +x appendix/ops/daily-check.sh
./appendix/ops/daily-check.sh | tee "check-$(date +%F).log"
# 定时:每天 08:00
echo '0 8 * * * /opt/hv-ops/daily-check.sh >> /var/log/hv-check.log 2>&1' | sudo tee /etc/cron.d/harvester-daily-check7.2 备份与快照
7.2.1 先配 Backup Target(一次性)
备份数据必须存在集群外部的 NFS 或 S3,存在集群自己的存储里等于没备份。
路径:UI → Advanced → Settings → backup-target
| 类型 | 关键参数 | 本项目建议 |
|---|---|---|
| NFS | Endpoint,形如 nfs://10.181.0.200:/data/harvester-backup | 用独立备份服务器 |
| S3 | Endpoint、BucketName、BucketRegion、AccessKeyID、SecretAccessKey、Certificate(自签证书)、VirtualHostedStyle | 有对象存储优先选这个 |
| 通用 | Refresh Interval(秒) | 0 = 仅在所有备份卷 Ready 时同步;生产建议 300 |
bash
kubectl get setting backup-target -n harvester-system -o yaml
kubectl get backups.harvesterhci.io -A # 已有备份
kubectl get restores.harvesterhci.io -A # 恢复任务⚠️ 备份目标必须能被所有节点访问(走管理网
10.181.0.0/24)。容量按「VM 磁盘总量 × 保留份数」估算。底层走 Longhorn 备份机制:首次全量,之后增量。
7.2.2 VM 备份 / 恢复
| 操作 | UI 路径 | 说明 |
|---|---|---|
| 创建备份 | Virtual Machines → 选中 VM → ⋮ → Take Backup | VM 需处于 Running 或 Stopped |
| 恢复成新 VM | Backups → ⋮ → Restore → New | 生成新名字的 VM,记得给它新 IP |
| 替换现有 VM | Backups → ⋮ → Restore → Replace existing | 目标 VM 必须 Stopped;会替换其卷 |
| 跨集群恢复 | 新集群配同一 backup-target → 上传同名镜像 → Restore | 镜像名不一致会失败 |
7.2.3 定时备份(VM Schedule)
UI → Virtual Machine Schedules → Create Schedule:
| 参数 | 建议值 | 说明 |
|---|---|---|
| Type | Backup | 或 Snapshot(本地快照,不出集群) |
| Cron Schedule | 0 2 * * * | 间隔必须 ≥ 1 小时;同粒度的多个 schedule 偏移 ≥ 10 分钟,否则 I/O 打爆存储 |
| Retain | 7 | 超出后删最旧的,Longhorn 触发快照清理 |
| Max Failure | 3 | 连续失败超过该值,schedule 自动挂起,需人工恢复 |
bash
kubectl get vmschedule -A
kubectl get vmschedule -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CRON:.spec.cron,RETAIN:.spec.retain,SUSPEND:.spec.suspend'⚠️ 恢复被挂起的 schedule 前,先手工做一次备份确认能成功;备份目标不可达时 Harvester 不允许 resume。
⚠️ 不要用 Longhorn 自带的 recurring snapshot/backup:官方明确不支持——它与 VM 附加、集群升级冲突,还会在 Harvester 不知情时产生重 I/O,可能拖垮集群。只用 VM Schedule。
7.2.4 快照空间配额
备份/快照都会吃集群磁盘空间,默认无上限,务必设置:
- Namespace 级:Namespaces → 选中 → ⋮ → Edit Quota
- VM 级:Virtual Machines → 选中 → ⋮ → Edit VM Quota(VM 详情 Quotas 页签可见生效值)
7.2.5 文件系统冻结:一致性备份的前提
装了 qemu-guest-agent 的 VM,Harvester 会通过 KubeVirt 的 virt-freezer 在备份/快照时冻结文件系统(Linux 用 fsfreeze,Windows 用 VSS API),保证时间点一致性。这也是第 5、6 章把 guest agent 列为必装项的原因之一。
| 客户机 OS | 注意事项(官方) |
|---|---|
| Ubuntu / Debian | 一般开箱可用 |
| RHEL / SLE Micro | 默认权限可能不足,可能需要自定义 SELinux 策略 |
| Windows | 必须启用 VSS 才支持冻结 |
验证:
bash
VM=web-front-01; NS=default
POD=$(kubectl get pods -n $NS -l vm.kubevirt.io/name=$VM -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it $POD -n $NS -c compute -- virt-freezer --freeze-all
kubectl exec -it $POD -n $NS -c compute -- virt-freezer --unfreeze-all失败时先在 VM 内 systemctl status qemu-guest-agent。
7.2.6 演练:没恢复过的备份等于没有备份
text
每季度一次:
1) 选一台非关键 VM → Take Backup
2) 从备份 Restore 成新 VM(新名字、新 IP,走第 6 章 CSV 流程,IP 从 .101-.200 段取)
3) 登录新 VM 校验数据(行数 / 关键文件 hash / 服务能起)
4) 记录耗时:备份 N 分钟、恢复 M 分钟 → 这就是 RTO 依据
5) 删除演练 VM,归档演练记录7.3 在线迁移与自动均衡
7.3.1 哪些 VM 能热迁移
前置条件:除当前节点外,集群中至少有一个可调度节点满足该 VM 的全部调度规则,且目标节点资源足够、卷与设备能在目标节点重建。
不可迁移的 VM(官方列举):
- 直通宿主设备(PCI passthrough、vGPU)
- 设置了绑定特定节点的 node selector
- 调度规则只匹配到一个节点
- VM 附加的 cluster network 只覆盖一个节点 ← 本项目最需要注意的一条
- 开启 CPU pinning,但只有一个节点启用了 CPU Manager
- 存在严格的反亲和规则
⚠️ 直接后果:第 4 章创建
VlanConfig时若把nodeSelector限制到少数节点,使用net-91的 VM 就只能跑在这些节点上,升级/维护时无法自动迁移,只能停机。本项目建议cn-vm覆盖全部 12 台。
bash
# 检查是否有 VM 被绑到特定节点
kubectl get vm -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{" nodeSelector="}{.spec.template.spec.nodeSelector}{"\n"}{end}'
# 检查 VLAN 网络覆盖范围
kubectl get vlanconfig -o custom-columns='NAME:.metadata.name,CLUSTERNET:.spec.clusterNetwork,NODESEL:.spec.nodeSelector'7.3.2 手工热迁移
bash
# UI:Virtual Machines → 选中 VM → ⋮ → Migrate(推荐,Harvester 自动选目标节点)
# 观察过程
kubectl get vmi -n default web-front-01 -o wide -w
kubectl get virtualmachineinstancemigration -n default
kubectl describe vmi web-front-01 -n default | tail -207.3.3 迁移不收敛(写密集型 VM)
热迁移一边复制内存页一边让 VM 运行。若 VM 的内存脏页速率超过网络带宽(典型:活跃数据库),迁移无法收敛,到达超时后被中止。
官方缓解措施:
- 配置专用迁移网络提升带宽并隔离流量(参见
storagenetwork设置)。 - 调参:启用
auto-converge或post-copy,调整bandwidthPerMigration,或为特定 VM 配置迁移策略。 - 选在写入低谷期执行。
- 需要可预测窗口时:直接计划性关机(见 7.5.2)。
7.3.4 自动均衡(实验性 Add-on)
virtual-machine-auto-balance(v1.7.0 起,实验性)用 Kubernetes Descheduler 驱逐放置不优的 VM Pod 来均衡负载。
text
启用:Advanced → Add-ons → virtual-machine-auto-balance (Experimental) → ⋮ → Enable
前提:集群节点数 > 1(本项目 12 节点满足)
部署物:kube-system 下的 descheduler + kube-system/descheduler ConfigMap
策略插件:DefaultEvictor(通用驱逐配置)、LowNodeUtilization(按阈值把 Pod 从高负载节点驱逐到低负载节点)
默认只驱逐 VM Podbash
kubectl get addon -n kube-system | grep -i descheduler
kubectl get cm descheduler -n kube-system -o yaml
kubectl get pods -n kube-system | grep descheduler⚠️ 实验性功能,且驱逐路径不等于热迁移:若 VM 因 7.3.1 的原因不可迁移,驱逐会导致停机。生产环境建议先观察一段时间,或只在维护窗口启用;策略可通过 ⋮ → Edit YAML 调整阈值。
7.4 节点维护
7.4.1 计划内维护流程
bash
NODE=$(kubectl get nodes -o name | head -1 | cut -d/ -f2) # 换成目标节点名
# 1) 禁止新调度
kubectl cordon "$NODE"
# 2) 清空 VM:优先用 UI 对该节点执行 "Enable Maintenance Mode",
# Harvester 会把可迁移 VM 迁走、不可迁移 VM 关机
# 3) 确认节点上没有 VM
kubectl get vmi -A --field-selector status.nodeName="$NODE"
# 4) 执行硬件维护 / 重启
# 5) 恢复调度
kubectl uncordon "$NODE"
kubectl get node "$NODE" -o jsonpath='{.spec.unschedulable}{"\n"}'⚠️ 不要只靠
kubectl drain:Harvester 的 VM 由virt-launcherPod 承载,粗暴 drain 可能变成强制关机而非优雅迁移。让 Harvester 的维护模式来编排。
7.4.2 12 节点集群的滚动维护排期
text
原则:一次只动一个节点;任意时刻剩余容量 ≥ 峰值负载 + 一个节点余量。
1) 先维护纯 worker 节点,再维护管理面节点
2) 3 个管理面节点必须逐台维护,保住 etcd 仲裁
3) 每台维护前确认其上 VM 可迁移(7.3.1);不可迁移的先约业务窗口关机
4) 维护后等 Longhorn 卷重建完成再动下一台:
kubectl get volumes.longhorn.io -n longhorn-system -o wide | grep -i -E 'rebuild|degraded|faulted'7.4.3 扩容 / 缩容节点
| 操作 | 做法 | 注意 |
|---|---|---|
| 加节点 | 用同版本 ISO 安装,选 Join 模式,填 VIP 10.181.0.100 与集群 token | 加入后必须补 cn-vm 的 VlanConfig(第 4 章);若用了静态路由还要加 HostNetworkConfig |
| 减节点 | 维护模式清空 VM → UI 删除节点 | 管理面 3 节点是仲裁底线;有副本的盘要等卷重建完成 |
⚠️ "新节点上的 VM 没网"十有八九是忘记给新节点配 VLAN 网络。把第 4 章的验证清单当作节点入网流程的必经步骤。
7.5 集群升级
7.5.1 版本策略与升级路径
Harvester 采用新的生命周期策略:每 4 个月一个次版本(4 月、8 月、12 月),每 2 个月一个补丁版(尽力而为)。
⚠️ 不支持降级。 一旦升级出问题,只能靠备份恢复,所以升级前的备份与演练是硬要求。
v1.8 的组件矩阵(升级时一并变化,评估兼容性时看这张表):
| 组件 | v1.5.x | v1.6.x | v1.7.x | v1.8.x |
|---|---|---|---|---|
| KubeVirt | v1.4 | v1.5 | v1.6 | v1.7 |
| Longhorn | v1.8 | v1.9 | v1.10 | v1.11 |
| Rancher | v2.11 | v2.12 | v2.13 | v2.14 |
| RKE2 | v1.32 | v1.33 | v1.34 | v1.35 |
| SUSE Linux Micro | 5.5 | 5.5 | 6.1 | 6.2 |
升级路径要点:
- 支持跨一个次版本直升(如 v1.7.x → v1.8.x),中间的补丁版不必逐个安装。
- 支持升级到更晚的补丁版(如 v1.8.0 → v1.8.1)。
- 不支持跨多个 Kubernetes 次版本(上游 Version Skew 限制),这是路径受限的根本原因。
- 如果用 Rancher 管理 Harvester:必须先升 Rancher,再升 Harvester。两者升级过程相互独立,Rancher 升级期间仍可通过 VIP 访问 Harvester,且 Harvester 不会被自动升级。
⚠️ 从 v1.5.0 到 v1.8.x 需要逐次版本走(v1.5→v1.6→v1.7→v1.8),不能一步跳。规划停机窗口时要把多次升级算进去。
7.5.2 升级期间的 VM 处理:热迁移 vs 计划性关机
- 可迁移的 VM:升级前由 Harvester 批量热迁移到其他节点,零停机。
- 不可迁移的 VM:取决于
upgrade-config设置里的restoreVM:false:仍有不可迁移 VM 在运行时,Harvester 拒绝升级,你必须手工关机。true:升级该节点时自动关机这些 VM,节点重启后自动恢复。
两种策略对比(官方表格):
| 维度 | 热迁移 | 计划性关机 |
|---|---|---|
| 停机 | 无 | 短暂业务中断 |
| 写密集 VM 的收敛风险 | 高(可能无法收敛而中止) | 无(不涉及迁移) |
| 窗口可预测性 | 低(取决于脏页速率与带宽) | 高 |
| 操作成本 | 低(自动) | 高(需规划与协调) |
| 资源开销 | 高(迁移期间目标节点要同时承载 VM) | 低 |
官方建议:默认用热迁移;只有当「热迁移在窗口内难以收敛」或「必须有可预测的窗口」时,才对业务关键/写密集 VM 提前优雅关机。
⚠️ 关机重启的两个坑:
- 不要一次性开机一大批 VM——会引发 CPU 与存储 I/O 尖峰(boot storm),拖慢甚至搞崩集群。分批启动,关键业务优先。
restoreVM只对 Harvester 自动关机的不可迁移 VM 生效;你手工关机的 VM 不会自动开机,升级后必须人工启动(并记得更新台账)。
7.5.3 升级前检查清单(照做,别跳)
官方明确要求的前置条件:
| # | 检查项 | 命令 / 做法 | 不满足的后果 |
|---|---|---|---|
| 1 | 备份 VM | 7.2 节;确认 backup-target 可用、关键 VM 有近期 Ready 备份 | 升级失败无退路 |
| 2 | 暂停所有 VM Schedule | kubectl get vmschedule -A,UI 逐个 Suspend | 升级会被拒绝("schedules are active") |
| 3 | 无正在进行/使用中的备份或快照 | Backups / Snapshots 页面无进行中任务 | 升级会被拒绝 |
| 4 | 系统分区空闲 ≥ 30 GiB | 每个节点 df -h /usr/local/ | 升级被直接拒绝 |
| 5 | 运行官方 pre-check 脚本 | 在控制面节点上执行,逐项处理失败项 | 隐藏问题在升级中爆发 |
| 6 | 所有节点时间同步 | timedatectl status;未配 NTP 就手工加:编辑 /etc/systemd/timesyncd.conf 的 [ntp] NTP=...,再 timedatectl set-ntp true | 证书/etcd/迁移异常 |
| 7 | 证书有效期 | Harvester 会检查,7 天内到期直接报错 | 升级被拒;可用注解 harvesterhci.io/minCertsExpirationInDay 调整阈值 |
| 8 | 硬件满足推荐配置 | 升级会产生中间资源占用 | 升级中途资源不足 |
| 9 | 升级期间不要操作集群 | 不建新 VM、不传镜像 | 状态错乱 |
| 10 | Pod Security Admission | 升级会在 harvester-system、cattle-system 创建一次性特权 Pod | 若启用了 PSA,需放行这些 Pod |
| 11 | upgrade-config 参数确认 | 检查 restoreVM、imagePreloadOption、nodeUpgradeOption | 行为与预期不符 |
| 12 | NIC 可能被重命名 | 官方提醒:连接在 PCI bridge 上的网卡升级后可能改名 | 升级后 VlanConfig 里的网卡名失效 → VM 断网 |
⚠️ 第 12 条对本项目影响很大:第 4 章的
VlanConfig是按网卡名(eno3/eno4)配置的。升级后第一件事就是核对网卡名有没有变,变了要立刻更新VlanConfig,否则整条 VLAN 91 通路断掉。
⚠️ 从 v1.7.0 起,升级仓库改为 Deployment 形式(不再是 VM),性能与可靠性更好;老文档里的 upgrade-repo VM 说明已过期。
7.5.4 执行升级
在线环境(最简单)
text
UI Dashboard → 出现 Upgrade 按钮 → 选择目标版本 → 点击进度圈查看各阶段状态离线 / 内网环境(本项目大概率是这种)
v1.8.0 起 UI 提供专门的离线升级页面:
text
Harvester UI → Advanced → Settings → 找到 server-version 行 → ⋮ → Upgrade
→ Upload New Image:填镜像名,然后二选一
- Upload:上传本地 .iso
- Download:填内网 HTTP 地址(如 http://10.181.0.200/harvester-v1.8.1-amd64.iso)让 Harvester 自己下载
→ 勾选 Enable Logging(建议开,便于排障)→ Upgrade
→ Select Existing Image:复用之前导入过的镜像
→ Delete Existing Image:清理旧镜像CLI 方式(用 Version CRD):
bash
# 1) 把 ISO 放到内网 HTTP 服务器,例如 http://10.181.0.200/harvester.iso
# 2) 取官方 version.yaml:https://releases.rancher.com/harvester/{version}/version.yaml
# 3) 改 isoURL 为内网地址,isoChecksum 填 ISO 的 SHA-512
cat <<EOF | kubectl create -f -
apiVersion: harvesterhci.io/v1beta1
kind: Version
metadata:
name: v1.8.1 # 自定义时改名避免与官方重名,如 v1.8.1-internal
namespace: harvester-system
spec:
isoChecksum: '<ISO 的 SHA-512>'
isoURL: http://10.181.0.200/harvester.iso
releaseDate: '20260901'
EOF
# 也可以直接让控制面节点从内网拉取
sudo -i
kubectl create -f http://10.181.0.200/version.yaml
kubectl get version -n harvester-system⚠️ 官方提示:新版本发布后 UI 上的 Upgrade 按钮不会立刻出现。要提前升就走离线导入这条路。生产环境推荐用 UI 升级。
⚠️ 如果用私有容器仓库并设置
imagePreloadOption.strategy.type=skip跳过镜像预加载:千万不要依赖公共仓库(Docker Hub 限流/断网都会让升级失败并把集群留在中间状态)。
7.5.5 逐节点控制:暂停与恢复节点升级(v1.7.0+)
节点升级阶段会逐台升级 OS 与 RKE2。想在某些节点上先做人工验证,可以暂停:
bash
# 方式一:改 upgrade-config 设置里的 nodeUpgradeOption
# mode: manual → 暂停所有节点
# pauseNodes: [node-a, node-b] → 只暂停列出的节点(其余自动升级)
kubectl get setting upgrade-config -n harvester-system -o yaml
# 方式二(升级已开始后):改 Upgrade CR 的注解,这才是「事实来源」
kubectl -n harvester-system get upgrades -l harvesterhci.io/latestUpgrade=true
# 查看当前暂停状态
kubectl -n harvester-system get upgrades -l harvesterhci.io/latestUpgrade=true -o yaml \
| grep -A8 'node-upgrade-pause-map\|nodeStatuses'
# 恢复某个节点(把 pause 改成 unpause)
kubectl -n harvester-system annotate --overwrite upgrades hvst-upgrade-xxxxx \
harvesterhci.io/node-upgrade-pause-map='{"node-a":"unpause","node-b":"pause"}'⚠️
nodeUpgradeOption只在升级初始化阶段生效,初始化之后再改对当前这轮无效,只影响下一轮。要在升级中途调整,就用上面的注解。⚠️ 被暂停的节点仍然处于 cordon 状态(不会调度新负载),而且它的 pre-drain job 还没创建。这类节点上只应做维护动作(比如手工关 VM)。
⚠️ 节点多的时候,unpause 可能要执行多次才能推完整个集群。
7.5.6 升级后验证
bash
# 1) 版本
kubectl get setting server-version -n harvester-system -o jsonpath='{.value}{"\n"}'
kubectl get nodes -o custom-columns='NODE:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion,OS:.status.nodeInfo.osImage'
# 2) 所有节点 Ready、无 cordon
kubectl get nodes
# 3) 组件健康
kubectl get pods -n harvester-system -o wide | grep -v Running
kubectl get pods -n longhorn-system -o wide | grep -v Running
kubectl get pods -n kube-system -o wide | grep -v Running
# 4) ⭐ 网卡名有没有变(第 4 章的 VlanConfig 依赖网卡名)
kubectl get vlanconfig -o yaml | grep -A5 uplink
kubectl get vlanstatus -A
for ip in $(seq 11 22); do
echo "--- 10.181.0.$ip"; ssh -o BatchMode=yes "rancher@10.181.0.$ip" 'ip -br link' 2>/dev/null
done
# 5) VM 全部回到 Running,且双网卡都在
kubectl get vm -A
kubectl get vmi -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,NODE:.status.nodeName,PHASE:.status.phase'
# 6) 网络功能复测(第 4/5 章的验证命令再跑一遍)
# 网关 ping、跨节点同 VLAN 两台 VM 互 ping、外部访问 VIP/VM IP
# 7) 恢复定时备份
# UI 里把 7.5.3 第 2 步暂停的 VM Schedule 逐个 Resume(先手工备份一次确认可用)
# 8) 升级 CR 状态归档
kubectl -n harvester-system get upgrades -l harvesterhci.io/latestUpgrade=true -o yaml > upgrade-$(date +%F).yaml7.5.7 官方列出的升级已知坑
| 坑 | 说明与处理 |
|---|---|
| Pre-drained 状态卡住 | 通常是该节点上的 VM 热迁移失败;先处理迁移失败原因(资源不足/网络/不可迁移),再让升级继续 |
| Longhorn Manager 因 backing image eviction 崩溃 | 升级到 v1.4.x 时,若任何节点或磁盘的 EvictionRequested=true 会触发竞态导致崩溃 → 升级前确认全部为 false |
| RKE2 ingress-nginx admission webhook(CVE-2025-1974) | 若当初为缓解该 CVE 关掉了 webhook,升级到 v1.5.0+ 后必须重新开启:先确认 nginx-ingress >= v1.12.1,再 kubectl -n kube-system edit helmchartconfig rke2-ingress-nginx 删掉 admissionWebhooks.enabled: false 与 enable-annotation-validation: true |
| 系统分区空间不足 | kubelet 触发镜像 GC,在离线环境会把镜像删掉导致升级失败;Harvester 会做检查并阻止升级 |
| VM 备份兼容性 | v1.4.2+ 在涉及外部存储的备份创建/恢复上存在限制,升级前查对应版本文档 |
bash
# 确认 ingress-nginx 版本(CVE 修复检查)
kubectl -n kube-system get po -l "app.kubernetes.io/name=rke2-ingress-nginx" \
-o jsonpath='{.items[].spec.containers[].image}{"\n"}'7.6 监控与告警
7.6.1 内建监控栈
Harvester 安装时自动启用 Prometheus 监控;UI 上有 Grafana 仪表盘入口。
| 事项 | 说明 |
|---|---|
| 访问 | UI 上点击 Grafana dashboard 链接 |
| 权限 | 只有 admin 用户能看集群仪表盘指标 |
| Grafana 默认密码 | xxx(由 rancher-monitoring 提供)→ 装完立刻改 |
| 指标保留 | 实时指标保留 5 天,长期趋势要自己外送 |
| 组件 | Prometheus、prometheus-node-exporter、Alertmanager(默认启用) |
资源调优(12 节点 + 上百 VM 时很可能需要):
text
UI → Advanced → Settings → monitoring → 从 Prometheus 页签调整 requests/limits
- 多个管理节点硬件不同:按**较小**的那台来设
- VM 数量增多时 prometheus-node-exporter 可能 OOM 被杀 → 提高 limits.memorybash
kubectl get pods -n cattle-monitoring-system -o wide 2>/dev/null | head
kubectl top pods -n cattle-monitoring-system 2>/dev/null | grep -E 'prometheus|node-exporter' | head7.6.2 告警外送
text
UI → Monitoring & Logging → Monitoring → Alertmanager Configs → Create
→ 选 Namespace、填 Name → Create → 再进入配置 receivers / routes需要 Teams / 短信 webhook 时,先装 rancher-alerting-drivers:
bash
helm install rancher-alerting-drivers rancher-charts/rancher-alerting-drivers \
--namespace cattle-monitoring-system # 离线环境先 helm pull 再本地安装⚠️ Harvester 不负责升级
rancher-alerting-drivers(它不属于 Harvester 项目),必须自己维护版本。
7.6.3 本项目必须有的告警项
| 告警 | 触发条件 | 为什么重要 |
|---|---|---|
| 节点 NotReady | 任一节点非 Ready 超 2 分钟 | 12 节点集群的容量与仲裁 |
| VM 非 Running | kubevirt_vmi_status_phase != Running | 业务直接受损 |
| vlanstatus 异常 | 任一 VlanStatus Ready=False | VLAN 91 通路断了,VM 全部失联 |
| ClusterNetwork 未就绪 | cn-vm Ready=False | 同上,影响面更大 |
| bond 成员 down | 上联口成员链路 down | 冗余丢失,单链路故障即断网 |
| 存储卷 degraded/faulted | Longhorn 卷状态异常 | 副本数不足,再有盘坏就丢数据 |
| 节点系统分区 < 30 GiB | df /usr/local | 会导致升级被拒 |
| 备份失败 | VM Schedule 连续失败 / 挂起 | 静默失去恢复能力 |
| LB IP 池将耗尽 | .50-.99 已用 > 80% | 新 Service 拿不到 IP |
| DHCP 池将耗尽 | Managed DHCP 租约接近 .201-.250 上限 | 新 VM 拿不到地址 |
| 证书临期 | 剩余 < 30 天 | 7 天内到期会阻止升级 |
| etcd/仲裁 | 管理面节点同时挂 2 台 | 集群不可写 |
7.7 凭据与安全加固
7.7.1 关闭节点 SSH 密码登录(装完就该做)
安装期间 Harvester 默认开启 SSH 密码认证(方便排障)。装完官方建议关掉——用 CloudInit CRD 一次性下发到所有节点:
bash
cat <<'EOF' | kubectl apply -f -
apiVersion: node.harvesterhci.io/v1beta1
kind: CloudInit
metadata:
name: ssh-config
spec:
matchSelector:
harvesterhci.io/managed: "true" # 应用到所有 Harvester 节点
filename: 99-ssh-config
contents: |
stages:
network:
- name: "disable password login"
commands:
- sed -i -E 's/^#?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
- sed -i -E 's/^#?ChallengeResponseAuthentication .*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
- sed -i -E 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config
- systemctl restart sshd
paused: false
EOF⚠️ 所有受影响的节点必须重启,CloudInit 配置才会生效。
⚠️ 关密码前先确认 SSH 公钥已经下发(第 3 章
config.yaml的ssh_authorized_keys),否则重启后你自己也进不去——只能靠 IPMI/iDRAC 控制台。
验证:
bash
kubectl get cloudinit -A
ssh -o PreferredAuthentications=password rancher@10.181.0.11
# 期望:Permission denied (publickey,keyboard-interactive).
ssh -o BatchMode=yes rancher@10.181.0.11 'echo key login OK'
# 期望:key login OK💡 同一个
CloudInit机制还能干别的批量节点配置:统一 NTP、写/etc/hosts、加运维专用公钥、下发 sysctl。matchSelector支持按标签只作用于部分节点,paused: true可以先创建后择机启用。
7.7.2 SSH 公钥轮换
bash
# 1) 新公钥先"加",不要直接替换(避免把自己锁在外面)
kubectl get cloudinit -A
cat <<'EOF' | kubectl apply -f -
apiVersion: node.harvesterhci.io/v1beta1
kind: CloudInit
metadata:
name: ssh-keys-rotate
spec:
matchSelector:
harvesterhci.io/managed: "true"
filename: 98-ssh-keys
contents: |
stages:
network:
- name: "add new ops keys"
commands:
- mkdir -p /home/rancher/.ssh && chmod 700 /home/rancher/.ssh
- grep -qxF 'ssh-ed25519 AAAA...new-key ops-new@company' /home/rancher/.ssh/authorized_keys ||
echo 'ssh-ed25519 AAAA...new-key ops-new@company' >> /home/rancher/.ssh/authorized_keys
- chmod 600 /home/rancher/.ssh/authorized_keys
paused: false
EOF
# 2) 逐台重启生效(配合 7.4 维护流程,一次一台)
# 3) 新密钥验证通过后,再下发一次 CloudInit 移除旧公钥7.7.3 其它凭据清单
| 凭据 | 位置 | 轮换建议 |
|---|---|---|
| Harvester admin 密码 | 首次登录时设置(单集群模式只有一个默认 admin 用户) | 90 天;多租户请接 Rancher |
| Grafana admin | 默认 prom-operator | 装完立即改 |
| backup-target 的 S3 AK/SK | Settings → backup-target | 跟随对象存储策略;改完立刻做一次备份验证 |
| kubeconfig / 客户端证书 | /etc/rancher/rke2/rke2.yaml(节点上) | RKE2 会自动轮换;升级前确认有效期 > 7 天 |
| 集群 join token | 装节点时使用 | 泄露就重置,并审计已加入节点 |
VM 内 ops 用户密钥 | cloud-init ssh_authorized_keys | 与 7.7.2 同批轮换 |
| IPPool / Managed DHCP 凭据 | Add-on 配置 | 同上 |
bash
# 证书有效期速查(升级前必做)
openssl s_client -connect 10.181.0.100:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -subject -enddate7.8 故障处置与恢复
7.8.1 单节点故障(宕机/掉电)
text
预期行为:
1) 节点 NotReady → 其上的 VM 由 Harvester/KubeVirt 在其他节点重建(前提是卷健康、资源够、VM 可调度)
2) Longhorn 卷副本在其他节点重建,期间卷处于 degraded
3) VIP 由 witness/管理面机制漂移,UI 仍可通过 10.181.0.100 访问bash
kubectl get nodes | grep -v ' Ready'
kubectl get vmi -A -o wide | awk '{print $1,$2,$8}' | sort # 看 VM 落到哪些节点
kubectl get volumes.longhorn.io -n longhorn-system | grep -iE 'degraded|faulted'
kubectl get events -A --sort-by=.lastTimestamp | tail -30⚠️ VM 能不能自动恢复,取决于第 4 章的网络配置:如果
cn-vm只覆盖部分节点,或者 VM 带了 nodeSelector,它就没地方可去。这就是 7.3.1 那条警告的实际代价。⚠️ 节点硬件修好回来后,先确认它 Ready、Longhorn 卷重建完成,再解除 cordon。
7.8.2 支持包与日志收集
bash
# UI:右上角/Advanced → Generate Support Bundle(打包全集群诊断信息,用于提工单)
# 节点上的关键日志路径(排障必备)
sudo less /var/lib/rancher/rke2/agent/logs/kubelet.log # kubelet
sudo less /var/lib/rancher/rke2/agent/containerd/containerd.log # containerd
kubectl logs -n <ns> virt-launcher-<vm>-xxxxx -c compute --tail=200 # VM 的宿主侧日志
kubectl describe vmi -n <ns> <vm> | tail -40 # 事件里通常有答案
journalctl -u kubelet --no-pager | tail -100
dmesg -T | tail -50 # 硬件/网卡/OOM配置资产快照:cluster-snapshot.sh
排障、升级、扩容之前都应该先「拍一张照」——事后对比才知道什么变了。脚本导出平台对象 YAML、节点网络实况与版本信息,打成一个带时间戳的归档:
bash
#!/usr/bin/env bash
# appendix/ops/cluster-snapshot.sh —— 导出集群配置资产与网络实况,生成可归档快照
# 用法:./cluster-snapshot.sh [输出目录] 默认 ./hv-snapshot-<时间戳>
set -uo pipefail
STAMP="$(date +%Y%m%d-%H%M%S)"
DIR="${1:-hv-snapshot-$STAMP}"
mkdir -p "$DIR/k8s" "$DIR/nodes"
command -v kubectl >/dev/null 2>&1 || { echo "ERROR: 找不到 kubectl(请在节点上 sudo -i 后执行)" >&2; exit 1; }
kubectl cluster-info >/dev/null 2>&1 || { echo "ERROR: 无法连接集群" >&2; exit 1; }
log(){ printf '[%s] %s\n' "$(date +%T)" "$*"; }
dump(){ # dump <资源名> <文件名>
if kubectl get "$1" -A -o yaml > "$DIR/k8s/$2.yaml" 2>/dev/null; then
log "导出 $1 → k8s/$2.yaml"
else
log "跳过 $1(CRD 不存在或无权限)"; rm -f "$DIR/k8s/$2.yaml"
fi
}
log "开始快照:$DIR"
# 1) 版本与全局设置
kubectl get setting -n harvester-system -o yaml > "$DIR/k8s/settings.yaml" 2>/dev/null
{ echo "server-version: $(kubectl get setting server-version -n harvester-system -o jsonpath='{.value}' 2>/dev/null)"
echo "ui-index: $(kubectl get setting ui-index -n harvester-system -o jsonpath='{.value}' 2>/dev/null)"
echo "snapshot-time: $(date -Is)"; } > "$DIR/version.txt"
# 2) 网络对象(本书核心资产)
dump nodes node-list
dump clusternetwork clusternetwork
dump vlanconfig vlanconfig
dump vlanstatus vlanstatus
dump hostnetworkconfig hostnetworkconfig
dump network-attachment-definition nad
dump ippools.network.harvesterhci.io ippool-vm-dhcp
dump ippools.networking.harvesterhci.io ippool-lb
# 3) VM 与模板资产
dump virtualmachine vm
dump virtualmachineinstance vmi
dump virtualmachinetemplate vm-template
dump virtualmachinetemplateversion vm-template-version
dump vmschedule vm-schedule
dump virtualmachinebackup vm-backup
dump keypair keypair
dump vmimage vmimage
dump storageclass storageclass
dump svc service
# 4) 事件与 Pod 现状(排障上下文)
kubectl get events -A --sort-by=.lastTimestamp > "$DIR/k8s/events.txt" 2>/dev/null
kubectl get pods -A -o wide > "$DIR/k8s/pods.txt" 2>/dev/null
# 5) 每个节点的网络实况(升级后核对网卡名重命名就靠这份)
NODE_IPS="$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}{end}' 2>/dev/null)"
while read -r name ip; do
[[ -n "$ip" ]] || continue
if ssh -o BatchMode=yes -o ConnectTimeout=3 "rancher@$ip" true 2>/dev/null; then
ssh -o BatchMode=yes "rancher@$ip" bash -s > "$DIR/nodes/$name.txt" 2>/dev/null <<'EOS' || log "WARN: $name 采集失败"
echo "### hostname / kernel"; hostname; uname -r
echo "### ip -br link"; ip -br link
echo "### ip -br addr"; ip -br addr
echo "### ip route"; ip route
echo "### bridge link"; bridge link 2>/dev/null
echo "### bridge vlan show"; bridge vlan show 2>/dev/null
echo "### bonding"; for b in /proc/net/bonding/*; do echo "--- $b"; cat "$b"; done 2>/dev/null
echo "### mtu"; for d in /sys/class/net/*; do echo "$(basename $d) $(cat $d/mtu)"; done
echo "### /usr/local 可用空间"; df -h /usr/local 2>/dev/null || df -h /
echo "### 时间同步"; timedatectl 2>/dev/null | egrep 'synchronized|NTP'
EOS
log "采集节点 $name ($ip)"
else
echo "无法免密登录 rancher@$ip,请手工采集(见第 8 章 8.2.2/8.2.3)" > "$DIR/nodes/$name.txt"
log "WARN: 跳过节点 $name(SSH 免密不可用)"
fi
done <<< "$NODE_IPS"
# 6) 打包
tar czf "$DIR.tar.gz" "$DIR" 2>/dev/null
log "完成:$DIR 与 $DIR.tar.gz"
log "建议:归档到备份目标,并在升级/扩容前后各留一份用于对比"bash
chmod +x appendix/ops/cluster-snapshot.sh
./appendix/ops/cluster-snapshot.sh # 升级/扩容前拍一张
diff -ru hv-snapshot-<升级前> hv-snapshot-<升级后> | less # 升级后对比网卡名、VlanConfig、版本⚠️ 快照里含
settings.yaml(可能包含敏感配置)与节点网络信息,归档目录按机密资料管理,别丢进公开仓库。
7.8.3 VM 常见宿主侧故障(官方 troubleshooting)
| 症状 | 根因 | 处置 |
|---|---|---|
| VM 处于 Off,但 UI 上没有 Start 按钮 | vm/vmi/pod 状态不一致(残留 virt-launcher Pod) | kubectl delete pod virt-launcher-<vm>-xxxxx -n <ns> --force,Start 按钮就会出现 |
VM 卡在 Starting,Pod 为 CreateContainerError,报 failed to generate spec: not a device node | 集群/节点重启后 kubelet 未先调用 NodeStageVolume,Longhorn CSI 把普通空文件 bind mount 到了设备路径 | 找到受影响 VM 的 backing pod 与对应 Longhorn 卷,按官方步骤在集群级/节点级修复后重启 VM(v1.3.0 已知问题) |
| VM Running 但拿不到 IP | 见第 8 章网络分层排查 | — |
bash
# 快速判断三件套是否一致
VM=web-front-01; NS=default
kubectl get vm -n $NS $VM -o wide
kubectl get vmi -n $NS $VM -o wide
kubectl get pod -n $NS -l vm.kubevirt.io/name=$VM -o wide7.8.4 灾难恢复(最坏情况)
text
优先级从高到低:
1) 有备份 + 集群还在 → 从备份 Restore(7.2.2),最快
2) etcd/管理面损坏 → 用 RKE2 etcd 快照恢复(恢复流程与快照位置以当前版本 KB 为准,务必先在测试环境演练)
3) 集群彻底不可用 → 按第 2、3 章重装 12 节点集群(同版本 ISO)
→ 重建网络(第 4 章:ClusterNetwork/VlanConfig/NAD/HostNetworkConfig)
→ 配同一个 backup-target
→ 上传同名镜像
→ 从备份 Restore 全部 VM(用第 6 章 CSV 保证命名/IP 一致)
→ 复跑第 5、8 章验证清单⚠️ 第 3 条能走通的前提是:网络配置的 YAML 和 VM 清单都归档了。请把
appendix/network/、appendix/vm/vms.csv、ip-registry-*.txt、模板与 Cloud Config Template 的导出 YAML 一起放进版本库或异地存储。这是整套书里最值钱的 20 分钟。
bash
# 一键导出集群配置资产(建议每周跑一次并异地保存)
mkdir -p hv-assets-$(date +%F) && cd hv-assets-$(date +%F)
for r in clusternetwork vlanconfig vlanstatus hostnetworkconfig networkattachmentdefinition \
virtualmachinetemplate virtualmachinetemplateversion virtualmachine vmschedule \
setting addon; do
kubectl get $r -A -o yaml > "$r.yaml" 2>/dev/null || echo "skip $r"
done
kubectl get vm -A -o yaml > vms.yaml
tar czf ../hv-assets-$(date +%F).tar.gz . && cd .. && ls -lh hv-assets-*.tar.gz7.9 本章检查清单
| # | 检查项 | 命令 | 期望 |
|---|---|---|---|
| 1 | 每日巡检脚本可运行 | ./appendix/ops/daily-check.sh | 无 FAIL |
| 2 | 所有节点 Ready | kubectl get nodes | 12/12 Ready,无 cordon |
| 3 | 系统 Pod 健康 | 7.1.1 第 2 步 | 全部 Running |
| 4 | VM 全部 Running | kubectl get vm -A | 无非 Running |
| 5 | 直通网络就绪 | kubectl get clusternetwork cn-vm; kubectl get vlanstatus -A | 全 Ready |
| 6 | backup-target 已配 | kubectl get setting backup-target -n harvester-system | 非空且可用 |
| 7 | 定时备份存在且未挂起 | kubectl get vmschedule -A | Ready,suspend=false |
| 8 | 恢复演练做过 | 演练记录 | 每季度一次,有 RTO 数据 |
| 9 | 文件系统冻结可用 | 7.2.5 virt-freezer | freeze/unfreeze 无报错 |
| 10 | 快照配额已设 | Namespaces / VM Quota | 有上限 |
| 11 | VM 可迁移性已确认 | 7.3.1 检查命令 | 关键 VM 无 nodeSelector,cn-vm 覆盖全部节点 |
| 12 | 升级前置达标 | /usr/local 空闲、NTP、证书、pre-check、Schedule 已暂停 | 全部满足 |
| 13 | Grafana 默认密码已改 | 登录 Grafana | 不再是 prom-operator |
| 14 | 节点 SSH 密码登录已关 | xxx | Permission denied |
| 15 | 告警能送达 | 触发测试告警 | 收到通知 |
| 16 | 配置资产已归档 | 7.8.4 导出脚本 | tar 包存在且异地备份 |
下一章:把所有验证动作串成"分层排查法",并给出一张覆盖物理链路到 DNS 的故障对照表。