主题
03 · 部署实战 A:ISO 一体机无人值守装机
形态:Harvester OS 一体机。用 KVM/libvirt 起 3 台 UEFI 虚拟机,通过 netboot(vmlinuz + initrd + rootfs squashfs)+ HTTP 托管
config.yaml完成 v1.8.2 无人值守安装(等效官方 PXE/iPXE 自动安装流程),节点自带 RKE2 + Longhorn + KubeVirt。实测结果:3 节点全部
Ready control-plane,etcd,etcd 成员 3/3,异常 Pod 0, 总编排 42 分钟(不含 9.5 GB 介质下载约 44 分钟)。 裸金属部署同理,只需把「libvirt 域定义」换成「PXE/iPXE 服务器 + 物理机引导」。
1. 方案与架构
1.1 为什么用 netboot 而不是挂载 ISO
| 方式 | 交互性 | 可否全自动 | 说明 |
|---|---|---|---|
| 挂载 ISO 引导 | 需人工走安装向导 | ❌ | 即使配 harvester.install.automatic,仍要处理引导菜单 |
| libvirt 直接内核引导(本方案) | 无 | ✅ | <kernel> + <initrd> + <cmdline> 直接注入,等效 iPXE 的 kernel/initrd 加载 |
关键收益:引导参数与装机配置全部由宿主机脚本控制,虚拟机开机即进入自动安装, 装完 poweroff,宿主机再把域定义切成硬盘启动并 autostart——全程零人工。
1.2 引导链路(实测)
宿主机 <BASE>/
boot/ vmlinuz + initrd + rootfs.squashfs + ISO ← 00-download.sh 拉取
│ (符号链接)
http/ python3 -m http.server 绑定 <HTTP_IP>:<PORT> ← 20-http-server.sh
│ ├── harvester-v1.8.2-vmlinuz-amd64
│ ├── harvester-v1.8.2-initrd-amd64
│ ├── harvester-v1.8.2-rootfs-amd64.squashfs (root=live:http://…)
│ ├── harvester-v1.8.2-amd64.iso (install.iso_url)
│ └── configs/config-nodeNN.yaml (install.config_url)
▼
libvirt 域 harvester-NN(UEFI/OVMF)
│ ① 直接内核引导 → dracut 取 rootfs → 进入 live 安装环境
│ ② harvester-installer 读 config_url → 应用网络/分区/装机
│ ③ elemental install → 格式化 data 盘 → 取 ISO → 预加载镜像 → 写 GRUB
▼ poweroff(install.poweroff=true)
宿主机 60-deploy.sh:切 disk 模式重定义 + virsh autostart + 开机
▼
Harvester OS 启动 → RKE2 起 → node01 create 出集群;node02/03 用 server_url+token join
▼
harvester-promote-node-controller 自动把 join 节点提升为 control-plane,etcd
▼
kube-vip 承载 VIP → Dashboard 可用1.3 目录结构
<BASE>/ # 实测为 /mnt/xfs/harvester
├── boot/ # 安装介质实体:vmlinuz / initrd / rootfs squashfs / ISO
├── disks/ # data 盘实体(400GiB qcow2,机械盘)
│ # + os 盘【符号链接】→ NVMe 目录(★ 见 §7 磁盘布局)
├── nvram/ # 每节点 UEFI 变量存储(*_VARS.fd + .bak),保留安装后的 EFI 启动项
├── http/ # HTTP 服务根目录:介质符号链接 + configs/ + images/
│ └── configs/ # config-node01.yaml(create) / config-node02,03.yaml(join)
├── configs/ # 虚拟机 XML(probe/install/disk 三态)、token.txt、credentials.txt(600)
├── logs/ # deploy.log / deploy-all.log / deploy.status / progress.log
│ # / harvester-NN-console.log(+.prev) / download/
├── import/ # 独立的镜像导入工作流(未纳入部署链路)
└── scripts/ # 19 个自动化脚本(见 §5)
http/下的介质都是符号链接指向boot/,因此重下介质不需要重建 HTTP 目录。
2. 内核引导参数(cmdline 逐项)
libvirt 域 XML 的 <os> 段直接指定内核引导(实测原文,单行):
xml
<kernel><BASE>/boot/harvester-v1.8.2-vmlinuz-amd64</kernel>
<initrd><BASE>/boot/harvester-v1.8.2-initrd-amd64</initrd>
<cmdline>ip=dhcp rd.neednet=1 net.ifnames=1 rd.cos.disable rd.noverifyssl
console=ttyS0,115200n8
root=live:http://<HTTP_IP>:<PORT>/harvester-v1.8.2-rootfs-amd64.squashfs
harvester.install.automatic=true
harvester.install.config_url=http://<HTTP_IP>:<PORT>/configs/config-node01.yaml
harvester.install.iso_url=http://<HTTP_IP>:<PORT>/harvester-v1.8.2-amd64.iso
harvester.install.skipchecks=true
harvester.install.tty=ttyS0,115200n8</cmdline>
<boot dev='hd'/>| 参数 | 作用 | 不写会怎样 |
|---|---|---|
ip=dhcp | dracut 早期网络用 DHCP 取地址 | initramfs 无网络,拉不到 rootfs |
rd.neednet=1 | 强制 initramfs 阶段建网 | 同上(root=live:http:// 依赖它) |
net.ifnames=1 | 启用可预测网卡名 | 引导期叫 eth0、安装后叫 enp1s0,配置里的接口名对不上 |
rd.cos.disable | 官方 PXE 安装要求的 dracut 参数 | COS 的 dracut 模块接管,引导流程与官方不一致 |
rd.noverifyssl | 不校验 SSL | HTTPS 源会失败(本例是 HTTP,照抄官方要求) |
console=ttyS0,115200n8 | 内核与 systemd 输出到串口 | 无法录制安装全过程,失败不可复盘 |
root=live:http://…squashfs | live 根文件系统来自 HTTP | 无根文件系统 |
harvester.install.automatic=true | 进入自动安装 | 停在交互式安装向导 |
harvester.install.config_url=… | 指定装机配置 | 自动安装无参数可用 |
harvester.install.iso_url=… | 装机镜像源(get_iso() 下载 8 GB ISO 到目标盘) | 无镜像源,装机失败;★ 该 URL 走 cmdline,不写在 config.yaml 里 |
harvester.install.skipchecks=true | 跳过硬件最小要求检查 | 虚拟机(8C/32G/250G)不满足生产门槛,安装被拒 |
harvester.install.tty=ttyS0,115200n8 | 安装器自身 TTY | 安装器输出跑到 VNC,串口日志缺失 |
install.poweroff: true写在config.yaml里而非 cmdline:安装完成后关机而非重启, 避免再次进入网络安装循环;随后由编排器用disk模式重定义域并设autostart。
3. 虚拟机定义要点(裸金属可跳到 §3.4)
| 配置项 | 取值 | 为什么必须这样 |
|---|---|---|
<memory> | 33554432 KiB(32 GiB) | Harvester 生产门槛;配合 skipchecks 才能过 |
<vcpu> / topology | 8,sockets=1 dies=1 cores=8 threads=1 | 固定拓扑,避免 guest 内 CPU 计数漂移 |
<cpu mode> | host-passthrough + check='none' + migratable='off' | ★ migratable='on' 会过滤掉 vmx → guest 内 KubeVirt 跑不了虚机 |
<machine> | pc-q35-6.2 | 与 libvirt 8.0 兼容的固定机型 |
<loader> + <nvram> | OVMF_CODE_4M.fd(readonly/pflash)+ 每节点独立 nvram/harvester-NN_VARS.fd | v1.8 强制 UEFI;NVRAM 必须独立,否则三节点共享 EFI 变量会互相覆盖启动项 |
<on_poweroff> | destroy | 装机 poweroff 后域进入 shut off,编排器据此判断安装结束 |
<on_reboot> | restart | ★ 自愈 L3 靠 virsh reboot 重跑装机;netboot 参数不变,重启即重新自动安装 |
<disk> cache | writeback | 装机期大量写入(18 GB 镜像预加载),默认 cache 太慢 |
| PCI 地址固定 | 网卡 bus=0x01、os 盘 0x02、data 盘 0x03 | ★ 保证 guest 内命名确定为 enp1s0/vda/vdb,装机配置才能写死设备名 |
<serial> / <console> | type='pty' | type='file' 会被 libvirt 置为 root:root 600,普通用户读不到日志 |
<rng model='virtio'> | backend /dev/urandom | 装机期大量加密运算,缺熵会显著拖慢 |
<graphics type='vnc'> | port='-1' autoport='yes' listen='0.0.0.0' | 兜底人工介入:virsh vncdisplay harvester-NN |
3.1 三态域定义
| 模式 | 用途 | 特征 |
|---|---|---|
probe | 探测引导:确认 guest 内真实网卡名/盘名 | 加 rd.debug、不带 root=(不落盘、不装机) |
install | netboot 自动装机 | 注入 kernel/initrd/cmdline |
disk | 装完后从硬盘正常启动 | 移除引导注入,只留 <boot dev='hd'/> |
bash
# 网卡名不要猜:先跑一次探测引导,从串口日志实测得到设备名再写进装机配置
bash scripts/40-define-vm.sh 01 probe
bash scripts/30-console-capture.sh 01 start && virsh start harvester-01
grep -oE 'enp[0-9a-z]+|vd[ab]' logs/harvester-01-console.log | sort -u # → enp1s0 / vda / vdb
virsh destroy harvester-013.2 libvirt 8.0 兼容性三连(实测)
| 现象 | 根因 | 修复 |
|---|---|---|
virsh define 报 XML 不合法 | libvirt 8.0 不支持 driver discard='unpin' | 从 <disk><driver> 移除该属性 |
cannot undefine domain with nvram | UEFI 域有独立 NVRAM 文件 | virsh undefine harvester-NN --keep-nvram |
domain 'harvester-01' already exists with uuid … | 未先 undefine 就重复 define | 脚本内先 undefine --keep-nvram 再 define |
3.3 串口日志录制(可复盘的前提)
bash
bash scripts/30-console-capture.sh 01 start # virsh console + pty + script 后台录制
tail -f logs/harvester-01-console.log # 实测装机 8 万+ 行完整留存
bash scripts/30-console-capture.sh 01 status两个坑(均已修):① 只杀
virsh console子进程没杀重连循环; ②: > log会覆盖失败现场 → 改为轮转成.prev。
3.4 裸金属怎么做(对照表)
| 本方案的 VM 手段 | 裸金属等价物 |
|---|---|
libvirt <kernel>/<initrd>/<cmdline> | PXE/iPXE 菜单项(kernel/initrd/append 同参数) |
python3 -m http.server 托管介质 | 内网 HTTP/Nginx(或 iPXE 脚本 + TFTP) |
| DHCP 静态保留(MAC→IP→主机名) | 机房 DHCP 保留 + 交换机端口规划 |
virsh destroy/start | IPMI/BMC 电源控制(ipmitool power cycle) |
virsh console 录制 | BMC SOL(Serial-over-LAN)录制 |
自愈 L3 virsh reboot | ipmitool chassis power cycle |
| PCI 地址固定保证设备名 | 用 hwAddr(MAC)匹配网卡,不要写死内核设备名 |
4. 装机配置(config-nodeNN.yaml)逐字段说明
由 50-gen-configs.sh 生成,通过 HTTP 提供给安装器(cmdline 的 config_url)。 下面是实测可用的 config-node01.yaml(create)全文骨架:
yaml
scheme_version: 1 # v1.8.2 为 1;★ 不写会被安装器拒绝
os:
hostname: harvester-01
password: "xxx" # ★ OS/SSH(rancher 用户)密码,【不是】dashboard 密码
ssh_authorized_keys:
- "<宿主机公钥>" # 注入后 rancher 用户免密 SSH,编排器全靠它
ntp_servers:
- ntp.aliyun.com # 内网优先,其次公共池;时钟漂移会影响 etcd/TLS
- ntp1.aliyun.com
modules:
- kvm # ★ guest 内跑 VM 必需;配合域定义 migratable='off'
dns_nameservers:
- 192.168.150.1 # ★ 只写宿主 dnsmasq 一个(详见 4.5 与第 07 章)
install:
device: /dev/vda # 系统盘:ESP / COS_STATE / COS_RECOVERY / COS_PERSISTENT
mode: create # 首节点 create;node02/03 为 join
automatic: true # 无人值守
management_interface:
interfaces:
- name: enp1s0 # ★ 实测名,由 PCI 地址 0x01 固定推导,不要猜
hwAddr: "52:54:00:xx:xx:11" # ★ 必须与域 XML/物理网卡 MAC 完全一致
method: dhcp
vip: 192.168.150.200 # ★ 仅 create 节点
vip_mode: static # ★ 仅 create 节点
poweroff: true # 装完关机,避免再次进入网络安装循环
skipchecks: true # 虚拟机不满足生产硬件门槛时必须
tty: ttyS0,115200n8 # 安装器自身 TTY,串口可观测
# iso_url: http://192.168.150.1:8080/harvester-v1.8.2-amd64.iso ← 已在 cmdline 传入
system_settings:
auto-disk-provision-paths: "" # ★ 关闭自动纳管,避免误把 /dev/vda 交给 Longhorn
overcommit-config: "" # 资源超分(CPU/内存/存储),留空=默认
cluster-dns: "" # 集群 DNS 域
vm-force-reset-policy: "" # VM 强制重置策略
harvester-log-level: info # ★ 排障期可临时调 debug
ssl-certificates: {} # 自定义证书
ssl-parameters: {} # TLS 参数(如最小版本/套件)
additional-ca: "" # 追加 CA(私仓/内网 CA 常用)
ui-source: auto # UI 资源来源:auto / bundled / external
ui-index: "" # 外部 UI 索引
containerd-registry: {} # ★ 私有镜像仓库配置(声明源,见 4.6)
support-bundle-timeout: "" # support bundle 收集超时
csi-driver-config: {} # CSI 驱动参数
default-storage-class: "" # 默认 StorageClass(留空=harvester-longhorn)
harvester-csi-ccm-versions: "" # CSI/CCM 镜像版本
token: <RKE2 加入令牌> # ★ 顶层键;三节点必须一致;【不是】Dashboard API token
server_url: https://192.168.150.200:6443 # ★ 顶层键,仅 join 节点需要4.1 create 与 join 的差异(只有 4 处)
| 字段 | node01(create) | node02/03(join) |
|---|---|---|
install.mode | create | join |
server_url | 无(自己就是 server) | https://<VIP>:6443(★ 端口是 6443,不是 443) |
install.vip / install.vip_mode | <VIP> / static | 不写 |
os.hostname / hwAddr | 各自的值 | 各自的值 |
token | 三节点完全一致 | 三节点完全一致 |
4.2 字段位置的五个易错点
server_url是顶层键,写成install.server_url会被静默忽略 → join 节点找不到集群; 且端口必须是 6443(RKE2 API),写 443 会连到 ingress 而非 API Server。token也是顶层键,不是install.token。hwAddr必须与 MAC 完全一致(大小写/冒号格式),Harvester 靠它挑管理网卡; 写错会导致 bondmgmt-bo/ bridgemgmt-br建在错误接口上。interfaces[].name必须是实测名,可预测网卡名由 PCI 拓扑推导(见 §3 的 PCI 固定)。iso_url走 cmdline,不必写进config.yaml(两处都写不冲突,但 cmdline 优先)。
4.3 ★ 装机配置不能设定 Dashboard 密码(实测证明)
对 rootfs.squashfs 内的 /usr/bin/harvester-installer 做字符串统计:
| 字符串 | 出现次数 | 结论 |
|---|---|---|
scheme_version | 1 | ★ 对照组:证明该二进制确实解析装机配置,统计方法有效 |
admin_password | 0 | 无此配置能力 |
admin_token | 0 | 无此配置能力 |
server_url | 0 | 由其他组件处理,不在 installer 二进制内 |
所以全新集群的 Dashboard 初始状态只能是引导密码 xxx + mustChangePassword=xxx 必须由脚本在装完后补一次首登改密(75-set-admin-password.sh`,幂等)。 完整根因链见 02 快速上手 §4。
💡 方法要点:做「不存在」的证明时必须设对照组。 若只统计
admin_password=xxxscheme_version=1` 恰好排除了这种可能。
4.4 VIP 选址规则
| 规则 | 原因 |
|---|---|
| 必须落在 DHCP 动态池之外 | 池(实测 .100-.199)内的地址会被动态分配,导致 VIP 冲突、kube-vip 抢地址 |
| 必须与管理网同网段 | vip_mode: static 走二层 ARP 通告 |
前置检查用 arping | arping -I virbr10 <VIP> 无应答才算空闲;curl -sk https://<VIP>/ping 返回 pong 则说明已有集群在跑(本次为重跑/续跑,不是冲突) |
4.5 ★ DNS 只写一个上游(否则集群会「间歇性抽风」)
dns_nameservers 里不要并列公网 DNS。根因链(实测,最隐蔽的一个故障):
节点 /etc/resolv.conf 并列 192.168.150.1(宿主 dnsmasq)+ 223.5.5.5(公网)
→ Harvester 的 CoreDNS 是 forward . /etc/resolv.conf,默认 policy=random
→ 每次查询【随机】选一个上游
→ 直查 CoreDNS 20 次:10 次返回内网 IP,10 次返回公网 IP
→ cattle-cluster-agent 连 wss://<域名>/v3/connect/register 时拿到公网文档站首页 HTML
→ websocket: bad handshake,Rancher 侧集群 state=error
→ CoreDNS cache 30 + 上游 TTL 过期后自愈 → 表现为【反复抖动】而非持续失败定位手段(把「随机」变成可统计的分布,而不是多试几次碰运气):
bash
for i in $(seq 1 20); do dig +short <内网域名> @10.53.0.10 | tr '\n' ' '; echo; done \
| sort | uniq -c
# 期望:20 行全部是内网 IP;出现公网 IP 即命中修复三处缺一不可:
| # | 动作 | 命令 |
|---|---|---|
| 1 | 节点 DNS 收敛为只用宿主 dnsmasq | nmcli con mod bridge-mgmt ipv4.dns 192.168.150.1 + nmcli device reapply mgmt-br(★ 用 device reapply 而不是 con up,不断链) |
| 2 | 重启 CoreDNS | kubectl -n kube-system rollout restart deploy/rke2-coredns-rke2-coredns(★ forward 插件只在启动时读 /etc/resolv.conf) |
| 3 | 装机配置同步收敛 | 50-gen-configs.sh 与 config-node0*.yaml 的 dns_nameservers 只留内网 DNS(原文件备份 *.bak-dns),否则重装后又引入随机上游 |
去掉公网 DNS 无副作用:宿主 dnsmasq 自身会转发公网域名(实测
www.baidu.com正常解析)。 完整网络专题见 07 网络实战。
4.6 ★ containerd-registry:改声明源,不要改被渲染的产物
Harvester OS 是不可变系统,/etc/rancher/rke2/registries.yaml 由控制器按 settings.harvesterhci.io/containerd-registry 渲染。实测:手工写入 167 B, 约 1 分钟后被清成 0 字节。
bash
# ❌ 手写文件(会被回收)
# ✅ 改设置(value 是一段 JSON 字符串)
kubectl patch settings.harvesterhci.io containerd-registry --type merge \
--patch-file /tmp/containerd-registry.patch.jsonjson
{"mirrors":{"<reg>":{"endpoints":["http://<reg>"]}},
"configs":{"<reg>":{"tls":{"insecureSkipVerify":true}}}}| 要点 | 说明 |
|---|---|
| 字段名怎么确定的 | 无官方文档时,从 /usr/bin/rancherd 二进制的 struct tag 反查:mirrors.<reg>.endpoints[] + configs.<reg>.tls.insecureSkipVerify;渲染到 rke2 时转成 endpoint / insecure_skip_verify |
| 为什么必须显式声明 http | containerd 默认按 HTTPS 访问 registry;纯 HTTP 私仓会报 http: server gave HTTP response to HTTPS client |
| 生效验证 | 设置生效后 3 节点 registries.yaml 均为 266 B;rke2 随即重写 certs.d,文件头变成 # File generated by rke2. DO NOT EDIT.;crictl pull 成功 |
| 通用铁律 | 在有控制器管文件的系统里,永远改声明源(CRD/设置),不要改被渲染的产物;产物文件头的 DO NOT EDIT 就是明确提示 |
5. 脚本资产清单与编排状态机
scripts/ 下 19 个脚本 + 1 个一键入口(编号 = 阶段序,9x 为自愈/运维层):
| 编号 | 脚本 | 作用 | 幂等 |
|---|---|---|---|
| — | run-all.sh | ★ 一键入口:前置检查 + 按序调用下列脚本,自身不含任何新部署逻辑 | ✅ |
| 00 | 00-download.sh | 拉取 4 个介质到 boot/(9.5 GB,断点续传),建 http/ 符号链接 | ✅ |
| 10 | 10-prepare.sh | 磁盘/NVRAM/HTTP 符号链接/DHCP 静态保留;建目录树,credentials.txt = 600 | ✅ |
| 20 | 20-http-server.sh | 启停查 python3 -m http.server(192.168.150.1:8080,start/stop/status) | ✅ |
| 30 | 30-console-capture.sh | 串口录制(pty + script),日志轮转 .prev;start/stop/status | ✅ |
| 40 | 40-define-vm.sh | 生成并 define 三态域定义 probe/install/disk | ✅ |
| 50 | 50-gen-configs.sh | 渲染 config-nodeNN.yaml(create/join 差异)+ token.txt + credentials.txt | ✅ |
| 60 | 60-deploy.sh | 单节点编排:装机 → 等 poweroff → 切盘 → autostart → 等 Ready | ✅ |
| 61 | 61-autostart-after-iso.sh | 等 ISO 下完自动接力 node01(长下载期间的自动衔接) | ✅ |
| 65 | 65-deploy-all.sh | ★ 总编排:node01 + node02/03 + 改密 + 验证 + 自愈挂载 | ✅ |
| 70 | 70-verify.sh | 9 段验证(见 §10),输出可直接粘贴的结论 | ✅ |
| 75 | 75-set-admin-password.sh | ★ Dashboard 首登改密(幂等短路,见 02 §4.2) | ✅ |
| 80 | 80-smoke-vm.sh | 端到端测试 VM(create/clean);⚠️ 当前受已知问题阻塞 | ⚠️ |
| 90 | 90-progress.sh | 30 s 粒度进度记录到 logs/progress.log | ✅ |
| 95 | 95-rescue-image-preload.sh | 自愈 L2 宿主侧:探测并推送救援体 | ✅ |
| 95 | 95-rescue-image-preload-guest.sh | 自愈 L2 救援体(guest 内执行) | ✅ |
| 96 | 96-install-retry.sh | 自愈 L3:安装器卡死自动 virsh reboot 重试 | ✅(限流) |
| 97 | 97-installer-netfix.sh | 自愈 L1:抢注入 nm-online 兜底包装 | ✅ |
| 98 | 98-nm-online-wrapper.sh | 自愈 L1:被注入的包装体 | ✅ |
| 99 | 99-rancher-internal-net.sh | 可选:接入内网 Rancher(A~F 六步,见 09) | ✅ |
一键入口与参数:
bashbash scripts/run-all.sh --preflight # 只做前置检查后退出(不改状态) setsid bash scripts/run-all.sh </dev/null >/tmp/run-all.log 2>&1 & bash scripts/run-all.sh --skip-download # 跳过 9.5 GB 介质下载 bash scripts/run-all.sh --from configs # 中断后从指定阶段续跑 # 退出码:0 成功 | 1 前置检查/参数错误 | 2 某阶段失败
setsid+</dev/null是必需的:编排耗时 42 分钟,SSH 断线会把它带走; 且60-deploy.sh与65-deploy-all.sh各只能有一个实例(pgrep -af '60-deplo[y]|65-deplo[y]'核对)。
5.1 编排阶段与前置检查
阶段顺序(固定):
preflight → download → prepare → http → configs → node01 → all → verify
│ └─ 65-deploy-all.sh:node02/03 + 改密 + 验证 + 自愈
└─ 60-deploy.sh:create 节点单独跑每步写 logs/progress.log(30 s 粒度)与 logs/deploy.status(当前阶段),便于复盘「卡在哪一段」。
--preflight 的 8 项检查(全部来自实测踩坑,只读不改状态):
| # | 检查 | 不满足的后果 |
|---|---|---|
| 1 | os 盘实体在 NVMe(OS_DISK_DIR 与 /mnt/xfs 不同设备) | 镜像预加载 > 30 min → 装机永久卡死(§7) |
| 2 | VIP 在 DHCP 池之外 | 地址冲突,Dashboard 时通时断 |
| 3 | /mnt/xfs 剩余空间 > 2 TB | data 盘 qcow2 写满,Longhorn 卷损坏 |
| 4 | NVMe 剩余空间 > 100 GiB | os 盘 qcow2 增长失败 |
| 5 | virbr10 存在且 active | VM 无网络 |
| 6 | HTTP 端口未被占用 | 引导服务起不来 |
| 7 | OVMF_CODE_4M.fd 存在 | UEFI 无法引导(v1.8 强制 UEFI) |
| 8 | nc / sshpass 已安装 | 编排器探活与自动登录失败 |
5.2 ★ ensure_booted_from_disk:装机后切盘开机的分支判定
装机 poweroff 后,域可能停在多种状态。65-deploy-all.sh 每约 30 s 调一次 ensure_booted_from_disk,判定失败就 virsh reboot。分支必须覆盖全:
| 域状态 | 域定义 | 判定 | 动作 |
|---|---|---|---|
shut off | 仍含 <kernel>(install 模式) | 装机刚完成 | 重定义为 disk → autostart → virsh start |
shut off | 已是 disk 模式 | 曾被打断 | 仍要 virsh start(★ 见下方缺陷 B) |
running | 仍含 <kernel> | 还在网络装机 | 不干预,继续等;超时才 destroy 重来 |
running | 无 <kernel> | 已从硬盘运行 | return 0,进入 wait_node_ready |
实测修掉的两个缺陷(都让编排器「以为成功了」):
| 缺陷 | 旧代码 | 事故 | 修复 |
|---|---|---|---|
A · dumpxml 偶发返回空 | if ! virsh dumpxml … | grep -q '<kernel>' | 空输出也命中「无 kernel」→ 误报 running from disk,跳过全部恢复动作,后续节点再没被推进 | 先把 XML 存变量并判空,空则记日志继续等,绝不据此下结论 |
B · shut off + 已是 disk 定义时不开机 | 只打印 already defined for disk boot 就 return 0 | 节点永远不会开机,而调用方以为成功 | 补上 virsh start,并保留失败重试 || { log …; sleep 10; continue; } |
💡 通用教训:任何「返回成功」的分支都必须对应一个真实发生的动作。 「什么都不做」不能算成功。
5.3 ★ 节点就绪判定:既不能用子串,也不能过度精确
bash
# ❌ 子串匹配:NotReady 里也含 "Ready"
kctl get nodes --no-headers | grep "harvester-${n}.*Ready"
# ❌ 过度精确:漏掉合法就绪态 Ready,SchedulingDisabled(角色自动提升期)
awk '$2=="Ready"'
# ✅ 精确节点名 + 锚定前缀
kctl get nodes --no-headers 2>/dev/null \
| awk -v n="harvester-${n}" '$1==n && $2 ~ /^Ready(,|$)/ {f=1} END{exit !f}'| 节点状态 | 应判定 | grep Ready | $2=="Ready" | ^Ready(,|$) |
|---|---|---|---|---|
Ready | ✅ 就绪 | ✅ | ✅ | ✅ |
Ready,SchedulingDisabled | ✅ 就绪(提升期) | ✅ | ❌ 漏判 | ✅ |
NotReady | ❌ 未就绪 | ❌ 误判 | ✅ | ✅ |
NotReady,SchedulingDisabled | ❌ 未就绪 | ❌ 误判 | ✅ | ✅ |
Ready,SchedulingDisabled+ 角色<none>是 Harvester 自动提升节点为control-plane,etcd期间的正常中间态 (harvester-promote-node-controller行为),不要手工uncordon,会干扰控制器。 也因此装机配置里install.role留空即可——3 节点场景全靠自动提升拿到正确角色。 曾因这个误判,70-verify.sh在 node03 还没入集群时就跑完了。
6. 完整实施流程(阶段 0 → 6,含验收点)
阶段 0 · 环境准备(一次性,约 10 分钟)
bash
# 宿主机(Ubuntu 22.04 / libvirt 8.0)
sudo apt install -y qemu-kvm libvirt-daemon-system virtinst dnsmasq \
python3 curl jq ncurses-bin sshpass netcat-openbsd isoinfo
sudo usermod -aG libvirt,kvm $USER && newgrp libvirt
# libvirt 网络:独立的 virbr10 + NAT + DHCP 池(.100-.199)
sudo virsh net-define configs/harvester-net.xml && sudo virsh net-start virbr10
sudo virsh net-autostart virbr10
virsh net-dhcp-leases virbr10 # 验收:能列出三个静态保留
# UEFI 固件
ls /usr/share/OVMF/OVMF_CODE_4M.fd # 验收:存在验收点:virsh net-info virbr10 为 active: yes / autostart: yes;OVMF 固件存在。
阶段 1 · 介质与磁盘(44 分钟下载 / 建盘秒级)
bash
bash scripts/00-download.sh # 9.5 GB,断点续传
NVME_DISK_DIR=/home/$USER/harvester-disks bash scripts/10-prepare.sh
bash scripts/20-http-server.sh && bash scripts/20-http-server.sh status
# 验收
ls -l boot/ # 4 个文件,字节数见 02 章 §2.1
readlink -f disks/harvester-01-os.qcow2 # ★ 必须指向 NVMe 目录
curl -sI http://192.168.150.1:8080/configs/config-node01.yaml | head -1 # 200阶段 2 · 装机配置与探测引导(5 分钟)
bash
bash scripts/50-gen-configs.sh # config-nodeNN.yaml + token.txt + credentials.txt
grep -h 'mode:' http/configs/*.yaml # 验收:create ×1,join ×2
diff <(grep -v 'hostname\|hwAddr\|mode\|server_url\|vip' http/configs/config-node01.yaml) \
<(grep -v 'hostname\|hwAddr\|mode\|server_url\|vip' http/configs/config-node02.yaml) # 应只差这几行
# 探测引导:实测设备名,不要猜
bash scripts/40-define-vm.sh 01 probe
bash scripts/30-console-capture.sh 01 start && virsh start harvester-01
grep -oE 'enp[0-9a-z]+|vd[ab]' logs/harvester-01-console.log | sort -u # → enp1s0 / vda / vdb
virsh destroy harvester-01阶段 3 · 部署 node01(create,约 14 分钟)
bash
setsid bash scripts/60-deploy.sh node01 </dev/null >/dev/null 2>&1 &
tail -f logs/harvester-01-console.log关键里程碑(串口日志里能逐条对上):
| 里程碑 | 关键字 | 说明 |
|---|---|---|
| dracut 取 rootfs | root=live:http://… | initramfs 阶段建网成功 |
| 安装器读配置 | config_url | 拿到 config-node01.yaml |
| elemental 落盘 | elemental install | 写 COS_STATE / COS_RECOVERY |
| 数据盘格式化 | do_data_disk_format | /dev/vdb → Longhorn |
| 取 ISO | get_iso | 8 GB 下载到目标盘(★ 磁盘性能敏感,见 §7) |
| 镜像预加载 | ctr-check-images.sh | 180+ 个镜像导入(★ 30 分钟死线) |
| GRUB 收尾 | update_grub_settings | 写 /oem/grubenv、grubcustom、third_party_kernel_args |
| 保存日志 | save_installation_log | 装机日志落盘 |
| 关机 | powered off after install (~1006s) | install.poweroff=true 生效 |
⚠️ 若在
do_preload处失败,GRUB 补丁与安装日志都不会做, 即使/oem里有配置也只能重装(缺update_grub_settings会在启动时卡约 30 分钟找 grub 文件)。
阶段 4 · 部署 node02 / node03(join,串行,约 14 分钟/节点)
bash
setsid bash scripts/65-deploy-all.sh </dev/null >/dev/null 2>&1 &
pgrep -af '60-deplo[y]|65-deplo[y]' # 验收:各只有一个实例
tail -f logs/deploy-all.log必须串行:并行 join 会同时抢 etcd 成员加入,导致 quorum 抖动。
阶段 5 · 集群收敛与首登改密(约 5 分钟)
bash
K='sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml'
H='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null rancher@192.168.150.11'
$H "$K get nodes -o wide" # 验收:3 行 Ready control-plane,etcd
$H "$K -n kube-system get pods | grep '^etcd-'" # 验收:3 个 etcd-harvester-0N Running
bash scripts/75-set-admin-password.sh; echo rc=$? # 验收:rc=0阶段 6 · 验证与收尾
bash
bash scripts/70-verify.sh # 9 段体检,见 §10
bash scripts/80-smoke-vm.sh create # 端到端 VM(⚠️ 见 §11.3 已知问题)7. ★ 磁盘布局:机械盘会让装机永久卡死(本方案最大的坑)
7.1 为什么 OS 盘必须放 NVMe
harv-install 的 get_iso() 用 mktemp -p ${TARGET}/usr/local 把 8 GB ISO 下载到目标盘, 随后又从同一块盘读回 tar 包写入 containerd 内容库——读写同轴争抢。
而 chroot 内那个「引导用 rke2 server」(只为借它内嵌的 containerd 导入镜像)有一条 约 30 分钟的自杀死线:
chroot 内没有 systemd
→ kubelet: "Failed to create cgroup: systemd not running on this host" 每 5s 失败
→ static pods 永远同步不了
→ 约 30 分钟后 rke2 自杀:
level=fatal "Failed waiting for static pods to sync: context deadline exceeded"
→ containerd 随之消失
→ /usr/sbin/ctr-check-images.sh 是 while true 无上限重试 → 永久卡死实测时间线(OS 盘在 5400 rpm 机械盘上):
| 时刻 | 事件 |
|---|---|
| 12:57 | 引导用 rke2 启动(30 分钟死线开始计时) |
| 13:25 | 最后一个列表 rke2-images.linux-amd64-v1.35.7-rke2r1.txt 才开始核对(前 4 个列表共 180 个镜像已导入) |
| 13:27:59 | rke2 自杀 → containerd 消失 → 永久卡死 |
预加载耗时约 34 分钟,刚好越过死线 → node01 装机 TIMEOUT 40 分钟。
⚠️ 易误判点:串口每 2 秒刷两行
ctr: failed to list images: … containerd.sock: connect: connection refused+level=warning msg="Failed to check deprecations"。 后者只是症状,真因是 containerd/socket 没了;只盯这行 warning 会一路查错方向。
7.2 修复:实体迁 NVMe + 符号链接兼容既有路径
/home/<user>/harvester-disks/harvester-NN-os.qcow2 ← 实体(NVMe,宿主根文件系统)
/mnt/xfs/harvester/disks/harvester-NN-os.qcow2 ← 符号链接(★ 兼容所有既有脚本路径)
/mnt/xfs/harvester/disks/harvester-NN-data.qcow2 ← 实体仍留机械盘| 设计选择 | 理由 |
|---|---|
| OS 盘实体在 NVMe | 消除读写同轴争抢 |
| 用符号链接而非改脚本路径 | 所有既有脚本/XML/日志路径无需改动,回滚容易 |
| data 盘故意留机械盘 | 400 GiB × 3 节点 × Longhorn 三副本,放 NVMe 会撑爆宿主根分区 |
| SELinux 未启用 | 无需 semanage/chcon;若启用则须处理镜像标签 |
效果:ISO 下载 ~95 MB/s(原 55–70 MB/s),预加载从 ~34 分钟降到数分钟; 修复后 node02 实测 powered off after install (~1006s),node01/03 同级。
7.3 兜底:镜像预加载救援(自愈 L2)
95-rescue-image-preload.sh 已挂进 60-deploy.sh: wait_poweroff 与 65-deploy-all.sh: ensure_booted_from_disk,每 3 轮(约 30 s)探测一次,命中即推送救援体, 并写 logs/rescue-NN.done 防重复。救援体(guest 内)做四件事:
- 用与
harv-install完全相同的参数重启 containerd (-c …/config.toml -a /run/k3s/containerd/containerd.sock --state /run/k3s/containerd --root /var/lib/rancher/rke2/agent/containerd) → 已导入的 18 GB 内容库立即可见(参数不一致会看到空库,前功尽弃); - 逐列表比对,缺哪个就
zstd -d+ctr -n k8s.io images import --no-unpack补哪个; - 起一个名为
rke2的诱饵进程(cp /bin/sleep /tmp/rke2后运行),让收尾的pkill rke2返回 0 ——★ 该 heredoc 在set -e区块内,返回 1 会直接中止安装, 且do_preload之后的update_grub_settings/save_installation_log都不会执行,只能重装; - 核对一结束就停 containerd(连续 0.6 s 看不到核对进程),否则后续
umount proc/umount ${TARGET}/${RKE2_IMAGES_DIR}会因 EBUSY 在set -e下失败。
bash
bash scripts/95-rescue-image-preload.sh 01 run # 推送并执行救援
bash scripts/95-rescue-image-preload.sh 01 log # 看 guest 内 /tmp/rescue-image-preload.log💡 通用教训:在
set -e的脚本里,任何「清理类」命令(pkill/umount/rm)都必须显式容错。 上游把pkill rke2写在set -e区块里,是只在「rke2 已死」这条罕见路径上才暴露的缺陷。
8. 耗时基线与判卡阈值
健康一轮实测(08:03:50 → 08:45:08):
| 阶段 | 实测耗时 | 日志依据 |
|---|---|---|
| node01(已装完)从硬盘运行 → ssh up | ~10 s | harvester-01: ssh up (~10s) |
| VIP 响应 | ~10 s | VIP https://<VIP> responding (~10s) |
| node01 Ready | ~10 s | harvester-01: Ready (~10s) |
| node02 netboot 装机 → poweroff | ~1006 s(16.8 分钟) | powered off after install (~1006s) |
| node02 硬盘启动 → ssh up | ~50 s | harvester-02 (…12): ssh is up (~50s) |
| node02 → Ready | ~50 s | harvester-02 is Ready (~50s) |
| node03 装机 → Ready 全程 | ~22 分钟 | 08:22:33 → 08:44:57 |
| 三节点总编排(node01 已就绪起算) | 42 分钟 | === ALL DONE === 08:45:08 |
| 介质下载 9.5 GB | ~44 分钟 | 20:03 → 20:47 |
判卡阈值(可直接抄进监控):
| 现象 | 阈值 | 动作 |
|---|---|---|
单节点装机未 powered off | > 25 分钟 | 介入看串口日志 |
| 同上 | > 30 分钟 | 基本可断定踩中预加载死线(§7)→ 跑 L2 救援 |
| 串口日志静默 | ≥ 60 s 且干活进程全不在 | L3 自动 virsh reboot |
节点 NotReady | > 10 分钟 | 查 kubelet / containerd / CNI |
失败样本对照(均为修复前的真实记录):
| 时间 | 事件 | 耗时 | 根因 |
|---|---|---|---|
| 09-05 20:49 → 21:29 | node01 装机 TIMEOUT | 40 分钟 | OS 盘在机械盘 → 预加载 ~34 分钟越线(§7) |
| 09-05 23:00 → 09-06 00:31 | node02 装机 TIMEOUT | 90 分钟 | nm-online 亚秒竞态致安装器报错卡死(§9 L1);期间还被误触发一次救援 |
| 09-06 08:03 → 08:45 | 三节点全部成功 | 42 分钟 | OS 盘迁 NVMe + L1 注入 + L3 自动重试后 |
9. ★ 三层自愈机制(无人值守的关键)
无人值守不是「写个 while 循环等」,而是给每一类已知故障配一个判据明确、防误伤的自动修复。 本方案落地了三层,全部由编排器每轮调用:
| 层 | 脚本 | 触发判据(全部满足) | 动作 | 防误伤设计 |
|---|---|---|---|---|
| L1 网络竞态 | 97-installer-netfix.sh + 98-nm-online-wrapper.sh | SSH 一可用就开始(开机 +47~55 s),从 virsh start 起每 0.5 s 轮询 | 把兜底 nm-online 包装注入 live 环境的 /usr/local/bin/ 与 /usr/sbin/ | 三次 SSH 往返压成一次(实测 1.4 s 完成注入);没赶上由 L3 兜底 |
| L2 预加载卡死 | 95-rescue-image-preload.sh + -guest.sh | /run/cos/target 已挂载 + images-lists 存在 + ctr-check-images.sh 已跑 >300 s + containerd 不在 + 真正的引导用 rke2 不在 | ①用与 harv-install 完全相同的参数重启 containerd ②逐列表比对,缺哪个就 zstd -d + ctr images import --no-unpack ③起名为 rke2 的诱饵进程让收尾的 pkill rke2 返回 0 ④核对结束就停 containerd | 模式串一律用 [x] 括号写法避免 pgrep -f 自匹配;mkdir 原子锁防两个看门狗同秒重复救援;guest 侧前置校验不满足就什么都不做直接退出 |
| L3 安装器卡死 | 96-install-retry.sh | ①level=error 出现在最后 5 行非空行内 ②harv-install/elemental/ctr-check-images/rsync/mkfs/qemu-img/parted 等干活进程全都不在 ③日志静默 ≥60 s | virsh reboot(域定义 <on_reboot>restart</on_reboot> + netboot 参数不变 → 自动安装重跑) | 每节点 ≤3 次、两次间隔 ≥120 s;重启后重新调 L1 注入(live 环境是内存 overlay,重启即失效) |
9.1 L1 为什么必须「抢时间」:一条亚秒级竞态
node02 曾卡死 8 小时,真因是 nm-online 的亚秒级竞态。guest 内 /var/log/console.log 时间戳:
15:02:04.938 manager: disable requested -> NetworkManager state is now ASLEEP
15:02:05 level=error msg="exit status 1\rConnecting............... 30s [offline]\n"
15:02:05.31 dhcp4 (mgmt-br): activation: beginning transaction
15:02:05.44 device (mgmt-br): Activation: successful, device activated
15:02:06.90 manager: NetworkManager state is now CONNECTED_GLOBAL网络其实 0.5 秒后就完全就绪,安装器却已报错并永久停在错误界面 (harvester-installer / start-installer.sh 两个进程会一直挂着,磁盘一个字节都没写)。
💡 找竞态只能靠时间戳对齐到毫秒。两个曾经把排查带偏的地方: ① 看到
Connecting... 30s [offline]就以为网络真的不通(其实 0.5 s 后就通了); ② 看到进程还在就以为安装器还活着(其实是僵尸等待)。
9.2 ★ L3 的判据绝不能用「日志里存在 level=error」
健康安装也有无害错误。实测 node02 就有:
unable to determine NIC speed from /sys/class/net/enp1s0/speed (got -1)所以判据必须是三条件同时满足(错误在末尾 + 干活进程全无 + 静默超时)。 该判据已做双向验证:对健康安装中的节点返回 1(不触发);对真失败的归档日志能命中。
💡 判据必须双向验证:既要证明「该触发时触发」,也要证明「不该触发时不触发」。 只验证前者,等于给自己埋一颗误杀健康节点的雷。
9.3 pgrep -f 经 SSH 执行会自匹配(误救援事故)
bash
pgrep -f 'ctr-check-images.sh' # ❌ 这条命令自己的命令行里含该串 → 恒命中
pgrep -f '[c]tr-check-images.sh' # ✅ 括号写法,正则匹配但命令行字面量不含原串实测曾因此误触发一次救援,把健康安装搞坏。所有自愈脚本的模式串统一用 [x] 括号写法。
9.4 编排器的静默 || exit 1
bash
ensure_booted_from_disk "$n" || exit 1 # ❌ 一超时整个脚本无声退出,日志里看不到原因
if ! ensure_booted_from_disk "$n"; then # ✅ 显式分支 + 记日志 + 继续/告警
log "harvester-$n: boot-from-disk failed"; …
fi💡 远程执行不要吞 stderr;
set -e环境下的清理命令(pkill/umount/rm)必须显式容错。
10. 部署验证
10.1 70-verify.sh 的 9 个检查段
| # | 检查段 | 判据 |
|---|---|---|
| 1 | 虚拟机状态(libvirt) | 3 个域 running,且 Autostart: enable |
| 2 | VIP 连通性 / Dashboard | arp 有条目;/ping → 200(响应体 pong);/dashboard/ → 200;能取到 <title> |
| 3 | 节点 | kubectl get nodes -o wide 三行且 STATUS 匹配 ^Ready(,|$) |
| 4 | Harvester 版本 / 节点角色 | harvesterhci.io/node-role 标签、osImage、kubeletVersion |
| 5 | 系统 Pod 异常统计 | 非 Running/Completed/Succeeded 的行应为空 |
| 6 | Longhorn 存储 | nodes.longhorn.io 的 Ready 与 disks;get sc 含默认 harvester-longhorn |
| 7 | Harvester 管理网络 | vlanconfigs、networks.harvesterhci.io |
| 8 | 各节点登录验证 | 三个 IP 均 OK <hostname> <kernel>(密钥认证) |
| 9 | 凭据 | 打印 configs/credentials.txt |
10.2 手工深度核对(实测通过的命令)
bash
K='sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml'
H='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null rancher@192.168.150.11'
# ① 三节点角色与 etcd quorum(HA 的硬指标)
$H "$K get nodes -o wide"
$H "$K -n kube-system get pods | grep '^etcd-'" # 期望 3 个 etcd-harvester-0N Running
# ② kube-vip 是否三节点都有
$H "$K -n kube-system get pods -o wide | grep kube-vip"
# ③ Longhorn 默认盘是否可调度(虚机存储的地基)
$H "$K -n longhorn-system get nodes.longhorn.io -o json | \
jq -r '.items[] | .metadata.name as \$n | .spec.disks | to_entries[] |
\"\(\$n) \(.key) schedulable=\(.value.schedulable)\"'"
# ④ Dashboard 凭据是否【真的】可用(不能只看 HTTP 200)
curl -sk -o /dev/null -w '%{http_code}\n' https://192.168.150.200/ping # 200 + pong
bash scripts/75-set-admin-password.sh # 幂等,短路即 OK实测健康基线:
harvester-01 Ready control-plane,etcd
harvester-02 Ready control-plane,etcd
harvester-03 Ready control-plane,etcd
etcd 成员: 3 异常 Pod: 0⚠️ HTTP 200 ≠ 能登录。验收必须包含一次真实的登录调用(
75号脚本短路即代表凭据可用)。 详见 02 快速上手 §4。
10.3 验收清单(可直接抄)
- [ ]
virsh list --all→ 3 个running;virsh dominfo harvester-0N | grep Autostart→enable - [ ]
curl -sk https://<VIP>/ping→200且响应体pong - [ ]
kubectl get nodes→ 3 行Ready control-plane,etcd - [ ]
kubectl -n kube-system get pods | grep '^etcd-'→ 3 个 Running(etcd quorum) - [ ]
kubectl -n kube-system get pods -o wide | grep kube-vip→ 3 节点各 1 个 - [ ]
kubectl get sc→ 含默认harvester-longhorn - [ ]
kubectl -A get pods | grep -vE 'Running|Completed|Succeeded'→ 空 - [ ]
bash scripts/75-set-admin-password.sh; echo rc=$?→rc=0(Dashboard 凭据可用) - [ ] 三节点
ssh rancher@…密钥免密可登录
11. 参数定制与已知问题
11.1 可参数化项
| 环境变量 | 默认 | 说明 |
|---|---|---|
BASE | /mnt/xfs/harvester | 项目根目录 |
VIP | 192.168.150.200 | 管理 VIP(★ 必须在 DHCP 池之外) |
HTTP_IP / HTTP_PORT | 192.168.150.1 / 8080 | 引导服务监听 |
OS_DISK_DIR | $HOME/harvester-disks | ★ os 盘实体目录,必须在 NVMe |
脚本内还有 5 处硬编码点(BASE / 网段 / VIP / HTTP / 节点名),迁移时批量替换后必须 bash -n 语法检查:
bash
find scripts/ -name '*.sh' -exec sed -i \
-e 's#/mnt/xfs/harvester#/data/harvester#g' \
-e 's/192\.168\.150\./10.0.99./g' {} +
grep -rn '192\.168\.150\|/mnt/xfs/harvester' scripts/ | head # 验收:应为空
for f in scripts/*.sh; do bash -n "$f" || echo "语法错误: $f"; done # ★ 改完必须语法检查11.2 换网段的连带影响
| 改了网段 | 还必须改 |
|---|---|
节点 IP .11/.12/.13 | dnsmasq 静态保留(virsh net-update … dhcp-host) |
VIP | config-node01.yaml 的 install.vip;node02/03 的 server_url |
HTTP_IP | cmdline 的 config_url / iso_url;20-http-server.sh 的监听地址 |
| DHCP 池 | VIP 必须仍在池外(池实测 .100-.199),否则地址冲突 |
11.3 ⚠️ 已知问题(部署前请知悉)
| 问题 | 影响 | 状态 |
|---|---|---|
80-smoke-vm.sh 创建 VM 被 KubeVirt 准入拒绝:doc is missing path: /spec/template/spec/domain/cpu/maxSockets | 端到端 VM 验证未完成;集群本身健康不受影响 | ❌ 未解决(已定位到 VM spec 缺 domain.cpu 段) |
残留 PVC test-vm-disk0(default,20 Gi,Bound,空卷) | 占一份 Longhorn 卷,无功能影响 | ⚠️ 待清理 |
logs/deploy-all.log 末尾是旧版凭据块 | 看日志尾部会拿到过期凭据说明 | ⚠️ 以 configs/credentials.txt 为准 |
| smoke VM 镜像走公网 URL | 生产不可依赖 | ⚠️ 建议改为内网镜像源 |
11.4 重置与重跑
| 场景 | 做法 |
|---|---|
| 某节点装机失败要重来 | virsh destroy harvester-0N → 重建盘 → bash scripts/60-deploy.sh node0N(域定义仍是 install 模式) |
| 想复用已有集群只补节点 | 确认 curl -sk https://<VIP>/ping → pong,再单独跑该节点的 60-deploy.sh |
| 全量重装 | 清 NVRAM(nvram/*_VARS.fd)+ 清盘 + run-all.sh --from prepare;凭据文件会重新生成 |
| 只想改 Dashboard 密码 | xxx(幂等,已改过则短路) |
⚠️ 重置后节点 SSH host key 会变:脚本统一加
-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no,否则报WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!。
12. 排障分层与方法论
12.1 分层可观测点(排障前先定位在哪一层)
| 层 | 可观测点 | 本章对应的坑 |
|---|---|---|
| 宿主(libvirt/磁盘/网桥) | virsh list/domstate/dumpxml、df、ls -l disks/、iptables -L FORWARD | §7(机械盘同轴争抢)、§5.2(dumpxml 返回空) |
| 引导(HTTP/dracut) | logs/harvester-NN-console.log、HTTP 状态码 | §3(探测引导定网卡名)、§2(rd.neednet) |
安装器(harv-install) | guest 内 /var/log/console.log、/rke2.log、进程表 | §7(预加载死线)、§9.1(nm-online 竞态) |
| 集群(RKE2/K8s) | kubectl get nodes/pods、etcd 成员数 | §5.3(NotReady 子串)、§10(quorum) |
| 应用(Harvester/KubeVirt) | API 响应码、webhook 报错 | §11.3(maxSockets 准入拒绝) |
| DNS / 镜像分发 | dig @<coredns> 跑 N 次看分布、crictl pull、registries.yaml 字节数与文件头 | §4.5(随机上游)、§4.6(声明源) |
12.2 十条可直接复用的方法论
| # | 原则 | 一句话理由 |
|---|---|---|
| 1 | 先分层,每层都要有可观测点 | 不分层就只能靠猜,猜会一路带偏 |
| 2 | 判据必须双向验证 | 既要「该触发时触发」,也要「不该触发时不触发」 |
| 3 | 状态匹配永远不要用子串 | NotReady 里含 Ready;用锚定正则或精确字段比较 |
| 4 | 找竞态只能靠时间戳对齐到毫秒 | 只看「卡住了」永远找不到竞态 |
| 5 | 远程执行不要吞 stderr | 2>/dev/null 曾让 command not found 隐形,白等 40 分钟 |
| 6 | 自愈必须限流 + 幂等 + 加锁 | 没有限流的自愈比不自愈更危险 |
| 7 | 先预演,再正式 | ISO 没下完就先跑一次真实引导,一次排除网络/引导/配置三类问题 |
| 8 | 修复要落在「稳定的层」 | 放在被依赖最少的独立文件里,目录回滚也毁不掉 |
| 9 | set -e 区块里的清理命令是雷区 | pkill/umount/rm 无匹配即中止全流程 |
| 10 | 改声明源,不改被渲染的产物 | 产物文件头 DO NOT EDIT 就是提示;「先成功后失效」最迷惑人 |
补充两条凭据类原则(详见 02 快速上手 §4): 同类不同用途的凭据不要同值(同值会让「登录失败」这个信号失去信息量); 用「三态判定」切开客户端与服务端问题(把所有候选密码逐一
xxx打登录接口,看 HTTP 码分布)。
13. 快速参考卡
bash
# ── 部署 ────────────────────────────────────────────────
bash scripts/run-all.sh --preflight # 前置检查(安全,不改状态)
setsid bash scripts/run-all.sh </dev/null >/tmp/run-all.log 2>&1 & # 一键部署(~42 分钟)
bash scripts/run-all.sh --skip-download # 介质已下载
bash scripts/run-all.sh --from configs # 断点续跑
# ── 观察 ────────────────────────────────────────────────
tail -f logs/deploy-all.log # 总编排
tail -f logs/harvester-02-console.log # 装机串口
cat logs/deploy.status # 当前阶段
tail -f logs/progress.log # 30s 粒度进度
# ── 验收 ────────────────────────────────────────────────
bash scripts/70-verify.sh # 集群 9 项体检
bash scripts/75-set-admin-password.sh; echo rc=$? # rc=0 → dashboard 凭据可用
# ── 访问 ────────────────────────────────────────────────
Dashboard : https://<VIP>/ admin / <dashboard 密码>
SSH : rancher@<节点IP> (密钥免密;OS 密码 ★ ≠ dashboard 密码)
kubectl : sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml
# ── 自愈(自动,无需干预;手工触发时用这些)────────────────
bash scripts/97-installer-netfix.sh 01 # L1 nm-online 注入
bash scripts/95-rescue-image-preload.sh 01 run # L2 预加载卡死救援
bash scripts/96-install-retry.sh 01 # L3 安装器卡死重试判卡阈值速记:装机 >25 min 介入,>30 min 基本是预加载死线;日志静默 ≥60 s 且进程全无 → L3。
14. 小结与下一步
| 本章要点 | 一句话 |
|---|---|
| 引导方式 | libvirt 直接内核引导(<kernel>+<initrd>+<cmdline>),不挂 ISO、不走交互向导 |
| 设备名 | 固定 PCI 地址换稳定网卡名;migratable='off' 让嵌套虚拟化可用 |
| 配置 | server_url/token 是顶层键;vip 仅 create;iso_url 走 cmdline |
| 磁盘 | OS 盘实体必须在 NVMe,否则 30 分钟预加载死线必然踩中 |
| 编排 | 阶段 preflight→download→prepare→http→configs→node01→all→verify;setsid 脱离会话 |
| 自愈 | L1 网络竞态 / L2 预加载救援 / L3 安装器重试,全部限流 + 幂等 + 加锁 |
| 凭据 | 装机配置无法设 Dashboard 密码 → 装完必须跑一次首登改密 |
接下来:
- 已有 K8s 集群、想把虚拟化「装进去」而不是「换底座」→ 04 部署实战 B:容器化 KubeVirt
- 装完要接入 Rancher / 内外网分离解析 → 09 接入 Rancher 与生态集成
- 日常巡检、重置、网络打通 → 07 网络实战、08 高可用与迁移、14 运维 SOP
- 全部踩坑原始记录 →
worker/harvester-iso/02-故障排查与修复记录.md(28 个案例)