大带宽服务器自动化初始化的 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——因为这类故障不会在部署当天暴露,只会在业务洪峰的那三分钟里暴露。