← 返回博客

2026年软路由性能瓶颈定位指南:softnet_stat、中断分布、conntrack 表与多 WAN 分流的逐段取证

📌 一分钟总结

软路由慢不等于性能不够:用 softnet_stat、中断分布与连接跟踪表逐段取证,分清转发面、表容量、上游线路与分流四类瓶颈。

软路由变慢以后最贵的处理方式是直接换硬件。但同一台机器上的「慢」至少分成四类,证据完全不同:转发面被 CPU 吃满、连接跟踪表撞上限、上游线路本身不达标、目标服务在限速。换硬件(CPU、网卡)只能影响第一类里的一部分,剩下三类换了也白换。

所以这篇不做优化(调参手法是另一条路线),只做一件事:把「瓶颈在哪一段」用可以被复核的计数器读出来。下面所有读数都来自 /proc,不需要额外装东西,普通 Linux 主机与 OpenWrt 的读法一致——软路由上唯一不同的是要 ssh 进去敲。

一、先立规矩:四类慢分开看,计数器只对其中一类负责

「慢」是一个结果,不是一个证据。先把它拆成四类,再决定看哪张表,否则你会在错误的层面上找半天。

类别典型现象该看的证据结论怎么下
转发面 CPU 饱和多设备同时用就慢,单台设备也慢;高负载时网页转圈软中断计数、time_squeeze、单核占用、中断分布第二节到第四节:计数器涨在哪个核,瓶颈就在哪一段
表容量撞顶用久了开始丢新连接,重启就恢复;测速正常但应用异常连接跟踪条目数与上限、内核日志第六节:条目数贴近上限并伴随丢包日志,就是这一类
上游线路不达标任何时段都跑不快,或只有晚高峰掉换订阅/换出口做对照,本机计数器全绿本机没有丢包、没有挤压、表也不满 → 问题不在路由器
目标服务限速只有某个站慢,其它站正常单站点与多站点对比、连接级读数同一出口对其它站正常,就是服务侧

出口侧那两类,最快的排除办法是换一份已知可用的订阅做对照:同一台机器、同一个目标、同一个时段,只换订阅。这一步能把「路由器的问题」和「线路的问题」一刀切开,比在路由器上翻日志快得多。对照用的小流量档就够,比如瑶瑶领先的入门档(¥9.9 / 384GB / 3 台设备),它的作用是当参照物,不是长期方案。

剩下这两类都在路由器本机,排查顺序按成本从低到高排成固定的四步:先看有没有丢包或挤压(第二节),再看是不是单核饱和(第三、四节),然后看是不是表满或路径被旁路(第五、六节),最后才轮到多 WAN 分流(第七节)。顺序别倒——先查分流的人,最后往往发现瓶颈根本不在分流上。

二、软中断与 NAPI:先看有没有人在「挤压」

网卡收包早就不是「来一个包中断一次、中断里处理完」这种模型了。现代内核走 NAPI:中断只负责把设备挂进轮询队列,真正的处理在软中断(NET_RX)里批量做。这带来一个反直觉的后果:包太多的时候,CPU 占用看起来不高,包却已经在丢。原因就是批量处理有预算,预算用完就得让出 CPU,剩下的包要么排队、要么被丢。

这笔预算由三个参数控制,默认值都在 sysctl 文档里写死:

$ sysctl net.core.dev_weight net.core.netdev_budget net.core.netdev_budget_usecs
net.core.dev_weight = 64
net.core.netdev_budget = 300
net.core.netdev_budget_usecs = 2000

三个值的含义(来自内核 admin-guide/sysctl/net.rst):dev_weight 是「内核在一次 NAPI 中断里最多处理多少个包」的每 CPU 变量,默认 64(支持 LRO/GRO_HW 的驱动里,一个硬件聚合包只算一个);netdev_budget 是一次 NAPI 轮询周期里从所有接口最多取走多少个包(按轮询方式逐个接口探测);netdev_budget_usecs 是一次轮询最多占用多少微秒——两个上限谁先到就以谁为准,到点就让出 CPU。三者共同决定「这轮收包干活的上限」。

再看计数表。这份表不看总量,只看每 CPU 的挤压情况:

$ cat /proc/net/softnet_stat
00037356 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00033423 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 00000000 00000000
...
053173f5 00000000 000007a0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000006 00000000 00000000

一行一个 CPU,十六进制。列的含义不能靠猜,直接看内核源码 net/core/net-procfs.c 里 softnet_seq_show() 的 seq_printf 顺序:

列含义怎么用
1该核处理过的包总数(processed)看分布是否均衡
2丢弃数(drop,含 NUMA 相关丢包)非零=队列溢出,这是硬伤
3挤压数(time_squeeze)非零增长=预算用完了还有活没干
4–9保留位(曾是 fastroute、cpu_collision)全 0,忽略
10经由 RPS 转给本核处理的包数判断 RPS 是否真的生效
11flow limit 命中次数单流限速(每 CPU 独立)触发过几次
12backlog 队列长度(input + process)持续非零=收不过来、在排队
13该行对应的 CPU 编号新版内核才有,省得自己对行号
14–15input 队列长度、process 队列长度拆开看排队发生在哪一段

拿这台机器(8 核 i5-8265U,内核 6.17)当例子,把 1、2、3、13 列读出来:

cpu0: processed=226,220  dropped=0  time_squeeze=0
cpu1: processed=244,817  dropped=0  time_squeeze=0
cpu2: processed=211,191  dropped=0  time_squeeze=0
cpu3: processed=226,952  dropped=0  time_squeeze=0
cpu4: processed=225,939  dropped=0  time_squeeze=0
cpu5: processed=227,830  dropped=0  time_squeeze=0
cpu6: processed=87,215,777 dropped=0 time_squeeze=1,952
cpu7: processed=278,850  dropped=0  time_squeeze=0

这里同时出现了两个信号。第一,分布极度不均:CPU6 处理了 8715 万个包,另外 7 个核各 20 多万个,差 300 倍以上。第二,只有 CPU6 累计了 1952 次挤压,其它核一次都没有。换句话说,这台机器的收包路径几乎是「一个核在扛」,而且那个核偶尔会干不完活——原因在第四节,是中断绑定的问题。

判读规则可以固化成四条:

看到什么说明什么往哪查
第 2 列(丢弃)非零且在涨backlog 队列满了,包在队列层就被丢先把 netdev_max_backlog 提高,再看是不是单核挤压(第三节)
第 3 列(挤压)在涨预算不够,一轮软中断干不完dev_weight / 中断分布 / 是否该开 RPS(第四节)
分布悬殊(某核是其它核的几十倍)中断或队列绑死在少数核上/proc/interrupts(第四节)
全部为 0 或只有零星计数瓶颈不在这层跳到第五节,看路径与表容量

有一点要提前说清楚:softnet_stat 是按 CPU 统计「所有」设备的,包含 lo,所以它的总量一定大于 ethtool -S 里单个网卡的收包数。这两组数字永远对不上,别拿它们互相验算,只看各自的趋势。

三、单核饱和:把「哪个核在忙、忙在哪」量出来

第二步是把上面那张表的「不均」量化:哪个核在忙、时间花在哪儿。不用装 mpstat,/proc/stat 前后各读一次做差就够:

import time
def snap():
    out = {}
    for line in open('/proc/stat'):
        if line.startswith('cpu') and not line.startswith('cpu '):
            f = line.split(); out[f[0]] = list(map(int, f[1:11]))
    return out
a = snap(); time.sleep(10); b = snap()
print(f"{'cpu':>4}{'busy%':>8}{'softirq':>10}")   # softirq = 第 7 个字段的增量
for c in sorted(a, key=lambda x: int(x[3:])):
    d = [y - x for x, y in zip(a[c], b[c])]
    tot = sum(d[:8])
    print(f"{c[3:]:>4}{100.0*(tot-d[3]-d[4])/tot if tot else 0:>8.1f}{d[6]:>10}")

同一个脚本,一边跑「一条下载流」、一边跑「四条并发流」,结果完全不一样(本机回环台架,服务端钉在 CPU6,各采样 10 秒,单位是这段时间内的增量):

场景参与收包的核NET_RX 软中断增量分布说明
一条流1、3、6 三个核5094 / 10898 / 13391,其余 5 个核 0–1单条流只会落到少数核上,其它核全程旁观
四条并发流1、2、3、4、5、6、7 七个核最高 12141、最低 524,摊开明显并发流一起来,核才被用上

为什么单条流只能占少数核?两个原因叠在一起。第一,软中断的执行上下文属于某个 CPU,某个核的队列只能由它自己跑,别人的核帮不上忙——这就是「单核饱和」在路由上普遍存在的原因。第二,RPS(Receive Packet Steering,软件收包分发)的哈希是按流算的:内核文档 networking/scaling.rst 里明确写着,「把同一条流的所有包送到同一个 CPU」的代价是,当某条流的包速率远高于其它流时会出现 CPU 负载不均,极端情况就是单条流吃掉全部流量。所以 RPS 能帮你摊开并发连接,摊不开单条流。

结论也很实用:单线程下载跑不满带宽,不等于路由器不行。判断转发能力要用多流,取吞吐时也要按多线程口径。测法本身(单线程/多线程基线、晚高峰采样、记录表)在机场线路带宽自测手册里有一套完整流程,可以直接照搬。

还有一个对照实验特别好用:把出口这一段从路由器上「摘出去」,用一台设备直连客户端软件,绕开软路由的转发与代理。如果直连快、过路由器慢,瓶颈在路由器;两边一样慢,问题在线路或目标站。做这个对照时不用非得用机场,客户端方案也能当参照物——比如自由猫走自家客户端(它不适合装到软路由上做透明代理,但装在电脑手机上做「对着看」的参照正合适)。两类方案比一比,结论就清楚了。

四、中断与队列:一个核扛一张网卡是常态

续上一节:既然分布长得那么歪,就得去看中断到底落在哪。这张表的意思是「每个中断号在 8 个核上分别计数了多少次」:

$ grep -i iwlwifi /proc/interrupts
 137:  0  0  0  0  0  0  28045407  0  IR-PCI-MSI-0000:00:14.3  0-edge  iwlwifi

2.8 千万次中断,全部落在 CPU6,其它 7 个核一次都没有。这就是第二节里那 8715 万个包和 1952 次挤压的来源:中断绑定的核,同时也是收包软中断干活的核,两个负担叠在同一个 CPU 上。

再看硬件能不能分摊。网卡队列是软件分发的物理基础:

$ ls /sys/class/net/wlp0s20f3/queues/
rx-0
tx-0
$ cat /sys/class/net/wlp0s20f3/queues/rx-0/rps_cpus
00

只有一个收包队列、一个发包队列,而且 RPS 是关的(00 表示没有任何 CPU 参与软件分发)。单队列设备没法用硬件 RSS 把包分给多个核,只能靠 RPS 在软件层再分一次——而它是关闭的。这解释了为什么一个核独自扛了整张网卡。四类分发机制各自负责哪一段,区别如下(依据内核 networking/scaling.rst):

机制在哪一层分前提能否摊开单条流
RSS网卡硬件按四元组哈希分到多个队列网卡支持多队列不能,同一条流固定进同一个队列
RPS软件把每队列的包再按流哈希分给多个 CPU写 rx-N/rps_cpus 掩码不能,但能把多条流摊到多核
RFS按「应用线程所在 CPU」反过来决定包给谁先设 rps_sock_flow_entries 与每队列 rps_flow_cnt不能,但能减少跨核带来的缓存开销
XPS发送方向按 CPU 选发送队列多发送队列不涉及收包

把这套用在软路由上,三个高频现象就都能解释了:一是家用一体化路由或小主机常配单队列网卡(USB 网卡、无线、部分千兆口),天生摊不开;二是多个核在 /proc/interrupts 里全是 0,只有一个核有数字,说明中断没绑开;三是「把代理打开就慢」很多时候不是代理算法慢,而是代理进程本身也被调度到那个已经扛着中断和软中断的核上,进一步抢 CPU。

顺带说一句观测与开销的关系:路由上做精细统计(按设备、按连接、按协议)本身也要花 CPU,有些统计方式还会让加速功能失效。要按设备长期记账、又要留住加速能力,取舍方式在软路由流量监控与带宽归因里讲得比较细,两边配合看更省事。

五、转发路径:flow offloading 加速了什么、没加速什么

软路由的两条转发路径是完全不同的东西,混在一起谈就会出现「我明明开了硬件加速,代理还是慢」。

第一条是内核转发(纯路由/NAT)。OpenWrt 用 firewall4 生成 nftables 规则,开启 offload 后,转发链上会多出一条把已建立流交给 flowtable 的规则,源码里就是一行:转发链上对 TCP/UDP 执行 flow offload @ft,同时在建规则时生成 flowtable ft { devices = { ... } }。nftables 官方文档对 flowtable 的定义很直接:它靠基于 conntrack 的网络栈旁路来加速转发,条目由「入接口 + 源/目的地址 + 源/目的端口 + 三四层协议」构成,还会缓存目的接口和网关地址。也就是说,被卸载之后的包走的是旁路,不再走常规转发路径。

三条容易踩的实现细节,都能从源码读出来:第一,flow_offloading 与 flow_offloading_hw 的默认值都是 0,即默认全关;第二,硬件卸载不是「勾上就生效」,firewall4 会先用 nft -c 试建一个带 flags offload 的 flowtable 来验证支持性,不支持就打一条日志(原文是 Hardware flow offloading unavailable, falling back to software offloading)并自动降级回软件卸载;第三,软件卸载的设备清单是从防火墙区域里已存在的物理设备解析出来的,只要设备在 /sys/class/net 里就会被纳入。

第二条路径是用户态代理。透明代理(redirect / TPROXY)会把 LAN 过来的连接交给路由器本机的 socket,再由代理进程自己发起对外连接。这条流量不进转发链,因此 flowtable 那行规则对它无效——offload 能加速的是「LAN 直连出去的转发流量」,被代理的那部分该走用户态还是走用户态。反过来说,被 offload 卸载的流也不再逐个经过 netfilter 的常规钩子,所以限速、统计、按连接限速这类基于钩子的功能,会看不到它们的字节数。

这一节对代理面的实际意义是:如果你要在软路由上做透明代理,代理程序能不能被插件以标准订阅方式导入,直接决定你后面排查的复杂度。以 OpenWrt 为例,PassWall / OpenClash 这类插件走的是「粘贴 Clash 订阅链接 → 自动更新节点 → 选低延迟节点」的标准流程,订阅能直接导入就少一层转换;SS-ID这类官方支持 OpenWrt(含梅林等全平台)的机场,订阅直接进插件即可,纯 IEPL 线路也省得再判断线路层级的差别(线路标签怎么分等级,见国际线路科普)。

如果你的代理是「自建 + 代理插件」混合架构,转发与代理的边界会比纯路由器复杂:自建节点的排障顺序(五段链路、三个正交指标)在自建节点晚高峰分段排障里有完整取证表,和本篇的「本机转发面」是两段互补的链路。

六、连接跟踪表:路由器最容易撞上的隐形上限

前面几节讲的是「每个包怎么被处理」,这一节讲容量层面的瓶颈,它更像天花板:平时看不出来,一到全家设备同时在线就开始丢新连接。

Linux 的连接跟踪(conntrack)给每条连接建一个条目。这件事在软路由上有两个自带属性:第一,条目数有硬上限;第二,条目是成对占用的。内核文档写得很清楚:连接跟踪条目会往表里加两次,一次是原始方向、一次是应答方向(地址相反),所以表满时平均哈希链长度是 2 而不是 1。也就是说 6 万条连接的机器,实际上占了 12 万个条目。

上限怎么定?文档里给的是:哈希表大小(buckets)默认按「内存总量 ÷ 16384」计算,下限 1024、上限 262144,而且这个 sysctl 只在初始网络命名空间里可写;条目上限默认等于 buckets。按这个公式算一遍常见路由器内存,规模差异非常大:

内存哈希桶(内存 ÷ 16384)折算成连接数(条目成对占用)典型设备
128 MB8192约 4000 条老式小内存路由
256 MB16384约 8000 条入门软路由 / 老款硬路由
512 MB32768约 1.6 万条主流家用硬路由
1 GB65536约 3.3 万条入门级 x86 软路由、R2S 一类
2 GB131072约 6.5 万条常见的 x86 小主机
4 GB 及以上262144(到顶)约 13 万条高配 x86,再往上加内存也没用

一半的条目额度听起来还多,但现实里消耗得很快:一个浏览器同时开十几个标签就是几十条,PT/下载器挂着几百上千条,再加上全家手机、电视、摄像头轮询,两三万条并不难到。更麻烦的是「用完不还」——连接不会自己消失,靠超时回收。内核文档里 tcp_timeout_established 的默认值是 432000 秒,也就是五天;OpenWrt 则在自己的 sysctl 文件里把它改成 7440 秒(约 2 小时 4 分),同时把 UDP 超时设成 60 秒、流式 UDP 设成 180 秒,并打开流量记账:

net.netfilter.nf_conntrack_acct=1
net.netfilter.nf_conntrack_checksum=0
net.netfilter.nf_conntrack_tcp_timeout_established=7440
net.netfilter.nf_conntrack_udp_timeout=60
net.netfilter.nf_conntrack_udp_timeout_stream=180

判读就两条命令加一条日志。条目数与上限:cat /proc/sys/net/netfilter/nf_conntrack_count 与 nf_conntrack_max,或者对全部条目按状态统计(/proc/net/nf_conntrack)。表满的行为内核源码里写得很直白:条目数超过上限时先尝试提前回收;回收不到就直接把包丢掉,并按限速打印内核日志 nf_conntrack: table full, dropping packet(在非初始命名空间里会带上命名空间编号)。所以「网页转圈但测速正常、重启就好」这种症状,第一件事就是查这两行和 dmesg。

需要说明的是,本机(普通 Linux 桌面)默认没加载 conntrack 模块,/proc/sys/net/netfilter/ 下只有零星几个文件,nf_conntrack_count 根本不存在——这本身就是一条判据:看不到那个文件,说明这台机器没在跟踪连接,容量问题就与它无关。软路由上只要跑过 NAT 或透明代理就一定在跟踪,那两个文件必然存在。

还有一类问题也常被塞进「带宽不够」里,但方向完全不同:出口 IP 的属性。跨境账号、流媒体、部分风控接口看的是「你从哪个 IP 出去」,不是「你有多少带宽」,所以把内存加满、把 offload 打开都不会有改善。排查到最后如果确认本机计数器全绿、目标站仍然给特殊待遇,那要解决的是出口属性而不是性能——仙路湾提供的原生 IP 与住宅 IP 两档就是针对这一类(原生 IP 档 50GB 起、住宅 IP 档 100GB 起),它解决的是「服务侧怎么看你」,跟转发性能是两条线。

七、多 WAN 与分流:把「哪条路在慢」量出来

最后一段是分流。双线、多拨、旁挂一路 5G,本意是「宽带叠加 + 冗余」,但排查时经常变成新的噪声源,因为你不知道慢的是哪条路。mwan3 是 OpenWrt 上最常见的多 WAN 方案,它自带一组查询命令,比翻日志快(子命令清单来自上游脚本 files/usr/sbin/mwan3):

命令看什么排查用途
mwan3 interfaces每个 WAN 的在线/离线状态与当前带宽先确认某条线是不是已经被判死
mwan3 policies当前生效的策略与成员确认流量该走哪条路
mwan3 rules生效的规则列表看哪条规则先匹配、优先级如何
mwan3 status全部状态汇总一次看全
mwan3 use <iface> <cmd>把命令绑定到指定接口执行在不改配置的前提下,单独测某一条线

第二条线索是「粘性会话」,这里有个容易误解的地方。sticky 不是把一条连接粘住,而是按源 IP粘:上游实现里为每条带 sticky 1 的规则建一个 hash:ip,mark 的集合,默认超时 600 秒,同一个源地址在这段时间内保持走同一条成员线路。OpenWrt 默认配置里就带着一条针对 443 端口的粘性规则、策略是 balanced——意味着「你以为 HTTPS 会被均匀分摊,实际上按源 IP 各粘一路,600 秒内不变」。这不算错,它对支付、登录这类需要固定出口的场景反而是好事,但如果你用并发下载来验证叠加效果,就要意识到粘性会压住均衡。

第三条线索最好用:mwan3 的规则和策略都会落到带计数的链上(链名形如 mwan3_rules、mwan3_rule_ 前缀、mwan3_policy_ 前缀、mwan3_iface_in_ 前缀),用 iptables -t mangle -L -v -n 或 nft 看每条链的包数与字节数,就能判断「流量实际走了哪条策略」——预期走 A 路的规则,计数却在 B 路的成员链上增长,问题就出在规则顺序或标记上,不用猜。这一套配置与调平的完整写法(权重、健康检测、粘性会话、上线前压测)在单线多拨与宽带叠加里更系统。

分流没生效通常只有两种形态,分开看能省很多时间。第一种是「规则没命中」:规则按顺序匹配,一条更靠前的规则先把流量拿走了,后面的策略再对也没用;判断方法是看 mwan3 rules 输出的顺序,再对着策略链的计数看流量实际落进了哪条链。第二种是「标记丢了」:mwan3 用报文标记(firewall mark)配合路由表决定出口,任何会重写或清掉标记的环节——另一套防火墙规则、隧道封装、某些加速功能——都会让流量掉回默认路由。这类问题的典型表现是「策略显示已生效,但流量一直走主 WAN」。

最省事的验证方式是逐条单测:用 mwan3 use 把一次下载绑定到指定接口跑一遍,再不加绑定跑一遍,两次结果一致说明分流没起作用,两次不同说明分流生效、问题在别处。这一步能在改动任何配置之前就把范围缩小一半。

还要提醒一句:健康检测(track_ip、reliability)判断的是「探测目标可达」,不是「这条线对当前业务好用」。运营商侧限速、跨网劣化、仅对某些目标不可达,都可能让一条线在检测里显示在线、实际却不能用于某个业务。所以多 WAN 场景下的定位顺序应该是:先用 mwan3 use 逐条单测,再回到本文前面几节的计数器上找原因,别一上来就调权重。

多 WAN 与代理的位置也要摆清。宽带叠加解决的是「同一时间能承载多少总量」,它不会让单条跨境连接变快——一条流只走一条线,出口还在代理的另一端,两条宽带加起来不等于单线程更快;会变快的是「很多条流同时跑」的场景(多设备、下载器、多人同时在线)。如果你的诉求是「单条连接更稳」,要动的是出口侧与线路层级,不是加第二条宽带。反过来,多 WAN 对代理也有实际好处:某条线掉线时整机不至于全断,前提是分流策略别把代理的入接口写死在某一条 WAN 上。

最后是旁挂线路带来的额外变量:USB 网卡、5G 模块、无线中继这类 WAN 的可用带宽本身会波动,而且它们的驱动往往只有单队列、中断集中在一个核——上一节的「单核挤压」在这里会被放大。如果两条线里只有旁挂那条慢,先按第四节的思路看它的中断分布,再看它是不是被放在了一个已经很忙的核上,别急着怀疑运营商。

八、处置顺序:按成本从低到高

取证做完,处置是有明确成本顺序的。改配置验证、改队列分布、改表容量,这三步都不花钱,而且都能在一台设备上当天完成;换硬件排最后。

顺序动作适用证据代价
1提高 netdev_max_backlog,观察第 2 列是否还涨softnet_stat 第 2 列非零一条 sysctl,可随时回退
2调整 dev_weight / netdev_budget,观察第 3 列time_squeeze 持续增长一条 sysctl,注意别让软中断吃满 CPU
3按中断分布给队列设 RPS某核独占中断、其余核空转写 rps_cpus 掩码,重启需重设
4缩短连接跟踪超时、必要时提高上限条目数贴近上限、有 table full 日志改 sysctl,注意内存占用
5重新排分流策略与规则顺序策略链计数与预期不符改配置,影响面较大,建议先备份
6换硬件(多队列网卡、更强 CPU)前面五步都做完仍不够用花钱,且只解决第一类瓶颈

补充两点取舍。一是加速与观测:开了 offload 之后,被卸载的流不再逐个进钩子,限速和统计会失真,所以「要精细统计」和「要最高转发性能」通常得选一个当主目标。二是架构:同一台硬件摆在主路由位置还是旁路由位置,瓶颈出现的地方并不一样(转发路径长度与故障域不同),选型判断在旁路由 vs 主路由架构决策里有一条完整对照;如果你正卡在「旁路由模式怎么接」的层面,先看单臂旁路由搭建与透明网关配置更直接。

改完参数以后有个动作不能省:用同一套命令把同一张表再读一遍。sysctl 这类改动最容易出现「写了但没生效」——临时改动重启就丢、参数被启动脚本覆盖、或者改的键名和真正生效的键不是同一个(路由设备与普通主机上有些同名键的取值范围并不一样)。判断方法很简单:改之前记下这一列的数值,改之后重新采样一次,看增量是否发生变化。看不到变化就不要继续下一步,否则六个动作的效果搅在一起,最后永远说不清是哪个起的作用。

还有一条容易忽略的:改动的验证要在同样的负载条件下做。空载时改 dev_weight、调 RPS,数值都不会有肉眼可见的变化,因为根本没有排队;要复现的是「多设备同时在线、或者大流量下载」那一档负载,验证才有意义。如果问题只在晚高峰出现,白天改完就去比对,结论必然是「改了没用」。

如果时间只够做一件事,按这个顺序挑:先读 softnet_stat 的丢弃与挤压两列,有数字就先处理它;两列都是 0,再去读连接跟踪的条目数与内核日志。这两处覆盖了软路由上绝大多数「不是线路问题」的慢,而且都只需要读一次表、改一两个参数就能验证,性价比明显高于调 QoS 或刷固件。

把回滚条件提前写下来,比事后争论有用得多:动任何一项之前先记下原值(sysctl 记数字,防火墙与分流配置留一份副本),并约定一条停止线——例如「改后在同样的负载下 30 分钟内丢弃数没有下降就回滚」。软路由是全家的出口,改动把家里网络弄断的代价,远高于多花十分钟做验证。

还要注意有些参数会互相抵消:提高 netdev_max_backlog 只是让队列更长,如果根本原因是单核挤压(第 3 列在涨),包会在更长的队列里等更久,表现为延迟变差而不是丢包消失——这时该去动队列分布,而不是继续加缓冲。同理,把 dev_weight 调得很高会让一轮软中断干更久,挤压计数可能下降,但别的任务(包括代理进程)会被推迟,整体体验未必变好。改一个、测一个,是这个环节唯一可靠的方法。

九、五个常见误判

说法为什么是误判正确处理
「CPU 占用不高,所以不是 CPU 的问题」软中断是分批干活的,预算用完就丢包,占用率可以不高看 softnet_stat 的丢弃与挤压两列
「单线程只有几百兆,路由器就这水平」单条流只能落少数核,多流才能反映转发能力用多流复测(第三节、自测手册)
「开了硬件加速,代理也会变快」透明代理流量走本机 socket,不进转发链、不享受旁路分开看待直连转发与代理转发
「测速正常,所以网络没问题」测速是短时新建连接,恰好避开了表满与长会话劣化查连接跟踪条目数与内核日志
「双 WAN 已经均衡了」默认配置里带 443 端口的粘性规则,按源 IP 粘 600 秒看策略链计数,或按需求改规则

十、常见问题(FAQ)

Q1:这套读数在 OpenWrt 上需要装什么?
核心读数都在 /proc 与 /sys 里,busybox 就能读。要 mpstat 的格式化输出,装 sysstat 包即可;要更细的抓包或逐跳工具,tcpdump、mtr 这类在官方源里都有;ethtool 也在基础源里。反而是「按设备长期记账」需要在 nlbwmon 这类工具与加速功能之间做取舍。

Q2:为什么我的 softnet_stat 里 4 到 9 列全是 0?
那几列是历史遗留位置,早期内核里叫 fastroute 与 cpu_collision,现代内核不再写入,保持 0 是正常的,直接忽略。

Q3:RPS 打开就一定更快吗?
不一定。多队列网卡已经用 RSS 把包分开了,再叠加 RPS 往往是重复劳动;单队列设备才有必要,而且要在「目前扛中断的那个核」之外选目标核,让它专心处理中断。

Q4:连接跟踪上限调大有没有副作用?
有,条目要占内存,且哈希桶数量的 sysctl 只在初始命名空间可写。建议先把超时收短(比如把 established 的超时从内核默认的五天降到小时级),再看还需要多少上限——大多数「表满」是被长期不回收的长连接占住的。

Q5:怎么确认瓶颈真的在线路而不是路由器?
两条证据同时成立就可以下结论:路由器侧的关键计数器全绿(无丢弃、无挤压、分布尚可、表不满),同时换一份订阅或换一个出口后现象消失。这两步做完,再去怀疑硬件才有意义。

Q6:为什么晚高峰才慢,白天完全正常?
晚高峰慢通常是「上游侧」或者「同一条线路上别人也在抢」的问题,本机计数器一般不会同步变差。先用分段排障的方法确定是哪一段在晚高峰劣化,再决定要不要动路由器配置,不要在白天复现不了的时段里改参数。

Q7:能不能用 irqbalance 自动把中断摊开?
可以让它试,但要验证结果:它按负载动态调整,调整完用 /proc/interrupts 看同一行是否真的换了核,再用 softnet_stat 看该核的挤压是否下降。有些网卡的中断与队列绑定关系不允许随便迁移,自动均衡可能什么都不做;在多队列设备上还可能把缓存局部性弄差。

Q8:开了转发加速,mwan3 这类策略路由会不会受影响?
受影响的是「看得见」这件事:被卸载的流不再逐个经过常规钩子,基于钩子的计数与限速会失真,所以用策略链计数判断分流效果之前,要先确认这条流有没有被卸载。建议把两件事拆开验证——先关加速把分流策略验证清楚,再开加速复测转发性能,否则「策略对不对」和「性能够不够」会互相掩盖。

Q9:加了内存是不是就能解决表满?
只有在「桶数确实按内存算出来、且上限还没到 262144 的封顶」时才有效。一旦到了封顶(大约 4GB 内存对应的规模),再加内存也不会扩大哈希表,这时唯一的办法是让条目更快释放——缩短连接超时、减少长连接占用,或者把 P2P/下载类流量做策略隔离。

Q10:小内存设备能不能靠精简固件腾出连接跟踪空间?
能腾出一点,但方向容易搞反:桶数按总内存算出来,省下几十兆对桶数的提升有限,而真正决定「撑不撑得住」的是条目回收速度与封顶上限。更有效的做法是把占着长连接的来源找出来(下载器、监控轮询、App 心跳),用超时和分流把它压下去。精简固件的主要收益是省 flash 与常驻内存,不是扩大跟踪表。

Q11:这些读数要不要长期记录?
值得记,但只记三样:softnet_stat 的丢弃与挤压、连接跟踪的条目峰值、以及出问题的时间点。有了这三个序列,下次再出问题就能一眼看出是容量趋势(条目数在缓慢爬升)还是突发(挤压在某次大流量后跳起来),比事后复现快得多。这也是把家庭网络当系统来维护的唯一技巧——不靠记忆,靠留痕。

推荐阅读