大带宽服务器自动化初始化的 4 个静默坑:巨帧 MTU 改反顺序、ethtool -L 热改不幂等、irqbalance 抢走 25G 中断、SNMP 计数器 3.4 秒回绕

大带宽服务器的自动化运维,坑不在"跑不起来",而在"跑起来了、脚本全绿、监控正常,业务一上量就废"。10G/25G 网卡上,很多配置项的失败是静默的——命令 exit 0,日志没报错,但实际没生效。下面四个坑,是我在批量交付 25G 机器时踩过的,每个都配了能直接抄的验证命令。

坑 1:巨帧 MTU 改错顺序,ping 通不代表通

先算个账:1GB 数据在 1500 MTU 下要切约 71.6 万个包,9000 MTU 只要约 11.9 万个。25G 线速跑满时,PPS 直接差 6 倍,这个不是"优化",是能不能跑满的前提。

但新手最容易在这里翻车的三件事,全都不是内核的问题:

  • 只 ping 了 56 字节就收工。ICMP 默认 payload 56,1500 MTU 的链路照样通。必须带 -M do 禁分片,把包撑到 9000 才叫验证过。
  • 只改了主机侧,交换机/对端没改。LACP 照样协商成功,链路 up,小包全通,大包 100% 丢。这是最恶心的一种,因为 ip -s link 里 errors 都不涨,丢在上游设备。
  • 改 MTU 触发 carrier 抖动,Ansible 把任务判成 unreachable。 任务实际执行成功了,但 SSH 连接在返回前断了。第二次重跑时幂等检查发现"已经是 9000",输出 ok 绿色,于是没人知道第一次到底跑没跑。
# 正确的巨帧验证(IPv4:9000 - 20 IP头 - 8 ICMP头 = 8972)
ping -M do -s 8972 -c 3 10.0.0.1

# IPv6 走 8952(9000 - 40 - 8)

# 批量核对 bond 与 slave 的 MTU 是否一致
IF=bond0
echo "master mtu=$(cat /sys/class/net/$IF/mtu)"
for s in $(cat /sys/class/net/$IF/bonding/slaves); do
  printf "%s mtu=%s\n" "$s" "$(cat /sys/class/net/$s/mtu)"
done

自动化里的正确处理:改 MTU 的任务一律走 async 异步下发(poll: 0),或者干脆用带外/IPMI 通道执行,别和 SSH 会话赌运气。

还有一个隐藏项:Overlay 网络的 MTU 没跟着改。VXLAN 封装吃 50 字节,GRE 吃 24 字节。底层 9000、上层还写着 1450,跨节点大包一样丢。CNI 的 mtu 参数是独立配置项,不在 /sys/class/net 里,自动化模板里必须单独一个变量管它。

坑 2:ethtool -L 热改不幂等,ignore_errors 一挂就是永久性配置缺失

先看现状,重点是对比 Pre-set maximums 和 Current hardware settings:

ethtool -l ens1f0
# Channel parameters for ens1f0:
# Pre-set maximums:
# Combined:       16
# Current hardware settings:
# Combined:       4      <-- 默认只给了 4 个队列,25G 打不满

坑在哪:很多驱动(mlx5、部分 ixgbe 固件)不支持接口 up 状态下热改队列数,直接返回:

Cannot set device channel parameters: Device or resource busy

而在 Ansible 里如果写了 ignore_errors: yes 或者 failed_when: false,这个任务永远绿,队列数永远停在 4。更糟的是——ethtool 的配置本来就不持久化,就算这次改成功了,重启后驱动重置回默认值,而你的自动化只在装机时跑一次。

正确姿势:先 down 再改,用 systemd oneshot 保证每次开机都做幂等校验。

#!/bin/bash
# /usr/local/sbin/nic-channels.sh
set -u
WANT=16
for dev in ens1f0 ens1f1; do
  cur=$(ethtool -l "$dev" | sed -n '/Current hardware settings/,$p' | awk '/Combined:/{print $2; exit}')
  if [ "$cur" = "$WANT" ]; then
    echo "$dev already $WANT"
    continue
  fi
  ip link set dev "$dev" down
  if ethtool -L "$dev" combined "$WANT"; then
    ip link set dev "$dev" up
    echo "$dev set to $WANT"
  else
    ip link set dev "$dev" up
    echo "FAILED $dev" >&2
    exit 1
  fi
done

注意:如果在 bond 里,down 单个 slave 会引发一次聚合成员摘除,务必确认是主备模式还是 802.3ad,交付窗口内做。

改完必须验证队列真的在收包,否则你只是改了数字。各家驱动的字段名还不一样(ixgbe 是 rx_queue_N_packets,mlx5 是 rxN_packets),先 grep 再写正则:

ethtool -S ens1f0 | grep -E 'queue|rx[0-9]+_packets' | head -20

这里有个新手高频误判:用 iperf3 -c ... 单流跑,发现只有 queue_0 在涨,就断定 RSS 没生效去改中断。错。RSS 是按四元组哈希的,单条 TCP 流天然只落一个队列。验证必须多流:iperf3 -c 10.0.0.1 -P 16 -t 30,能看到 16 个队列均匀分布才算过。

坑 3:irqbalance 一秒钟就抢走你刚写的中断亲和

脚本里往 /proc/irq/N/smp_affinity 写了掩码,cat 出来确实变了,看着挺好。但 irqbalance 默认在跑,下一秒就给你重排回去。两个坑叠在一起:

  • 你写的是掩码,irqbalance 读的是列表,两者格式不同。smp_affinity 是十六进制位图(CPU0 = 1,CPU1 = 2,CPU0-3 = f),smp_affinity_list 是十进制列表(0-3)。掩码写错会返回 EINVAL,而自动化里又是 ignore_errors,静默失败。
  • 网卡改名后,脚本 grep 不到中断,静默 continue。自动化镜像开了 net.ifnames=0 之后,ens1f0 变成 eth2,脚本拿 grep eth2 /proc/interrupts 直接空手而归——因为 mlx5 的中断名压根不带网卡名,长这样:mlx5_comp0@pci:0000:3b:00.0。ixgbe 才是 ens1f0-TxRx-0 这种。

唯一稳的定位方式是走 PCI BDF,绕开设备名:

IF=ens1f0
BDF=$(basename "$(readlink -f /sys/class/net/$IF/device)")
echo "BDF=$BDF"   # 0000:3b:00.0

grep "pci:$BDF" /proc/interrupts | awk -F: '{print $1}' | while read -r irq; do
  printf "irq %-5s aff=%s\n" "$irq" "$(cat /proc/irq/$irq/smp_affinity_list)"
done

落地策略二选一,别混着来:

  • 保留 irqbalance,用 /etc/sysconfig/irqbalance 里的 IRQBALANCE_BANNED_CPULIST 把留给 DPDK/业务绑核的 CPU 排除掉,让 irqbalance 自己在这之外找平衡。这是运维成本最低的做法。
  • 彻底关掉,用 systemd oneshot 按 NUMA 拓扑手工绑死。注意网卡所在的 NUMA 节点:cat /sys/class/net/$IF/device/numa_node,跨节点绑中断在 25G 上是实打实的性能损失。

坑 4:SNMP 抓了 32 位计数器,10G 上 3.4 秒回绕一次

这个坑最隐蔽,因为监控图"有数据",只是数据是错的。算一下:32 位计数器上限 232,也就是 4294967296 字节,10Gbps 满速时回绕周期 = 4294967296 × 8 ÷ 10×10⁹ ≈ 3.43 秒。你的轮询间隔是 60 秒,采回来的值是模 2³² 的余数,画出来的曲线在 0 到 4.29GB 之间随机跳,看起来像"流量剧烈波动"。

OID 必须换成 Counter64 版本:

# 错误:32 位(IF-MIB 老 OID)
# ifInOctets  1.3.6.1.2.1.2.2.1.10
# ifOutOctets 1.3.6.1.2.1.2.2.1.16

# 正确:64 位
# ifHCInOctets  1.3.6.1.2.1.31.1.1.1.6
# ifHCOutOctets 1.3.6.1.2.1.31.1.1.1.10

snmpget -v2c -c public 10.0.0.1 1.3.6.1.2.1.31.1.1.1.6

Counter64 必须用 SNMPv2c 或 v3。SNMPv1 协议层不支持 64 位整数类型,snmpget -v1 取这个 OID 会直接返回 noSuchName 或者 0——很多老设备的 v1 兼容模式就是这么坑人的。升级 snmpd 配置的时候顺手把 syslocation 之外的东西也看一下,别只改 rocommunity。

另外两件事:

  • Zabbix/Cacti 的模板里要勾上 "64bit counters" 选项,只换 OID 不勾选项,模板还会按 32 位做差值计算,等于白换。
  • 自动化巡检取本地计数器,别解析 ifconfig 的文本输出——部分 net-tools 版本在 32 位字段溢出时会打印负数。直接用 JSON 输出:ip -s -j link show dev ens1f0,取 stats64.rx.bytes,是干净的无符号 64 位整数。

交付前 60 秒自检脚本

这套检查我固定在装机流程最后一步跑,出机即验。轻云互联那批 25G 大带宽机器交付时,我们的做法是把下面这段塞进 cloud-init 的 runcmd 末尾,跑不过就不许进调度池——因为这个阶段机器是干净的,任何一项不达标都说明是模板问题,不是业务问题。

#!/bin/bash
# /usr/local/sbin/bw-selfcheck.sh
set -u
IF=${1:-ens1f0}
FAIL=0

echo "== MTU =="
M=$(cat /sys/class/net/$IF/mtu); echo "mtu=$M"
[ "$M" -ge 9000 ] || { echo "  WARN: mtu below 9000"; FAIL=1; }

echo "== channels =="
ethtool -l $IF | sed -n '/Current hardware settings/,$p'

echo "== irq count (by BDF) =="
BDF=$(basename "$(readlink -f /sys/class/net/$IF/device)")
N=$(grep -c "pci:$BDF" /proc/interrupts || true)
echo "irqs=$N"
[ "$N" -ge 4 ] || { echo "  FAIL: too few irqs, check channels"; FAIL=1; }

echo "== discard/drop =="
ethtool -S $IF | grep -Ei 'discard|drop|err' | grep -v ': 0$' || echo "  clean"

echo "== stats64 =="
ip -s -j link show dev $IF | jq -c '.[0].stats64'

exit $FAIL

最后一句提醒:大带宽机器的问题,几乎全都出现在"命令成功了但没生效"这一层。自动化脚本里每一个 ignore_errors 都是一颗定时炸弹,凡是涉及队列数、MTU、中断、计数器的任务,宁可直接 fail,也不要 ignore——因为这类故障不会在部署当天暴露,只会在业务洪峰的那三分钟里暴露。