Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
1 | 作者:李晓辉 |
- Linux 性能调优系列(一) 建立科学的性能分析思路
- Linux 性能调优系列(二) 掌握基础性能监控命令
- Linux 性能调优系列(三) sysstat实现性能数据持久采集
- Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
- Linux 性能调优系列(五) 给你的服务器做一次全面体检
- Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
- Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
- Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
- Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
- Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
- Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
- Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
- Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
- Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
- Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
- Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
- Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
- Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
- Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
- Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
- Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
- Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
- Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
- Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
- Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优
前面几篇博客我们聊 Linux 性能调优时,更多关注的是 CPU、内存和磁盘。今天换个方向,来看看网络性能。
不知道你有没有遇到过这样的情况:服务器 CPU 使用率不高,内存也很充足,磁盘没有明显的 I/O 瓶颈,但业务访问就是慢。SSH 连接偶尔卡顿,接口响应时间忽高忽低,两台服务器之间传输文件,速度却怎么也上不去。
这时候,你可能会想到调整 Linux 网络参数。
比如增大 TCP 缓冲区、修改网卡队列、调整中断亲和性,甚至直接把几个 sysctl 参数改成网上推荐的“大值”。
先别急。
网络调优最怕的不是参数不会改,而是还没找到瓶颈,就开始到处改参数。
缓冲区太小,可能装不下足够多的在途数据;缓冲区太大,又可能让数据在队列里排队太久。网卡队列太小可能导致丢包,但一味增加队列长度,也不一定能解决 CPU 处理不过来的问题。建议先搞清楚网络数据是怎么传输的,再学习如何测量吞吐量、分析网络瓶颈,以及使用 BDP 估算合理的 TCP 缓冲区大小。
网络调优的第一步:先测量,再修改
假设现在有两台 RHEL 服务器。
host1 是业务服务器,host2 是测试客户端。两台机器之间的网络标称带宽是 1 Gbps,但实际传输文件时,速度只有几十 MB/s。
这时候,你最应该做的是什么?
不是马上修改 net.ipv4.tcp_rmem,而是先测量两台机器之间的实际网络吞吐量。
使用 iperf3 建立性能基线
iperf3 是一个常用的网络性能测试工具,可以在两台主机之间产生测试流量,测量 TCP 或 UDP 的吞吐量等指标。
先在两台机器上安装:
1 | [root@host1 ~]# dnf install -y iperf3 |
再安装到host2上
1 | [root@host2 ~]# dnf install -y iperf3 |
默认监听 TCP 5201 端口,防火墙开一下
1 | [root@host1 ~]# firewall-cmd --add-port=5201/tcp |
在 host1 上启动服务端:
1 | [root@host1 ~]# iperf3 -s |
然后在 host2 上执行:
1 | [root@host2 ~]# iperf3 -c 192.168.8.128 -t 10 |
其中:
-c host1:指定测试服务端。-t 10:持续测试 10 秒。iperf3 -s:启动服务端,等待客户端连接。
如果没有 DNS 解析,可以直接使用 host1 的 IP 地址。
| 字段 | 含义 |
|---|---|
Interval | 统计的时间区间,每行这里是 1 秒 |
Transfer | 这个区间传输的数据量 |
Bitrate | 这个区间测得的平均传输速率 |
Retr | 发送端统计的 TCP 重传次数 |
Cwnd | TCP 拥塞窗口大小 |
这里有个容易忽略的细节:输出中的 GBytes 和 Gbits/sec 采用十进制单位,而计算机内存中常见的 GiB、KiB 采用二进制单位。因此6.39 Gbits/sec 则相当于约 799 MB/s。
这意味着,在这次测试条件下,两台机器之间的 TCP 数据传输速率接近每秒 800 MB。
另外,iperf3 的默认 TCP 测试主要关注吞吐量。它不能单凭这一行输出,就告诉你网络延迟、抖动和丢包分别是多少。
如果想测试反方向的传输,可以使用:
1 | [root@host2 ~]# iperf3 -c 192.168.8.128 -t 10 -R |
-R 表示反向测试,即由服务端向客户端发送数据。
如果怀疑单个 TCP 流无法充分利用带宽,还可以测试多个并行连接:
1 | [root@host2 ~]# iperf3 -c 192.168.8.128 -t 10 -P 4 |
-P 4 表示使用 4 个并行客户端流。
不过,多个流的总吞吐量上去了,不代表单个业务连接的性能也一定改善了。并行连接可能掩盖单流的窗口限制、拥塞控制或 CPU 处理瓶颈。
为什么一定要先建立基线?
假设你修改了 TCP 缓冲区参数,测试结果从 300 Mbps 上升到 700 Mbps。这至少说明,在当前测试条件下,这次调整与吞吐量提升有关。
但如果你没有修改前的测试结果,只知道修改后的吞吐量是 700 Mbps,就很难判断这次调优究竟有没有效果。
因此,建议每次调优都遵循同一个流程:
flowchart TD
A["确认网络拓扑和链路速率"] --> B["记录修改前的性能基线"]
B --> C["检查丢包、重传、CPU和队列"]
C --> D["提出一个明确的调优假设"]
D --> E["只修改相关参数"]
E --> F["重复相同测试"]
F --> G{"吞吐量或延迟改善了吗?"}
G -->|是| H["验证业务负载下的效果"]
G -->|否| I["恢复参数并继续排查"]这里还有一个很重要的原则:不要只看吞吐量,还要看延迟、丢包、CPU 使用率和业务响应时间。 网络性能不是单一指标。
比如吞吐量提高了,但请求延迟从 5 ms 上升到 100 ms,对于大文件传输业务来说可能可以接受,对于在线交易、API 调用和交互式 SSH 来说却未必是好事。
网络数据到底是怎么传输的?
要理解为什么网络调优会涉及缓冲区、中断和 CPU,首先得知道一个数据包从应用程序出发,最终到达另一台服务器,中间经历了什么。
发送数据:从应用程序到网卡
假设你在服务器上运行一个应用程序,需要通过 TCP 向另一台服务器发送数据。
大致过程如下:
flowchart LR
A["应用程序调用 send()"] --> B["TCP 发送缓冲区"]
B --> C["TCP 分段、处理序号和确认机制"]
C --> D["IP 层封装"]
D --> E["网络设备队列和 qdisc"]
E --> F["网卡发送队列 / TX Ring"]
F --> G["网卡通过 DMA 读取数据"]
G --> H["数据从网卡发出"]这里需要注意,实际发送路径会受到内核版本、网络设备、驱动、卸载功能以及虚拟化环境的影响,上图展示的是常见的逻辑过程。
应用程序调用 send(),并不意味着数据立刻就从网卡发出。数据可能先进入套接字发送缓冲区,再经过 TCP、IP 等协议层处理,进入网络设备的发送队列,最后由驱动和网卡完成发送。
如果某一层处理不过来,数据就可能在对应位置排队。
例如:
- TCP 发送缓冲区受限,可能影响发送端维持足够多的在途数据。
- 网络设备队列拥堵,可能增加排队延迟。
- 网卡发送队列资源不足,可能限制发送效率。
- CPU 忙于处理其他任务,也可能无法及时完成网络协议栈处理。
所以,网络传输慢,不一定是网卡带宽不够,也可能是某一层的处理能力或者队列资源不足。
接收数据:网卡收到数据后发生了什么?
接收方向的过程也很有意思。
flowchart TD
A["网卡收到数据帧"] --> B["DMA 将数据写入内存"]
B --> C["硬件中断 / NAPI 调度"]
C --> D["内核通过软中断等机制处理数据包"]
D --> E["IP 层处理"]
E --> F["TCP 校验、排序和重组"]
F --> G["TCP 接收缓冲区"]
G --> H["应用程序调用 recv() 读取"]这里有一个容易误解的地方。
并不是每收到一个数据包,CPU 就一定要处理一次独立的硬件中断。
现代 Linux 网络驱动通常会使用 NAPI 机制:网卡通过中断通知内核有数据需要处理,随后内核可以采用轮询方式批量处理数据包,从而降低高流量场景下的中断开销。
但这也意味着,如果数据包到达速度远远超过 CPU 的处理能力,接收队列、软中断处理和应用程序读取都可能出现瓶颈。
这就是为什么排查网络性能时,不能只盯着网卡速率,还要看 CPU、队列和协议栈的处理能力。
网络性能的三个常见问题:活锁、环形缓冲区和 CPU 争用
了解了数据传输过程,我们再来看三个与网络调优密切相关的问题。
什么是网络活锁?
先用一个生活中的例子理解。
假设你在一家餐厅负责接单。顾客不断进来,你每处理完一份订单,就马上有新顾客进来。你忙着接单,连已经接好的订单都没时间交给厨房。
结果看起来你一直很忙,实际上真正完成的工作却很少。
网络处理中也可能出现类似的问题。
当数据包到达速度非常快时,内核可能需要花费大量 CPU 时间处理接收事件、中断或软中断。如果这些处理持续占用 CPU,其他任务就可能得不到足够的运行机会。
在某些极端情况下,系统会表现出类似活锁的现象:CPU 非常忙碌,但有效工作进展很少。
需要注意,网络活锁并不是所有高负载网络场景的统称。CPU 忙、软中断占用高或出现丢包,并不自动意味着发生了活锁。
最小化系统没有 mpstat,可以安装相应工具包:
1 | [root@host2 ~]# dnf install -y sysstat |
运行看看
1 | [root@host2 ~]# mpstat -P ALL 1 |
再查看软中断统计:
1 | cat /proc/softirqs |
重点关注 NET_RX 和 NET_TX。
NET_RX:与网络接收软中断处理有关。NET_TX:与网络发送软中断处理有关。
如果某个 CPU 的网络软中断计数增长特别快,同时该 CPU 的软中断占用很高,就值得进一步检查网卡队列分布、中断亲和性、RPS/RFS 配置以及流量模式。
当然,计数增长快本身不是故障。高流量服务器本来就会产生大量网络处理事件,必须结合 CPU 利用率、丢包和实际业务表现一起判断。
网卡环形缓冲区为什么会影响性能?
网卡通常会使用环形队列,也就是常说的 Ring Buffer。可以把它理解为一个循环使用的收发任务列表。网卡和驱动通过描述符记录哪些缓冲区可以使用、哪些数据已经到达、哪些发送任务尚未完成。如果接收数据持续到达,而驱动和内核来不及处理,接收资源就可能耗尽,进而导致丢包。
先查看网卡名称:
1 | [root@host1 ~]# ip -br link |
先安装调试工具
1 | [root@host1 ~]# dnf install ethtool |
假设网卡名称为 ens160,可以查看驱动信息和队列设置:
1 | [root@host1 ~]# ethtool -i ens160 |
1 | [root@host1 ~]# ethtool -g ens160 |
其中:
ethtool -i:查看网卡驱动和相关信息。ethtool -g:查看网卡支持的 Ring Buffer 参数,以及当前设置和最大支持值。
如果驱动支持,也可以查看统计信息:
1 | [root@host1 ~]# ethtool -S ens160 |
不同驱动提供的统计项并不完全相同。可以重点寻找带有 drop、miss、no_buffer、overrun 等含义的计数器,但一定要结合具体驱动的文档判断其含义。如果确实发现接收队列资源不足,而且驱动支持更大的队列,可以评估是否增大 Ring Buffer。
假设驱动明确支持接收队列深度 2048,可以尝试:
1 | ethtool -G ens160 rx 2048 |
这里的 2048 只是示例,不能直接照搬到所有网卡。
增大 Ring Buffer 也有代价:队列更深,可能让更多数据等待处理,进而增加排队延迟和内存占用。
而且,如果真正的问题是 CPU 根本处理不过来,那么单纯增加队列深度,可能只是让数据包在队列里多等一会儿。
因此,先确认有没有队列资源不足,再考虑增大 Ring Buffer。
CPU 争用与中断亲和性
网络处理同样需要 CPU。一台服务器可能有多个 CPU 核心,而网卡也可能提供多个接收和发送队列。驱动会根据硬件能力、中断配置和系统设置,将网络处理任务分配到不同的 CPU 上。
如果流量集中在少数队列,或者网络处理主要落在某一个繁忙的 CPU 上,就可能出现部分核心过载、其他核心却比较空闲的情况。
查看中断分布:
1 | [root@host1 ~]# cat /proc/interrupts |
如果某个网卡队列对应的中断几乎全部集中在 CPU0,而 CPU0 又承担了大量其他任务,就有必要检查中断分配是否合理。
不过,这里不能简单地得出“中断和产生中断的进程必须放在同一个 CPU 上”的结论。
现代网络栈中,中断处理、NAPI 轮询、软中断、RPS/RFS 和应用程序运行位置之间的关系比较复杂。把它们放在同一个 CPU 上,有时可以改善缓存局部性;但如果该 CPU 已经很忙,也可能适得其反。
生产环境中,应该结合以下因素进行判断:
- 网卡是否支持多队列。
- 各队列的流量是否均衡。
- IRQ 和软中断是否集中在少数 CPU。
- 应用程序线程的 CPU 亲和性。
- NUMA 节点与网卡所在位置。
- 调整前后的延迟、吞吐量和 CPU 使用率。
RHEL 网络缓冲区:这些参数到底管什么?
前面提到了网络队列和缓冲区,现在来看最常见的一组内核参数。
先把当前配置查出来:
1 | sysctl net.core.rmem_max |
这些参数看起来相似,但作用范围并不一样。
4.1 系统级 TCP 和 UDP 内存限制
先看:
1 | [root@host1 ~]# sysctl net.ipv4.tcp_mem |
这两个参数与 TCP、UDP 协议栈可使用的内存管理有关。
在常见的 Linux 内核实现中,tcp_mem 和 udp_mem 使用三个数值,分别表示不同的内存压力阈值。
以 tcp_mem 为例:
1 | net.ipv4.tcp_mem = 123456 164608 246912 |
这里的数字仅为示例。
三个数值分别表示:
- 第一个值:低水位,内存使用量低于该值时,通常不会因为这一机制进入内存压力状态。
- 第二个值:压力水位,超过该值后,TCP 内存管理会更加积极地控制内存使用。
- 第三个值:高水位,用于限制 TCP 协议栈的整体内存使用。
这里的单位通常是内存页,而不是字节。
可以使用下面的命令查看系统页大小:
1 | [root@host1 ~]# getconf PAGESIZE |
在常见的 x86_64 RHEL 系统上,页大小通常为 4096 字节。
假设某个阈值为 100000 页,那么对应的字节数就是:
1 | 100000 × 4096 = 409600000 Bytes |
约为 390.6 MiB。
但要注意,不能简单地把 tcp_mem 的第三个值理解为整个系统所有 TCP 连接能够使用的总内存,也不能认为超过第二个值就会立即丢弃所有数据。
它们属于协议栈的内存压力管理机制,具体表现还会受到内核版本、连接状态和内存管理策略影响。
另外,默认值通常会根据系统内存等因素计算或初始化,实际数值应以当前系统的 sysctl 输出为准。
net.core.rmem_max 和 net.core.wmem_max
再来看两个更常见的参数:
1 | [root@host1 ~]# sysctl net.core.rmem_max |
它们分别与套接字接收缓冲区和发送缓冲区的最大设置值有关。
简单理解:
net.core.rmem_max:套接字接收缓冲区的上限之一。net.core.wmem_max:套接字发送缓冲区的上限之一。
为什么说是“上限之一”?
因为 Linux 套接字缓冲区的实际行为还与 TCP 专用参数、自动调优机制、应用程序设置以及内核版本有关。
例如,应用程序可能通过 setsockopt() 设置套接字缓冲区,也可能使用默认值让内核自动管理。
所以,即使你把 rmem_max 设置得很大,也不意味着每一个 TCP 连接都会立即分配这么多内存。
同样,系统里有很多 TCP 连接时,如果每个连接都大量使用缓冲区,整体内存占用也可能明显增加。
net.ipv4.tcp_rmem 和 net.ipv4.tcp_wmem
再看 TCP 专用参数:
1 | net.ipv4.tcp_rmem = 4096 131072 6291456 |
通常可以看到三个数值,例如:
1 | net.ipv4.tcp_rmem = 4096 131072 6291456 |
这里同样只是示例,不代表所有 RHEL 版本的默认值。
这三个数值通常分别对应:
- 最小值。
- 默认值。
- 自动调优可使用的最大值。
对于 TCP 接收缓冲区,tcp_rmem 决定了 TCP 接收缓冲区的相关范围;对于发送缓冲区,tcp_wmem 则决定发送缓冲区的相关范围。
Linux TCP 自动调优机制可以根据连接状况动态调整缓冲区使用量,而不是始终固定使用第二个数值。
需要特别注意,TCP 缓冲区大小并不是简单地由这几个参数中的某一个独立决定的。套接字选项、全局限制和内核的具体实现都会影响最终结果。
如果希望查看窗口缩放功能是否开启,可以执行:
1 | [root@host1 ~]# sysctl net.ipv4.tcp_window_scaling |
后面我们还会讲它与 TCP 窗口大小的关系。
先记住一个原则:缓冲区参数定义的是可用范围和限制,不代表连接一定会使用到最大值。
重点来了:什么是 BDP?为什么网络调优离不开它?
前面介绍了很多参数,但究竟应该把 TCP 缓冲区设置成多大?
如果只是根据网上的经验,把缓冲区设置成 16 MiB、64 MiB,甚至 256 MiB,显然不够严谨。
这时候就需要用到一个重要概念:BDP。
BDP 的英文是 Bandwidth-Delay Product,中文通常称为带宽延迟乘积。
它描述的是:在给定带宽和往返时延的情况下,链路上大约可以容纳多少尚未完成确认的在途数据。
计算公式为:
1 | BDP = 带宽 × 往返时延(RTT) |
注意,带宽与数据量必须使用一致的单位。如果带宽使用 bit/s,那么计算得到的 BDP 就是 bit;如果需要字节数,还要除以 8。
用高速公路理解 BDP
我们可以把网络想象成一条高速公路。
- 带宽:相当于高速公路的车道通行能力。
- RTT:相当于一辆车从出发到收到返回确认所需的时间。
- 在途数据:相当于已经上路、但发送方还没有收到确认的数据。
如果高速公路通行能力很强,但车辆往返时间很长,那么想要充分利用这条高速公路,就必须让足够多的车辆同时在路上。
网络也是一样。
假设链路带宽是 1 Gbps,RTT 是 100 ms。
如果发送方每次只能发送很少的数据,然后就必须等待接收方确认,那么发送方就会频繁等待,链路的利用率自然上不去。
而当允许更多数据在途时,发送方就可以在等待确认的同时继续发送数据,更充分地利用链路带宽。
这就是 BDP 的核心意义。
先测量 RTT
我们可以先使用 ping 观察两台主机之间的往返时延:
1 | ping -c 10 host2 |
假设结果中的统计信息如下:
1 | rtt min/avg/max/mdev = 0.335/0.341/0.347/0.015 ms |
这里:
min:最小往返时延。avg:平均往返时延。max:最大往返时延。mdev:往返时延的离散程度指标。
本例平均 RTT 为 0.341 ms。
换算成秒:
1 | 0.341 ms = 0.000341 s |
需要注意,ping 测量的是 ICMP 往返时延,不一定与 TCP 业务的实际 RTT 完全相同。对于高精度调优,最好结合真实 TCP 连接的 RTT 统计来分析。
计算 1 Gbps 链路的 BDP
现在假设:
- 带宽:1 Gbps。
- RTT:0.341 ms。
计算过程如下:
1 | BDP = 带宽 × RTT |
这意味着,在上述带宽和 RTT 条件下,链路的 BDP 约为 41.63 KiB。
也就是说,如果希望单个 TCP 流持续以 1 Gbps 的速率发送数据,并且 RTT 约为 0.341 ms,那么发送方通常需要允许大约 41.63 KiB 的数据处于在途状态。
但这里有一个很容易犯的错误。
BDP 并不等于 TCP 缓冲区应该设置的唯一正确值。
它是估算在途数据需求的重要参考,而不是一条可以直接套用的参数配置公式。
实际需要的缓冲区还会受到 TCP 窗口、接收端处理速度、丢包、拥塞控制算法、套接字实现以及应用程序读写方式影响。
如果要充分利用一条高带宽、长 RTT 的链路,实际可用的 TCP 窗口通常至少需要能够覆盖相应的 BDP。
反过来,如果链路的 BDP 很小,却将所有连接的缓冲区都设置得特别大,就可能增加内存占用和排队延迟。
同样是 1 Gbps,为什么不同网络需要不同的缓冲区?
我们再看一个例子。
假设两条网络链路的带宽都是 1 Gbps。
第一条链路的 RTT 是 0.5 ms,第二条链路的 RTT 是 50 ms。
它们的 BDP 分别是:
第一条链路:
1 | 1,000,000,000 × 0.0005 ÷ 8 |
第二条链路:
1 | 1,000,000,000 × 0.05 ÷ 8 |
可以看到,两条链路的带宽相同,但 BDP 相差了 100 倍。
这也解释了为什么数据中心内的低延迟网络,与跨地域、跨数据中心的长距离网络,可能需要完全不同的 TCP 窗口和缓冲区配置。
所以,网络调优不能只看网卡标称带宽,还要看 RTT。
TCP 窗口缩放:为什么 64 KiB 有时远远不够?
现在已经知道 BDP 代表在途数据需求,接下来就要了解 TCP 窗口。
TCP 并不是每发送一个数据包,都必须等待对方确认后才能继续发送。
为了提高传输效率,TCP 允许发送方在收到确认之前,继续发送一定数量的数据。
这部分可以发送、但尚未得到确认的数据量,受到发送窗口、接收窗口和拥塞控制等因素共同约束。
其中,接收窗口用于反映接收端愿意接收多少数据;拥塞窗口则用于控制发送方认为网络可以承载多少在途数据。
最终能够发送的数据量,不能简单超过这两者所允许的范围。
传统 TCP 窗口为什么有 64 KiB 的限制?
传统 TCP 首部中的接收窗口字段只有 16 位。
因此,未使用窗口缩放选项时,窗口字段能够表示的最大值是:
1 | 2^16 - 1 = 65535 Bytes |
也就是接近 64 KiB。
对于低带宽、低延迟网络,这通常不是特别大的问题。
但是,对于高速、长距离链路,64 KiB 可能远远不够。
举个例子,假设带宽为 1 Gbps,RTT 为 50 ms。
前面算过,这条链路的 BDP 约为 5.96 MiB。
如果 TCP 接收窗口只能达到约 64 KiB,即使网络本身没有拥塞,单个连接也可能因为窗口限制而无法充分利用带宽。
这里说的是窗口限制可能成为瓶颈,而不是所有情况下都必然只能跑出某个固定速度。
TCP 窗口缩放是怎么解决这个问题的?
为了解决这个限制,TCP 引入了窗口缩放选项。
在 RHEL 上,可以查看相关参数:
1 | sysctl net.ipv4.tcp_window_scaling |
常见的配置是:
1 | net.ipv4.tcp_window_scaling = 1 |
值为 1 表示启用 TCP 窗口缩放功能。
在 TCP 三次握手期间,双方可以协商窗口缩放参数。协商成功后,后续报文中的窗口字段就可以通过缩放因子表示更大的接收窗口。
因此,实际接收窗口可以远远超过传统的 64 KiB 限制。
但要注意两点:
第一,双方必须在连接建立时支持并协商窗口缩放。单方面修改配置,不会让已经建立的连接重新协商这个选项。
第二,窗口缩放只是解决接收窗口表示范围的问题,并不意味着实际窗口一定会自动增长到某个固定大小。缓冲区上限、自动调优、接收端处理能力等因素仍然会影响最终结果。
如果 BDP 已经远远超过 64 KiB,却又受到传统 TCP 窗口限制,那么链路带宽就可能无法得到充分利用。
这也是为什么在高带宽、长 RTT 的网络中,窗口缩放和 TCP 缓冲区配置特别重要。
缓冲区是不是越大越好?小心缓冲区膨胀
讲到这里,你可能会觉得:既然更大的窗口可以让更多数据在途,那我直接把 TCP 缓冲区设置得很大,不就行了吗?
还真不行。
因为缓冲区太大,可能带来另一个问题:缓冲区膨胀,也就是 Bufferbloat。
用排队买咖啡理解缓冲区膨胀
假设一家咖啡店只有一个制作窗口,每分钟最多制作 20 杯咖啡。
平时只有几个人排队,大家很快就能拿到咖啡。
但如果店门口允许无限排队,一下子来了 200 个人,即使每个人都能成功排上队,也不代表服务速度提高了。
相反,最后排队的人可能要等很长时间。
网络也是一样。
当数据包进入网络的速度超过某个链路或设备的处理速度时,数据就可能在队列里等待。
如果队列过深,数据包不一定马上丢失,但延迟可能显著增加。
这就可能出现一种情况:
- 大文件传输速度看起来不错。
- SSH 输入命令却有明显延迟。
- 网页请求响应时间忽高忽低。
- 实时语音或交互业务出现卡顿。
为什么会这样?
因为大流量持续占用队列资源,其他数据包可能需要排在后面等待处理。
需要强调的是,TCP 套接字缓冲区和网络设备排队队列不是同一个东西。Bufferbloat 经常与瓶颈链路或设备队列的过度排队有关,并不意味着只要 TCP 缓冲区设置大,就一定发生 Bufferbloat。
但不合理的缓冲区和队列配置,确实可能加剧延迟问题。
多个 TCP 连接时,内存占用会怎样?
再考虑一种情况。
一台服务器不是只有一个 TCP 连接,而是同时运行几百个、几千个连接。
如果每个连接都可能使用数 MiB 的缓冲区,那么整体内存需求就可能很可观。
例如,假设有 1000 个活跃连接,每个连接实际使用的相关缓冲区合计达到 4 MiB,那么仅按这个简化假设计算:
1 | 1000 × 4 MiB = 4000 MiB |
也就是约 3.91 GiB。
这只是为了说明规模效应的估算,并不意味着每个连接都会立即分配 4 MiB,也不意味着实际内存占用会与这个数值完全一致。
Linux 通常会按实际需求管理套接字内存,TCP 自动调优也会影响缓冲区的使用情况。
但对于高并发服务器来说,不能只考虑单连接性能,还必须关注所有连接叠加后的内存消耗。
所以,我们需要在吞吐量、延迟和内存使用之间找到平衡,而不是盲目追求最大缓冲区。
在 RHEL 上应该怎样调整 TCP 缓冲区?
理论已经讲得差不多了,现在来看实际操作。
先查看当前参数:
1 | sysctl net.core.rmem_max |
建议先保存输出,作为修改前的记录。
接下来,根据测试结果决定是否需要调整。
例如,假设测试发现:
- 网络带宽较高。
- RTT 相对较大。
- TCP 窗口或缓冲区限制了吞吐量。
- 当前配置不足以满足目标 BDP。
那么可以考虑评估 TCP 缓冲区上限和自动调优范围。
不过,先别直接把某个固定数值写进配置文件。
先理解参数之间的关系
通常来说:
net.core.rmem_max和net.core.wmem_max与套接字缓冲区的上限有关。net.ipv4.tcp_rmem和net.ipv4.tcp_wmem控制 TCP 接收、发送缓冲区的相关范围。net.ipv4.tcp_window_scaling控制是否启用 TCP 窗口缩放。
这几个参数之间存在关联,但并不是简单的“一定要全部设成相同的数值”。
此外,Linux 对自动调优缓冲区和显式设置缓冲区的处理也有所不同。对于应用程序明确指定的套接字缓冲区,还需要考虑相应的套接字选项语义。
因此,调整时应该以当前系统的内核文档和实际测试结果为依据。
不要把临时测试值直接当成生产配置
如果确实需要尝试修改,可以先在测试环境中操作。
例如,使用 sysctl -w 临时调整某个参数:
1 | sysctl -w net.ipv4.tcp_window_scaling=1 |
这个命令会尝试将窗口缩放设置为启用状态。
但如果原来已经是 1,那它自然不会带来变化。对于窗口缩放功能,应该先确认当前值,并注意它是在 TCP 连接建立时协商的。
至于缓冲区上限,具体应设置多大,必须结合当前 RHEL 版本、内核文档、链路带宽、RTT、并发连接数量和内存容量决定。
如果准备持久化配置,可以在 /etc/sysctl.d/ 下创建单独的配置文件,再通过 sysctl --system 应用。
但在没有明确瓶颈证据之前,不建议直接复制网上的“大缓冲区”配置。
网络调优到底应该怎么做?给你一套排查思路
前面讲了这么多知识点,现在把它们串起来。
假设业务反馈:两台 RHEL 服务器之间传输大文件时速度很慢,而且 SSH 偶尔卡顿。
我们可以按照下面的顺序排查。
第一步:确认链路和基础网络状态
查看网卡信息:
1 | ip -br link |
重点关注:
- 网卡是否处于正常状态。
- 协商速率是否符合预期。
- 收发错误和丢包计数是否持续增长。
- 是否存在链路、驱动或配置异常。
第二步:建立吞吐量基线
使用 iperf3 测试:
1 | iperf3 -c host1 -t 10 |
再测试反方向:
1 | iperf3 -c host1 -t 10 -R |
如果双向表现差异明显,就需要进一步检查两个方向的链路、队列和处理能力。
第三步:观察延迟和 TCP 重传
测试 RTT:
1 | ping -c 10 host1 |
查看 TCP 统计:
1 | ss -ti |
ss -ti 可以展示 TCP 连接的详细信息。不同内核版本显示的字段可能不同,可以重点关注 RTT、拥塞窗口、重传相关信息等。
如果出现明显重传,应继续排查丢包、拥塞和链路质量,而不是直接把所有缓冲区调大。
第四步:检查 CPU、中断和网卡队列
1 | mpstat -P ALL 1 |
这些命令分别帮助我们观察 CPU 使用情况、网络软中断分布、中断分布、Ring Buffer 配置以及驱动提供的统计信息。
如果发现单个 CPU 长期繁忙、某些队列异常或者接收资源不足,就可以针对相应的问题继续分析。
第五步:根据 BDP 判断 TCP 窗口是否可能成为瓶颈
使用测得的链路带宽和 RTT 估算 BDP。
例如:
1 | BDP = 带宽 × RTT ÷ 8 |
如果带宽以 bit/s 计,RTT 以秒计,结果就是字节数。
接下来检查当前 TCP 缓冲区范围和窗口缩放配置,判断它们是否可能限制在途数据量。
如果 BDP 远大于可用窗口,就需要进一步验证窗口限制是否确实存在。
第六步:每次只修改一个主要变量
比如确认缓冲区上限确实不足,就只调整相关缓冲区参数,然后重复相同的 iperf3 测试。
如果怀疑网卡接收队列不足,则应在确认驱动支持、资源确实不足之后,再评估 Ring Buffer 调整。
每次修改后,都应该比较:
- 吞吐量是否提高。
- RTT 和业务延迟是否恶化。
- 重传和丢包是否减少。
- CPU 使用率是否发生明显变化。
- 内存占用是否增加。
只有这样,才能知道究竟是哪项调整发挥了作用。
最后总结一下
这一篇我们主要建立了 Linux 网络性能调优的基础思路。
第一,先测量,再修改。iperf3 可以帮助我们建立吞吐量基线,但网络延迟、丢包和 CPU 瓶颈还需要其他工具配合分析。
第二,网络数据从应用程序到网卡,需要经过套接字缓冲区、协议栈、网络设备队列和驱动等多个环节。发送、接收、中断、软中断以及 CPU 处理能力,都可能成为瓶颈。
第三,网卡 Ring Buffer、CPU 亲和性和网络软中断都值得关注,但不能看到丢包或者 CPU 忙碌,就认定只需要增大队列或调整中断。
第四,TCP 缓冲区参数各有作用。tcp_mem 和 udp_mem 涉及协议栈内存压力管理,而 rmem_max、wmem_max、tcp_rmem 和 tcp_wmem 则涉及套接字缓冲区的相关限制与范围。
第五,BDP 是理解 TCP 窗口和缓冲区需求的重要工具。它告诉我们,在给定带宽和 RTT 的情况下,要充分利用链路,可能需要多大的在途数据量。
最后,缓冲区不是越大越好。缓冲区太小可能限制吞吐量,队列和缓冲区过大则可能增加排队延迟和内存压力。
真正的网络调优,不是把参数改到最大,而是找到瓶颈,用合适的参数解决它,再用测试结果证明调整有效。
