主题
08 · 全链路验证与故障排查
适用版本:Harvester v1.8 | 基线:12 节点,管理网
10.181.0.0/24(VIP10.181.0.100),VM 直通网10.181.91.0/24(VLAN 91),cn-vm+default/net-91本章是全书的"验收与灭火"手册:先给分层排查法,再给每层可复制的命令与判定标准,最后是一张症状对照表和真实案例复盘。
8.1 分层排查法:从物理口一路查到业务
网络问题最忌"到处乱试"。按下面 8 层自下而上查,每层只回答一个是/否问题,定位时间通常能压到 15 分钟内。
text
L0 物理链路 网线/光模块插好?端口 up?交换机端口 VLAN 配置对?
↓
L1 链路聚合 bond 成员都 up?LACP 协商成功(有 partner MAC)?模式两端一致?
↓
L2 桥接/VLAN bridge 存在且 up?VLAN 91 在 bridge 上放行?
↓
L3 平台对象 ClusterNetwork Ready?每个节点 VlanStatus Ready?NAD 存在且配置正确?
↓
L4 VM 网卡 VM 有两块网卡?第二块挂的是 net-91?tap 设备已建?
↓
L5 IP 获取 VM 拿到了 10.181.91.x?(静态 / 外部 DHCP / Managed DHCP)
↓
L6 路由/网关 能 ping 通网关 10.181.91.1?默认路由走对网卡(metric)?
↓
L7 外部访问 外部机能 ping/SSH 到 VM IP?LB Service 有 EXTERNAL-IP?DNS 解析对?
↓
L8 业务 服务端口通、证书对、后端健康判定规则:某一层失败,就只修这一层,修完从这一层重新往上验证。跨层猜测(比如 L1 没通就去改 VM 里的 netplan)是最常见的时间黑洞。
两个方向的验证要分开做:
| 方向 | 含义 | 失败常见层 |
|---|---|---|
| 内部验证 | 宿主机 → VM、VM → VM(同 VLAN 跨节点) | L0–L6 |
| 外部验证 | 办公网/其他网段 → VM IP、→ VIP、→ LB IP | L6–L7(含交换机路由/防火墙) |
8.2 逐层验证命令
约定:宿主机命令用
ssh rancher@10.181.0.<11-22>登录后sudo -i;VM指被测虚拟机(示例web-front-01,IP10.181.91.101)。
8.2.1 L0 物理链路
bash
# 宿主机侧:网卡是否存在、是否 up、有无载波
ip -br link show eno3; ip -br link show eno4
ethtool eno3 | egrep 'Link detected|Speed|Duplex'
ethtool eno4 | egrep 'Link detected|Speed|Duplex'
# 期望:State UP,Link detected: yes,Speed 与规划一致(如 10000Mb/s)
# 驱动与硬件层错误(丢包/CRC 会在这里暴露)
ethtool -S eno3 | egrep -i 'err|drop|crc' | grep -v ': 0$' || echo "无错误计数"
dmesg -T | egrep -i 'eno3|eno4|link (up|down)' | tail -20判定标准:
| 检查 | 通过 | 不通过的常见原因 |
|---|---|---|
Link detected | yes | 网线/光模块、交换机端口 shutdown、SFP 不兼容 |
Speed | 与规划一致 | 协商到 1G(线材/端口能力)、双工不匹配 |
| 错误计数 | 全 0 或不增长 | 线缆质量、光衰、EMI |
| 交换机侧 | 端口 up,属 VLAN 91(trunk 放行或 access) | VLAN 未放行、native VLAN 配错 |
⚠️ 交换机侧必须同时确认:端口模式(trunk 且放行 VLAN 91,或 access VLAN 91)、LACP 组号/模式、MTU(若用巨帧)、以及端口有没有被 STP block。宿主机一切正常但 VLAN 不通,一半原因在交换机。
8.2.2 L1 链路聚合(bond / LACP)
bash
# bond 名遵循 <clusternetwork>-bo 约定
ip -br link | grep -E 'cn-vm'
cat /proc/net/bonding/cn-vm-bo/proc/net/bonding/cn-vm-bo 的关键字段判定:
| 字段 | 期望值 | 说明 |
|---|---|---|
Bonding Mode | IEEE 802.3ad Dynamic link aggregation | 与 VlanConfig 的 bondMode 一致 |
MII Status | up | bond 整体 |
每个 Slave Interface 的 MII Status | up | 成员链路 |
Partner Mac Address | 非 00:00:00:00:00:00 | 0 表示 LACP 没协商成功 |
Aggregator ID | 所有成员相同 | 不同说明两端 LACP 组不匹配 |
Transmit Hash Policy | 与 VlanConfig 配置一致(如 layer3+4) | 影响流量分布 |
LACP rate | 与交换机一致(slow/fast) | 不一致可能频繁掉线 |
⚠️ 802.3ad 配错的迷惑性极强:
cn-vm-bo接口存在、bridge vlan里也有 VLAN 91,但 VM 就是拿不到 IP、ping 不通网关,或者时通时不通(取决于 hash 落到哪条链路)。第一步永远先看Partner Mac Address。⚠️ 如果交换机侧配的是静态聚合(无 LACP),而
VlanConfig用了802.3ad,就会协商失败——两端模式必须一致;不能做 LACP 时,改用balance-tlb/active-backup并同步改VlanConfig.spec.uplink.bondOptions.mode。
8.2.3 L2 桥接与 VLAN
bash
# bridge 名遵循 <clusternetwork>-br 约定
ip -br link show cn-vm-br
bridge link show # 哪些设备挂在 bridge 上
bridge vlan show dev cn-vm-bo # bond 上放行了哪些 VLAN
bridge vlan show dev cn-vm-br
ip -d link show cn-vm-br | egrep 'vlan_filtering|mtu'| 检查 | 期望 | 不通过含义 |
|---|---|---|
cn-vm-br 状态 | UP | ClusterNetwork 没生效 |
cn-vm-bo 在 bridge 成员里 | 是 | uplink 没接进 bridge |
bridge vlan show dev cn-vm-bo | 含 91 | VLAN 未放行 → VM 报文被丢 |
vlan_filtering | 1 | 关闭时 VLAN 隔离失效 |
| bridge MTU | ≥ 成员口最小 MTU(用巨帧则 = 9000) | MTU 不一致导致大包被丢(能 ping 通小包,业务卡死) |
⚠️ MTU 陷阱:
ping通不代表链路健康。若 bridge/交换机/VM 任一段 MTU 偏小,TCP 大包会黑洞。验证:bash# 在 VM 内(MTU 1500 时 payload=1472;巨帧 9000 时 payload=8972) ping -M do -s 1472 -c3 10.181.91.1
8.2.4 L3 平台对象(ClusterNetwork / VlanConfig / VlanStatus / NAD)
bash
# 1) ClusterNetwork 是否 Ready
kubectl get clusternetwork cn-vm -o yaml | sed -n '/status:/,$p'
# 2) VlanConfig 是否正确指向 cn-vm 与物理口
kubectl get vlanconfig -o custom-columns='NAME:.metadata.name,NET:.spec.clusterNetwork,NODESEL:.spec.nodeSelector'
kubectl get vlanconfig -o yaml | sed -n '/uplink:/,/^[a-z]/p' | head -40
# 3) ⭐ 每个节点一条 VlanStatus,且 Ready=True
kubectl get vlanstatus -A
kubectl get vlanstatus -o custom-columns='NAME:.metadata.name,NET:.status.clusterNetwork,READY:.status.conditions[?(@.type=="Ready")].status,MSG:.status.conditions[?(@.type=="Ready")].message'
# 4) NAD 是否存在、config 里的 bridge/vlan 是否对
kubectl get networkattachmentdefinition -A
kubectl get nad -n default net-91 -o jsonpath='{.spec.config}' | python3 -m json.tool
# 5) 节点数 vs VlanStatus 数(漏配节点的快速判据)
echo "nodes=$(kubectl get nodes --no-headers | wc -l) vlanstatus=$(kubectl get vlanstatus -A --no-headers | wc -l)"| 检查 | 期望 | 不通过的处置 |
|---|---|---|
cn-vm Ready | True | 看 conditions message;通常是某节点 VlanConfig 报错 |
| VlanStatus 条数 | = 节点数(12) | 少了就是该节点没被 nodeSelector 覆盖 → 该节点上 VM 无 VLAN 网络 |
| VlanStatus Ready | 全 True | False 时 message 会直说:网卡不存在、bond 建不起来、bridge 冲突 |
NAD spec.config | bridge: cn-vm-br、vlan: 91、type bridge | 写错 bridge 名是最常见笔误 |
| NAD 的 namespace | 与 VM 同 namespace(或 VM 引用 default/net-91) | 跨 namespace 引用不到 |
⚠️
VlanConfig.spec.nodeSelector是扁平标签 map(如kubernetes.io/hostname: node-11),而HostNetworkConfig.spec.nodeSelector用的是matchLabels。两者写错格式都会被静默忽略,表现为"某些节点上没有 VLAN 网络"。
8.2.5 L4 VM 网卡
bash
VM=web-front-01; NS=default
# 1) VM 规格里有几块网卡、分别连到哪个网络
kubectl get vm -n $NS $VM -o jsonpath='{range .spec.template.spec.domain.devices.interfaces[*]}{.name}{" "}{end}{"\n"}'
kubectl get vm -n $NS $VM -o jsonpath='{range .spec.template.spec.networks[*]}{.name}{" -> "}{.multus.networkName}{"\n"}{end}'
# 期望:default -> (Management Network);nic-1 -> default/net-91
# 2) 运行态:接口、MAC、以及是否上报了 IP
kubectl get vmi -n $NS $VM -o jsonpath='{range .status.interfaces[*]}{.name}{" mac="}{.mac}{" ip="}{.ipAddress}{" ips="}{.ipAddresses}{"\n"}{end}'
# 3) 宿主机侧 tap 设备是否建好(在 VM 所在节点执行)
NODE=$(kubectl get vmi -n $NS $VM -o jsonpath='{.status.nodeName}')
NODE_IP=$(kubectl get node $NODE -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
ssh rancher@$NODE_IP "ip -br link | egrep 'tap|vnet'"
# 4) virt-launcher 日志(网络插件报错都在这里)
POD=$(kubectl get pod -n $NS -l vm.kubevirt.io/name=$VM -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n $NS $POD -c compute --tail=100 | egrep -i 'network|multus|nad|bridge|error'| 检查 | 期望 | 不通过含义 |
|---|---|---|
| interfaces 数量 | 2(default + nic-1) | 少了就是 VM 规格没加第二块网卡 |
| networks 映射 | nic-1 → default/net-91 | 引错 NAD 名字/命名空间 |
status.interfaces[].mac | 两块都有 MAC | 无 MAC 说明网卡没真正创建 |
第二块的 ipAddresses | 有值(装了 guest agent 才上报) | 空值不一定是网络故障,见下方警告 |
| 宿主机 tap 设备 | 每块网卡一个 tap | 没有则是 multus/CNI 失败 |
⚠️
qemu-guest-agent未安装时,Harvester/UI 无法上报第二块网卡的 IP,status.interfaces[].ipAddresses会是空的。这会让排查者误判"网络不通"。判断 IP 的唯一权威来源是进 VM 里ip -br addr,UI 只是便利展示。
bash
# VM 内自检(SSH 或 UI 控制台)
ip -br addr # 两块网卡都要有地址
ip link # 网卡名与 MAC,核对与 vmi.status.interfaces 一致
ethtool -i eth1 2>/dev/null || true # 网卡名以 VM 内 ip -br link 实际输出为准(可能是 ens4/enp1s0 等)8.2.6 L5 IP 获取(静态 / 外部 DHCP / Managed DHCP)
VLAN 类型的 VM 网络不提供 CNI 层 IPAM——net-91 只负责把 VM 的二层接到 VLAN 91 上,IP 必须由下面三种方式之一给出:
| 方式 | 配置位置 | 排查入口 |
|---|---|---|
| 静态(推荐用于生产关键 VM) | cloud-init networkdata | VM 内 ip -br addr;kubectl get secret <vm>-cloudinit -o jsonpath='{.data.networkdata}' | base64 -d |
| 外部 DHCP(企业网 DHCP 服务器) | 交换机/DHCP 侧 | 抓包看 DISCOVER/OFFER;DHCP 服务器租约表 |
| Harvester Managed DHCP(独立 Add-on + IPPool) | IPPool(network.harvesterhci.io) | kubectl get ippools.network.harvesterhci.io -A;租约在 IPPool status |
bash
# 1) VM 内看地址(最权威)
ip -br addr show
# 期望:eth1(第二块网卡,名字以实际为准)有 10.181.91.x/24
# 2) 静态配置:确认 cloud-init 真的写进去了
NS=default; VM=web-front-01
kubectl get secret -n $NS ${VM}-cloudinit -o jsonpath='{.data.networkdata}' | base64 -d
# VM 内确认 netplan 已应用
cat /etc/netplan/*.yaml
sudo netplan get # 看最终生效配置
sudo netplan apply # 改完必须 apply
networkctl status eth1 2>/dev/null | head -20
# 3) DHCP 路径:在 VM 内抓 DHCP 交互
sudo tcpdump -i eth1 -n -c 20 'port 67 or port 68' -e
# 期望看到 DISCOVER → OFFER → REQUEST → ACK 四步
# 只有 DISCOVER 没有 OFFER = 二层没到 DHCP 服务器(回 L0–L2 查 VLAN)
journalctl -u systemd-networkd --no-pager | tail -30 # 或 NetworkManager
# 4) Managed DHCP 路径(注意:必须带全限定名,区别于 LB 的 IPPool)
kubectl get ippools.network.harvesterhci.io -A -o wide
kubectl get ippools.network.harvesterhci.io -A -o jsonpath='{range .items[*]}{.metadata.name}{" cidr="}{.spec.ipv4Config.cidr}{" pool="}{.spec.ipv4Config.pool.start}{"-"}{.spec.ipv4Config.pool.end}{" gw="}{.spec.ipv4Config.router}{"\n"}{end}'
kubectl get addon -A | egrep -i 'dhcp|managed'| 现象 | 判定 |
|---|---|
ip -br addr 只有 mgmt 网卡有地址 | 第二块网卡没配 IP:查 cloud-init networkdata 或 DHCP |
| 抓到 DISCOVER 但无 OFFER | 二层不通(VLAN/交换机)或 DHCP 服务器没在该 VLAN 提供服务 |
拿到 169.254.x.x | DHCP 超时自赋地址(APIPA)→ 同上 |
| 拿到 IP 但不在规划段 | DHCP 池配错,或 Managed DHCP IPPool 的 ipv4Config.cidr / pool 写错 |
| 两台 VM 拿到同一个 IP | 克隆体没改 dhcp-identifier: mac(见 8.4 案例 4) |
⚠️ Ubuntu/netplan 用 DHCP 时必须加
dhcp-identifier: mac:默认 netplan 用 DUID(基于 machine-id)标识客户端,克隆/快照出来的 VM 会带着相同的 DUID 去请求,DHCP 服务器就会发同一个 IP,造成地址冲突。yamlnetwork: version: 2 ethernets: eth1: dhcp4: true dhcp-identifier: mac # ← 关键
8.2.7 L6 路由与网关(双网卡最容易翻车的一层)
bash
# VM 内
ip route # 全部路由
ip route get 10.181.91.1 # 去网关走哪块卡
ip route get 8.8.8.8 # 默认路由走哪块卡 ← 关键
ping -c3 10.181.91.1 # VLAN 网关
ping -c3 10.181.0.100 # 管理网 VIP(若保留了 Management Network)双网卡默认路由冲突(本项目最高频故障):
Management Network 的网卡也会带一个默认路由,于是 VM 里有两条 default,内核按 metric 选一条。如果选中的是管理网那条,就会出现:
- 同 VLAN 内 ping 通、外部 ping 不通
- 外部能 ping 通 VM 的
10.181.91.x,但 SSH 超时(回包从 mgmt 网卡出去,源地址不对称被丢) - 时通时不通(重启后 metric 变化)
三种修复方案(按推荐度排序):
| 方案 | 做法 | 适用 |
|---|---|---|
| A. 给 VLAN 默认路由更低 metric(首选,改动最小) | netplan 里 routes: [{to: default, via: 10.181.91.1, metric: 100}],mgmt 用 dhcp4-overrides: {route-metric: 900} 或不设默认路由 | 需要保留 mgmt 网卡做带外/集群通信 |
| B. 去掉 mgmt 网卡的默认路由 | netplan 里 mgmt 网卡只配 IP,不配 gateway;或 dhcp4: true + routes 只写到 10.181.0.0/24 的路由 | 大多数业务 VM 的正确形态 |
| C. 干脆不接 Management Network | 建 VM 时只加 net-91 一块网卡 | VM 不需要被集群管理网访问时最干净 |
yaml
# 方案 A/B 的 netplan 示例(cloud-init networkdata)
network:
version: 2
ethernets:
eth0: # Management Network
dhcp4: true
dhcp-identifier: mac
dhcp4-overrides:
route-metric: 900 # 让 mgmt 的默认路由优先级更低
use-routes: false # 更彻底:不接受 mgmt 下发的路由
eth1: # VLAN 91
addresses: [10.181.91.101/24]
routes:
- to: default
via: 10.181.91.1
metric: 100
nameservers:
addresses: [10.181.0.2, 223.5.5.5]bash
# 验证修复效果
ip route | grep default # 期望:只有(或首先是)via 10.181.91.1 dev eth1 metric 100
ip route get 1.1.1.1 # 期望 dev eth1
# 从外部机器验证
ping -c3 10.181.91.101 && ssh -o ConnectTimeout=5 ops@10.181.91.101 'hostname'⚠️ Management Network 的 VM IP 外部不可直接访问,且 VM 重启后可能变化。任何需要稳定外部访问的场景,都必须用 VLAN IP(
10.181.91.x)或 LoadBalancer IP,绝不能依赖 mgmt IP。
8.2.8 L7 外部访问(LoadBalancer / DNS / 防火墙)
bash
# 1) LoadBalancer 类型 Service 是否拿到 EXTERNAL-IP
kubectl get svc -A | egrep 'LoadBalancer|EXTERNAL-IP'
kubectl get svc -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLUSTERIP:.spec.clusterIP,EXTERNAL:.status.loadBalancer.ingress[0].ip,PORTS:.spec.ports[*].port'
# 2) LB 地址池使用情况(本项目规划 .50-.99;注意是 networking.harvesterhci.io 这个 group)
kubectl get ippools.networking.harvesterhci.io -A -o yaml | egrep 'name:|subnet|ranges|allocated' | head -40
# 3) klipper-lb / svc-lb 的 Pod 是否正常(Harvester 内建 LB 实现)
kubectl get pods -A | egrep 'klipper|svclb|lb-'
# 4) 从外部机器验证访问链路
ping -c3 10.181.91.50 # LB IP
nc -vz 10.181.91.50 80 # 端口
curl -sS -m5 -o /dev/null -w '%{http_code}\n' http://10.181.91.50/
dig +short web.example.internal # DNS 解析
traceroute -n -m 8 10.181.91.101 # 路径上哪一跳断的| 现象 | 判定 |
|---|---|
Service 一直 <pending> | LB IPPool 耗尽或 IPPool 未创建/网段不对 |
| EXTERNAL-IP 有,但端口不通 | 后端 Pod/VM 未监听、防火墙、或 LB Pod 异常 |
| VM IP 通但 LB IP 不通 | LB 的转发路径与 VLAN 二层问题(查 svclb/klipper Pod 所在节点是否有 VLAN 网络) |
| 外部机 ping 不通、同 VLAN VM 能 ping 通 | 交换机侧路由/ACL/防火墙,不是 Harvester 的问题 |
| DNS 解析失败但 IP 直连通 | VM 内 resolv.conf / nameserver 配置,或 DNS 服务器不在可达网段 |
⚠️ IP 分配纪律(避免池耗尽与冲突):宿主机
.11-.22、LoadBalancer.50-.99、静态 VM.101-.200、Managed DHCP.201-.250。任何越界分配都会在几周后以"莫名冲突"的形式爆出来。
8.2.9 L8 业务层验证(平台没问题之后)
L0–L7 全通只代表「网络与平台可用」,业务是否真的健康要单独验一遍。这一层的责任方通常是应用团队,但平台方要能提供可复现的验证入口。
bash
VM_IP=10.181.91.101
# 1) 端口是否真的在监听(从外部机执行,最接近真实用户视角)
nc -vz -w3 $VM_IP 22 && nc -vz -w3 $VM_IP 8080
nmap -p22,80,443,8080 $VM_IP 2>/dev/null | egrep 'open|filtered|closed'
# 2) 应用层探活(不要只测 TCP,要测到 HTTP 状态码)
curl -sS -m5 -o /dev/null -w 'code=%{http_code} time=%{time_total}s\n' http://$VM_IP:8080/healthz
# 3) TLS 证书(有效期、SAN、链)—— 用 IP 直连时注意 SNI
echo | openssl s_client -connect $VM_IP:443 -servername web.example.internal 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
# 4) DNS 与反向解析(业务常用域名访问时必须验)
dig +short web.example.internal A
dig +short -x $VM_IP
# 5) 后端健康与容量(VM 内执行)
ssh ops@$VM_IP 'systemctl is-active <服务名>; ss -ltnp | head; df -h /; free -m | head -2'
# 6) 通过 LB / VIP 的端到端访问(最终用户路径)
curl -sS -m5 -o /dev/null -w 'lb_code=%{http_code}\n' http://10.181.91.60:8080/healthz
curl -sS -m5 -k -o /dev/null -w 'vip_code=%{http_code}\n' https://10.181.0.100/ # Harvester UI 本身| 检查项 | 通过标准 |
|---|---|
| 监听端口 | 业务端口 open,且只有该开的端口开着(ss -ltnp 核对) |
| HTTP 探活 | /healthz 返回 200,响应时间在预期内 |
| TLS | 未过期、SAN 覆盖访问域名、证书链完整 |
| DNS | 正解与反解都符合台账;VM 内 resolvectl status 指向内网 DNS |
| 端到端 | 从真实用户网段通过 LB/VIP/域名访问成功(不是从宿主机或同 VLAN VM 测) |
| 重启后复验 | VM 重启、迁移、节点重启后重跑一次——很多业务只在首次启动时正常 |
⚠️ 验收时最容易含糊的就是「谁在哪台机器上测」。写清测试源(办公网跳板机 / 同 VLAN VM / 宿主机),否则「我这儿是通的」没有任何意义。
8.2.10 一键网络体检脚本
bash
#!/usr/bin/env bash
# appendix/ops/net-check.sh —— VLAN 直通网络分层体检(在能连集群的运维机上执行)
# 用法:./net-check.sh [vm-name] [namespace]
set -uo pipefail
VM="${1:-web-front-01}"; NS="${2:-default}"
GW="${GW:-10.181.91.1}"; CN="${CN:-cn-vm}"; NAD="${NAD:-default/net-91}"
HDR=$'\n=== '
p(){ printf ' %-42s %s\n' "$1" "$2"; }
jsonpath(){ kubectl get "$1" -n "$2" "$3" -o jsonpath="$4" 2>/dev/null; }
echo "${HDR}L3 平台对象 ==="
p "ClusterNetwork $CN" "$(kubectl get clusternetwork $CN -o jsonpath='{range .status.conditions[?(@.type=="Ready")]}Ready={.status} {.message}{end}' 2>/dev/null || echo '不存在')"
NODES=$(kubectl get nodes --no-headers 2>/dev/null | wc -l | tr -dc 0-9)
VS=$(kubectl get vlanstatus -A --no-headers 2>/dev/null | wc -l | tr -dc 0-9)
VS_BAD=$(kubectl get vlanstatus -A --no-headers 2>/dev/null | grep -vc ' True ' || true)
p "VlanStatus 数量 / 节点数" "$VS / $NODES"
p "VlanStatus 非 Ready" "${VS_BAD:-0}"
p "NAD $NAD" "$(kubectl get nad -n "${NAD%%/*}" "${NAD##*/}" -o jsonpath='{.spec.config}' 2>/dev/null | head -c 120 || echo '不存在')"
echo "${HDR}L4 VM 网卡 ==="
p "VM 状态" "$(kubectl get vm -n $NS $VM -o jsonpath='{.status.printableStatus}' 2>/dev/null || echo '不存在')"
p "所在节点" "$(jsonpath vmi $NS $VM '{.status.nodeName}')"
p "规格网卡" "$(kubectl get vm -n $NS $VM -o jsonpath='{range .spec.template.spec.domain.devices.interfaces[*]}{.name} {end}' 2>/dev/null)"
p "网络映射" "$(kubectl get vm -n $NS $VM -o jsonpath='{range .spec.template.spec.networks[*]}{.name}={.multus.networkName} {end}' 2>/dev/null)"
p "运行态接口/MAC" "$(kubectl get vmi -n $NS $VM -o jsonpath='{range .status.interfaces[*]}{.name}:{.mac} {end}' 2>/dev/null)"
p "上报 IP(需 guest agent)" "$(kubectl get vmi -n $NS $VM -o jsonpath='{range .status.interfaces[*]}{.name}={.ipAddresses} {end}' 2>/dev/null)"
echo "${HDR}L1/L2 宿主机(VM 所在节点)==="
NODE=$(jsonpath vmi $NS $VM '{.status.nodeName}')
NIP=$(kubectl get node "$NODE" -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
if [[ -n "$NIP" ]] && ssh -o BatchMode=yes -o ConnectTimeout=3 "rancher@$NIP" true 2>/dev/null; then
p "bridge ${CN}-br" "$(ssh -o BatchMode=yes rancher@$NIP "ip -br link show ${CN}-br 2>/dev/null || echo 不存在")"
p "bond ${CN}-bo" "$(ssh -o BatchMode=yes rancher@$NIP "ip -br link show ${CN}-bo 2>/dev/null || echo 不存在")"
p "LACP partner MAC" "$(ssh -o BatchMode=yes rancher@$NIP "grep -m1 'Partner Mac Address' /proc/net/bonding/${CN}-bo 2>/dev/null || echo '无 bond/非 802.3ad'")"
p "bridge vlan 91" "$(ssh -o BatchMode=yes rancher@$NIP "bridge vlan show dev ${CN}-bo 2>/dev/null | grep -w 91 || echo '未放行 VLAN 91'")"
else
p "宿主机 SSH" "跳过(无法免密登录 ${NIP:-unknown}),请手工执行 8.2.2/8.2.3"
fi
echo "${HDR}下一步 ==="
echo " L5/L6/L8 需要进 VM 内执行:ip -br addr; ip route get 1.1.1.1; ping -c3 $GW; curl -sS http://<VM_IP>:<PORT>/healthz"
echo " L7 外部验证:从办公网 ping/ssh VM 的 VLAN IP;kubectl get svc -A | grep LoadBalancer"bash
chmod +x appendix/ops/net-check.sh
./appendix/ops/net-check.sh web-front-01 default8.3 故障对照表(症状 → 命令 → 原因 → 处置)
8.3.1 网络类
| # | 症状 | 第一步命令 | 最可能原因 | 处置 |
|---|---|---|---|---|
| N1 | 所有 VM 的第二块网卡都没 IP | kubectl get vlanstatus -A | VlanConfig 未生效 / 网卡名写错 | 修 VlanConfig.spec.uplink.nics,等 vlanstatus 转 Ready |
| N2 | 只有部分节点上的 VM 没网 | echo nodes=$(kubectl get nodes --no-headers|wc -l) vs=$(kubectl get vlanstatus -A --no-headers|wc -l) | nodeSelector 没覆盖这些节点(或新节点未加标签) | 扩 VlanConfig.spec.nodeSelector / 给新节点补配置 |
| N3 | VlanStatus Ready=False | kubectl get vlanstatus -o yaml | grep -A3 message | 网卡不存在、被占用、bond 建不起来 | 按 message 修物理口或 nics 列表 |
| N4 | bond 存在但 Partner Mac Address 全 0 | cat /proc/net/bonding/cn-vm-bo | 交换机侧没配 LACP,或模式不一致 | 交换机配 802.3ad 同组;或改用 active-backup |
| N5 | 间歇性丢包/时通时不通 | cat /proc/net/bonding/cn-vm-bo | grep Aggregator | 两条成员链路落在不同 aggregator(一端 LACP 组不匹配) | 交换机侧把两口放进同一 port-channel |
| N6 | VM 能 ping 网关,但大包失败/业务卡死 | ping -M do -s 1472 -c3 $GW | MTU 不一致(交换机未开巨帧 / NAD MTU 与 bridge 不符) | 统一 MTU:交换机端口、VlanConfig mtu、NAD mtu、VM 内 |
| N7 | 同 VLAN 两台 VM(不同节点)互 ping 不通 | bridge vlan show dev cn-vm-bo | grep -w 91 | 交换机端口未放行 VLAN 91,或两节点上联口 VLAN 配置不同 | 交换机统一 trunk 放行 91 |
| N8 | VM 拿到 169.254.x.x | VM 内 tcpdump -i eth1 -n 'port 67 or port 68' | DHCP 请求出不去(二层)或无 DHCP 服务 | 查 VLAN/交换机;或改静态 IP |
| N9 | 两台 VM 拿到同一个 IP | VM 内 cat /etc/netplan/*.yaml | grep dhcp-identifier | 克隆体 DUID 相同 | 加 dhcp-identifier: mac;重建 machine-id |
| N10 | 外部 ping 通 VM 但 SSH 超时 | VM 内 ip route get <外部机 IP> | 双默认路由,回包走 mgmt 网卡 | 8.2.7 方案 A/B/C |
| N11 | VM 重启后外部访问时好时坏 | VM 内 ip route | grep default | mgmt DHCP 默认路由 metric 变化 | 固定 metric;或去掉 mgmt 默认路由 |
| N12 | UI 看不到第二块网卡的 IP,但 VM 内其实有 | kubectl get vmi $VM -o jsonpath='{.status.interfaces}' | 未装/未运行 qemu-guest-agent | VM 内 systemctl enable --now qemu-guest-agent |
| N13 | Service 一直 <pending> 拿不到 EXTERNAL-IP | kubectl get ippools.networking.harvesterhci.io -A -o yaml | LB IPPool 未建或已耗尽(.50-.99) | 扩池;清理废弃 Service |
| N14 | LB IP 通、VM IP 不通 | kubectl get pods -A | egrep 'svclb|klipper' | LB Pod 所在节点无 VLAN 网络 | 补该节点的 VlanConfig |
| N15 | 升级后 VM 集体断网 | ip -br link 对比升级前 | PCI bridge 上的网卡被重命名,VlanConfig 里网卡名失效 | 更新 VlanConfig.spec.uplink.nics 为新名字 |
| N16 | 节点重启后 VLAN 网络没自动恢复 | kubectl get vlanstatus -A;节点 ip -br link | 网卡启动顺序/驱动加载慢,或 HostNetworkConfig 与 VlanConfig 抢同一网卡 | 等待或重启网络;同一块物理口不能同时被两者使用 |
8.3.2 VM / 平台类
| # | 症状 | 第一步命令 | 最可能原因 | 处置 |
|---|---|---|---|---|
| V1 | VM 卡 Starting,Pod CreateContainerError: not a device node | kubectl describe pod virt-launcher-<vm>-xxx | 节点/集群重启后 CSI bind mount 成普通文件(v1.3.0 已知) | 按官方步骤修复对应 Longhorn 卷后重启 VM |
| V2 | VM Off 但 UI 没有 Start 按钮 | kubectl get vm,vmi,pod -n $NS | grep $VM | 残留 virt-launcher Pod 导致状态不一致 | kubectl delete pod virt-launcher-<vm>-xxx -n $NS --force |
| V3 | VM 无法迁移(Migrate 灰掉/失败) | kubectl get vm $VM -o jsonpath='{.spec.template.spec.nodeSelector}' | nodeSelector、设备直通、cluster network 只覆盖单节点、CPU pinning | 去掉约束 / 扩 VLAN 网络覆盖节点 |
| V4 | 迁移长时间不完成后中止 | kubectl get virtualmachineinstancemigration -n $NS -o yaml | 内存脏页速率 > 带宽(写密集) | 7.3.3 缓解措施,或计划性关机 |
| V5 | VM 起不来,报资源不足 | kubectl describe vmi $VM | tail -20 | 节点 CPU/内存不够,或 hugepages/CPU pinning 约束 | 腾资源、调 requests、扩节点 |
| V6 | VM 磁盘满 / 卷 degraded | kubectl get volumes.longhorn.io -n longhorn-system | grep -i degraded | 节点故障导致副本不足 | 恢复节点,等重建;必要时扩盘 |
| V7 | cloud-init 没生效(用户/密钥/网络都没配) | kubectl get secret <vm>-cloudinit -o jsonpath='{.data.userdata}' | base64 -d | Secret 名字/引用不对;或 VM 用旧磁盘启动(cloud-init 只在首启动跑) | 核对引用;需要重跑就 cloud-init clean 或重建 VM |
| V8 | 备份按钮报错 / Schedule 挂起 | kubectl get setting backup-target -n harvester-system -o yaml | backup-target 不可达、凭据过期 | 修目标;先手工备份成功再 Resume schedule |
| V9 | 升级被拒绝 | UI 报错信息 | Schedule 未暂停 / 有进行中的备份 / /usr/local < 30 GiB / 证书 7 天内到期 | 对照 7.5.3 清单逐项消除 |
| V10 | 新节点加入后 VM 调度不上去 | kubectl describe node <new> | grep -A5 Taints | 节点有 taint / 未 Ready / 无 VLAN 网络 | 处理 taint;补 VlanConfig |
8.4 真实案例复盘
每个案例都按「现象 → 误判方向 → 定位过程 → 根因 → 修复 → 预防」写,重点是误判方向,那是最花时间的部分。
案例 1:LACP 两端模式不一致 → 时通时不通
- 现象:VM 能拿到
10.181.91.x,ping 网关成功率约 50%;同一台 VM 迁到另一节点后完全不通。 - 误判方向:怀疑 DHCP、怀疑 netplan、重写了三遍 cloud-init,浪费 2 小时。
- 定位过程:bash
cat /proc/net/bonding/cn-vm-bo | egrep 'Mode|MII Status|Partner Mac|Aggregator ID' # → Mode: 802.3ad,两条 Slave 都 up,但 Partner Mac Address = 00:00:00:00:00:00 # → 两个 Slave 的 Aggregator ID 不同 - 根因:交换机侧端口配成了静态聚合(无 LACP),宿主机侧是 802.3ad。LACP 未协商,交换机把两条链路当成独立端口,只有其中一条真正在转发 VLAN 91,hash 落到另一条就丢包。
- 修复:交换机侧把两口配成同一
port-channel且mode active;宿主机侧VlanConfig.spec.uplink.bondOptions.mode: 802.3ad保持不变。重连后Partner Mac Address出现真实 MAC,Aggregator ID一致,丢包归零。 - 预防:把「
Partner Mac Address非全 0」写进节点入网验收清单(第 4 章);无法做 LACP 的环境统一用active-backup并记录在案。
案例 2:新节点加入后,该节点上的 VM 全部无网
- 现象:扩容 3 台节点后,调度到新节点的 VM 第二块网卡拿不到 IP,老节点上的 VM 正常。
- 误判方向:以为新节点硬件/网线有问题,换口换线无效。
- 定位过程:bash
echo "nodes=$(kubectl get nodes --no-headers | wc -l) vlanstatus=$(kubectl get vlanstatus -A --no-headers | wc -l)" # → nodes=15 vlanstatus=12 ← 一眼看出少了 3 条 kubectl get vlanconfig cn-vm-uplink -o jsonpath='{.spec.nodeSelector}' # → nodeSelector 只列了原来 12 台的主机名标签 - 根因:
VlanConfig.spec.nodeSelector是显式主机名列表,新节点不在其中,Harvester 就没在这些节点上创建 bond/bridge,VLAN 网络自然不存在。 - 修复:给
nodeSelector加上新节点(或改成按通用标签选择,如按机架/角色标签),等vlanstatus补齐到 15 条且全 Ready。 - 预防:把「补
VlanConfig+ 验证vlanstatus条数 = 节点数」写进扩容 SOP(7.4.3)。
案例 3:双默认路由 → 外部 ping 通但 SSH 超时
- 现象:外部机能
ping 10.181.91.101成功,ssh ops@10.181.91.101卡住超时;VM 自己访问外网正常。 - 误判方向:怀疑防火墙、怀疑 SSH 服务、怀疑安全组。
- 定位过程(VM 内):bash
ip route | grep default # default via 169.254.1.1 dev eth0 proto dhcp src 10.52.7.31 metric 100 # default via 10.181.91.1 dev eth1 proto static metric 100 ip route get 10.181.5.20 # 外部机 # → 10.181.5.20 via 169.254.1.1 dev eth0 ← 回包走了管理网卡! - 根因:两条默认路由 metric 相同,内核选了 mgmt 那条。入向包从
eth1(VLAN 91)进来,出向包却从eth0(管理网)发给假网关169.254.1.1,经宿主机 NAT 后源地址变成节点管理地址,与外部机期望的10.181.91.101不一致,被上游设备按非对称路由/反向路径校验丢弃——于是 ping(部分设备宽松)通、TCP 握手失败。 - 修复:netplan 里把 mgmt 网卡
dhcp4-overrides: {route-metric: 900, use-routes: false},VLAN 默认路由 metric 100;netplan apply后ip route get走eth1,SSH 立刻恢复。 - 预防:cloud-init 基线模板里默认就写好 metric(第 5、6 章的模板已内置);不需要管理网的 VM 直接不接 Management Network。
案例 4:克隆 VM 抢了同一个 IP
- 现象:从模板批量创建后,两台 VM 都拿到
10.181.91.205,交换机日志出现 MAC flapping,业务偶发中断。 - 定位过程:bash
# VM 内 cat /etc/netplan/50-cloud-init.yaml | grep -A3 eth1 # dhcp4: true,无 dhcp-identifier cat /etc/machine-id # 两台完全相同 ← 克隆残留 - 根因:netplan DHCP 默认用 DUID(源自 machine-id)标识客户端;模板镜像里 machine-id 没清,克隆体带着同一个 DUID 请求,DHCP 认为是同一台机器,回了同一个租约。
- 修复:所有模板镜像执行
cloud-init clean --logs --machine-id+truncate -s 0 /etc/machine-id;netplan 加dhcp-identifier: mac;重建受影响 VM。 - 预防:黄金镜像制作 SOP 里把「清 machine-id」列为强制步骤(6.6.1 第 3 步);批量脚本对 IP 做唯一性校验(6.5.5)。
案例 5:UI 不显示第二块网卡 IP,被当成网络故障
- 现象:批量交付时 UI 上 VM 的
nic-1一栏 IP 为空,验收方认为网络没通,要求返工。 - 定位过程:bash
ssh ops@10.181.91.101 'ip -br addr' # 直接就能登上,且 eth1 有正确 IP kubectl get vmi web-front-01 -o jsonpath='{.status.interfaces}' # 第二块只有 mac,无 ipAddresses kubectl exec $POD -c compute -- virsh qemu-agent-command ... 2>/dev/null || \ ssh ops@10.181.91.101 'systemctl status qemu-guest-agent' # 未安装/未运行 - 根因:Harvester 通过 qemu-guest-agent 读取客户机内的接口信息;agent 缺失时 UI 无法上报 IP——网络本身是好的。
- 修复:VM 内安装并启用
qemu-guest-agent;黄金镜像与 cloud-init 基线里预装(第 5、6 章模板已含)。 - 预防:验收标准写清「以 VM 内
ip -br addr+ 外部 ping/SSH 为准,UI 展示仅作参考」。
案例 6:升级后 VLAN 网络集体失效
- 现象:升级到新补丁版并重启节点后,多台节点的
vlanstatus变 False,VM 断网。 - 定位过程:bash
kubectl get vlanstatus -o yaml | grep -B2 -A3 'message' # "nic eno3 not found" 之类 ssh rancher@10.181.0.15 'ip -br link' # 原来的 eno3/eno4 变成了 enp65s0f0/enp65s0f1 - 根因:官方明确提醒——连接在 PCI bridge 上的网卡在升级后可能被重命名;
VlanConfig里写的是旧名字,uplink 找不到网卡。 - 修复:更新
VlanConfig.spec.uplink.nics为新名字(同一 ClusterNetwork 下的所有 VlanConfig 一起改),等vlanstatus转 Ready,必要时重启受影响 VM。 - 预防:升级前记录每节点
ip -br link快照;升级后把「核对网卡名」列为第一项验证(7.5.6 第 4 步);条件允许时用 BIOS/udev 固定网卡名。
8.5 收集证据与提工单
排障超过 30 分钟没进展,就一边继续一边收证据——事后回溯和提工单都靠这些。
bash
STAMP=$(date +%Y%m%d-%H%M); DIR="hv-incident-$STAMP"; mkdir -p "$DIR"
# 1) 平台对象快照
for r in nodes clusternetwork vlanconfig vlanstatus hostnetworkconfig networkattachmentdefinition \
virtualmachine virtualmachineinstance vmschedule \
ippools.network.harvesterhci.io ippools.networking.harvesterhci.io setting; do
kubectl get $r -A -o yaml > "$DIR/$r.yaml" 2>/dev/null || true
done
kubectl get events -A --sort-by=.lastTimestamp > "$DIR/events.txt"
kubectl get pods -A -o wide > "$DIR/pods.txt"
# 2) 相关 VM 的宿主侧日志
VM=web-front-01; NS=default
POD=$(kubectl get pod -n $NS -l vm.kubevirt.io/name=$VM -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -n "$POD" ]] && kubectl logs -n $NS $POD --all-containers --tail=2000 > "$DIR/virt-launcher.log"
kubectl describe vmi -n $NS $VM > "$DIR/vmi-describe.txt" 2>/dev/null
# 3) 节点侧日志(在 VM 所在节点执行)
# /var/lib/rancher/rke2/agent/logs/kubelet.log
# /var/lib/rancher/rke2/agent/containerd/containerd.log
# journalctl -u kubelet / dmesg -T / ip -br link / bridge vlan show / cat /proc/net/bonding/*
# 4) VM 内部(业务侧最有力的证据)
# ip -br addr; ip route; ip route get <目标>; resolvectl status
# tcpdump -i eth1 -n -c 200 -w /tmp/vm.pcap 'vlan or host 10.181.91.1'
# 5) 打包 + UI 生成 support bundle
tar czf "$DIR.tar.gz" "$DIR"; ls -lh "$DIR.tar.gz"
# UI:Generate Support Bundle(提工单时一并附上)提工单/升级事件时,把这 6 项写清楚,能省掉好几轮来回:
- Harvester 版本(
kubectl get setting server-version -n harvester-system -o jsonpath='{.value}')与是否刚升级过 - 集群规模(节点数、管理面/worker 划分)与网络拓扑(bond 模式、VLAN、MTU、交换机型号与端口配置)
- 症状的精确边界:全部 VM 还是部分?哪些节点?什么时候开始?是否可复现?
- 已执行的排查步骤与结果(用第 8.2 节的分层结论:L0–L8 各层是/否)
- 附件:support bundle + 上面打包的证据目录
- 业务影响与期望时效
8.6 全书总验收清单
上线/交付前从头到尾过一遍。任何一项不通过都不要签字。
A. 规划与安装(第 1–3 章)
| # | 项 | 通过标准 |
|---|---|---|
| A1 | IP 台账 | VIP .100、节点 .11-.22、LB .50-.99、静态 VM .101-.200、DHCP .201-.250 已登记且无重叠 |
| A2 | 交换机配置 | 管理网 VLAN + VM 网 VLAN 91 放行;聚合组与模式记录在案;MTU 一致 |
| A3 | 节点 config.yaml | 12 份文件均已归档,含 authorized_keys、NTP、server_url(VIP) |
| A4 | 集群创建完成 | UI 可通过 https://10.181.0.100 访问,12/12 节点 Ready |
| A5 | 版本 | server-version = v1.8.x,与升级路径规划一致 |
B. 网络直通(第 4 章)
| # | 项 | 命令 | 通过标准 |
|---|---|---|---|
| B1 | ClusterNetwork | kubectl get clusternetwork cn-vm | Ready=True |
| B2 | VlanConfig 覆盖 | kubectl get vlanconfig -o wide | 全部 12 节点被 nodeSelector 覆盖 |
| B3 | VlanStatus | kubectl get vlanstatus -A | 12 条,全 Ready=True |
| B4 | bond/LACP | cat /proc/net/bonding/cn-vm-bo | 成员 up,Partner Mac Address 非全 0,Aggregator 一致 |
| B5 | bridge/VLAN | bridge vlan show dev cn-vm-bo | 含 VLAN 91 |
| B6 | NAD | kubectl get nad -n default net-91 -o jsonpath='{.spec.config}' | bridge=cn-vm-br,vlan=91 |
| B7 | 宿主机侧连通 | 临时 VLAN 子接口 arping 10.181.91.1 | 有应答 |
| B8 | MTU | cat /sys/class/net/cn-vm-br/mtu 与 VM 内一致 | 与规划一致(1500 或 9000) |
C. VM 与 cloud-init(第 5–6 章)
| # | 项 | 通过标准 |
|---|---|---|
| C1 | 双网卡 VM | 每台都有 default + nic-1(→ default/net-91),两块都有 MAC |
| C2 | cloud-init 生效 | ops 用户可用密钥登录;主机名正确;qemu-guest-agent running |
| C3 | VLAN IP | VM 内 ip -br addr 显示 10.181.91.x,与台账一致 |
| C4 | 默认路由 | ip route get 1.1.1.1 走 VLAN 网卡 |
| C5 | 内部验证 | 同 VLAN 跨节点两台 VM 互 ping 通;VM → 网关通 |
| C6 | 外部验证 | 外部机 ping + SSH + 业务端口全通;大包 ping -M do -s 1472 通 |
| C7 | LoadBalancer | Service 有 EXTERNAL-IP,外部可访问 |
| C8 | 模板体系 | VirtualMachineTemplate/Version 存在;模板内不含 IP |
| C9 | 批量脚本 | gen-vms.sh render 退出码 0;非法输入被拦截;apply 后全 Running |
| C10 | 克隆身份唯一 | 任意两台 cat /etc/machine-id 不同;SSH host key 不同 |
| C11 | 交付物 | vms.csv + ip-registry-*.txt 已归档 |
D. 运维与可靠性(第 7–8 章)
| # | 项 | 通过标准 |
|---|---|---|
| D1 | 巡检脚本 | daily-check.sh 无 FAIL;已挂 cron |
| D2 | backup-target | 已配置且能成功备份 |
| D3 | 定时备份 | Schedule Ready、未挂起;间隔 ≥ 1h;Retain/MaxFailure 已设 |
| D4 | 恢复演练 | 至少一次成功恢复,有 RTO 记录 |
| D5 | 快照配额 | Namespace/VM 级配额已设 |
| D6 | 可迁移性 | 关键 VM 无 nodeSelector;cn-vm 覆盖全部节点;实测一次热迁移成功 |
| D7 | 升级前置 | /usr/local ≥ 30 GiB、NTP 同步、证书 > 7 天、pre-check 通过 |
| D8 | 安全 | Grafana 默认密码已改;节点 SSH 密码登录已关;密钥轮换流程可用 |
| D9 | 告警 | 7.6.3 清单里的告警项已配置并测试送达 |
| D10 | 证据收集 | net-check.sh 可用;事件打包脚本演练过 |
| D11 | 配置归档 | 网络/模板/VM 清单 YAML 已导出并异地保存 |
| D12 | 文档落地 | 本书第 2、3、4、6、7 章的 SOP 已转成团队内部 runbook 并指派负责人 |
结语:把这本书用起来
这套环境的稳定性不取决于某个 YAML 写得多漂亮,而取决于三件事有没有形成习惯:
- IP 与命名有纪律:
.11-.22/.50-.99/.101-.200/.201-.250四段各守其位,台账随变更更新。 - 网络配置有档案:
ClusterNetwork、VlanConfig、NAD、HostNetworkConfig、模板与 cloud-init 基线全部 YAML 化并入库——重装和排障都靠它。 - 验证有脚本:装机后跑第 4 章清单,建 VM 后跑第 5 章清单,批量交付跑第 6 章校验,日常跑
daily-check.sh,出问题跑net-check.sh。人只负责判断,不负责记忆。
⚠️ 最后一条提醒:书里涉及 CRD 字段名、UI 菜单路径与 Add-on 名称的部分,都基于 Harvester v1.8 官方文档整理。正式落地前,请在你的集群上用
kubectl api-resources、kubectl explain <resource>.spec和kubectl get <obj> -o yaml复核一遍字段名——不同补丁版本之间字段偶有增删,以集群实际为准。
祝集群长命百岁,告警群里安静如常。