環境準備
服務器信息
兩臺騰訊云機器 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 小、丟包率低。
- 收的快:接收端數據處理快,接收窗口大。
在實際工作場景中,需要結合具體場景探查性能問題出現在哪一點,然后在尋找針對性的優化方案。