「linux」Socket 緩存是如何影響 TCP 性能的?
一直以來我們都知道 socket 的緩存會對 tcp 性能產生影響,也有無數文章告訴我們應該調大 socke 緩存。但是究竟調多大?什麼時候調?有哪些手段調?具體影響究竟如何?這些問題似乎也沒有人真正說明白。下面我們就構建起一個簡單的實驗環境,在兩臺虛擬機之間探究一下 Socket 緩存究竟如何影響 TCP 的性能?對分析過程不感興趣的可以直接看最後的結論。
影響 Socket 緩存的參數
首先,我們要先來列出 Linux 中可以影響 Socket 緩存的調整參數。在 proc 目錄下,它們的路徑和對應說明爲:
/proc/sys/net/core/rmem_default /proc/sys/net/core/rmem_max /proc/sys/net/core/wmem_default /proc/sys/net/core/wmem_max
這些文件用來設置所有 socket 的發送和接收緩存大小,所以既影響 TCP,也影響 UDP。
針對 UDP:
這些參數實際的作用跟 SO_RCVBUF 和 SO_SNDBUF 的 socket option 相關。如果我們不用 setsockopt 去更改創建出來的 socket buffer 長度的話,那麼就使用 rmem_default 和 wmem_default 來作爲默認的接收和發送的 socket buffer 長度。如果修改這些 socket option 的話,那麼他們可以修改的上限是由 rmem_max 和 wmem_max 來限定的。
針對 TCP:
除了以上四個文件的影響外,還包括如下文件:
/proc/sys/net/ipv4/tcp_rmem
/proc/sys/net/ipv4/tcp_wmem
對於 TCP 來說,上面 core 目錄下的四個文件的作用效果一樣,只是默認值不再是 rmem_default 和 wmem_default ,而是由 tcp_rmem 和 tcp_wmem 文件中所顯示的第二個值決定。通過 setsockopt 可以調整的最大值依然由 rmem_max 和 wmem_max 限制。
查看 tcp_rmem 和 tcp_wmem 的文件內容會發現,文件中包含三個值:
[root@localhost network_turning]# cat /proc/sys/net/ipv4/tcp_rmem
4096 131072 6291456
[root@localhost network_turning]# cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
三個值依次表示:min default max
min:決定 tcp socket buffer 最小長度。
default:決定其默認長度。
max:決定其最大長度。在一個 tcp 鏈接中,對應的 buffer 長度將在 min 和 max 之間變化。導致變化的主要因素是當前內存壓力。如果使用 setsockopt 設置了對應 buffer 長度的話,這個值將被忽略。相當於關閉了 tcp buffer 的動態調整。
/proc/sys/net/ipv4/tcp_moderate_rcvbuf
這個文件是服務器是否支持緩存動態調整的開關,1 爲默認值打開,0 爲關閉。
另外要注意的是,使用 setsockopt 設置對應 buffer 長度的時候,實際生效的值將是設置值的 2 倍。
當然,這裏面所有的 rmem 都是針對接收緩存的限制,而 wmem 都是針對發送緩存的限制。
我們目前的實驗環境配置都採用默認值:
[root@localhost network_turning]# cat /proc/sys/net/core/rmem_default
212992
[root@localhost network_turning]# cat /proc/sys/net/core/rmem_max
212992
[root@localhost network_turning]# cat /proc/sys/net/core/wmem_default
212992
[root@localhost network_turning]# cat /proc/sys/net/core/wmem_max
212992
另外需要說明的是,我們目前的實驗環境是兩臺虛擬機,一個是 centos 8,另一個是 fedora 31:
[root@localhost network_turning]# uname -r
5.5.15-200.fc31.x86_64
[root@localhost zorro]# uname -r
4.18.0-147.5.1.el8_1.x86_64
我們將要做的測試也很簡單,我們將在 centos 8 上開啓一個 web 服務,並共享一個 bigfile。然後在 fedora 31 上去下載這個文件。通過下載的速度來觀察 socket 緩存對 tcp 的性能影響。我們先來做一下基準測試,當前在默認設置下,下載速度爲:
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 14:01:33-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 100%[=====================================>] 1.00G 337MB/s in 3.0s
2020-04-13 14:01:36 (337 MB/s) - 'bigfile' saved [1073741824/1073741824]
bigfile 是個 1G 的文件,在同一個宿主機的兩個虛擬機之間,他們的傳輸速率達到了 337MB/s。這是當前基準環境狀態。影響虛擬機之間的帶寬的因素較多,我們希望在測試過程中儘量避免其他因素干擾。所以這裏我們打算對 web 服務器的 80 端口進行限速。爲了不影響其他進程的速率,我們使用 htb 進行限速,腳本如下:
[root@localhost zorro]# cat htb.sh
#!/bin/bash
tc qd del dev ens33 root
tc qd add dev ens33 root handle 1: htb default 100
tc cl add dev ens33 parent 1: classid 1:1 htb rate 20000mbit burst 20k
tc cl add dev ens33 parent 1:1 classid 1:10 htb rate 1000mbit burst 20k
tc cl add dev ens33 parent 1:1 classid 1:100 htb rate 20000mbit burst 20k
tc qd add dev ens33 parent 1:10 handle 10: fq_codel
tc qd add dev ens33 parent 1:100 handle 100: fq_codel
tc fi add dev ens33 protocol ip parent 1:0 prio 1 u32 match ip sport 80 0xffff flowid 1:10
使用 htb 給網絡流量做了 2 個分類,針對 80 端口的流量限制了 1000mbit/s 的速率限制,其他端口是 20000mbit/s 的限制,這在當前環境下相當於沒有限速。之後,我們在 centos 8 的 web 服務器上執行此腳本並在 fedora 31 上測試下載速率:
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 14:13:38-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 100%[=====================================>] 1.00G 91.6MB/s in 11s
2020-04-13 14:13:49 (91.7 MB/s) - 'bigfile' saved [1073741824/1073741824]
1000mbit 的速率限制基本符合要求。
那麼問題來了,此時 socket 緩存在這個 1000mbit 的帶寬限制下,對 tcp 的傳輸性能有什麼影響呢?
如果你喜歡折騰的話,你可以在這個環境上分別調大調小客戶端和服務端的緩存大小來分別測試一下,你會發現,此時對 socket 的緩存大小做任何調整,似乎對 tcp 的傳輸效率都沒有什麼影響。
所以這裏我們需要先分析一下,socket 緩存大小到底在什麼情況下會對 tcp 性能有影響?
緩存對讀寫性能的影響
這其實是個通用問題:緩存到底在什麼情況下會影響讀寫性能?
答案也很簡單:在讀寫的相關環節之間有較大的性能差距時,緩存會有比較大的影響。比如,進程要把數據寫到硬盤裏。因爲硬盤寫的速度很慢,而內存很快,所以可以先把數據寫到內存裏,然後應用程度寫操作就很快返回,應用程序此時覺得很快寫完了。後續這些數據將由內核幫助應用把數據從內存再寫到硬盤裏。
無論如何,當寫操作產生數據的速度,大於實際要接受數據的速度時,buffer 纔有意義。
在我們當前的測試環境中,數據下載時,web 服務器是數據發送方,客戶端是數據接收方,中間通過虛擬機的網絡傳輸。在計算機上,一般原則上講,讀數據的速率要快於寫數據的速率。所以此時兩個虛擬機之間並沒有寫速率大於度速率的問題。所以此時,調整 socket 緩存對 tcp 基本不存在性能影響。
那麼如何才能讓我們的模型產生影響呢?
答案也很簡單,給網絡加比較大的延時就可以了。如果我們把每個 tcp 包的傳輸過程當作一次寫操作的話,那麼網絡延時變大將導致寫操作的處理速度變長。網絡就會成爲應用程序寫速度的瓶頸。我們給我們的 80 端口再加入一個 200ms 的延時:
[root@localhost zorro]# cat htb.sh
#!/bin/bash
tc qd del dev ens33 root
tc qd add dev ens33 root handle 1: htb default 100
tc cl add dev ens33 parent 1: classid 1:1 htb rate 20000mbit burst 20k
tc cl add dev ens33 parent 1:1 classid 1:10 htb rate 1000mbit burst 20k
tc cl add dev ens33 parent 1:1 classid 1:100 htb rate 20000mbit burst 20k
tc qd add dev ens33 parent 1:10 handle 10: netem delay 200ms
tc qd add dev ens33 parent 1:100 handle 100: fq_codel
tc fi add dev ens33 protocol ip parent 1:0 prio 1 u32 match ip sport 80 0xffff flowid 1:10
再次在 web 服務器上執行此腳本,在客戶端 fedora 31 上在延時前後使用 httping 測量一下 rtt 時間:
[root@localhost zorro]# httping 192.168.247.129
PING 192.168.247.129:80 (/):
connected to 192.168.247.129:80 (426 bytes), seq=0 time= 17.37 ms
connected to 192.168.247.129:80 (426 bytes), seq=1 time= 1.22 ms
connected to 192.168.247.129:80 (426 bytes), seq=2 time= 1.25 ms
connected to 192.168.247.129:80 (426 bytes), seq=3 time= 1.47 ms
connected to 192.168.247.129:80 (426 bytes), seq=4 time= 1.55 ms
connected to 192.168.247.129:80 (426 bytes), seq=5 time= 1.35 ms
^CGot signal 2
--- http://192.168.247.129/ ping statistics ---
6 connects, 6 ok, 0.00% failed, time 5480ms
round-trip min/avg/max = 1.2/4.0/17.4 ms
[root@localhost zorro]# httping 192.168.247.129
PING 192.168.247.129:80 (/):
connected to 192.168.247.129:80 (426 bytes), seq=0 time=404.59 ms
connected to 192.168.247.129:80 (426 bytes), seq=1 time=403.72 ms
connected to 192.168.247.129:80 (426 bytes), seq=2 time=404.61 ms
connected to 192.168.247.129:80 (426 bytes), seq=3 time=403.73 ms
connected to 192.168.247.129:80 (426 bytes), seq=4 time=404.16 ms
^CGot signal 2
--- http://192.168.247.129/ ping statistics ---
5 connects, 5 ok, 0.00% failed, time 6334ms
round-trip min/avg/max = 403.7/404.2/404.6 ms
200ms 的網絡延時,體現在 http 協議上會有 400ms 的 rtt 時間。此時,網絡的速率會成爲傳輸過程的瓶頸,雖然帶寬沒有下降,但是我們測試一下真實下載速度會發現,帶寬無法利用滿了:
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 14:37:28-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 15%[=====> ] 162.61M 13.4MB/s eta 87s
下載速率穩定在 13.4MB/s,離 1000mbit/s 的真實速率還差的很遠。此時就體現出了 tcp 在大延時網絡上的性能瓶頸了。那麼如何解決呢?
大延時網絡提高 TCP 帶寬利用率
我們先來分析一下當前的問題,爲什麼加大了網絡延時會導致 tcp 帶寬利用率下降?
因爲我們的帶寬是 1000mbit/s,做個換算爲字節數是 125mB/s,當然這是理論值。爲了運算方便,我們假定網絡帶寬就是 100mB/s。在這樣的帶寬下,假定沒有 buffer 影響,網絡發送 1m 數據的速度需要 10ms,之後這 1m 數據需要通過網絡發送給對端。然後對端返回接收成功給服務端,服務端接收到寫成功之後理解爲此次寫操作完成,之後發送下一個 1m。
在當前網絡上我們發現,1m 本身之需 10ms,但是傳輸 1m 到對端在等對端反會接收成功的消息,要至少 400ms。因爲網絡一個 rtt 時間就是 400ms。那麼在寫 1m 之後,我們至少要等 400ms 之後才能發送下一個 1M。這樣的帶寬利用率僅爲 10ms(數據發送時間)/400ms(rtt 等待時間) = 2.5%。這是在沒有 buffer 影響的情況下,實際上我們當前環境是有 buffer 的,所以當前的帶寬利用率要遠遠大於沒有 buffer 的理論情況。
有了這個理論模型,我們就大概知道應該把 buffer 調整爲多大了,實際上就是應該讓一次寫操作的數據把網絡延時,導致浪費的帶寬填滿。在延時爲 400ms,帶寬爲 125mB/s 的網絡上,要填滿延時期間的浪費帶寬的字節數該是多少呢?那就是著名的帶寬延時積了。即:帶寬 (125mB/s) X 延時 rtt(0.4s) = 50m。
所以,如果一次寫可以寫滿到 50m,發送給對方。那麼等待的 400ms 中理論上將不會有帶寬未被利用的情況。那麼在當前測試環境中,應該調整的就是發送方的 tcp_wmem 緩存大小。根據上述的各個文件的含義,我們知道只要把 / proc/sys/net/ipv4/tcp_wmem 文件中的對應值做調整,那麼就會有效影響當前服務端的 tcp socekt buffer 長度。我們來試一下,在 centos 8 上做如下調整:
[root@localhost zorro]# echo 52428800 52428800 52428800 >/proc/sys/net/ipv4/tcp_wmem
[root@localhost zorro]# cat !$
cat /proc/sys/net/ipv4/tcp_wmem
52428800 52428800 52428800
然後在 fedora 31 測試下載速度:
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 15:08:54-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 21%[=======> ] 222.25M 14.9MB/s eta 69s
發現目前下載速率穩定在 15M/s 左右。雖然有所提升,但是依然並沒達到真正充分利用帶寬的效果。這是爲啥呢?理論錯了麼?
如果我們對 TCP 理解比較深入的話,我們會知道,TCP 傳輸過程中,真正能決定一次寫長度的並不直接受 tcp socket wmem 的長度影響,嚴格來說,是受到 tcp 發送窗口大小的影響。而 tcp 發送窗口大小還要受到接收端的通告窗口來決定。就是說,tcp 發送窗口決定了是不是能填滿大延時網絡的帶寬,而接收端的通告窗口決定了發送窗口有多大。
那麼接受方的通告窗口長度是怎麼決定的呢?在內核中,使用 tcp_select_window() 方法來決定通告窗口大小。詳細分析這個方法,我們發現,接受方的通告窗口大小會受到接受方本地的 tcp socket rmem 的剩餘長度影響。就是說,在一個 tcp 鏈接中,發送窗口受到對端 tcp socket rmem 剩餘長度影響。
所以,除了調整發送方 wmem 外,還要調整接受方的 rmem。我們再來試一下,在 fedora 31 上執行:
[root@localhost zorro]# echo 52428800 52428800 52428800 >/proc/sys/net/ipv4/tcp_rmem
[root@localhost zorro]# cat !$
cat /proc/sys/net/ipv4/tcp_rmem
52428800 52428800 52428800
再做下載測試:
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 15:21:40-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 100%[=====================================>] 1.00G 92.7MB/s in 13s
2020-04-13 15:21:53 (77.8 MB/s) - 'bigfile' saved [1073741824/1073741824]
這時的下載速率才比較符合我們理論中的狀況。當然,因爲發送窗口大小受到的是 “剩餘” 接收緩存大小影響,所以我們推薦此時應該把 / proc/sys/net/ipv4/tcp_rmem 的大小調的比理論值更大一些。比如大一倍:
[root@localhost zorro]# echo 104857600 104857600 104857600 > /proc/sys/net/ipv4/tcp_rmem
[root@localhost zorro]# cat /proc/sys/net/ipv4/tcp_rmem
104857600 104857600 104857600
[root@localhost zorro]# wget --no-proxy http://192.168.247.129/bigfile
--2020-04-13 15:25:29-- http://192.168.247.129/bigfile
Connecting to 192.168.247.129:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1073741824 (1.0G)
Saving to: 'bigfile'
bigfile 100%[=====================================>] 1.00G 89.2MB/s in 13s
2020-04-13 15:25:43 (76.9 MB/s) - 'bigfile' saved [1073741824/1073741824]
此時理論上應該獲得比剛纔更理想的下載速率。另外還有一個文件需要注意:
/proc/sys/net/ipv4/tcp_adv_win_scale
這個值用來影響緩存中有多大空間用來存放 overhead 相關數據,所謂 overhead 數據可以理解爲比如 TCP 報頭等非業務數據。假設緩存字節數爲 bytes,這個值說明,有 bytes/2 的 tcp_adv_win_scale 次方的空間用來存放 overhead 數據。默認值爲 1 表示有 1/2 的緩存空間用來放 overhead,此值爲二表示 1/4 的空間。當 tcp_adv_win_scale <= 0 的時候,overhead 空間運算爲:bytes-bytes/2^(-tcp_adv_win_scale)。取值範圍是:[-31, 31]。
可以在下載過程中使用 ss 命令查看 rcv_space 和 rcv_ssthresh 的變化:
[root@localhost zorro]# ss -io state established '( dport = 80 or sport = 80 )'
Netid Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp 0 0 192.168.247.130:47864 192.168.247.129:http
ts sack cubic wscale:7,11 rto:603 rtt:200.748/75.374 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:149 bytes_acked:150 bytes_received:448880 segs_out:107 segs_in:312 data_segs_out:1 data_segs_in:310 send 577.0Kbps lastsnd:1061 lastrcv:49 lastack:50 pacing_rate 1.2Mbps delivery_rate 57.8Kbps delivered:2 app_limited busy:201ms rcv_rtt:202.512 rcv_space:115840 rcv_ssthresh:963295 minrtt:200.474
[root@localhost zorro]# ss -io state established '( dport = 80 or sport = 80 )'
Netid Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp 0 0 192.168.247.130:47864 192.168.247.129:http
ts sack cubic wscale:7,11 rto:603 rtt:200.748/75.374 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:149 bytes_acked:150 bytes_received:48189440 segs_out:1619 segs_in:33282 data_segs_out:1 data_segs_in:33280 send 577.0Kbps lastsnd:2623 lastrcv:1 lastack:3 pacing_rate 1.2Mbps delivery_rate 57.8Kbps delivered:2 app_limited busy:201ms rcv_rtt:294.552 rcv_space:16550640 rcv_ssthresh:52423872 minrtt:200.474
[root@localhost zorro]# ss -io state established '( dport = 80 or sport = 80 )'
Netid Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp 0 0 192.168.247.130:47864 192.168.247.129:http
ts sack cubic wscale:7,11 rto:603 rtt:200.748/75.374 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:149 bytes_acked:150 bytes_received:104552840 segs_out:2804 segs_in:72207 data_segs_out:1 data_segs_in:72205 send 577.0Kbps lastsnd:3221 lastack:601 pacing_rate 1.2Mbps delivery_rate 57.8Kbps delivered:2 app_limited busy:201ms rcv_rtt:286.159 rcv_space:25868520 rcv_ssthresh:52427352 minrtt:200.474
總結
從原理上看,一個延時大的網絡不應該影響其帶寬的利用。之所以大延時網絡上的帶寬利用率低,主要原因是延時變大之後,發送方發的數據不能及時到達接收方。導致發送緩存滿之後,不能再持續發送數據。接收方則因爲 TCP 通告窗口受到接收方剩餘緩存大小的影響。接收緩存小的話,則會通告對方發送窗口變小。進而影響發送方不能以大窗口發送數據。所以,這裏的調優思路應該是,發送方調大 tcp_wmem,接收方調大 tcp_rmem。那麼調成多大合適呢?如果我們把大延時網絡想象成一個緩存的話,那麼緩存的大小應該是帶寬延時(rtt)積。假設帶寬爲 1000Mbit/s,rtt 時間爲 400ms,那麼緩存應該調整爲大約 50Mbyte 左右。接收方 tcp_rmem 應該更大一些,以便在接受方不能及時處理數據的情況下,不至於產生剩餘緩存變小而影響通告窗口導致發送變慢的問題,可以考慮調整爲 2 倍的帶寬延時積。在這個例子中就是 100M 左右。此時在原理上,tcp 的吞度量應該能達到高延時網絡的帶寬上限了。
但是網絡環境本身很複雜。首先:網絡路徑上的一堆網絡設備本身會有一定緩存。所以我們大多數情況不用按照上述理論值調整本地的 tcp 緩存大小。其次,高延時網絡一般伴隨着丟包幾率高。當產生丟包的時候,帶寬利用率低就不再只是緩存的影響了。此時擁塞控制本身會導致帶寬利用率達不到要求。所以,選擇不同的擁塞控制算法,更多影響的是丟包之後的快速恢復過程和慢啓動過程的效果。比如,bbr 這種對丟包不敏感的擁塞控制算法,在有丟包的情況下,對窗口的影響比其他擁塞控制算法更小。而如果網絡僅僅是延時大,丟包很少的話,選什麼擁塞控制算法對帶寬利用率影響並不大,緩存影響會更大。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Zd2Ug9sAsrt3X79nuU6zCg