1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. 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
2
3
4
[root@host1 ~]# firewall-cmd --add-port=5201/tcp
success
[root@host1 ~]# firewall-cmd --add-port=5201/tcp --permanent
success

在 host1 上启动服务端:

1
2
3
4
[root@host1 ~]# iperf3 -s 
-----------------------------------------------------------
Server listening on 5201 (test #1)
-----------------------------------------------------------

然后在 host2 上执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@host2 ~]# iperf3 -c 192.168.8.128 -t 10
Connecting to host 192.168.8.128, port 5201
[ 5] local 192.168.8.129 port 37390 connected to 192.168.8.128 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 758 MBytes 6.35 Gbits/sec 26 1.06 MBytes
[ 5] 1.00-2.00 sec 782 MBytes 6.56 Gbits/sec 0 1.31 MBytes
[ 5] 2.00-3.00 sec 781 MBytes 6.55 Gbits/sec 5 1.21 MBytes
[ 5] 3.00-4.00 sec 778 MBytes 6.53 Gbits/sec 3 1.25 MBytes
[ 5] 4.00-5.00 sec 753 MBytes 6.31 Gbits/sec 1 1.27 MBytes
[ 5] 5.00-6.00 sec 760 MBytes 6.38 Gbits/sec 3 1.31 MBytes
[ 5] 6.00-7.00 sec 733 MBytes 6.15 Gbits/sec 1 1.34 MBytes
[ 5] 7.00-8.00 sec 776 MBytes 6.51 Gbits/sec 2 1.38 MBytes
[ 5] 8.00-9.00 sec 741 MBytes 6.22 Gbits/sec 4 1.39 MBytes
[ 5] 9.00-10.00 sec 750 MBytes 6.28 Gbits/sec 2 1.41 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 7.44 GBytes 6.39 Gbits/sec 47 sender
[ 5] 0.00-10.00 sec 7.44 GBytes 6.39 Gbits/sec receiver

iperf Done.

其中:

  • -c host1:指定测试服务端。
  • -t 10:持续测试 10 秒。
  • iperf3 -s:启动服务端,等待客户端连接。

如果没有 DNS 解析,可以直接使用 host1 的 IP 地址。

字段含义
Interval统计的时间区间,每行这里是 1 秒
Transfer这个区间传输的数据量
Bitrate这个区间测得的平均传输速率
Retr发送端统计的 TCP 重传次数
CwndTCP 拥塞窗口大小

这里有个容易忽略的细节:输出中的 GBytes 和 Gbits/sec 采用十进制单位,而计算机内存中常见的 GiB、KiB 采用二进制单位。因此6.39 Gbits/sec 则相当于约 799 MB/s。

这意味着,在这次测试条件下,两台机器之间的 TCP 数据传输速率接近每秒 800 MB。

另外,iperf3 的默认 TCP 测试主要关注吞吐量。它不能单凭这一行输出,就告诉你网络延迟、抖动和丢包分别是多少。

如果想测试反方向的传输,可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@host2 ~]# iperf3 -c 192.168.8.128 -t 10 -R
Connecting to host 192.168.8.128, port 5201
Reverse mode, remote host 192.168.8.128 is sending
[ 5] local 192.168.8.129 port 54162 connected to 192.168.8.128 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 854 MBytes 7.16 Gbits/sec
[ 5] 1.00-2.00 sec 869 MBytes 7.29 Gbits/sec
[ 5] 2.00-3.00 sec 946 MBytes 7.93 Gbits/sec
[ 5] 3.00-4.00 sec 942 MBytes 7.90 Gbits/sec
[ 5] 4.00-5.00 sec 941 MBytes 7.89 Gbits/sec
[ 5] 5.00-6.00 sec 941 MBytes 7.89 Gbits/sec
[ 5] 6.00-7.00 sec 952 MBytes 7.99 Gbits/sec
[ 5] 7.00-8.00 sec 949 MBytes 7.96 Gbits/sec
[ 5] 8.00-9.00 sec 951 MBytes 7.98 Gbits/sec
[ 5] 9.00-10.00 sec 944 MBytes 7.92 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 9.07 GBytes 7.79 Gbits/sec 24 sender
[ 5] 0.00-10.00 sec 9.07 GBytes 7.79 Gbits/sec receiver

iperf Done.

-R 表示反向测试,即由服务端向客户端发送数据。

如果怀疑单个 TCP 流无法充分利用带宽,还可以测试多个并行连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
[root@host2 ~]# iperf3 -c 192.168.8.128 -t 10 -P 4
Connecting to host 192.168.8.128, port 5201
[ 5] local 192.168.8.129 port 46650 connected to 192.168.8.128 port 5201
[ 7] local 192.168.8.129 port 46658 connected to 192.168.8.128 port 5201
[ 9] local 192.168.8.129 port 46672 connected to 192.168.8.128 port 5201
[ 11] local 192.168.8.129 port 46682 connected to 192.168.8.128 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 238 MBytes 1.99 Gbits/sec 89 385 KBytes
[ 7] 0.00-1.00 sec 189 MBytes 1.59 Gbits/sec 65 215 KBytes
[ 9] 0.00-1.00 sec 116 MBytes 969 Mbits/sec 14 385 KBytes
[ 11] 0.00-1.00 sec 224 MBytes 1.87 Gbits/sec 66 419 KBytes
[SUM] 0.00-1.00 sec 767 MBytes 6.43 Gbits/sec 234
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 1.00-2.00 sec 170 MBytes 1.43 Gbits/sec 14 414 KBytes
[ 7] 1.00-2.00 sec 196 MBytes 1.64 Gbits/sec 13 293 KBytes
[ 9] 1.00-2.00 sec 114 MBytes 952 Mbits/sec 14 272 KBytes
[ 11] 1.00-2.00 sec 311 MBytes 2.62 Gbits/sec 35 297 KBytes
[SUM] 1.00-2.00 sec 790 MBytes 6.63 Gbits/sec 76
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 2.00-3.00 sec 165 MBytes 1.38 Gbits/sec 22 334 KBytes
[ 7] 2.00-3.00 sec 248 MBytes 2.08 Gbits/sec 27 273 KBytes
[ 9] 2.00-3.00 sec 126 MBytes 1.06 Gbits/sec 7 266 KBytes
[ 11] 2.00-3.00 sec 232 MBytes 1.94 Gbits/sec 27 308 KBytes
[SUM] 2.00-3.00 sec 770 MBytes 6.46 Gbits/sec 83
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 3.00-4.00 sec 132 MBytes 1.11 Gbits/sec 9 331 KBytes
[ 7] 3.00-4.00 sec 236 MBytes 1.98 Gbits/sec 26 300 KBytes
[ 9] 3.00-4.00 sec 108 MBytes 907 Mbits/sec 12 273 KBytes
[ 11] 3.00-4.00 sec 233 MBytes 1.96 Gbits/sec 12 498 KBytes
[SUM] 3.00-4.00 sec 709 MBytes 5.95 Gbits/sec 59
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 4.00-5.00 sec 195 MBytes 1.64 Gbits/sec 10 451 KBytes
[ 7] 4.00-5.00 sec 197 MBytes 1.65 Gbits/sec 11 451 KBytes
[ 9] 4.00-5.00 sec 68.9 MBytes 578 Mbits/sec 26 194 KBytes
[ 11] 4.00-5.00 sec 296 MBytes 2.48 Gbits/sec 28 297 KBytes
[SUM] 4.00-5.00 sec 756 MBytes 6.35 Gbits/sec 75
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 5.00-6.00 sec 131 MBytes 1.10 Gbits/sec 9 314 KBytes
[ 7] 5.00-6.00 sec 222 MBytes 1.86 Gbits/sec 23 403 KBytes
[ 9] 5.00-6.00 sec 120 MBytes 1.01 Gbits/sec 8 280 KBytes
[ 11] 5.00-6.00 sec 265 MBytes 2.22 Gbits/sec 12 322 KBytes
[SUM] 5.00-6.00 sec 739 MBytes 6.20 Gbits/sec 52
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 6.00-7.00 sec 108 MBytes 909 Mbits/sec 15 315 KBytes
[ 7] 6.00-7.00 sec 213 MBytes 1.79 Gbits/sec 28 255 KBytes
[ 9] 6.00-7.00 sec 147 MBytes 1.24 Gbits/sec 10 250 KBytes
[ 11] 6.00-7.00 sec 296 MBytes 2.48 Gbits/sec 3 622 KBytes
[SUM] 6.00-7.00 sec 764 MBytes 6.41 Gbits/sec 56
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 7.00-8.00 sec 97.2 MBytes 816 Mbits/sec 15 264 KBytes
[ 7] 7.00-8.00 sec 211 MBytes 1.77 Gbits/sec 15 354 KBytes
[ 9] 7.00-8.00 sec 158 MBytes 1.33 Gbits/sec 13 351 KBytes
[ 11] 7.00-8.00 sec 281 MBytes 2.36 Gbits/sec 23 426 KBytes
[SUM] 7.00-8.00 sec 748 MBytes 6.27 Gbits/sec 66
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 8.00-9.00 sec 89.8 MBytes 753 Mbits/sec 9 218 KBytes
[ 7] 8.00-9.00 sec 253 MBytes 2.12 Gbits/sec 15 431 KBytes
[ 9] 8.00-9.00 sec 160 MBytes 1.34 Gbits/sec 13 339 KBytes
[ 11] 8.00-9.00 sec 260 MBytes 2.18 Gbits/sec 13 492 KBytes
[SUM] 8.00-9.00 sec 762 MBytes 6.40 Gbits/sec 50
- - - - - - - - - - - - - - - - - - - - - - - - -
[ 5] 9.00-10.00 sec 101 MBytes 849 Mbits/sec 15 219 KBytes
[ 7] 9.00-10.00 sec 234 MBytes 1.97 Gbits/sec 33 321 KBytes
[ 9] 9.00-10.00 sec 159 MBytes 1.33 Gbits/sec 12 307 KBytes
[ 11] 9.00-10.00 sec 294 MBytes 2.47 Gbits/sec 32 358 KBytes
[SUM] 9.00-10.00 sec 789 MBytes 6.62 Gbits/sec 92
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 1.39 GBytes 1.20 Gbits/sec 207 sender
[ 5] 0.00-10.00 sec 1.39 GBytes 1.20 Gbits/sec receiver
[ 7] 0.00-10.00 sec 2.15 GBytes 1.84 Gbits/sec 256 sender
[ 7] 0.00-10.00 sec 2.15 GBytes 1.84 Gbits/sec receiver
[ 9] 0.00-10.00 sec 1.25 GBytes 1.07 Gbits/sec 129 sender
[ 9] 0.00-10.00 sec 1.25 GBytes 1.07 Gbits/sec receiver
[ 11] 0.00-10.00 sec 2.63 GBytes 2.26 Gbits/sec 251 sender
[ 11] 0.00-10.00 sec 2.63 GBytes 2.26 Gbits/sec receiver
[SUM] 0.00-10.00 sec 7.42 GBytes 6.37 Gbits/sec 843 sender
[SUM] 0.00-10.00 sec 7.41 GBytes 6.36 Gbits/sec receiver

iperf Done.
[root@host2 ~]#

-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@host2 ~]# mpstat -P ALL 1
Linux 6.12.0-55.12.1.el10_0.x86_64 (host2) 10/12/2026 _x86_64_ (2 CPU)

11:04:33 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
11:04:34 PM all 0.00 0.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 99.00
11:04:34 PM 0 0.00 0.00 0.00 0.00 0.00 0.99 0.00 0.00 0.00 99.01
11:04:34 PM 1 0.00 0.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 99.00

11:04:34 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
11:04:35 PM all 0.00 0.00 0.50 0.00 0.00 0.00 0.00 0.00 0.00 99.50
11:04:35 PM 0 0.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 99.00
11:04:35 PM 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00

11:04:35 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
11:04:36 PM all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
11:04:36 PM 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
11:04:36 PM 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00

11:04:36 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
11:04:37 PM all 0.00 0.00 0.00 0.00 0.50 0.00 0.00 0.00 0.00 99.50
11:04:37 PM 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
11:04:37 PM 1 0.00 0.00 0.00 0.00 0.99 0.00 0.00 0.00 0.00 99.01

Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
Average: all 0.00 0.00 0.12 0.00 0.12 0.25 0.00 0.00 0.00 99.50
Average: 0 0.00 0.00 0.25 0.00 0.00 0.25 0.00 0.00 0.00 99.50
Average: 1 0.00 0.00 0.00 0.00 0.25 0.25 0.00 0.00 0.00 99.50

再查看软中断统计:

1
cat /proc/softirqs

重点关注 NET_RX 和 NET_TX。

  • NET_RX:与网络接收软中断处理有关。
  • NET_TX:与网络发送软中断处理有关。

如果某个 CPU 的网络软中断计数增长特别快,同时该 CPU 的软中断占用很高,就值得进一步检查网卡队列分布、中断亲和性、RPS/RFS 配置以及流量模式。

当然,计数增长快本身不是故障。高流量服务器本来就会产生大量网络处理事件,必须结合 CPU 利用率、丢包和实际业务表现一起判断。

网卡环形缓冲区为什么会影响性能?

网卡通常会使用环形队列,也就是常说的 Ring Buffer。可以把它理解为一个循环使用的收发任务列表。网卡和驱动通过描述符记录哪些缓冲区可以使用、哪些数据已经到达、哪些发送任务尚未完成。如果接收数据持续到达,而驱动和内核来不及处理,接收资源就可能耗尽,进而导致丢包。

先查看网卡名称:

1
2
3
4
[root@host1 ~]# ip -br link
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
ens160 UP 00:0c:29:29:c1:31 <BROADCAST,MULTICAST,UP,LOWER_UP>

先安装调试工具

1
[root@host1 ~]# dnf install ethtool

假设网卡名称为 ens160,可以查看驱动信息和队列设置:

1
2
3
4
5
6
7
8
9
10
11
[root@host1 ~]# ethtool -i ens160
driver: vmxnet3
version: 1.9.0.0-k-NAPI
firmware-version:
expansion-rom-version:
bus-info: 0000:03:00.0
supports-statistics: yes
supports-test: no
supports-eeprom-access: no
supports-register-dump: yes
supports-priv-flags: no
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@host1 ~]# ethtool -g ens160
Ring parameters for ens160:
Pre-set maximums:
RX: 4096
RX Mini: n/a
RX Jumbo: 4096
TX: 4096
TX push buff len: n/a
HDS thresh: n/a
Current hardware settings:
RX: 1024
RX Mini: n/a
RX Jumbo: 512
TX: 512
RX Buf Len: n/a
CQE Size: n/a
TX Push: off
RX Push: off
TX push buff len: n/a
TCP data split: n/a
HDS thresh: n/a

其中:

  • ethtool -i:查看网卡驱动和相关信息。
  • ethtool -g:查看网卡支持的 Ring Buffer 参数,以及当前设置和最大支持值。

如果驱动支持,也可以查看统计信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@host1 ~]# ethtool -S ens160
NIC statistics:
Tx Queue#: 0
TSO pkts tx: 6723440
TSO bytes tx: 10176253657
ucast pkts tx: 7095418
ucast bytes tx: 10210204467
mcast pkts tx: 11
mcast bytes tx: 738
bcast pkts tx: 1
bcast bytes tx: 324
pkts tx err: 0
pkts tx discard: 0
drv dropped tx total: 0
too many frags: 0
giant hdr: 0
hdr err: 0
tso: 0
ring full: 0
pkts linearized: 0
hdr cloned: 0
giant hdr: 0
xdp xmit: 0
xdp xmit err: 0
Tx Queue#: 1
...

不同驱动提供的统计项并不完全相同。可以重点寻找带有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
[root@host1 ~]# cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 43 0 0 0 IR-IO-APIC 2-edge timer
1: 0 9 0 0 IR-IO-APIC 1-edge i8042
8: 0 0 0 0 IR-IO-APIC 8-edge rtc0
9: 0 0 0 0 IR-IO-APIC 9-fasteoi acpi
12: 15 0 0 0 IR-IO-APIC 12-edge i8042
14: 0 0 0 0 IR-IO-APIC 14-edge ata_piix
15: 0 0 0 0 IR-IO-APIC 15-edge ata_piix
16: 0 0 0 0 IR-IO-APIC 16-fasteoi ehci_hcd:usb1, vmwgfx
18: 0 0 0 64 IR-IO-APIC 18-fasteoi uhci_hcd:usb2
19: 0 0 0 0 IR-IO-APIC 19-fasteoi snd_ens1371
24: 0 0 0 0 IR-PCI-MSI-0000:00:15.0 0-edge PCIe PME, pciehp
25: 0 0 0 0 IR-PCI-MSI-0000:00:15.1 0-edge PCIe PME, pciehp
26: 0 0 0 0 IR-PCI-MSI-0000:00:15.2 0-edge PCIe PME, pciehp
27: 0 0 0 0 IR-PCI-MSI-0000:00:15.3 0-edge PCIe PME, pciehp
28: 0 0 0 0 IR-PCI-MSI-0000:00:15.4 0-edge PCIe PME, pciehp
29: 0 0 0 0 IR-PCI-MSI-0000:00:15.5 0-edge PCIe PME, pciehp
30: 0 0 0 0 IR-PCI-MSI-0000:00:15.6 0-edge PCIe PME, pciehp
31: 0 0 0 0 IR-PCI-MSI-0000:00:15.7 0-edge PCIe PME, pciehp
32: 0 0 0 0 IR-PCI-MSI-0000:00:16.0 0-edge PCIe PME, pciehp
33: 0 0 0 0 IR-PCI-MSI-0000:00:16.1 0-edge PCIe PME, pciehp
34: 0 0 0 0 IR-PCI-MSI-0000:00:16.2 0-edge PCIe PME, pciehp
35: 0 0 0 0 IR-PCI-MSI-0000:00:16.3 0-edge PCIe PME, pciehp
36: 0 0 0 0 IR-PCI-MSI-0000:00:16.4 0-edge PCIe PME, pciehp
37: 0 0 0 0 IR-PCI-MSI-0000:00:16.5 0-edge PCIe PME, pciehp
38: 0 0 0 0 IR-PCI-MSI-0000:00:16.6 0-edge PCIe PME, pciehp
39: 0 0 0 0 IR-PCI-MSI-0000:00:16.7 0-edge PCIe PME, pciehp
40: 0 0 0 0 IR-PCI-MSI-0000:00:17.0 0-edge PCIe PME, pciehp
41: 0 0 0 0 IR-PCI-MSI-0000:00:17.1 0-edge PCIe PME, pciehp
42: 0 0 0 0 IR-PCI-MSI-0000:00:17.2 0-edge PCIe PME, pciehp
43: 0 0 0 0 IR-PCI-MSI-0000:00:17.3 0-edge PCIe PME, pciehp
44: 0 0 0 0 IR-PCI-MSI-0000:00:17.4 0-edge PCIe PME, pciehp
45: 0 0 0 0 IR-PCI-MSI-0000:00:17.5 0-edge PCIe PME, pciehp
46: 0 0 0 0 IR-PCI-MSI-0000:00:17.6 0-edge PCIe PME, pciehp
47: 0 0 0 0 IR-PCI-MSI-0000:00:17.7 0-edge PCIe PME, pciehp
48: 0 0 0 0 IR-PCI-MSI-0000:00:18.0 0-edge PCIe PME, pciehp
49: 0 0 0 0 IR-PCI-MSI-0000:00:18.1 0-edge PCIe PME, pciehp
50: 0 0 0 0 IR-PCI-MSI-0000:00:18.2 0-edge PCIe PME, pciehp
51: 0 0 0 0 IR-PCI-MSI-0000:00:18.3 0-edge PCIe PME, pciehp
52: 0 0 0 0 IR-PCI-MSI-0000:00:18.4 0-edge PCIe PME, pciehp
53: 0 0 0 0 IR-PCI-MSI-0000:00:18.5 0-edge PCIe PME, pciehp
54: 0 0 0 0 IR-PCI-MSI-0000:00:18.6 0-edge PCIe PME, pciehp
55: 0 0 0 0 IR-PCI-MSI-0000:00:18.7 0-edge PCIe PME, pciehp
56: 49 130 167 433 IR-PCI-MSI-0000:02:03.0 0-edge ahci[0000:02:03.0]
57: 0 0 36 0 IR-PCI-MSIX-0000:0b:00.0 0-edge nvme0q0
58: 2090 0 0 0 IR-PCI-MSIX-0000:0b:00.0 1-edge nvme0q1
59: 0 3539 0 0 IR-PCI-MSIX-0000:0b:00.0 2-edge nvme0q2
60: 0 0 673 0 IR-PCI-MSIX-0000:0b:00.0 3-edge nvme0q3
61: 0 0 0 842 IR-PCI-MSIX-0000:0b:00.0 4-edge nvme0q4
62: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 5-edge nvme0q5
63: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 6-edge nvme0q6
64: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 7-edge nvme0q7
65: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 8-edge nvme0q8
66: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 9-edge nvme0q9
67: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 10-edge nvme0q10
68: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 11-edge nvme0q11
69: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 12-edge nvme0q12
70: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 13-edge nvme0q13
71: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 14-edge nvme0q14
72: 0 0 0 0 IR-PCI-MSIX-0000:0b:00.0 15-edge nvme0q15
73: 0 0 0 0 IR-PCI-MSIX-0000:00:07.7 0-edge vmw_vmci
74: 0 0 0 0 IR-PCI-MSIX-0000:00:07.7 1-edge vmw_vmci
75: 0 1 0 0 IR-PCI-MSIX-0000:00:07.7 2-edge vmw_vmci
76: 207158 3181 0 45 IR-PCI-MSIX-0000:03:00.0 0-edge ens160-rxtx-0
77: 2481 10 99110 4 IR-PCI-MSIX-0000:03:00.0 1-edge ens160-rxtx-1
78: 0 109100 786 0 IR-PCI-MSIX-0000:03:00.0 2-edge ens160-rxtx-2
79: 1 9881 8 35818 IR-PCI-MSIX-0000:03:00.0 3-edge ens160-rxtx-3
80: 0 0 0 0 IR-PCI-MSIX-0000:03:00.0 4-edge ens160-event-4
NMI: 0 0 0 0 Non-maskable interrupts
LOC: 54228 46055 48973 45855 Local timer interrupts
SPU: 0 0 0 0 Spurious interrupts
PMI: 0 0 0 0 Performance monitoring interrupts
IWI: 1 1 0 1 IRQ work interrupts
RTR: 0 0 0 0 APIC ICR read retries
RES: 1850 2228 1488 1516 Rescheduling interrupts
CAL: 26939 64999 61142 54002 Function call interrupts
TLB: 102 162 60 79 TLB shootdowns
TRM: 0 0 0 0 Thermal event interrupts
THR: 0 0 0 0 Threshold APIC interrupts
DFR: 0 0 0 0 Deferred Error APIC interrupts
MCE: 0 0 0 0 Machine check exceptions
MCP: 5 6 6 6 Machine check polls
ERR: 0
MIS: 0
PIN: 0 0 0 0 Posted-interrupt notification event
NPI: 0 0 0 0 Nested posted-interrupt event
PIW: 0 0 0 0 Posted-interrupt wakeup event
PMN: 0 0 0 0 Posted MSI notification event

如果某个网卡队列对应的中断几乎全部集中在 CPU0,而 CPU0 又承担了大量其他任务,就有必要检查中断分配是否合理。

不过,这里不能简单地得出“中断和产生中断的进程必须放在同一个 CPU 上”的结论。

现代网络栈中,中断处理、NAPI 轮询、软中断、RPS/RFS 和应用程序运行位置之间的关系比较复杂。把它们放在同一个 CPU 上,有时可以改善缓存局部性;但如果该 CPU 已经很忙,也可能适得其反。

生产环境中,应该结合以下因素进行判断:

  • 网卡是否支持多队列。
  • 各队列的流量是否均衡。
  • IRQ 和软中断是否集中在少数 CPU。
  • 应用程序线程的 CPU 亲和性。
  • NUMA 节点与网卡所在位置。
  • 调整前后的延迟、吞吐量和 CPU 使用率。

RHEL 网络缓冲区:这些参数到底管什么?

前面提到了网络队列和缓冲区,现在来看最常见的一组内核参数。

先把当前配置查出来:

1
2
3
4
5
6
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.ipv4.tcp_mem
sysctl net.ipv4.udp_mem

这些参数看起来相似,但作用范围并不一样。

4.1 系统级 TCP 和 UDP 内存限制

先看:

1
2
3
4
5
[root@host1 ~]# sysctl net.ipv4.tcp_mem
net.ipv4.tcp_mem = 93090 124122 186180
[root@host1 ~]# sysctl net.ipv4.udp_mem
net.ipv4.udp_mem = 186183 248244 372366

这两个参数与 TCP、UDP 协议栈可使用的内存管理有关。

在常见的 Linux 内核实现中,tcp_mem 和 udp_mem 使用三个数值,分别表示不同的内存压力阈值。

以 tcp_mem 为例:

1
net.ipv4.tcp_mem = 123456 164608 246912

这里的数字仅为示例。

三个数值分别表示:

  • 第一个值:低水位,内存使用量低于该值时,通常不会因为这一机制进入内存压力状态。
  • 第二个值:压力水位,超过该值后,TCP 内存管理会更加积极地控制内存使用。
  • 第三个值:高水位,用于限制 TCP 协议栈的整体内存使用。

这里的单位通常是内存页,而不是字节。

可以使用下面的命令查看系统页大小:

1
2
[root@host1 ~]# getconf PAGESIZE
4096

在常见的 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
2
3
4
5
[root@host1 ~]# sysctl net.core.rmem_max
net.core.rmem_max = 212992
[root@host1 ~]# sysctl net.core.wmem_max
net.core.wmem_max = 212992

它们分别与套接字接收缓冲区和发送缓冲区的最大设置值有关。

简单理解:

  • 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
2
3
4
net.ipv4.tcp_rmem = 4096        131072  6291456
[root@host1 ~]# sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 16384 4194304

通常可以看到三个数值,例如:

1
2
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304

这里同样只是示例,不代表所有 RHEL 版本的默认值。

这三个数值通常分别对应:

  • 最小值。
  • 默认值。
  • 自动调优可使用的最大值。

对于 TCP 接收缓冲区,tcp_rmem 决定了 TCP 接收缓冲区的相关范围;对于发送缓冲区,tcp_wmem 则决定发送缓冲区的相关范围。

Linux TCP 自动调优机制可以根据连接状况动态调整缓冲区使用量,而不是始终固定使用第二个数值。

需要特别注意,TCP 缓冲区大小并不是简单地由这几个参数中的某一个独立决定的。套接字选项、全局限制和内核的具体实现都会影响最终结果。

如果希望查看窗口缩放功能是否开启,可以执行:

1
2
[root@host1 ~]# sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1

后面我们还会讲它与 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
2
3
4
5
6
7
8
9
BDP = 带宽 × RTT

= 1,000,000,000 bit/s × 0.000341 s

= 341,000 bit

= 42,625 Bytes

≈ 41.63 KiB

这意味着,在上述带宽和 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
2
3
1,000,000,000 × 0.0005 ÷ 8
= 62,500 Bytes
≈ 61.04 KiB

第二条链路:

1
2
3
1,000,000,000 × 0.05 ÷ 8
= 6,250,000 Bytes
≈ 5.96 MiB

可以看到,两条链路的带宽相同,但 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
2
3
4
5
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.ipv4.tcp_window_scaling

建议先保存输出,作为修改前的记录。

接下来,根据测试结果决定是否需要调整。

例如,假设测试发现:

  • 网络带宽较高。
  • 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
2
3
ip -br link
ip -s link
ethtool ens160

重点关注:

  • 网卡是否处于正常状态。
  • 协商速率是否符合预期。
  • 收发错误和丢包计数是否持续增长。
  • 是否存在链路、驱动或配置异常。

第二步:建立吞吐量基线

使用 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
2
3
4
5
mpstat -P ALL 1
cat /proc/softirqs
cat /proc/interrupts
ethtool -g ens160
ethtool -S ens160

这些命令分别帮助我们观察 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 的情况下,要充分利用链路,可能需要多大的在途数据量。

最后,缓冲区不是越大越好。缓冲区太小可能限制吞吐量,队列和缓冲区过大则可能增加排队延迟和内存压力。

真正的网络调优,不是把参数改到最大,而是找到瓶颈,用合适的参数解决它,再用测试结果证明调整有效。