Skip to content

08 · 全链路验证与故障排查

适用版本:Harvester v1.8 | 基线:12 节点,管理网 10.181.0.0/24(VIP 10.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 IPL6–L7(含交换机路由/防火墙)

8.2 逐层验证命令

约定:宿主机命令用 ssh rancher@10.181.0.<11-22> 登录后 sudo -iVM 指被测虚拟机(示例 web-front-01,IP 10.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 detectedyes网线/光模块、交换机端口 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 ModeIEEE 802.3ad Dynamic link aggregationVlanConfigbondMode 一致
MII Statusupbond 整体
每个 Slave InterfaceMII Statusup成员链路
Partner Mac Address非 00:00:00:00:00:000 表示 LACP 没协商成功
Aggregator ID所有成员相同不同说明两端 LACP 组不匹配
Transmit Hash PolicyVlanConfig 配置一致(如 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 状态UPClusterNetwork 没生效
cn-vm-bo 在 bridge 成员里uplink 没接进 bridge
bridge vlan show dev cn-vm-bo91VLAN 未放行 → VM 报文被丢
vlan_filtering1关闭时 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 ReadyTrue看 conditions message;通常是某节点 VlanConfig 报错
VlanStatus 条数= 节点数(12)少了就是该节点没被 nodeSelector 覆盖 → 该节点上 VM 无 VLAN 网络
VlanStatus Ready全 TrueFalse 时 message 会直说:网卡不存在、bond 建不起来、bridge 冲突
NAD spec.configbridge: cn-vm-brvlan: 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-1default/net-91引错 NAD 名字/命名空间
status.interfaces[].mac两块都有 MAC无 MAC 说明网卡没真正创建
第二块的 ipAddresses有值(装了 guest agent 才上报)空值不一定是网络故障,见下方警告
宿主机 tap 设备每块网卡一个 tap没有则是 multus/CNI 失败

⚠️ qemu-guest-agent 未安装时,Harvester/UI 无法上报第二块网卡的 IPstatus.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 networkdataVM 内 ip -br addrkubectl get secret <vm>-cloudinit -o jsonpath='{.data.networkdata}' | base64 -d
外部 DHCP(企业网 DHCP 服务器)交换机/DHCP 侧抓包看 DISCOVER/OFFER;DHCP 服务器租约表
Harvester Managed DHCP(独立 Add-on + IPPool)IPPoolnetwork.harvesterhci.iokubectl 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.xDHCP 超时自赋地址(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,造成地址冲突。

yaml
network:
  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 default

8.3 故障对照表(症状 → 命令 → 原因 → 处置)

8.3.1 网络类

#症状第一步命令最可能原因处置
N1所有 VM 的第二块网卡都没 IPkubectl get vlanstatus -AVlanConfig 未生效 / 网卡名写错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 / 给新节点补配置
N3VlanStatus Ready=Falsekubectl get vlanstatus -o yaml | grep -A3 message网卡不存在、被占用、bond 建不起来按 message 修物理口或 nics 列表
N4bond 存在但 Partner Mac Address 全 0cat /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
N6VM 能 ping 网关,但大包失败/业务卡死ping -M do -s 1472 -c3 $GWMTU 不一致(交换机未开巨帧 / 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
N8VM 拿到 169.254.x.xVM 内 tcpdump -i eth1 -n 'port 67 or port 68'DHCP 请求出不去(二层)或无 DHCP 服务查 VLAN/交换机;或改静态 IP
N9两台 VM 拿到同一个 IPVM 内 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
N11VM 重启后外部访问时好时坏VM 内 ip route | grep defaultmgmt DHCP 默认路由 metric 变化固定 metric;或去掉 mgmt 默认路由
N12UI 看不到第二块网卡的 IP,但 VM 内其实有kubectl get vmi $VM -o jsonpath='{.status.interfaces}'未装/未运行 qemu-guest-agentVM 内 systemctl enable --now qemu-guest-agent
N13Service 一直 <pending> 拿不到 EXTERNAL-IPkubectl get ippools.networking.harvesterhci.io -A -o yamlLB IPPool 未建或已耗尽(.50-.99扩池;清理废弃 Service
N14LB 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网卡启动顺序/驱动加载慢,或 HostNetworkConfigVlanConfig 抢同一网卡等待或重启网络;同一块物理口不能同时被两者使用

8.3.2 VM / 平台类

#症状第一步命令最可能原因处置
V1VM 卡 Starting,Pod CreateContainerError: not a device nodekubectl describe pod virt-launcher-<vm>-xxx节点/集群重启后 CSI bind mount 成普通文件(v1.3.0 已知)按官方步骤修复对应 Longhorn 卷后重启 VM
V2VM 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
V3VM 无法迁移(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 缓解措施,或计划性关机
V5VM 起不来,报资源不足kubectl describe vmi $VM | tail -20节点 CPU/内存不够,或 hugepages/CPU pinning 约束腾资源、调 requests、扩节点
V6VM 磁盘满 / 卷 degradedkubectl get volumes.longhorn.io -n longhorn-system | grep -i degraded节点故障导致副本不足恢复节点,等重建;必要时扩盘
V7cloud-init 没生效(用户/密钥/网络都没配)kubectl get secret <vm>-cloudinit -o jsonpath='{.data.userdata}' | base64 -dSecret 名字/引用不对;或 VM 用旧磁盘启动(cloud-init 只在首启动跑)核对引用;需要重跑就 cloud-init clean 或重建 VM
V8备份按钮报错 / Schedule 挂起kubectl get setting backup-target -n harvester-system -o yamlbackup-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-channelmode 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 applyip route geteth1,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 项写清楚,能省掉好几轮来回:

  1. Harvester 版本(kubectl get setting server-version -n harvester-system -o jsonpath='{.value}')与是否刚升级过
  2. 集群规模(节点数、管理面/worker 划分)与网络拓扑(bond 模式、VLAN、MTU、交换机型号与端口配置)
  3. 症状的精确边界:全部 VM 还是部分?哪些节点?什么时候开始?是否可复现?
  4. 已执行的排查步骤与结果(用第 8.2 节的分层结论:L0–L8 各层是/否)
  5. 附件:support bundle + 上面打包的证据目录
  6. 业务影响与期望时效

8.6 全书总验收清单

上线/交付前从头到尾过一遍。任何一项不通过都不要签字。

A. 规划与安装(第 1–3 章)

#通过标准
A1IP 台账VIP .100、节点 .11-.22、LB .50-.99、静态 VM .101-.200、DHCP .201-.250 已登记且无重叠
A2交换机配置管理网 VLAN + VM 网 VLAN 91 放行;聚合组与模式记录在案;MTU 一致
A3节点 config.yaml12 份文件均已归档,含 authorized_keys、NTP、server_url(VIP)
A4集群创建完成UI 可通过 https://10.181.0.100 访问,12/12 节点 Ready
A5版本server-version = v1.8.x,与升级路径规划一致

B. 网络直通(第 4 章)

#命令通过标准
B1ClusterNetworkkubectl get clusternetwork cn-vmReady=True
B2VlanConfig 覆盖kubectl get vlanconfig -o wide全部 12 节点被 nodeSelector 覆盖
B3VlanStatuskubectl get vlanstatus -A12 条,全 Ready=True
B4bond/LACPcat /proc/net/bonding/cn-vm-bo成员 up,Partner Mac Address 非全 0,Aggregator 一致
B5bridge/VLANbridge vlan show dev cn-vm-bo含 VLAN 91
B6NADkubectl get nad -n default net-91 -o jsonpath='{.spec.config}'bridge=cn-vm-br,vlan=91
B7宿主机侧连通临时 VLAN 子接口 arping 10.181.91.1有应答
B8MTUcat /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
C2cloud-init 生效ops 用户可用密钥登录;主机名正确;qemu-guest-agent running
C3VLAN IPVM 内 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
C7LoadBalancerService 有 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
D2backup-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 写得多漂亮,而取决于三件事有没有形成习惯:

  1. IP 与命名有纪律.11-.22 / .50-.99 / .101-.200 / .201-.250 四段各守其位,台账随变更更新。
  2. 网络配置有档案ClusterNetworkVlanConfig、NAD、HostNetworkConfig、模板与 cloud-init 基线全部 YAML 化并入库——重装和排障都靠它。
  3. 验证有脚本:装机后跑第 4 章清单,建 VM 后跑第 5 章清单,批量交付跑第 6 章校验,日常跑 daily-check.sh,出问题跑 net-check.sh。人只负责判断,不负责记忆。

⚠️ 最后一条提醒:书里涉及 CRD 字段名、UI 菜单路径与 Add-on 名称的部分,都基于 Harvester v1.8 官方文档整理。正式落地前,请在你的集群上用 kubectl api-resourceskubectl explain <resource>.speckubectl get <obj> -o yaml 复核一遍字段名——不同补丁版本之间字段偶有增删,以集群实际为准。

祝集群长命百岁,告警群里安静如常。