很多团队在优化 Linux 网络性能时,习惯打开 /etc/sysctl.conf 就把网上搜来的参数堆上去:netdev_budget、rmem_max、tcp_rmem,改完重启服务,然后盯着监控面板等结果。但问题是,数据包从网卡到用户态应用,中间要穿越硬件中断、软中断、协议栈、socket 队列整整一条长链路。每个环节都有自己的运行逻辑和瓶颈,如果不理解这条路径,参数调得再多也像盲人摸象。

先看清数据包从网卡到应用的路
在调整任何参数之前,先把这条路径完整走一遍。常见的接收路径大致是这样:
NIC -> DMA ring buffer -> 硬中断 -> 软中断(NAPI)-> IP/TCP 协议栈 -> socket 接收队列 -> epoll 通知 -> read() -> 用户态缓冲
网卡收到数据后,通过 DMA 写入内存中的环形缓冲区,然后触发硬件中断。中断处理程序会尽量短,只是把设备挂到后续处理队列上,剩下的工作在软中断里完成。软中断通过 NAPI 机制批量拉取数据包,经协议栈处理后放入对应 socket 的接收队列。最后应用通过 epoll 得知可读,再调用 recv 或 read 把数据拷贝出来。
这里每一个节点都可能成为瓶颈。很多人以为优化网络就是调内核参数,其实大部分生产问题发生在最容易被忽略的层次——中断分配和软中断负载。
第一道关卡:硬中断绑对 CPU 了吗
接受路径的第一步是响应硬件中断。如果你的网卡支持多队列(RSS),每个队列都有独立的中断号,而 `smp_affinity_list` 决定了这些中断分别由哪些 CPU 处理。默认设置往往不理想,尤其是有些驱动会把所有中断都放到 CPU0 上。
一个典型场景:反代服务器流量不高,但每隔一段时间出现几十毫秒的抖动。用 `top` 一看,CPU0 的 softirq 占用高得离谱,其他 CPU 几乎空闲。这种情况十有八九是中断没有做亲和性设置。调整方法很简单:
ethtool -l eth0 # 查看网卡队列数
cat /proc/interrupts # 观察各队列中断分布
echo 2 > /proc/irq/32/smp_affinity_list # 将第 32 号中断绑定到 CPU2
注意 `smp_affinity_list` 是 CPU 编号,比十六进制掩码直观很多。绑定之前别忘确认 CPU 与网卡所在 NUMA 节点一致,否则内存访问跨节点,反而会引入额外延迟。
如果网卡只有单队列,硬中断无法分散,那就需要借助中断合并(coalescing)来降低频率,或者直接考虑后面的 RPS。这个取舍没有绝对答案,低延迟业务宁愿多触发几次中断,高吞吐业务更愿意让硬中断少来几次。
第二道关卡:NAPI 让软中断批量干活,但别把业务进程饿死
硬中断处理完后,真正的数据包处理大多在软中断里进行。NAPI 的设计思路是:一次硬件中断将设备从中断模式切换到轮询模式,连续消费一批包,直到队列为空,再重新启用中断。这样能显著避免高流量下的中断风暴。
控制这个“一批”的上限,对应两个核心参数:`net.core.netdev_budget` 和 `net.core.netdev_budget_usecs`。前者限制一次软中断循环最多处理多少个包,后者限制耗时。很多人喜欢盲目调大 budget,觉得处理得越多越好。但别忘了,软中断是在 CPU 上抢占时间片的,和用户态业务进程抢资源。
我见过一个网关项目,流量上来后丢包率上升,团队把 netdev_budget 从默认的 300 调到 6000,丢包率确实降了,但应用响应时间反而翻倍。原因就是软中断几乎占满了 CPU,业务进程得不到调度。正确的做法是先看 `/proc/softirqs` 里的 NET_RX 分布在哪些 CPU 上,再决定是否值得调整,而不是拍脑袋。
grep NET_RX /proc/softirqs
如果软中断已经均匀分布且没有丢包,就不要再动 budget。调整时也要配合观察用户态 CPU 占比,保持两者平衡。
第三道关卡:协议栈的锁竞争和内存分配
通过 NAPI 拿到包,接下来要经过 IP/TCP 层处理。这段逻辑在内核里,普通参数能影响的不多,但有两个地方值得关注:一是 socket 接收队列的自旋锁,二是接收缓冲区大小。
当多核 CPU 同时向同一个 socket 写入不同连接的数据时,接收队列的锁会成为热点,尤其是短连接密集场景。一个常用的缓解方案是让多个进程分别监听同一个端口,配合 SO_REUSEPORT,让内核通过四元组 hash 连接分发到不同 socket。这能明显降低锁竞争,但也依赖应用自己维护多 worker 模式,没有银弹。
接收缓冲区方面,`net.core.rmem_max` 和 `net.ipv4.tcp_rmem` 控制 TCP 接收窗口能够增长的上限。缓冲区太小会丢包,太大则内存占用升高,数据在内核里待得更久,RTT 和内存拷贝延迟都会增加。很多团队上来就把 rmem 调到 16MB,结果内存翻倍,延迟却不见降低。
更靠谱的做法是观察 `netstat -s` 中的 receive errors、TCP checksum errors 和 receive queue 溢出计数。只有确认确实因为接收缓冲区不足丢包时,才去调大,而且建议按 2 的幂次逐步增加,每次做对比。
第四道关卡:怎么把数据高效交给应用
数据到达 socket 队列后,如何通知用户态进程同样会影响整体性能。epoll 已经是很成熟的机制,但很多团队在边缘触发(ET)和水平触发(LT)上踩坑。边缘触发只在状态变化时通知一次,如果应用没有一次读完所有数据,后续事件可能丢失,通常要把 socket 设为非阻塞,并在可读时循环读取直到 EAGAIN。
while (1) {
n = recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
if (errno == EAGAIN) break;
break;
}
process(buf, n);
}
这段代码看起来简单,但很多人漏掉 EAGAIN 的判断,或者用了阻塞 fd,导致忙等或卡住。epoll 只是解决通知效率,真正的数据拷贝开销还握在 read/recv 手里。对于追求极致吞吐,可以用 recvmmsg 批量接收,或者在满足条件时使用 splice、零拷贝,但这些优化的前提是先确认前面的路径没有瓶颈。
调优手段怎么选:一张表看懂
以下是我在项目中会优先排查的调优维度,按作用层次和适用场景整理成表格。注意它们不是互相替代的关系,而是相互配合。
| 调优维度 | 作用层次 | 关键点 | 适用场景 | 潜在风险 |
|---|---|---|---|---|
| 中断亲和性 | 硬中断 | smp_affinity_list | 多队列网卡,CPU 负载不均 | 误绑导致单 CPU 过载 |
| NAPI 参数 | 软中断 | netdev_budget | 高吞吐小包处理 | 参数过大会挤占用户态 |
| RPS/RFS | 协议栈分发 | rps_cpus 等 | 队列数不足或单队列网卡 | 跨 CPU 数据拷贝开销 |
| SO_REUSEPORT | socket 层 | 应用多进程 | 多进程 accept 竞争 | 分发不均或 hash 冲突 |
| 接收缓冲区 | socket 层 | rmem_max / tcp_rmem | 确认缓冲区溢出丢包时 | 延迟和内存开销上升 |
注意到一个问题:RPS/RFS 通常出现在单队列网卡或队列数不足时,它把数据包跨 CPU 分发,但会引入缓存同步成本。如果网卡已经开了多队列,优先配置 RSS 和中断亲和性,而不是叠加 RPS。
最常见的几个调优误区
- 只调参数不调整架构。很多问题是队列数量和中断分布造成的,参数调得再大也救不了硬件队列单一导致的瓶颈。
- 把 RPS 当万能钥匙。RPS 适合单队列网卡模拟多队列,但它在 CPU 间拷贝数据,可能比原来的开销更大,需要实测对比。
- 无视 NUMA 距离。中断绑在 CPU0,内存却和网卡在一个远端 NUMA 节点,这种跨节点访问会让延迟明显上涨。
- 把丢包率归零当作目标。TCP 在小丢包下能恢复,但增加重传延迟;UDP 丢包则无法恢复。要先判断丢包在哪一层,而不是一味调大缓冲区掩盖。
从哪开始调:一个可复用的流程
- 先采集基线:看 /proc/interrupts、/proc/softirqs、netstat -s,并用 iperf 测一下吞吐和延迟。
- 确认网卡队列数和 RSS 是否开启:ethtool -l eth0,ethtool -x eth0。
- 调整中断亲和性,让各队列分布到不同的 CPU,然后观察中断和 softirq 是否均匀。
- 如果队列数不够,再考虑 RPS/RFS,但必须 A/B 对比效果。
- 根据监控图谱,判断瓶颈在硬中断、软中断还是 socket 层,使用 perf top 或 bpftool 进一步确认。
- 每次只改变一个变量,观察至少 15 分钟,保留修改前后的性能数据。
- 确认有效后再固化到 sysctl.conf 或系统初始化脚本。
这套流程并不能保证你调出极端性能,但能让你在绝大多数场景下,用很低的成本拿到稳定的收益。实际上,很多号称性能差的系统,只要把中断分布弄对,就已经解决了一大半问题。
Linux 网络性能调优,本质上是在给数据包接收路径做一次体检。从 NIC 中断到用户态接收,每一层都有取舍:中断分散增加吞吐但可能拉高延迟,NAPI budget 提高吞吐却挤压业务 CPU,缓冲区调大缓解丢包却放大内存开销。理解这些权衡,你才能在具体业务面前做出合理判断。下次打开 sysctl.conf 时,不妨先问一句:我到底要解决哪一段路径的问题?
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/521/