Skip to content

07 · 日常运维手册:巡检、备份、迁移、升级、凭据

适用版本:Harvester v1.8 | 基线:12 节点 ISO 集群,管理网 10.181.0.0/24(VIP 10.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 失败)"
done

7.1.2 每周巡检

检查项命令期望
备份目标可达kubectl get setting backup-target -n harvester-system -o yaml已配置且能列出备份
定时备份成功kubectl get vmschedule -AReady,最近备份 Ready
API 证书有效期openssl s_client -connect 10.181.0.100:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -enddate剩余 > 30 天
时间同步各节点 timedatectl statusSystem 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-check

7.2 备份与快照

7.2.1 先配 Backup Target(一次性)

备份数据必须存在集群外部的 NFS 或 S3,存在集群自己的存储里等于没备份。

路径:UI → Advanced → Settings → backup-target

类型关键参数本项目建议
NFSEndpoint,形如 nfs://10.181.0.200:/data/harvester-backup用独立备份服务器
S3EndpointBucketNameBucketRegionAccessKeyIDSecretAccessKeyCertificate(自签证书)、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 BackupVM 需处于 Running 或 Stopped
恢复成新 VMBackups → ⋮ → Restore → New生成新名字的 VM,记得给它新 IP
替换现有 VMBackups → ⋮ → Restore → Replace existing目标 VM 必须 Stopped;会替换其卷
跨集群恢复新集群配同一 backup-target → 上传同名镜像 → Restore镜像名不一致会失败

7.2.3 定时备份(VM Schedule)

UI → Virtual Machine Schedules → Create Schedule:

参数建议值说明
TypeBackup或 Snapshot(本地快照,不出集群)
Cron Schedule0 2 * * *间隔必须 ≥ 1 小时;同粒度的多个 schedule 偏移 ≥ 10 分钟,否则 I/O 打爆存储
Retain7超出后删最旧的,Longhorn 触发快照清理
Max Failure3连续失败超过该值,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 -20

7.3.3 迁移不收敛(写密集型 VM)

热迁移一边复制内存页一边让 VM 运行。若 VM 的内存脏页速率超过网络带宽(典型:活跃数据库),迁移无法收敛,到达超时后被中止。

官方缓解措施:

  1. 配置专用迁移网络提升带宽并隔离流量(参见 storagenetwork 设置)。
  2. 调参:启用 auto-convergepost-copy,调整 bandwidthPerMigration,或为特定 VM 配置迁移策略。
  3. 选在写入低谷期执行。
  4. 需要可预测窗口时:直接计划性关机(见 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 Pod
bash
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-launcher Pod 承载,粗暴 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-vmVlanConfig(第 4 章);若用了静态路由还要加 HostNetworkConfig
减节点维护模式清空 VM → UI 删除节点管理面 3 节点是仲裁底线;有副本的盘要等卷重建完成

⚠️ "新节点上的 VM 没网"十有八九是忘记给新节点配 VLAN 网络。把第 4 章的验证清单当作节点入网流程的必经步骤。


7.5 集群升级

7.5.1 版本策略与升级路径

Harvester 采用新的生命周期策略:每 4 个月一个次版本(4 月、8 月、12 月)每 2 个月一个补丁版(尽力而为)

⚠️ 不支持降级。 一旦升级出问题,只能靠备份恢复,所以升级前的备份与演练是硬要求。

v1.8 的组件矩阵(升级时一并变化,评估兼容性时看这张表):

组件v1.5.xv1.6.xv1.7.xv1.8.x
KubeVirtv1.4v1.5v1.6v1.7
Longhornv1.8v1.9v1.10v1.11
Rancherv2.11v2.12v2.13v2.14
RKE2v1.32v1.33v1.34v1.35
SUSE Linux Micro5.55.56.16.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 提前优雅关机。

⚠️ 关机重启的两个坑:

  1. 不要一次性开机一大批 VM——会引发 CPU 与存储 I/O 尖峰(boot storm),拖慢甚至搞崩集群。分批启动,关键业务优先。
  2. restoreVM 只对 Harvester 自动关机的不可迁移 VM 生效;你手工关机的 VM 不会自动开机,升级后必须人工启动(并记得更新台账)。

7.5.3 升级前检查清单(照做,别跳)

官方明确要求的前置条件:

#检查项命令 / 做法不满足的后果
1备份 VM7.2 节;确认 backup-target 可用、关键 VM 有近期 Ready 备份升级失败无退路
2暂停所有 VM Schedulekubectl 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、不传镜像状态错乱
10Pod Security Admission升级会在 harvester-systemcattle-system 创建一次性特权 Pod若启用了 PSA,需放行这些 Pod
11upgrade-config 参数确认检查 restoreVMimagePreloadOptionnodeUpgradeOption行为与预期不符
12NIC 可能被重命名官方提醒:连接在 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).yaml

7.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: falseenable-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.memory
bash
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' | head

7.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 非 Runningkubevirt_vmi_status_phase != Running业务直接受损
vlanstatus 异常任一 VlanStatus Ready=FalseVLAN 91 通路断了,VM 全部失联
ClusterNetwork 未就绪cn-vm Ready=False同上,影响面更大
bond 成员 down上联口成员链路 down冗余丢失,单链路故障即断网
存储卷 degraded/faultedLonghorn 卷状态异常副本数不足,再有盘坏就丢数据
节点系统分区 < 30 GiBdf /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.yamlssh_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/SKSettings → 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 -enddate

7.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 wide

7.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.csvip-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.gz

7.9 本章检查清单

#检查项命令期望
1每日巡检脚本可运行./appendix/ops/daily-check.sh无 FAIL
2所有节点 Readykubectl get nodes12/12 Ready,无 cordon
3系统 Pod 健康7.1.1 第 2 步全部 Running
4VM 全部 Runningkubectl get vm -A无非 Running
5直通网络就绪kubectl get clusternetwork cn-vm; kubectl get vlanstatus -A全 Ready
6backup-target 已配kubectl get setting backup-target -n harvester-system非空且可用
7定时备份存在且未挂起kubectl get vmschedule -AReady,suspend=false
8恢复演练做过演练记录每季度一次,有 RTO 数据
9文件系统冻结可用7.2.5 virt-freezerfreeze/unfreeze 无报错
10快照配额已设Namespaces / VM Quota有上限
11VM 可迁移性已确认7.3.1 检查命令关键 VM 无 nodeSelector,cn-vm 覆盖全部节点
12升级前置达标/usr/local 空闲、NTP、证书、pre-check、Schedule 已暂停全部满足
13Grafana 默认密码已改登录 Grafana不再是 prom-operator
14节点 SSH 密码登录已关xxxPermission denied
15告警能送达触发测试告警收到通知
16配置资产已归档7.8.4 导出脚本tar 包存在且异地备份

下一章:把所有验证动作串成"分层排查法",并给出一张覆盖物理链路到 DNS 的故障对照表。