1. 网络性能优化的整体思路与排查框架
聊到 Linux 网络性能优化,很多人的第一反应是去调内核参数,改sysctl,或者换更贵的网卡。但我自己踩过的坑告诉我,绝大多数网络性能问题,根子不在内核参数上,而在排查思路的混乱上。你连瓶颈在哪一层都没定位清楚,就去盲目调参,结果往往是按下葫芦浮起瓢,甚至把原本正常的链路搞出新的问题。
这一篇作为网络优化系列的第三部分,我打算把重点放在“怎么系统性地定位瓶颈”以及“定位到之后怎么动手”这两件事上。前两部分我们聊过网络协议栈的基础路径和常用观测工具,这一篇更偏向实战:当你面对一台网络吞吐上不去、延迟抖动大、或者连接数一高就崩的机器时,到底该按什么顺序去看、去看哪些指标、每个指标背后对应的是什么问题。
先说清楚这套思路适合谁。如果你是中高级运维、后端开发、SRE,或者正在做高并发服务、数据库中间件、消息队列这类对网络敏感的系统,那这套东西你大概率用得上。如果你只是偶尔配个防火墙规则,那这篇可能偏重了,但里面的排查框架和避坑经验依然值得扫一眼。
我个人的核心观点是:网络优化是一个“自顶向下定位、自底向上验证”的过程。自顶向下,是从应用层的表现(QPS、P99 延迟、错误率)出发,一层层往下剥,直到找到真正拖后腿的那一层;自底向上,是从物理链路、驱动、内核协议栈往上验证,确认每一层的配置和能力是否匹配。这两条线要交叉进行,缺一不可。
为什么强调这个框架?因为网络问题有个很讨厌的特点:症状和病因经常不在同一层。应用层看到的是超时,病因可能在 TCP 重传;TCP 重传的原因可能是网卡丢包;网卡丢包又可能是中断亲和性没配好导致某个 CPU 核被打满。你要是只盯着应用层调超时时间,那永远治不好。
所以下面我会按照这个框架,把每一层的观测点、判断标准、以及对应的优化手段都拆开讲。中间会穿插我自己在实际环境里遇到的真实案例(当然,细节做了脱敏处理),以及一些文档里不会写、但实战中特别容易忽略的细节。
2. 从应用层指标反推网络瓶颈
2.1 先搞清楚“慢”到底慢在哪
应用层报出来的问题通常很模糊:“接口变慢了”“偶尔超时”“高峰期扛不住”。这些描述没法直接指导优化,第一步要做的,是把它们量化成可观测的指标。我一般会先抓三类数据:吞吐量、延迟分布、错误率。
吞吐量看的是单位时间处理了多少请求或多少字节,这个指标能告诉你系统的整体处理能力有没有下降。延迟分布比平均延迟重要得多,P50、P95、P99、P999 这几个分位数要一起看。平均延迟 10ms 但 P99 是 2s,这种系统对用户来说就是“时不时卡一下”,体验极差。错误率则包括连接超时、重置、应用层报错等。
这里有个经验:如果 P99 延迟高但 P50 正常,问题往往出在偶发的资源竞争或队列堆积上;如果整体延迟都高,那更可能是带宽或处理能力本身不足。这个判断能帮你快速缩小排查范围。
2.2 用 ss 和 netstat 看连接状态分布
定位到应用层有异常后,我习惯第一时间看连接状态。ss -s能给出一个总览,但更有用的是ss -ant配合状态统计。重点关注几个状态:
- ESTABLISHED 数量:如果远超预期,可能是连接泄漏或者上游压测没停。
- TIME_WAIT 数量:大量 TIME_WAIT 本身不一定是问题,但如果它挤占了本地端口资源,就会导致新连接建不起来。
- SYN-RECV 堆积:这个状态多,说明半连接队列被打满了,通常是 SYN Flood 或者 accept 速度跟不上。
- CLOSE_WAIT 堆积:这个几乎可以肯定是应用层没有正确关闭 socket,是代码 bug,不是内核问题。
我见过一个案例,某服务高峰期频繁超时,查下来是 CLOSE_WAIT 堆积了几万个。应用代码里有个异常分支没有 close 连接,平时量小看不出来,一上量就把文件描述符耗尽了。这种问题你调再多内核参数都没用,只能改代码。
提示:
ss比netstat快得多,尤其是在连接数上万的机器上。生产环境排查优先用ss,netstat在大连接数下可能直接卡死。
2.3 延迟分布怎么抓才准
抓延迟分布,应用层埋点是最准的,但很多时候你没法改代码。这时候可以用tcpdump抓包后分析,或者用 eBPF 工具在关键函数上打点。我比较常用的是基于 eBPF 的延迟直方图,能直接看到内核协议栈各阶段的耗时分布。
如果条件有限,退而求其次可以用ping和mtr看链路 RTT,但要注意这两者走的是 ICMP,和 TCP 的实际路径可能不同,只能作为参考。真正要精确,还是得在 TCP 层抓。
这里有个细节:抓包分析延迟时,一定要区分“网络 RTT”和“应用处理时间”。很多时候你以为是网络慢,其实是应用在收到请求后处理了很久才回包。区分方法很简单,看请求包和响应包的时间差,再减去链路 RTT,剩下的就是应用处理时间。
3. 传输层与内核协议栈的关键调优点
3.1 TCP 缓冲区:别让窗口成为瓶颈
TCP 吞吐量的一个核心限制因素是接收窗口和发送窗口的大小。如果窗口太小,发送方就得频繁停下来等 ACK,带宽根本跑不满。这个在长肥管道(高带宽、高延迟链路)上尤其明显。
内核里相关的参数主要是这几个:
# 接收缓冲区自动调优 net.ipv4.tcp_rmem = 4096 87380 6291456 # 发送缓冲区自动调优 net.ipv4.tcp_wmem = 4096 16384 4194304 # 是否开启自动调优 net.ipv4.tcp_moderate_rcvbuf = 1tcp_rmem和tcp_wmem各有三个值,分别是最小值、默认值、最大值。开启自动调优后,内核会根据连接的实际需求动态调整缓冲区大小,上限就是第三个值。
那这个最大值该设多大?有个简单的算法:带宽时延积(BDP)= 带宽 × RTT。比如你的链路是 1Gbps,RTT 是 50ms,那 BDP = 125MB/s × 0.05s ≈ 6.25MB。缓冲区至少要能容纳这么多数据,才能让链路跑满。所以如果你的服务面向跨地域调用,tcp_rmem和tcp_wmem的最大值要相应调大。
但也不是越大越好。缓冲区太大会增加内存占用,而且在丢包时会导致更长的恢复时间。我的经验是,先按 BDP 算一个理论值,然后在此基础上留 1.5 到 2 倍余量,再通过压测验证。
3.2 拥塞控制算法:选对算法比调参更重要
Linux 默认的拥塞控制算法是 CUBIC,在大多数场景下表现不错。但在高丢包或者高延迟的链路上,BBR 往往能带来明显提升。BBR 不依赖丢包来判断拥塞,而是基于带宽和 RTT 的估计,所以在有一定丢包的环境下吞吐量更稳定。
切换算法很简单:
# 查看当前可用算法 sysctl net.ipv4.tcp_available_congestion_control # 切换为 BBR sysctl -w net.ipv4.tcp_congestion_control=bbr但要注意,BBR 不是万能的。它在和其他 BBR 流竞争时比较激进,可能挤占 CUBIC 流的带宽。如果你的服务和其他业务共享链路,切换前要评估影响。另外 BBR 对 RTT 的估计在极端网络条件下也可能失准,需要结合实际压测数据判断。
我自己的做法是:内网服务保持 CUBIC,跨地域或公网服务在压测对比后决定是否上 BBR。不要盲目跟风,一定要用数据说话。
3.3 连接队列:accept 跟不上会丢连接
当并发连接建立速度超过应用 accept 的速度时,连接会堆积在队列里。内核有两个队列:半连接队列(SYN queue)和全连接队列(accept queue)。
半连接队列存放的是收到 SYN 但还没完成三次握手的连接,大小由net.ipv4.tcp_max_syn_backlog控制。全连接队列存放的是已完成握手、等待应用 accept 的连接,大小由net.core.somaxconn和应用的listen参数共同决定,取两者较小值。
net.core.somaxconn = 32768 net.ipv4.tcp_max_syn_backlog = 32768如果全连接队列满了,新的握手完成连接会被丢弃(或者根据tcp_abort_on_overflow决定是丢弃还是发 RST)。应用层看到的现象就是“连接超时”或“连接被重置”。
排查队列溢出,可以看netstat -s里的相关计数:
netstat -s | grep -i listen # 输出里的 "listen queue of a socket overflowed" 就是全连接队列溢出次数这个数字如果持续增长,说明要么调大队列,要么提升应用 accept 的速度。调大队列只是缓兵之计,根本还是要解决 accept 慢的问题。
4. 网卡、中断与多队列的实战配置
4.1 中断亲和性:别让一个核累死
现代网卡基本都支持多队列,每个队列可以绑定到不同的 CPU 核上处理中断。如果所有队列的中断都打到同一个核,那这个核会被中断处理占满,成为整个网络路径的瓶颈。
查看网卡队列和中断分布:
# 查看网卡队列数 ethtool -l eth0 # 查看中断分布 cat /proc/interrupts | grep eth0如果发现中断集中在某几个核上,可以手动设置亲和性。每个中断号在/proc/irq/<irq_num>/smp_affinity里对应一个 CPU 掩码。比如要把中断 128 绑定到 CPU 2,可以这样:
# CPU 2 对应的掩码是 0x4 echo 4 > /proc/irq/128/smp_affinity不过手动绑核很繁琐,尤其是队列多的时候。更省事的办法是用irqbalance服务,但要注意它的默认策略可能不适合高吞吐场景。我一般会关掉irqbalance,用脚本按队列和 CPU 的对应关系批量绑定,确保每个队列的中断落在不同的物理核上。
注意:绑核时要考虑 NUMA 架构。网卡插在哪个 NUMA 节点,中断就应该绑到那个节点的 CPU 上,否则跨节点访问内存会引入额外延迟。
4.2 RPS 和 RFS:软件层面的多队列
有些场景下网卡硬件队列不够,或者你用的是虚拟网卡,这时候可以用 RPS(Receive Packet Steering)在软件层面把收包处理分散到多个核上。RPS 通过设置/sys/class/net/<dev>/queues/rx-<n>/rps_cpus来指定哪些 CPU 参与处理。
RFS(Receive Flow Steering)则更进一步,它会把同一个流的包尽量送到处理该流的应用所在的 CPU 上,提高缓存命中率。开启 RFS 需要设置net.core.rps_sock_flow_entries和对应的rps_flow_cnt。
# 全局流表大小 net.core.rps_sock_flow_entries = 32768 # 每个队列的流表大小 echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cntRPS 和 RFS 会增加一些 CPU 开销,但在队列不足的场景下,它们能显著提升吞吐量。我实测下来,在虚拟化环境里开启 RPS 后,单网卡吞吐量能提升 30% 以上。
4.3 网卡卸载特性:该开的开,该关的关
网卡支持很多卸载特性,比如 TSO、GSO、GRO、LRO、checksum offload 等。这些特性把原本内核要做的事情下放到网卡硬件,能降低 CPU 占用、提升吞吐量。
查看和设置:
ethtool -k eth0 ethtool -K eth0 tso on但有个坑:某些卸载特性和抓包、NAT、桥接等场景会冲突。比如 LRO 会把多个包合并成一个大包,导致抓包时看到的包和实际不符,也会影响转发场景的准确性。所以在做网关或需要精确抓包的机器上,通常要关掉 LRO,甚至关掉 GRO。
我的建议是:纯终端服务器,卸载特性尽量开;做转发、NAT、抓包分析的机器,谨慎开启,必要时关闭。具体开哪些,最好用压测对比 CPU 占用和吞吐量后再决定。
5. 常见问题排查与避坑经验
5.1 问题速查表
| 现象 | 可能原因 | 排查命令 | 处理方向 |
|---|---|---|---|
| 吞吐上不去,CPU 不高 | 窗口太小、拥塞算法保守 | ss -ti看 cwnd 和 rwnd | 调大缓冲区、换 BBR |
| P99 延迟高,P50 正常 | 队列堆积、偶发丢包 | netstat -s看重传和队列溢出 | 调大队列、查丢包点 |
| 连接建立慢或失败 | 半/全连接队列满 | netstat -s | grep listen | 调大队列、优化 accept |
| 单核 CPU 被打满 | 中断集中、软中断不均 | cat /proc/interrupts、mpstat -P ALL | 绑核、开 RPS |
| 大量 CLOSE_WAIT | 应用未关闭连接 | ss -ant | grep CLOSE-WAIT | 改代码,非内核问题 |
| 大量 TIME_WAIT | 短连接过多 | ss -ant | grep TIME-WAIT | 连接复用、调 tcp_tw_reuse |
5.2 几个容易踩的坑
第一个坑:盲目调tcp_tw_recycle。这个参数在 NAT 环境下会导致连接异常,而且在新版本内核里已经被移除了。如果你的内核还有这个参数,建议保持关闭。
第二个坑:把somaxconn调得很大但应用 backlog 没改。全连接队列大小取somaxconn和listen参数的较小值,只调内核参数不改应用代码,等于白调。
第三个坑:忽略软中断的 CPU 分布。用mpstat -P ALL 1看每个核的%soft,如果某个核的软中断占比长期很高,那就是瓶颈点。这时候光看整体 CPU 使用率是看不出来的。
第四个坑:在虚拟化环境里照搬物理机的调优参数。虚拟网卡的中断机制和物理网卡不同,很多物理机上的绑核策略在虚拟机里不适用,反而可能因为 vCPU 调度问题导致性能下降。虚拟化环境优先考虑 RPS/RFS 和队列数调整。
5.3 压测验证:调完必须回头看
任何调优动作做完,都必须用压测验证效果。我一般用iperf3测纯网络吞吐,用wrk或ab测应用层 QPS 和延迟。压测时要注意:
- 压测机和被测机要在同一网络条件下,避免链路本身成为瓶颈。
- 压测时间要足够长,至少几分钟,避免瞬时波动误导判断。
- 每次只改一个变量,改完记录数据,这样才能知道是哪个参数起了作用。
我见过太多人一次性改一堆参数,结果性能提升了也不知道是哪个参数的功劳,下次遇到类似问题还是不会调。调优是个科学实验,不是玄学,控制变量是最基本的要求。
6. 我个人的几条实战心得
最后分享几条我自己在长期实践中总结的心得,不一定对所有人都适用,但至少能帮你少走点弯路。
第一条:先看应用,再看内核。前面反复强调过,大部分网络问题的根子在应用层。连接泄漏、锁竞争、序列化慢,这些都会表现为网络指标异常。上来就调内核参数,往往是在治标不治本。
第二条:建立基线,才能发现异常。平时就要收集正常状态下的各项指标,包括连接数、延迟分布、中断分布、CPU 软中断占比等。出了问题才有对比,否则你连“正常”是什么样都不知道。
第三条:调优要有取舍,没有银弹。调大缓冲区会占内存,开 BBR 可能影响其他流,绑核会牺牲灵活性。每个优化动作都有代价,关键是找到适合你当前场景的平衡点。
第四条:文档和社区经验要参考,但不能照搬。不同的内核版本、不同的硬件、不同的业务模型,最优参数可能完全不同。别人的“最佳实践”在你这里可能就是坑。永远以自己的压测数据为准。
网络性能优化这件事,说到底是个经验活。工具和参数是死的,但问题场景是活的。多动手、多观察、多记录,慢慢你就能形成自己的判断体系。到那时候,面对一台“网络慢”的机器,你不需要翻文档,凭直觉就能知道该从哪里下手。这种直觉,才是真正值钱的东西。