环境准备
服务器信息
两台腾讯云机器 t04(172.19.0.4)、t11(172.19.0.11),系统为 Ubuntu 22.04,内核为 5.15.0-139-generic。默认 RT 在 0.16s 左右。
$ ping 172.19.0.4
PING 172.19.0.4 (172.19.0.4) 56(84) bytes of data.
64 bytes from 172.19.0.4: icmp_seq=1 ttl=64 time=0.195 ms
64 bytes from 172.19.0.4: icmp_seq=2 ttl=64 time=0.216 ms
64 bytes from 172.19.0.4: icmp_seq=3 ttl=64 time=0.253 ms
64 bytes from 172.19.0.4: icmp_seq=4 ttl=64 time=0.158 ms
64 bytes from 172.19.0.4: icmp_seq=5 ttl=64 time=0.164 ms
64 bytes from 172.19.0.4: icmp_seq=6 ttl=64 time=0.139 ms
64 bytes from 172.19.0.4: icmp_seq=7 ttl=64 time=0.134 ms
64 bytes from 172.19.0.4: icmp_seq=8 ttl=64 time=0.153 ms
64 bytes from 172.19.0.4: icmp_seq=9 ttl=64 time=0.157 ms
64 bytes from 172.19.0.4: icmp_seq=10 ttl=64 time=0.149 ms
64 bytes from 172.19.0.4: icmp_seq=11 ttl=64 time=0.148 ms
64 bytes from 172.19.0.4: icmp_seq=12 ttl=64 time=0.157 ms
64 bytes from 172.19.0.4: icmp_seq=13 ttl=64 time=0.151 ms
64 bytes from 172.19.0.4: icmp_seq=14 ttl=64 time=0.156 ms
64 bytes from 172.19.0.4: icmp_seq=15 ttl=64 time=0.156 ms
64 bytes from 172.19.0.4: icmp_seq=16 ttl=64 time=0.160 ms
64 bytes from 172.19.0.4: icmp_seq=17 ttl=64 time=0.159 ms
^C
--- 172.19.0.4 ping statistics ---
17 packets transmitted, 17 received, 0% packet loss, time 16382ms
rtt min/avg/max/mdev = 0.134/0.165/0.253/0.028 ms
内核参数信息
内核默认参数值
$ sudo sysctl -a | egrep "rmem|wmem|tcp_mem|adv_win|moderate"
net.core.rmem_default = 212992
net.core.rmem_max = 212992
net.core.wmem_default = 212992
net.core.wmem_max = 212992
net.ipv4.tcp_adv_win_scale = 1
net.ipv4.tcp_mem = 41295 55062 82590
net.ipv4.tcp_moderate_rcvbuf = 1
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
vm.lowmem_reserve_ratio = 256 256 32 0 0
参数含义如下:
- 核心网络参数 (net.core.)
参数名称 | 作用范围 | 默认行为 | 约束关系 | 配置值 | 影响 |
---|---|---|---|---|---|
net.core.rmem_default | 所有协议套接字 | 未设置SO_RCVBUF时的默认接收缓冲区 | UDP默认值,TCP有专门设置时被覆盖 | 212992 (208KB) | UDP接收缓冲区默认208KB |
net.core.rmem_max | 所有协议套接字 | 接收缓冲区的硬性上限 | 覆盖所有协议的max设置 | 212992 (208KB) | 严重限制:TCP最大6MB被压缩到208KB |
net.core.wmem_default | 所有协议套接字 | 未设置SO_SNDBUF时的默认发送缓冲区 | UDP默认值,TCP有专门设置时被覆盖 | 212992 (208KB) | UDP发送缓冲区默认208KB |
net.core.wmem_max | 所有协议套接字 | 发送缓冲区的硬性上限 | 覆盖所有协议的max设置 | 212992 (208KB) | 严重限制:TCP最大4MB被压缩到208KB |
- TCP专用参数 (net.ipv4.tcp_)
参数名称 | 作用范围 | 格式说明 | 约束关系 | 配置值 | 实际效果 |
---|---|---|---|---|---|
net.ipv4.tcp_rmem | 仅TCP连接 | [最小值 默认值 最大值] | 受net.core.rmem_max 硬性限制 | 4096 131072 6291456 | 最小4KB,默认128KB,最大被限制到208KB |
net.ipv4.tcp_wmem | 仅TCP连接 | [最小值 默认值 最大值] | 受net.core.wmem_max 硬性限制 | 4096 16384 4194304 | 最小4KB,默认16KB,最大被限制到208KB |
net.ipv4.tcp_mem | 全局TCP内存池 | [低水位 压力位 高水位] (页) | 独立于单连接缓冲区设置 | 41295 55062 82590 | 全局限制161MB-215MB-323MB |
net.ipv4.tcp_moderate_rcvbuf | TCP动态调整 | 0 =关闭,1 =开启 | 在tcp_rmem范围内动态调整 | 1 | 开启动态调整,但被208KB限制 |
net.ipv4.tcp_adv_win_scale | TCP窗口计算 | 整数值 | 影响TCP窗口大小算法 | 1 | 适中的窗口缩放因子 |
- UDP专用参数 (net.ipv4.udp_)
参数名称 | 作用范围 | 含义 | 约束关系 | 配置值 | 实际效果 |
---|---|---|---|---|---|
net.ipv4.udp_rmem_min | 仅UDP连接 | UDP接收缓冲区最小值 | 受net.core.rmem_max 限制 | 4096 | UDP最小接收缓冲区4KB |
net.ipv4.udp_wmem_min | 仅UDP连接 | UDP发送缓冲区最小值 | 受net.core.wmem_max 限制 | 4096 | UDP最小发送缓冲区4KB |
- 内存管理参数 (vm.)
参数名称 | 作用范围 | 含义 | 配置值 | 影响 |
---|---|---|---|---|
vm.lowmem_reserve_ratio | 系统内存管理 | 各内存区域预留比例 | 256 256 32 0 0 | 防止内存区域被耗尽 |
上述参数结合 Socket 编程,对缓冲区的影响如下:
- TCP Socket缓冲区行为
场景 | 接收缓冲区 (SO_RCVBUF) | 发送缓冲区 (SO_SNDBUF) |
---|---|---|
不调用setsockopt() | 默认:131072 (128KB) | 默认:16384 (16KB) |
调用setsockopt(1MB) | 实际:~425984 (208KB×2) | 实际:~425984 (208KB×2) |
调用setsockopt(100KB) | 实际:~200KB (100KB×2) | 实际:~200KB (100KB×2) |
动态调整范围 | 4KB - 208KB (被限制) | 4KB - 208KB (被限制) |
- UDP Socket缓冲区行为
场景 | 接收缓冲区 (SO_RCVBUF) | 发送缓冲区 (SO_SNDBUF) |
---|---|---|
不调用setsockopt() | 默认:212992 (208KB) | 默认:212992 (208KB) |
调用setsockopt(1MB) | 实际:~425984 (208KB×2) | 实际:~425984 (208KB×2) |
最小值保证 | 不低于4KB | 不低于4KB |
- 参数间的优先级关系
1. net.core.*_max (硬性上限,覆盖一切)↓
2. net.ipv4.tcp_*mem (TCP专用设置)↓
3. net.ipv4.udp_*mem (UDP专用设置)↓
4. net.core.*_default (通用默认值)↓
5. 应用程序setsockopt()调用
服务端启动
下面是生成测试文件和启动服务端的命令。
# 创建测试文件
# ubuntu @ t04 in ~/labs/01-bdp-tcp [10:32:12]
$ dd if=/dev/zero of=testfile bs=1M count=2048
2048+0 records in
2048+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 8.3587 s, 257 MB/s# ubuntu @ t04 in ~/labs/01-bdp-tcp [10:32:30]
$ ll
total 2.1G
-rw-rw-r-- 1 ubuntu ubuntu 2.0G Jul 15 10:32 testfile# 启动服务端
# ubuntu @ t04 in ~/labs/01-bdp-tcp [10:32:50] C:1
$ python3 -m http.server 8089
Serving HTTP on 0.0.0.0 port 8089 (http://0.0.0.0:8089/) ...$ netstat -antp | grep 8089
tcp 0 0 0.0.0.0:8089 0.0.0.0:* LISTEN 12808/python3
实验分析
环境准备好有,我们利用 tc 工具调整 rtt、丢包率以及调节发送接收缓冲大小,来看下不同情况下的数据传输效率。
1. 默认 mem ,默认延迟
首先不做任何改动,内网下载 2GB 的文件,耗时 14s,吞吐为 975Mbps。
2. 默认 mem,100ms 延迟
我们用 tc 将延迟增加到 100ms:
# 服务端机器添加 100ms 延迟
# ubuntu @ t04 in ~
$ sudo tc qdisc add dev eth0 root netem delay 100ms# 添加完成后客户端执行 ping 操作,延迟已经变成 100ms 了。
# ubuntu @ t11 in ~ [10:10:05]
$ ping 172.19.0.4
PING 172.19.0.4 (172.19.0.4) 56(84) bytes of data.
64 bytes from 172.19.0.4: icmp_seq=1 ttl=64 time=100 ms
64 bytes from 172.19.0.4: icmp_seq=2 ttl=64 time=100 ms
再次执行下载并抓包,结果如下:
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 27.4M 0 0:01:14 0:01:14 --:--:-- 27.0M
可以看到整个下载耗时为 1 分 14s,吞吐为 229Mbps,和默认延迟相比,传输速度慢了不少,打开 tcptrace 查看传输过程,可以看到每 100ms 会暂停一次,因为服务端要等到 ack 后才会滑动窗口继续发送数据。
3. 默认 mem,默认延迟,1% 与 20% 丢包
服务端设置 1% 的丢包率
# ubuntu @ t04 in ~
sudo tc qdisc add dev eth0 root netem loss 1%
再次执行下载并抓包,结果如下:
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 185M 0 0:00:11 0:00:11 --:--:-- 255M
可以看到整体耗时 10s,平均吞吐为 185MBps。因为带宽足够并且 RT 非常小, 虽然引发了重传,但并没有导致拥塞窗口的减少,整体的传输速度没有受到明显的影响。
我们将丢包率调大到 20% 再次执行下载并抓包
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed0 2048M 0 15.0M 0 0 579k 0 1:00:20 0:00:26 0:59:54 410k
这次下载耗时预计达到了 1 个小时,传输过程查看 cwnd 可以看到已经缩小到了 1。
$ while true; do sudo ss -ti sport = :8089 ; sleep 1; done;
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 33792 172.19.0.4:8089 172.19.0.11:46002cubic wscale:7,7 rto:204 rtt:0.325/0.358 ato:40 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:2 bytes_sent:47062993 bytes_retrans:9648128 bytes_acked:37397969 bytes_received:87 segs_out:5623 segs_in:3496 data_segs_out:5622 data_segs_in:1 send 208Mbps lastsnd:172 lastrcv:91576 lastack:172 pacing_rate 499Mbps delivery_rate 520Mbps delivered:4472 busy:91572ms sndbuf_limited:9616ms(10.5%) unacked:2 retrans:1/1150 lost:1 sacked:1 rcv_space:57076 rcv_ssthresh:57076 notsent:16896 minrtt:0.067
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 59136 172.19.0.4:8089 172.19.0.11:46002cubic wscale:7,7 rto:408 backoff:1 rtt:0.648/1.036 ato:40 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:2 bytes_sent:47919057 bytes_retrans:9800192 bytes_acked:38093521 bytes_received:87 segs_out:5725 segs_in:3559 data_segs_out:5724 data_segs_in:1 send 104Mbps lastsnd:48 lastrcv:92592 lastack:260 pacing_rate 375Mbps delivery_rate 814Mbps delivered:4556 busy:92588ms sndbuf_limited:9828ms(10.6%) unacked:3 retrans:1/1168 lost:1 sacked:2 rcv_space:57076 rcv_ssthresh:57076 notsent:33792 minrtt:0.067
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 8448 172.19.0.4:8089 172.19.0.11:46002cubic wscale:7,7 rto:204 rtt:0.41/0.467 ato:40 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:2 ssthresh:2 bytes_sent:48088017 bytes_retrans:9850880 bytes_acked:38228689 bytes_received:87 segs_out:5745 segs_in:3571 data_segs_out:5744 data_segs_in:1 send 330Mbps lastsnd:16 lastrcv:93604 lastack:16 pacing_rate 395Mbps delivery_rate 845Mbps delivered:4570 busy:93600ms sndbuf_limited:9828ms(10.5%) unacked:1 retrans:0/1174 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.067
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 25344 172.19.0.4:8089 172.19.0.11:46002cubic wscale:7,7 rto:204 rtt:0.219/0.218 ato:40 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:2 bytes_sent:48483281 bytes_retrans:9926912 bytes_acked:38531025 bytes_received:87 segs_out:5792 segs_in:3601 data_segs_out:5791 data_segs_in:1 send 309Mbps lastsnd:184 lastrcv:94616 lastack:184 pacing_rate 1.11Gbps delivery_rate 583Mbps delivered:4608 busy:94612ms sndbuf_limited:9828ms(10.4%) unacked:3 retrans:1/1183 lost:1 sacked:2 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.067
分析抓包文件,可以看到吞吐会周期性断崖式下跌然后在缓慢爬升。
在丢包时,接收端收到的包会乱序,会影响其 ACK 响应的速度,导致某些包在缓冲区中多等待一会,因此接收窗口也会间歇性的下降,并在收到重传包后恢复。
4. 默认 mem 和延迟,BBR 算法,20% 丢包
服务器默认使用的是 cubic 算法,受丢包影响较大,我们将算法改为 bbr 算法在测试下传输性能。
首先启用 BBR 拥塞控制算法:
$ sudo modprobe tcp_bbr
$ sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
BBR 算法推荐结合 fq 调度算法使用,实验环境默认的是 fq_codel,我们利用 tc 来设置 fq 以及 20% 的丢包率,命令如下:
$ sudo tc qdisc add dev eth0 root handle 1: netem loss 20%# ubuntu @ t04 in ~/labs/01-bdp-tcp [12:09:13]
$ sudo tc qdisc add dev eth0 parent 1: handle 2: fq
完成后再次执行下载并抓包,结果如下:
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 84.1M 0 0:00:24 0:00:24 --:--:-- 140M
我们直接利用 tc 将拥塞控制算法设置为 bbr 并设置 20
- 默认 mem,默认延迟,bbr 算法,20% 丢包
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 84.1M 0 0:00:24 0:00:24 --:--:-- 140M$ sudo tc qdisc add dev eth0 root handle 1: netem loss 20%# ubuntu @ t04 in ~/labs/01-bdp-tcp [12:09:13]
$ sudo tc qdisc add dev eth0 parent 1: handle 2: fq# ubuntu @ t04 in ~/labs/01-bdp-tcp [12:09:20]
$ sudo tc qdisc show dev eth0
qdisc netem 1: root refcnt 3 limit 1000 loss 20%
qdisc fq 2: parent 1: limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 17028b initial_quantum 85140b low_rate_threshold 550Kbit refill_delay 40ms timer_slack 10us horizon 10s horizon_drop
5. 客户端 recvbuf 为 4Kb,默认延迟
我们将客户端的 recvbuf 设置为 4Kb。
$ sudo sysctl -w "net.ipv4.tcp_rmem=4096 4096 4096"
在默认延迟下执行下载,抓包如下:
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 14.8M 0 0:02:18 0:02:18 --:--:-- 14.8M
总体耗时两分多钟,抓包可以看到有大量 Window Full 的情况,但内网 RT 非常小,因此空出来后可以很快的通知给服务端,对整体传输速率的性能不算太大。
查看传输过程,可以看到大约每 40ms 接收窗口会上升,Linux 内核有一个宏定义来设置延迟确认的最小时间为 40ms,推测应该是 delayed ack 起了作用。
# https://elixir.bootlin.com/linux/v5.15.130/source/include/net/tcp.h#L135
# define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
6. 客户端 recvbuf 为 4Kb,100ms 延迟
将延迟增加到 100ms 后,整体传输时间预计需要 29 小时,这种情况下,数据只能一点点发,而且耗时还比较长,整体传输速度变得巨慢无比。
$ sudo tc qdisc add dev eth0 root netem delay 100ms$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed0 2048M 0 2789k 0 0 20419 0 29:12:50 0:02:19 29:10:31 20451
7. 服务端 sendbuf 为 4Kb,默认延迟
下载时间只需要 10 几秒,对性能没有明显影响。虽然发送 buffer 小,但因为 rtt 也很小,ACK 包能很快回来可以立即释放 wmem,因此对速度影响不大。好比即使我们只有两辆货车,但装货发货非常快,货车卸完货能立马回来继续拉,整体运货速度也是有保证的,但如果卸货贼慢或者货车路上跑的贼慢,整体发货效率也提不上去。
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 2048M 100 2048M 0 0 164M 0 0:00:12 0:00:12 --:--:-- 195M
这里可以和实验 5 做对比,在默认延迟下修改 recvbuf 和 sendbuf,可以看到修改 recvbuf 对性能的影响更加明显。都是 RT 很小,但 server 端收到 ACK 后 sendbuf 清理出空间,可以立即发送,是内存级别的演示;但接收端在有 recvbuf 有空间后返回 ACK 到服务端,是网络通信级别的延迟,两者相差几个数量级。
图片来自 ## TCP性能和发送接收窗口、Buffer的关系
8. 服务端 sendbuf 为 4Kb,100ms 延迟
将延迟增大到 100ms 后,下载时间预计需要 1 小时 37 分钟,整体效率下降了很多。
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed0 2048M 0 17.4M 0 0 357k 0 1:37:50 0:00:49 1:37:01 366k
抓包查看传输过程,可以看到整体传输过程是非常丝滑的,放大后看每 100ms 窗口才会增大,性能就是受 RT 的影响一直上不去。
性能问题复现
这里我们模拟任总遇到的场景,假设我们的带宽是 500Mbps,RT 为 10ms,通过调整发送 buffer 来优化发送效率。
BDP(Bandwidth-Delay Product) 带宽时延积
首先要理解一个概念带宽时延积:
Bandwidth-delay product (BDP)
Product of data link’s capacity and its end-to-end delay. The result is the maximum amount of unacknowledged data that can be in flight at any point in time.
《High Performance Browser Networking》
图片来自 High Performance Browser Networking
其含义就是整个传输链路上可以传输的最大数据,TCP 性能优化的一个关键点就是发送的数据要填满 BDP,从而充分利用带宽。就好比我们用货车拉货时,要尽可能将货车装满才能最大化其运力。
回我们 500Mbps 带宽,10ms RT 的场景,我们先来计算下 BDP 是 625KB,这意味着我们能够一下子发出 625KB 时才能最大化的利用网络带宽,对应到发送端的优化,则是将发送窗口大小设置为 BDP。
500Mbit/s * 0.01s = 5Mbits
# 转为 byte
5 * 10^6 bits / 8 = 625,000 bytes
# 转为 KB
625,000 bytes / 1000 = 625KB
在其他条件都满足的情况下,传输一个 512MB 大小的文件,理想传输速度大约为 8~10s 左右。下面是调整 sendbuf 后所得到的下载 512MB 大小文件的速度:
下面是调整 sendbuf 后所得到的下载 512MB 大小文件的速度:
- sendbuf 为 100KB,下载时长 56s.
$ curl 172.19.0.4:8089/testfile > testfile% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 512M 100 512M 0 0 9350k 0 0:00:56 0:00:56 --:--:-- 9363k
- sendbuf 为 200KB,下载时长 24s。
$ curl 172.19.0.4:8089/testfile > /dev/null% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 512M 100 512M 0 0 21.0M 0 0:00:24 0:00:24 --:--:-- 21.0M
- sendbuf 为 700KB 或者 100KB,下载速度均为 8 ~ 9s。
$ curl 172.19.0.4:8089/testfile > /dev/null% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed
100 512M 100 512M 0 0 58.4M 0 0:00:08 0:00:08 --:--:-- 58.7M
简要总结
整体来看,要想 TCP 数据传输的快,需要满足三个条件:
- 发得快:发送端窗口足够,能填满 BDP,数据发送快。
- 传得快:网络环境好,带宽大、RT 小、丢包率低。
- 收的快:接收端数据处理快,接收窗口大。
在实际工作场景中,需要结合具体场景探查性能问题出现在哪一点,然后在寻找针对性的优化方案。