裸金属服务器 2Gbps 带宽劣化排障实施手册
定位:公开版实施操作手册。目标:任何人或 AI 只读这一篇,就能在相同场景下完成诊断、修复、验证和持久化,并避开已确认的坑。
场景:裸金属服务器(非虚拟化),公网 IP 直发(非 NAT),2Gbps 出口带宽随连接数增加出现抖动/下降。
实测环境:Debian 12 + xanmod 6.x 内核, 双路 Xeon (72 逻辑核), 251GB RAM, ixgbe 10GbE 单口 + VLAN 子接口, 中继/转发类业务 ~4 万并发连接。
⚠️ 安全声明:本文所有 IP、端口、密码、密钥、节点名均为占位符。请替换为你自己的真实值,不要将真实值提交到公开仓库。
〇、占位符对照表
在执行本手册前,将以下占位符全局替换为你的实际值:
| 占位符 | 含义 | 示例格式 |
|---|---|---|
<SERVER_IP> | 服务器公网管理 IP | 198.51.100.10 |
<SSH_PORT> | SSH 端口 | 22 |
<SSH_USER> | SSH 用户名 | root |
<SSH_PASS> | SSH 密码 | your-password |
<SSH_HOSTKEY> | SSH 主机密钥指纹 | ssh-ed25519 255 SHA256:xxxxx |
<SSH_KEY_PATH> | 本地 SSH 私钥路径 | D:\keys\id_rsa |
<NIC> | 物理网卡名 | enp130s0 / eth0 / ens5 |
<VLAN_IF> | VLAN 子接口名 (如有) | enp130s0.100 |
<BIZ_IP_A> | 业务绑定 IP (主) | 198.51.100.20 |
<BIZ_IP_B> | 业务绑定 IP (副, 如有) | 198.51.100.21 |
<GW_A> | 策略路由网关 A | 198.51.100.1 |
<APP_BIN> | 业务进程二进制名 | my_relay_server |
<APP_PID> | 业务进程 PID | 12345 |
<INSTANCE_A> | 业务实例名 A | relay-primary |
<INSTANCE_B> | 业务实例名 B | relay-secondary |
Windows 端 SSH 辅助函数 (PowerShell + plink)
$HK = '<SSH_HOSTKEY>'
function rvps($c) {
plink -ssh <SSH_USER>@<SERVER_IP> -P <SSH_PORT> -hostkey $HK -pw '<SSH_PASS>' -batch $c 2>&1
}坑 #1 (PowerShell 转义):plink 传递复杂 bash 命令时,$()/$(())/awk '{}'会被 PowerShell 解析。对超过一行的命令,必须用 here-string 写成脚本 → 管道上传 → 远程执行,不要试图在rvps()的引号里硬塞。示例见「上传脚本模式」。
上传脚本模式
$script = @'
#!/bin/bash
echo "hello from remote"
whoami
'@
$script | plink -ssh <SSH_USER>@<SERVER_IP> -P <SSH_PORT> -hostkey $HK -pw '<SSH_PASS>' -batch "cat > /tmp/myscript.sh && chmod +x /tmp/myscript.sh && echo ok"
# 然后执行
rvps "bash /tmp/myscript.sh"一、诊断阶段(只读,不改任何东西)
1.1 环境摸底
uname -a # 内核版本
lscpu # CPU 拓扑、NUMA 节点
free -h # 内存
ip link show # 网卡列表 → 记录 <NIC> 和 <VLAN_IF>
ip addr show # 确认 IP 绑定
ip rule show # 策略路由
ip route show table all | head -50确认业务进程:
ps aux | grep <APP_BIN>
ls /proc/<APP_PID>/fd | wc -l # fd 数
cat /proc/<APP_PID>/status | grep -E 'Threads|VmRSS'
top -bn1 -p <APP_PID> | tail -1 # CPU%
ss -s # 连接总览1.2 网卡硬件层
ethtool <NIC> # 链路速率
ethtool -i <NIC> # 驱动名+版本
ethtool -g <NIC> # ★ Ring Buffer 当前值 vs 最大值
ethtool -S <NIC> | grep -iE 'drop|error|miss|over|fifo' # ★ 硬件丢包计数
ethtool -l <NIC> # 多队列数
ethtool -c <NIC> # coalesce判断矩阵:
| 指标 | 正常 | 异常 → 对应修复 |
|---|---|---|
rx_missed_errors 持续增长 | 0 或极低 | → Fix 1: Ring Buffer |
| Ring Buffer 当前值 远小于 最大值 | 差距 < 2x | → Fix 1: Ring Buffer |
rx_dropped 持续增长 | 0 | → 检查 netdev_max_backlog |
1.3 内核网络栈
# TCP 缓冲区
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_mem
# 队列与连接
sysctl net.core.somaxconn
sysctl net.core.netdev_max_backlog
sysctl net.core.netdev_budget
sysctl net.core.netdev_budget_usecs
# 拥塞控制
sysctl net.ipv4.tcp_congestion_control
# conntrack (如在用)
cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null
cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null1.4 softirq 与中断分布
# IRQ 亲和性
cat /proc/interrupts | grep <NIC> | head -10
# 每个 IRQ 绑定了哪些 CPU?
for irq in $(grep <NIC> /proc/interrupts | awk -F: '{print $1}' | head -5); do
echo "IRQ $irq: $(cat /proc/irq/$irq/smp_affinity_list)"
done
# softnet_stat — 全局快照
cat /proc/net/softnet_stat | head -10
# time_squeeze 10 秒增量
S1=$(cat /proc/net/softnet_stat); sleep 10; S2=$(cat /proc/net/softnet_stat)
# 对比第三列 (squeeze) 的变化
# irqbalance 状态
systemctl is-active irqbalanceNUMA 感知判断:
# NIC 在哪个 NUMA 节点?
cat /sys/class/net/<NIC>/device/numa_node
# 该节点对应哪些 CPU?
lscpu | grep "NUMA"坑 #2 (IRQ 亲和性误判):如果 NIC 在 NUMA node 1,IRQ 绑定到 node 1 的 CPU(如全部奇数核)是 正确的 NUMA-local 策略,不是"分布不均"。不要盲目把 IRQ 打散到所有核——跨 NUMA 访问内存会增加延迟。
1.5 RPS/RFS 状态
cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus
cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus 2>/dev/null
cat /proc/sys/net/core/rps_sock_flow_entries如果 rps_cpus 全零 → RPS 未启用,所有 softirq 仅在硬中断所在 CPU 处理。
1.6 TCP 异常计数器(关键指标)
nstat -az | grep -E 'TcpExtTCPRcvCollapsed|TcpExtPruneCalled|TcpExtListenDrops|TcpExtTCPAbortOnData|TcpRetransSegs'10 秒采样:
C1=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
sleep 10
C2=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
echo "delta=$((C2-C1)) per_sec=$(( (C2-C1)/10 ))"| 计数器 | 含义 | 增长率 > 0 意味着 |
|---|---|---|
TcpExtTCPRcvCollapsed | 内核合并接收缓冲区 skb | 应用 recv() 读取不够快 |
TcpExtPruneCalled | 内核主动丢弃接收缓冲区数据 | 比 Collapsed 更严重 |
TcpExtListenDrops | 监听队列溢出 | somaxconn / backlog 不够 |
TcpRetransSegs | TCP 重传 | 网络质量问题或对端处理慢 |
1.7 定时任务与杂项
crontab -l # 任何 drop_caches / clean_mem ?
dmesg -T | grep drop_caches | tail -5 # 曾触发过 drop_caches?
systemctl --failed # 有没有失败的服务
dmesg -T | grep -iE 'error|oom|panic|call trace' | tail -201.8 VLAN 子接口队列数
ls /sys/class/net/<VLAN_IF>/queues/ 2>/dev/null坑 #3 (VLAN 单队列):VLAN 子接口通常只有 1 个 rx 队列,即使物理网卡有 63 个队列。所有子接口流量只由一个 softirq 上下文处理。这就是为什么 RPS 对 VLAN 场景尤其重要——它是把单队列 softirq 负载分散到多核的唯一途径。
二、诊断结论模板
完成第一阶段后,填写此表:
| # | 问题 | 严重性 | 证据 |
|---|---|---|---|
| 1 | Ring Buffer 过小,rx_missed_errors 持续增长 | ★★★ / 无 | ethtool -g / ethtool -S 值 |
| 2 | TCPRcvCollapsed 持续增长 | ★★★ / 无 | nstat 10s 采样值 |
| 3 | softirq time_squeeze | ★★ / 无 | /proc/net/softnet_stat 值 |
| 4 | RPS/RFS 禁用 | ★★ / 无 | rps_cpus 值 |
| 5 | 定时 drop_caches | ★ / 无 | crontab 内容 |
| 6 | conntrack 表满 | ★★★ / 无 | count vs max |
| 7 | 其他 |
三、修复操作(按优先级排序)
Fix 1: 扩大 Ring Buffer(消除硬件丢包)
前提条件:ethtool -g <NIC> 显示当前值远小于最大值,且 rx_missed_errors 持续增长。
# 记录修复前基线
ethtool -S <NIC> | grep rx_missed
# 执行 (一条命令, 立即生效, 不中断流量)
ethtool -G <NIC> rx 8192 tx 8192
# 验证
ethtool -g <NIC> # 确认 Current = 8192
ethtool -S <NIC> | grep rx_missed # 记录值
# 30 秒后再查一次,确认增量 = 0
sleep 30
ethtool -S <NIC> | grep rx_missed风险:极低。热生效,无需中断网络。
持久化:重启后回退!需要 systemd service:
cat > /etc/systemd/system/nic-ring-buffer.service << 'EOF'
[Unit]
Description=Set NIC ring buffer size
After=network-pre.target
Before=network.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -G <NIC> rx 8192 tx 8192
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable nic-ring-buffer.serviceFix 2: 启用 RPS/RFS(softirq 多核分发)
前提条件:rps_cpus 全零,或 VLAN 子接口仅 1 个 rx 队列。
CPU 掩码计算:
# 获取 CPU 数
nproc # 例如 72
# 72 个 CPU 全覆盖的掩码:
# 72 bit = 9 bytes = 0xff,0xffffffff,0xffffffff
# 通用公式: python3 -c "n=72; h=hex((1<<n)-1)[2:]; print(','.join([h[max(0,len(h)-8*(i+1)):len(h)-8*i] for i in range(len(h)//8,-1,-1) if h[max(0,len(h)-8*(i+1)):len(h)-8*i]]))"# 物理网卡所有 rx 队列
for rxq in /sys/class/net/<NIC>/queues/rx-*/rps_cpus; do
echo "ff,ffffffff,ffffffff" > "$rxq"
done
# VLAN 子接口 (如有)
for rxq in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_cpus; do
echo "ff,ffffffff,ffffffff" > "$rxq"
done
# RFS 全局表
echo 65536 > /proc/sys/net/core/rps_sock_flow_entries
# RFS 每队列表 — 物理网卡
NRXQ=$(ls -d /sys/class/net/<NIC>/queues/rx-* | wc -l)
PER_Q=$((65536 / NRXQ))
for rxq in /sys/class/net/<NIC>/queues/rx-*/rps_flow_cnt; do
echo "$PER_Q" > "$rxq"
done
# RFS 每队列表 — VLAN 子接口
NRXQ_V=$(ls -d /sys/class/net/<VLAN_IF>/queues/rx-* 2>/dev/null | wc -l)
if [ "$NRXQ_V" -gt 0 ]; then
PER_Q_V=$((65536 / NRXQ_V))
for rxq in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_flow_cnt; do
echo "$PER_Q_V" > "$rxq"
done
fi验证:
# 确认配置写入
cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus
cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus
cat /proc/sys/net/core/rps_sock_flow_entries
# 30 秒后检查 softnet_stat — 之前全零的偶数 CPU 应该开始有 processed 计数
# (用 /proc/net/softnet_stat 第一列, 每行一个 CPU)风险:极低。热生效,如果有问题写回 00000000 即可还原。
持久化:重启后回退!systemd service:
cat > /etc/systemd/system/nic-rps-rfs.service << 'EOF'
[Unit]
Description=Enable RPS/RFS on all NIC queues
After=network-pre.target
Before=network.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
for q in /sys/class/net/<NIC>/queues/rx-*/rps_cpus; do echo ff,ffffffff,ffffffff > "$q"; done; \
for q in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_cpus 2>/dev/null; do echo ff,ffffffff,ffffffff > "$q"; done; \
echo 65536 > /proc/sys/net/core/rps_sock_flow_entries; \
for q in /sys/class/net/<NIC>/queues/rx-*/rps_flow_cnt; do echo 1024 > "$q"; done; \
for q in /sys/class/net/<VLAN_IF>/queues/rx-*/rps_flow_cnt 2>/dev/null; do echo 32768 > "$q"; done'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable nic-rps-rfs.serviceFix 3: IRQ 亲和性 / softirq 预算(按需)
先判断是否需要:
systemctl is-active irqbalance # 如果 active → 已在自动平衡
cat /sys/class/net/<NIC>/device/numa_node # NIC 的 NUMA 节点坑 #4 (盲目调整 IRQ):如果 NIC 在 NUMA node 1,irqbalance 把 IRQ 绑到 node 1 CPU 是 最优策略。不要强制打散到所有 CPU。跨 NUMA 内存访问比不均衡更致命。
如果 irqbalance 未运行:
apt install -y irqbalance
systemctl enable --now irqbalancesoftirq 预算(仅在 time_squeeze 持续增长时才调):
# 先看当前值
sysctl net.core.netdev_budget
sysctl net.core.netdev_budget_usecs
# 如果 budget < 600 或 budget_usecs < 4000, 考虑提高
# 但如果已经 >= 1000/8000, 不要动坑 #5 (盲目调参):我们实测发现服务器已有 netdev_budget=1000, budget_usecs=8000(远高于常见建议的 600/4000)。永远先查当前值再决定是否修改。 预设方案的值可能比线上低——"优化"反而是降级。Fix 4: TCP 缓冲区调整(通常已够大)
坑 #6 (最大的教训):我们在实测中发现服务器的 TCP 缓冲区已经是 32MB(而预设方案假设是 4MB 默认值),差点把 32MB 降到 16MB。必须先 sysctl net.ipv4.tcp_rmem 查看当前值,如果已经 >= 预设目标,不要碰。# 先看当前值!
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_mem仅当当前值明显偏小才执行:
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 32768 16777216"
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216回滚(如果不小心执行了):
# 立刻恢复原值 (替换为你之前记录的值)
sysctl -w net.ipv4.tcp_rmem="4096 131072 33554432"
sysctl -w net.ipv4.tcp_wmem="4096 131072 33554432"
sysctl -w net.core.rmem_max=33554432
sysctl -w net.core.wmem_max=33554432持久化:写入 /etc/sysctl.d/99-tcp-tuning.conf,然后 sysctl --system。
Fix 5: 停止定时 drop_caches
# 查找触发源
crontab -l | grep -i 'drop_caches\|clean_mem'
grep -rl 'drop_caches' /etc/cron* /var/spool/cron/ 2>/dev/null
systemctl list-timers | grep -i clean
# 注释掉 crontab 条目
crontab -e
# 在对应行前加 ## 注释
# 验证
dmesg -T | grep drop_caches | tail -3 # 记录最后一次时间
# 等 20 分钟后再查,如果没有新的 → 修复成功为什么不该 drop_caches:在 251GB RAM、仅用 10GB 的机器上,drop_caches 会强制释放所有 page cache/dentries/inodes,导致后续文件 I/O 全部 cache miss。对网络密集型中继服务来说,周期性 cache 抖动 = 周期性延迟毛刺。
四、修复后综合验证
4.1 快速验证脚本
#!/bin/bash
echo "=== 1. Ring Buffer ==="
ethtool -g <NIC> | grep -A4 "Current"
echo "=== 2. rx_missed_errors (应为 0 增长) ==="
V1=$(ethtool -S <NIC> | awk '/rx_missed_errors/{print $2}')
sleep 10
V2=$(ethtool -S <NIC> | awk '/rx_missed_errors/{print $2}')
echo "before=$V1 after=$V2 delta=$((V2-V1))"
echo "=== 3. RPS ==="
cat /sys/class/net/<NIC>/queues/rx-0/rps_cpus
cat /sys/class/net/<VLAN_IF>/queues/rx-0/rps_cpus 2>/dev/null
cat /proc/sys/net/core/rps_sock_flow_entries
echo "=== 4. time_squeeze (10s delta) ==="
T1=0
while read line; do
hex=$(echo "$line" | cut -c19-26)
val=$(printf "%d" "0x$hex" 2>/dev/null)
T1=$((T1+val))
done < /proc/net/softnet_stat
sleep 10
T2=0
while read line; do
hex=$(echo "$line" | cut -c19-26)
val=$(printf "%d" "0x$hex" 2>/dev/null)
T2=$((T2+val))
done < /proc/net/softnet_stat
echo "squeeze_delta_10s=$((T2-T1))"
echo "=== 5. TCPRcvCollapsed (10s delta) ==="
C1=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
sleep 10
C2=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
echo "collapsed_delta_10s=$((C2-C1)) per_sec=$(( (C2-C1)/10 ))"
echo "=== 6. drop_caches ==="
dmesg -T | grep drop_caches | tail -1
echo "=== 7. General ==="
uptime
free -h | head -2
ss -s坑 #7 (mawk vs gawk):Debian 默认 awk 是 mawk,不支持strtonum()。解析/proc/net/softnet_stat的十六进制时,用cut+printf "%d" "0x$hex"替代。上面的脚本已经规避了此问题。
4.2 验证通过标准
| 指标 | 预期 | 未通过则 |
|---|---|---|
| rx_missed_errors 10s 增量 | 0 | 检查 Ring Buffer 是否生效 |
| time_squeeze 10s 增量 | < 10 | 检查 RPS 和 netdev_budget |
| RPS cpumask | 非全零 | 重新写入 rps_cpus |
| drop_caches 最后时间 | 20 分钟前以上 | 检查 crontab |
| TCPRcvCollapsed 速率 | 观测记录即可 | 这是应用层问题,见第五章 |
五、应用层瓶颈识别(TCPRcvCollapsed)
如果内核层修复全部到位、rx_missed=0、squeeze≈0,但 TCPRcvCollapsed 仍持续增长(如 >1000/s) → 几乎可以确定是应用层处理跟不上。
5.1 证据采集
# 进程画像
echo "=== Process profile ==="
cat /proc/<APP_PID>/status | grep -E 'Threads|VmRSS|VmSize'
ls /proc/<APP_PID>/fd | wc -l
top -bn1 -p <APP_PID> | tail -1
# socket 队列分析
echo "=== Recv-Q top 10 ==="
ss -tn | awk 'NR>1 && $2>0' | sort -k2 -rn | head -10
echo "=== Send-Q top 10 ==="
ss -tn | awk 'NR>1 && $3>0' | sort -k3 -rn | head -10
# 连接分布
ss -tn | awk 'NR>1{split($4,a,":"); print a[1]}' | sort | uniq -c | sort -rn | head -105.2 判断逻辑
数据路径:
网卡 → Ring Buffer → 内核 softirq → TCP 协议栈 → socket recv 队列 → 应用 recv()
✅ Fix1 ✅ Fix2/3 ✅ Fix4 ⚠️ 瓶颈在此 ← 应用层如果 TCP 参数已到极致(32MB rmem、somaxconn >100K)、硬件层不丢包、softirq 不 squeeze,那 TCPRcvCollapsed 持续增长 只有一个可能:应用没来得及调 recv(),内核被迫合并 skb 腾空间。
5.3 典型应用层瓶颈特征
| 特征 | 含义 |
|---|---|
| 线程数 << 连接数/100 | 每线程负担的连接过多 |
| CPU < 200% 但连接 > 2 万 | 不是 CPU 瓶颈,是 I/O 调度受限 |
| RSS 恒定不变 | 没有内存泄漏,但也没动态扩展缓冲 |
| Send-Q 积压 > 10MB | 下游拉不走 → write 阻塞 → 线程被占 → 其他连接 recv 延迟 |
级联效应:少量慢客户端 Send-Q 满 → 该连接 write 阻塞 → 线程被占 → 同线程其他连接 recv 延后 → Recv-Q 积压 → TCPRcvCollapsed。
5.4 给应用开发者的建议
- 增加处理并行度:139 线程 / 38K 连接 → 273 连接/线程,偏高。72 核机器可支撑 500+ 线程
- 检查阻塞点:如果 CPU < 200%,说明线程在等 I/O,非阻塞读写分离可能缺失
- 连接级流控:对 Send-Q 持续积压的慢速下游设超时或限速
- 多实例部署:拆端口到多进程,各自 CPU 绑定
六、压力测试监测模板
用 iperf3 做高压测试时,同步监测系统指标:
#!/bin/bash
ROUNDS=8
INTERVAL=30
NIC="<NIC>"
BM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}')
BD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}')
BC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
BP=$(nstat -az TcpExtPruneCalled | awk 'NR==2{print $2}')
BR=$(nstat -az TcpRetransSegs | awk 'NR==2{print $2}')
echo "BASELINE: missed=$BM dropped=$BD collapse=$BC prune=$BP retrans=$BR"
for ((i=1; i<=ROUNDS; i++)); do
echo "==== ROUND $i $(date +%H:%M:%S) ===="
# 流量速率
RX1=$(cat /sys/class/net/$NIC/statistics/rx_bytes)
TX1=$(cat /sys/class/net/$NIC/statistics/tx_bytes)
RP1=$(cat /sys/class/net/$NIC/statistics/rx_packets)
sleep $INTERVAL
RX2=$(cat /sys/class/net/$NIC/statistics/rx_bytes)
TX2=$(cat /sys/class/net/$NIC/statistics/tx_bytes)
RP2=$(cat /sys/class/net/$NIC/statistics/rx_packets)
echo "traffic: rx=$(( (RX2-RX1)*8/INTERVAL/1000000 ))Mbps tx=$(( (TX2-TX1)*8/INTERVAL/1000000 ))Mbps rx_pps=$(( (RP2-RP1)/INTERVAL ))"
# NIC 错误 (与基线比)
CM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}')
CD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}')
echo "nic: rx_missed=$CM(+$((CM-BM))) rx_dropped=$CD(+$((CD-BD)))"
# TCP 计数器
CC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
CP=$(nstat -az TcpExtPruneCalled | awk 'NR==2{print $2}')
CR=$(nstat -az TcpRetransSegs | awk 'NR==2{print $2}')
echo "tcp: collapse=$CC(+$((CC-BC))) prune=$CP(+$((CP-BP))) retrans=$CR(+$((CR-BR)))"
# 系统
echo "load: $(cat /proc/loadavg)"
echo "mem_avail: $(awk '/MemAvailable/{print $2}' /proc/meminfo)kB"
ss -s | head -2
echo
done
echo "==== TOTAL DELTA ===="
FM=$(ethtool -S $NIC | awk '/rx_missed_errors/{print $2}')
FD=$(ethtool -S $NIC | awk '/rx_dropped/{print $2}')
FC=$(nstat -az TcpExtTCPRcvCollapsed | awk 'NR==2{print $2}')
echo "rx_missed: +$((FM-BM)) rx_dropped: +$((FD-BD)) collapse: +$((FC-BC))"七、持久化检查表
| 操作 | 重启后保留? | 持久化方式 |
|---|---|---|
| Ring Buffer 8192 | ❌ | systemd service (见 Fix 1) |
| RPS/RFS | ❌ | systemd service (见 Fix 2) |
| TCP sysctl 参数 | ✅ | /etc/sysctl.d/*.conf |
| netdev_budget sysctl | ✅ | /etc/sysctl.d/*.conf |
| drop_caches 停止 | ✅ | crontab 已注释 |
| irqbalance | ✅ | systemctl enable |
必须确认:ethtool -G和rps_cpus写入在 /sys 下的虚拟文件系统,重启即丢失。如果不做 systemd service,每次重启后必须手动重新执行。
八、已确认的坑汇总
| # | 坑 | 后果 | 正确做法 | ||
|---|---|---|---|---|---|
| 1 | PowerShell 里 rvps() 传复杂 bash | $() 被 PS 解析,` | ` 被拆分 | 写成脚本 → pipe 上传 → 远程执行 | |
| 2 | IRQ 只在奇数核 = "分布不均" | 强行打散 = 跨 NUMA 降性能 | 先查 numa_node,IRQ 在同 NUMA 是正确的 | ||
| 3 | VLAN 子接口仅 1 个 rx 队列 | 所有子接口流量挤一个 softirq | 用 RPS 分散(Fix 2) | ||
| 4 | 不看当前值就调 TCP 参数 | 32MB 降到 16MB = 降级 | 永远先 sysctl 查当前值 | ||
| 5 | netdev_budget 按文章写 600 | 服务器已经 1000,写 600 = 降级 | 同上,先查再调 | ||
| 6 | mawk 没有 strtonum() | 脚本静默失败或输出 0 | 用 printf "%d" "0x$hex" 替代 | ||
| 7 | nohup cmd > file 2>&1 & 在 PowerShell SSH 中 | 重定向被 PS 解析 | 整条命令包在单引号里,或走上传脚本模式 | ||
| 8 | TCPRcvCollapsed ≠ 内核问题 | 在内核参数已极致的前提下,唯一原因是应用 recv() 慢 | 排查应用层而非继续调内核 | ||
| 9 | iperf3 压测期间 Collapsed 倍增 | 正常现象:额外流量加重应用负担 | 关注 rx_missed 和 squeeze,不要被 Collapsed 涨了吓到 | ||
| 10 | ethtool -G / echo > rps_cpus 重启后丢失 | 问题"复发" | 必须做 systemd service 持久化 |
九、实测数据快照(本次行动)
以下数据供参考对比,数值会因环境不同而异。
修复前 vs 修复后
| 指标 | 修复前 | 修复后 | 变化 |
|---|---|---|---|
| Ring Buffer | 512 | 8192 | ×16 |
| rx_missed_errors 增量/30s | 数千~数万 | 0 | 归零 |
| time_squeeze 增量/30s | 数百 | <10 | 近零 |
| RPS even CPU processed/30s | 0 | ~12,000 | 从零到分担负载 |
| TCPRcvCollapsed/s (空闲) | ~7,400 | ~7,400 | 无变化 (应用层) |
| TCPRcvCollapsed/s (iperf3 压测) | — | ~30,000 | 高压下倍增 (预期) |
iperf3 高压测试期间 (8 轮 × 30s)
| 指标 | 结果 |
|---|---|
| 平均流量 | rx ≈ 2.7 Gbps, tx ≈ 2.7 Gbps |
| rx_missed_errors | +0 (全程零增长) |
| rx_dropped | +42 (极少, 可忽略) |
| TCPRcvCollapsed | +2,060,664 (应用层, 预期) |
| TcpRetransSegs | +416,540 |
| 系统负载 | 2.1~3.4 (72 核, 完全正常) |
| 内存 | 241 GB 可用 (无压力) |
结论:内核/网卡层修复在高压下完全稳定。rx_missed 全程归零,squeeze 近零。TCPRcvCollapsed 增长仅反映应用层在高压下更来不及消费数据。
十、故障排查决策树
带宽下降/抖动
├── ethtool -S <NIC> | grep rx_missed → 持续增长?
│ ├── 是 → Fix 1: Ring Buffer
│ └── 否 ↓
├── /proc/net/softnet_stat 第 3 列 → squeeze 持续增长?
│ ├── 是 → 检查 IRQ 亲和性 + RPS → Fix 2/3
│ └── 否 ↓
├── nstat TcpExtTCPRcvCollapsed → 持续增长?
│ ├── 是 → 内核 TCP 参数已经很高?
│ │ ├── 否 → Fix 4: 调大 tcp_rmem 等
│ │ └── 是 → 应用层瓶颈 → 第五章
│ └── 否 ↓
├── nstat TcpExtListenDrops → 持续增长?
│ ├── 是 → 调大 somaxconn / backlog
│ └── 否 ↓
├── conntrack count ≥ 80% max?
│ ├── 是 → 调大 conntrack_max + hashsize
│ └── 否 ↓
├── tc qdisc show → 有 tbf/htb 限速?
│ ├── 是 → 调整或移除
│ └── 否 ↓
└── dmesg | grep -i error → 硬件故障?
├── 是 → 联系供应商
└── 否 → 抓包分析具体连接手册版本: 2026-04-06 v1.0 — 基于火山引擎裸金属实测