面試官:換人!他連 TCP 這幾個(gè)參數(shù)都不懂
每日一句英語學(xué)習(xí),每天進(jìn)步一點(diǎn)點(diǎn):
前言
TCP 性能的提升不僅考察 TCP 的理論知識(shí),還考察了對(duì)于操作系統(tǒng)提供的內(nèi)核參數(shù)的理解與應(yīng)用。
TCP 協(xié)議是由操作系統(tǒng)實(shí)現(xiàn),所以操作系統(tǒng)提供了不少調(diào)節(jié) TCP 的參數(shù)。
如何正確有效的使用這些參數(shù),來提高 TCP 性能是一個(gè)不那么簡單事情。我們需要針對(duì) TCP 每個(gè)階段的問題來對(duì)癥下藥,而不是病急亂投醫(yī)。
接下來,將以三個(gè)角度來闡述提升 TCP 的策略,分別是:
TCP 三次握手的性能提升;
TCP 四次揮手的性能提升;
TCP 數(shù)據(jù)傳輸?shù)男阅芴嵘?/span>
正文
01 TCP 三次握手的性能提升
TCP 是面向連接的、可靠的、雙向傳輸?shù)膫鬏攲油ㄐ艆f(xié)議,所以在傳輸數(shù)據(jù)之前需要經(jīng)過三次握手才能建立連接。
那么,三次握手的過程在一個(gè) HTTP 請(qǐng)求的平均時(shí)間占比 10% 以上,在網(wǎng)絡(luò)狀態(tài)不佳、高并發(fā)或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調(diào)節(jié)三次握手中的參數(shù),就會(huì)對(duì)性能產(chǎn)生很多的影響。
如何正確有效的使用這些參數(shù),來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態(tài)變遷」,這樣當(dāng)出現(xiàn)問題時(shí),先用 netstat
命令查看是哪個(gè)握手階段出現(xiàn)了問題,再來對(duì)癥下藥,而不是病急亂投醫(yī)。
客戶端和服務(wù)端都可以針對(duì)三次握手優(yōu)化性能。主動(dòng)發(fā)起連接的客戶端優(yōu)化相對(duì)簡單些,而服務(wù)端需要監(jiān)聽端口,屬于被動(dòng)連接方,其間保持許多的中間狀態(tài),優(yōu)化方法相對(duì)復(fù)雜一些。
所以,客戶端(主動(dòng)發(fā)起連接方)和服務(wù)端(被動(dòng)連接方)優(yōu)化的方式是不同的,接下來分別針對(duì)客戶端和服務(wù)端優(yōu)化。
客戶端優(yōu)化
三次握手建立連接的首要目的是「同步序列號(hào)」。
只有同步了序列號(hào)才有可靠傳輸,TCP 許多特性都依賴于序列號(hào)實(shí)現(xiàn),比如流量控制、丟包重傳等,這也是三次握手中的報(bào)文稱為 SYN 的原因,SYN 的全稱就叫 Synchronize Sequence Numbers(同步序列號(hào))。
SYN_SENT 狀態(tài)的優(yōu)化
客戶端作為主動(dòng)發(fā)起連接方,首先它將發(fā)送 SYN 包,于是客戶端的連接就會(huì)處于 SYN_SENT
狀態(tài)。
客戶端在等待服務(wù)端回復(fù)的 ACK 報(bào)文,正常情況下,服務(wù)器會(huì)在幾毫秒內(nèi)返回 SYN+ACK ,但如果客戶端長時(shí)間沒有收到 SYN+ACK 報(bào)文,則會(huì)重發(fā) SYN 包,重發(fā)的次數(shù)由 tcp_syn_retries 參數(shù)控制,默認(rèn)是 5 次:
通常,第一次超時(shí)重傳是在 1 秒后,第二次超時(shí)重傳是在 2 秒,第三次超時(shí)重傳是在 4 秒后,第四次超時(shí)重傳是在 8 秒后,第五次是在超時(shí)重傳 16 秒后。沒錯(cuò),每次超時(shí)的時(shí)間是上一次的 2 倍。
當(dāng)?shù)谖宕纬瑫r(shí)重傳后,會(huì)繼續(xù)等待 32 秒,如果仍然服務(wù)端沒有回應(yīng) ACK,客戶端就會(huì)終止三次握手。
所以,總耗時(shí)是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。
你可以根據(jù)網(wǎng)絡(luò)的穩(wěn)定性和目標(biāo)服務(wù)器的繁忙程度修改 SYN 的重傳次數(shù),調(diào)整客戶端的三次握手時(shí)間上限。比如內(nèi)網(wǎng)中通訊時(shí),就可以適當(dāng)調(diào)低重試次數(shù),盡快把錯(cuò)誤暴露給應(yīng)用程序。
服務(wù)端優(yōu)化
當(dāng)服務(wù)端收到 SYN 包后,服務(wù)端會(huì)立馬回復(fù) SYN+ACK 包,表明確認(rèn)收到了客戶端的序列號(hào),同時(shí)也把自己的序列號(hào)發(fā)給對(duì)方。
此時(shí),服務(wù)端出現(xiàn)了新連接,狀態(tài)是 SYN_RCV
。在這個(gè)狀態(tài)下,Linux 內(nèi)核就會(huì)建立一個(gè)「半連接隊(duì)列」來維護(hù)「未完成」的握手信息,當(dāng)半連接隊(duì)列溢出后,服務(wù)端就無法再建立新的連接。
SYN 攻擊,攻擊的是就是這個(gè)半連接隊(duì)列。
如何查看由于 SYN 半連接隊(duì)列已滿,而被丟棄連接的情況?
我們可以通過該 netstat -s
命令給出的統(tǒng)計(jì)結(jié)果中, 可以得到由于半連接隊(duì)列已滿,引發(fā)的失敗次數(shù):
上面輸出的數(shù)值是累計(jì)值,表示共有多少個(gè) TCP 連接因?yàn)榘脒B接隊(duì)列溢出而被丟棄。隔幾秒執(zhí)行幾次,如果有上升的趨勢(shì),說明當(dāng)前存在半連接隊(duì)列溢出的現(xiàn)象。
如何調(diào)整 SYN 半連接隊(duì)列大???
要想增大半連接隊(duì)列,不能只單純?cè)龃?tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊(duì)列。否則,只單純?cè)龃?tcp_max_syn_backlog 是無效的。
增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內(nèi)核參數(shù):
增大 backlog 的方式,每個(gè) Web 服務(wù)都不同,比如 Nginx 增大 backlog 的方法如下:
最后,改變了如上這些參數(shù)后,要重啟 Nginx 服務(wù),因?yàn)?SYN 半連接隊(duì)列和 accept 隊(duì)列都是在 listen()
初始化的。
如果 SYN 半連接隊(duì)列已滿,只能丟棄連接嗎?
并不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接隊(duì)列的情況下成功建立連接。
syncookies 的工作原理:服務(wù)器根據(jù)當(dāng)前狀態(tài)計(jì)算出一個(gè)值,放在己方發(fā)出的 SYN+ACK 報(bào)文中發(fā)出,當(dāng)客戶端返回 ACK 報(bào)文時(shí),取出該值驗(yàn)證,如果合法,就認(rèn)為連接建立成功,如下圖所示。
syncookies 參數(shù)主要有以下三個(gè)值:
0 值,表示關(guān)閉該功能;
1 值,表示僅當(dāng) SYN 半連接隊(duì)列放不下時(shí),再啟用它;
2 值,表示無條件開啟功能;
那么在應(yīng)對(duì) SYN 攻擊時(shí),只需要設(shè)置為 1 即可:
SYN_RCV 狀態(tài)的優(yōu)化
當(dāng)客戶端接收到服務(wù)器發(fā)來的 SYN+ACK 報(bào)文后,就會(huì)回復(fù) ACK 給服務(wù)器,同時(shí)客戶端連接狀態(tài)從 SYN_SENT 轉(zhuǎn)換為 ESTABLISHED,表示連接建立成功。
服務(wù)器端連接成功建立的時(shí)間還要再往后,等到服務(wù)端收到客戶端的 ACK 后,服務(wù)端的連接狀態(tài)才變?yōu)?ESTABLISHED。
如果服務(wù)器沒有收到 ACK,就會(huì)重發(fā) SYN+ACK 報(bào)文,同時(shí)一直處于 SYN_RCV 狀態(tài)。
當(dāng)網(wǎng)絡(luò)繁忙、不穩(wěn)定時(shí),報(bào)文丟失就會(huì)變嚴(yán)重,此時(shí)應(yīng)該調(diào)大重發(fā)次數(shù)。反之則可以調(diào)小重發(fā)次數(shù)。修改重發(fā)次數(shù)的方法是,調(diào)整 tcp_synack_retries 參數(shù):
tcp_synack_retries 的默認(rèn)重試次數(shù)是 5 次,與客戶端重傳 SYN 類似,它的重傳會(huì)經(jīng)歷 1、2、4、8、16 秒,最后一次重傳后會(huì)繼續(xù)等待 32 秒,如果服務(wù)端仍然沒有收到 ACK,才會(huì)關(guān)閉連接,故共需要等待 63 秒。
服務(wù)器收到 ACK 后連接建立成功,此時(shí),內(nèi)核會(huì)把連接從半連接隊(duì)列移除,然后創(chuàng)建新的完全的連接,并將其添加到 accept 隊(duì)列,等待進(jìn)程調(diào)用 accept 函數(shù)時(shí)把連接取出來。
如果進(jìn)程不能及時(shí)地調(diào)用 accept 函數(shù),就會(huì)造成 accept 隊(duì)列(也稱全連接隊(duì)列)溢出,最終導(dǎo)致建立好的 TCP 連接被丟棄。
accept 隊(duì)列已滿,只能丟棄連接嗎?
丟棄連接只是 Linux 的默認(rèn)行為,我們還可以選擇向客戶端發(fā)送 RST 復(fù)位報(bào)文,告訴客戶端連接已經(jīng)建立失敗。打開這一功能需要將 tcp_abort_on_overflow 參數(shù)設(shè)置為 1。
tcp_abort_on_overflow 共有兩個(gè)值分別是 0 和 1,其分別表示:
0 :如果 accept 隊(duì)列滿了,那么 server 扔掉 client 發(fā)過來的 ack ;
1 :如果 accept 隊(duì)列滿了,server 發(fā)送一個(gè)
RST
包給 client,表示廢掉這個(gè)握手過程和這個(gè)連接;
如果要想知道客戶端連接不上服務(wù)端,是不是服務(wù)端 TCP 全連接隊(duì)列滿的原因,那么可以把 tcp_abort_on_overflow 設(shè)置為 1,這時(shí)如果在客戶端異常中可以看到很多 connection reset by peer
的錯(cuò)誤,那么就可以證明是由于服務(wù)端 TCP 全連接隊(duì)列溢出的問題。
通常情況下,應(yīng)當(dāng)把 tcp_abort_on_overflow 設(shè)置為 0,因?yàn)檫@樣更有利于應(yīng)對(duì)突發(fā)流量。
舉個(gè)例子,當(dāng) accept 隊(duì)列滿導(dǎo)致服務(wù)器丟掉了 ACK,與此同時(shí),客戶端的連接狀態(tài)卻是 ESTABLISHED,客戶端進(jìn)程就在建立好的連接上發(fā)送請(qǐng)求。只要服務(wù)器沒有為請(qǐng)求回復(fù) ACK,客戶端的請(qǐng)求就會(huì)被多次「重發(fā)」。如果服務(wù)器上的進(jìn)程只是短暫的繁忙造成 accept 隊(duì)列滿,那么當(dāng) accept 隊(duì)列有空位時(shí),再次接收到的請(qǐng)求報(bào)文由于含有 ACK,仍然會(huì)觸發(fā)服務(wù)器端成功建立連接。
所以,tcp_abort_on_overflow 設(shè)為 0 可以提高連接建立的成功率,只有你非??隙?TCP 全連接隊(duì)列會(huì)長期溢出時(shí),才能設(shè)置為 1 以盡快通知客戶端。
如何調(diào)整 accept 隊(duì)列的長度呢?
accept 隊(duì)列的長度取決于 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:
somaxconn 是 Linux 內(nèi)核的參數(shù),默認(rèn)值是 128,可以通過
net.core.somaxconn
來設(shè)置其值;backlog 是
listen(int sockfd, int backlog)
函數(shù)中的 backlog 大??;
Tomcat、Nginx、Apache 常見的 Web 服務(wù)的 backlog 默認(rèn)值都是 511。
如何查看服務(wù)端進(jìn)程 accept 隊(duì)列的長度?
可以通過 ss -ltn
命令查看:
Recv-Q:當(dāng)前 accept 隊(duì)列的大小,也就是當(dāng)前已完成三次握手并等待服務(wù)端
accept()
的 TCP 連接;Send-Q:accept 隊(duì)列最大長度,上面的輸出結(jié)果說明監(jiān)聽 8088 端口的 TCP 服務(wù),accept 隊(duì)列的最大長度為 128;
如何查看由于 accept 連接隊(duì)列已滿,而被丟棄的連接?
當(dāng)超過了 accept 連接隊(duì)列,服務(wù)端則會(huì)丟掉后續(xù)進(jìn)來的 TCP 連接,丟掉的 TCP 連接的個(gè)數(shù)會(huì)被統(tǒng)計(jì)起來,我們可以使用 netstat -s 命令來查看:
上面看到的 41150 times ,表示 accept 隊(duì)列溢出的次數(shù),注意這個(gè)是累計(jì)值。可以隔幾秒鐘執(zhí)行下,如果這個(gè)數(shù)字一直在增加的話,說明 accept 連接隊(duì)列偶爾滿了。
如果持續(xù)不斷地有連接因?yàn)?accept 隊(duì)列溢出被丟棄,就應(yīng)該調(diào)大 backlog 以及 somaxconn 參數(shù)。
如何繞過三次握手?
以上我們只是在對(duì)三次握手的過程進(jìn)行優(yōu)化,接下來我們看看如何繞過三次握手發(fā)送數(shù)據(jù)。
三次握手建立連接造成的后果就是,HTTP 請(qǐng)求必須在一個(gè) RTT(從客戶端到服務(wù)器一個(gè)往返的時(shí)間)后才能發(fā)送。
在 Linux 3.7 內(nèi)核版本之后,提供了 TCP Fast Open 功能,這個(gè)功能可以減少 TCP 連接建立的時(shí)延。
接下來說說,TCP Fast Open 功能的工作方式。
在客戶端首次建立連接時(shí)的過程:
客戶端發(fā)送 SYN 報(bào)文,該報(bào)文包含 Fast Open 選項(xiàng),且該選項(xiàng)的 Cookie 為空,這表明客戶端請(qǐng)求 Fast Open Cookie;
支持 TCP Fast Open 的服務(wù)器生成 Cookie,并將其置于 SYN-ACK 數(shù)據(jù)包中的 Fast Open 選項(xiàng)以發(fā)回客戶端;
客戶端收到 SYN-ACK 后,本地緩存 Fast Open 選項(xiàng)中的 Cookie。
所以,第一次發(fā)起 HTTP GET 請(qǐng)求的時(shí)候,還是需要正常的三次握手流程。
之后,如果客戶端再次向服務(wù)器建立連接時(shí)的過程:
客戶端發(fā)送 SYN 報(bào)文,該報(bào)文包含「數(shù)據(jù)」(對(duì)于非 TFO 的普通 TCP 握手過程,SYN 報(bào)文中不包含「數(shù)據(jù)」)以及此前記錄的 Cookie;
支持 TCP Fast Open 的服務(wù)器會(huì)對(duì)收到 Cookie 進(jìn)行校驗(yàn):如果 Cookie 有效,服務(wù)器將在 SYN-ACK 報(bào)文中對(duì) SYN 和「數(shù)據(jù)」進(jìn)行確認(rèn),服務(wù)器隨后將「數(shù)據(jù)」遞送至相應(yīng)的應(yīng)用程序;如果 Cookie 無效,服務(wù)器將丟棄 SYN 報(bào)文中包含的「數(shù)據(jù)」,且其隨后發(fā)出的 SYN-ACK 報(bào)文將只確認(rèn) SYN 的對(duì)應(yīng)序列號(hào);
如果服務(wù)器接受了 SYN 報(bào)文中的「數(shù)據(jù)」,服務(wù)器可在握手完成之前發(fā)送「數(shù)據(jù)」,這就減少了握手帶來的 1 個(gè) RTT 的時(shí)間消耗;
客戶端將發(fā)送 ACK 確認(rèn)服務(wù)器發(fā)回的 SYN 以及「數(shù)據(jù)」,但如果客戶端在初始的 SYN 報(bào)文中發(fā)送的「數(shù)據(jù)」沒有被確認(rèn),則客戶端將重新發(fā)送「數(shù)據(jù)」;
此后的 TCP 連接的數(shù)據(jù)傳輸過程和非 TFO 的正常情況一致。
所以,之后發(fā)起 HTTP GET 請(qǐng)求的時(shí)候,可以繞過三次握手,這就減少了握手帶來的 1 個(gè) RTT 的時(shí)間消耗。
注:客戶端在請(qǐng)求并存儲(chǔ)了 Fast Open Cookie 之后,可以不斷重復(fù) TCP Fast Open 直至服務(wù)器認(rèn)為 Cookie 無效(通常為過期)。
Linux 下怎么打開 TCP Fast Open 功能呢?
在 Linux 系統(tǒng)中,可以通過設(shè)置 tcp_fastopn 內(nèi)核參數(shù),來打開 Fast Open 功能:
tcp_fastopn 各個(gè)值的意義:
0 關(guān)閉
1 作為客戶端使用 Fast Open 功能
2 作為服務(wù)端使用 Fast Open 功能
3 無論作為客戶端還是服務(wù)器,都可以使用 Fast Open 功能
TCP Fast Open 功能需要客戶端和服務(wù)端同時(shí)支持,才有效果。
小結(jié)
本小結(jié)主要介紹了關(guān)于優(yōu)化 TCP 三次握手的幾個(gè) TCP 參數(shù)。
客戶端的優(yōu)化
當(dāng)客戶端發(fā)起 SYN 包時(shí),可以通過 tcp_syn_retries
控制其重傳的次數(shù)。
服務(wù)端的優(yōu)化
當(dāng)服務(wù)端 SYN 半連接隊(duì)列溢出后,會(huì)導(dǎo)致后續(xù)連接被丟棄,可以通過 netstat -s
觀察半連接隊(duì)列溢出的情況,如果 SYN 半連接隊(duì)列溢出情況比較嚴(yán)重,可以通過 tcp_max_syn_backlog、somaxconn、backlog
參數(shù)來調(diào)整 SYN 半連接隊(duì)列的大小。
服務(wù)端回復(fù) SYN+ACK 的重傳次數(shù)由 tcp_synack_retries
參數(shù)控制。如果遭受 SYN 攻擊,應(yīng)把 tcp_syncookies
參數(shù)設(shè)置為 1,表示僅在 SYN 隊(duì)列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。
服務(wù)端收到客戶端返回的 ACK,會(huì)把連接移入 accpet 隊(duì)列,等待進(jìn)行調(diào)用 accpet() 函數(shù)取出連接。
可以通過 ss -lnt
查看服務(wù)端進(jìn)程的 accept 隊(duì)列長度,如果 accept 隊(duì)列溢出,系統(tǒng)默認(rèn)丟棄 ACK,如果可以把 tcp_abort_on_overflow
設(shè)置為 1 ,表示用 RST 通知客戶端連接建立失敗。
如果 accpet 隊(duì)列溢出嚴(yán)重,可以通過 listen 函數(shù)的 backlog
參數(shù)和 somaxconn
系統(tǒng)參數(shù)提高隊(duì)列大小,accept 隊(duì)列長度取決于 min(backlog, somaxconn)。
繞過三次握手
TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請(qǐng)求減少了 1 個(gè) RTT 的時(shí)間,Linux 下可以通過 tcp_fastopen
開啟該功能,同時(shí)必須保證服務(wù)端和客戶端同時(shí)支持。
02 TCP 四次揮手的性能提升
接下來,我們一起看看針對(duì) TCP 四次揮手關(guān)不連接時(shí),如何優(yōu)化性能。
在開始之前,我們得先了解四次揮手狀態(tài)變遷的過程。
客戶端和服務(wù)端雙方都可以主動(dòng)斷開連接,通常先關(guān)閉連接的一方稱為主動(dòng)方,后關(guān)閉連接的一方稱為被動(dòng)方。
可以看到,四次揮手過程只涉及了兩種報(bào)文,分別是 FIN 和 ACK:
FIN 就是結(jié)束連接的意思,誰發(fā)出 FIN 報(bào)文,就表示它將不會(huì)再發(fā)送任何數(shù)據(jù),關(guān)閉這一方向上的傳輸通道;
ACK 就是確認(rèn)的意思,用來通知對(duì)方:你方的發(fā)送通道已經(jīng)關(guān)閉;
四次揮手的過程:
當(dāng)主動(dòng)方關(guān)閉連接時(shí),會(huì)發(fā)送 FIN 報(bào)文,此時(shí)發(fā)送方的 TCP 連接將從 ESTABLISHED 變成 FIN_WAIT1。
當(dāng)被動(dòng)方收到 FIN 報(bào)文后,內(nèi)核會(huì)自動(dòng)回復(fù) ACK 報(bào)文,連接狀態(tài)將從 ESTABLISHED 變成 CLOSE_WAIT,表示被動(dòng)方在等待進(jìn)程調(diào)用 close 函數(shù)關(guān)閉連接。
當(dāng)主動(dòng)方收到這個(gè) ACK 后,連接狀態(tài)由 FIN_WAIT1 變?yōu)?FIN_WAIT2,也就是表示主動(dòng)方的發(fā)送通道就關(guān)閉了。
當(dāng)被動(dòng)方進(jìn)入 CLOSE_WAIT 時(shí),被動(dòng)方還會(huì)繼續(xù)處理數(shù)據(jù),等到進(jìn)程的 read 函數(shù)返回 0 后,應(yīng)用程序就會(huì)調(diào)用 close 函數(shù),進(jìn)而觸發(fā)內(nèi)核發(fā)送 FIN 報(bào)文,此時(shí)被動(dòng)方的連接狀態(tài)變?yōu)?LAST_ACK。
當(dāng)主動(dòng)方收到這個(gè) FIN 報(bào)文后,內(nèi)核會(huì)回復(fù) ACK 報(bào)文給被動(dòng)方,同時(shí)主動(dòng)方的連接狀態(tài)由 FIN_WAIT2 變?yōu)?TIME_WAIT,在 Linux 系統(tǒng)下大約等待 1 分鐘后,TIME_WAIT 狀態(tài)的連接才會(huì)徹底關(guān)閉。
當(dāng)被動(dòng)方收到最后的 ACK 報(bào)文后,被動(dòng)方的連接就會(huì)關(guān)閉。
你可以看到,每個(gè)方向都需要一個(gè) FIN 和一個(gè) ACK,因此通常被稱為四次揮手。
這里一點(diǎn)需要注意是:主動(dòng)關(guān)閉連接的,才有 TIME_WAIT 狀態(tài)。
主動(dòng)關(guān)閉方和被動(dòng)關(guān)閉方優(yōu)化的思路也不同,接下來分別說說如何優(yōu)化他們。
主動(dòng)方的優(yōu)化
關(guān)閉的連接的方式通常有兩種,分別是 RST 報(bào)文關(guān)閉和 FIN 報(bào)文關(guān)閉。
如果進(jìn)程異常退出了,內(nèi)核就會(huì)發(fā)送 RST 報(bào)文來關(guān)閉,它可以不走四次揮手流程,是一個(gè)暴力關(guān)閉連接的方式。
安全關(guān)閉連接的方式必須通過四次揮手,它由進(jìn)程調(diào)用 close
和 shutdown
函數(shù)發(fā)起 FIN 報(bào)文(shutdown 參數(shù)須傳入 SHUT_WR 或者 SHUT_RDWR 才會(huì)發(fā)送 FIN)。
調(diào)用 close 函數(shù) 和 shutdown 函數(shù)有什么區(qū)別?
調(diào)用了 close 函數(shù)意味著完全斷開連接,完全斷開不僅指無法傳輸數(shù)據(jù),而且也不能發(fā)送數(shù)據(jù)。此時(shí),調(diào)用了 close 函數(shù)的一方的連接叫做「孤兒連接」,如果你用 netstat -p 命令,會(huì)發(fā)現(xiàn)連接對(duì)應(yīng)的進(jìn)程名為空。
使用 close 函數(shù)關(guān)閉連接是不優(yōu)雅的。于是,就出現(xiàn)了一種優(yōu)雅關(guān)閉連接的 shutdown
函數(shù),它可以控制只關(guān)閉一個(gè)方向的連接:
第二個(gè)參數(shù)決定斷開連接的方式,主要有以下三種方式:
SHUT_RD(0):關(guān)閉連接的「讀」這個(gè)方向,如果接收緩沖區(qū)有已接收的數(shù)據(jù),則將會(huì)被丟棄,并且后續(xù)再收到新的數(shù)據(jù),會(huì)對(duì)數(shù)據(jù)進(jìn)行 ACK,然后悄悄地丟棄。也就是說,對(duì)端還是會(huì)接收到 ACK,在這種情況下根本不知道數(shù)據(jù)已經(jīng)被丟棄了。
SHUT_WR(1):關(guān)閉連接的「寫」這個(gè)方向,這就是常被稱為「半關(guān)閉」的連接。如果發(fā)送緩沖區(qū)還有未發(fā)送的數(shù)據(jù),將被立即發(fā)送出去,并發(fā)送一個(gè) FIN 報(bào)文給對(duì)端。
SHUT_RDWR(2):相當(dāng)于 SHUT_RD 和 SHUT_WR 操作各一次,關(guān)閉套接字的讀和寫兩個(gè)方向。
close 和 shutdown 函數(shù)都可以關(guān)閉連接,但這兩種方式關(guān)閉的連接,不只功能上有差異,控制它們的 Linux 參數(shù)也不相同。
FIN_WAIT1 狀態(tài)的優(yōu)化
主動(dòng)方發(fā)送 FIN 報(bào)文后,連接就處于 FIN_WAIT1 狀態(tài),正常情況下,如果能及時(shí)收到被動(dòng)方的 ACK,則會(huì)很快變?yōu)?FIN_WAIT2 狀態(tài)。
但是當(dāng)遲遲收不到對(duì)方返回的 ACK 時(shí),連接就會(huì)一直處于 FIN_WAIT1 狀態(tài)。此時(shí),內(nèi)核會(huì)定時(shí)重發(fā) FIN 報(bào)文,其中重發(fā)次數(shù)由 tcp_orphan_retries 參數(shù)控制(注意,orphan 雖然是孤兒的意思,該參數(shù)卻不只對(duì)孤兒連接有效,事實(shí)上,它對(duì)所有 FIN_WAIT1 狀態(tài)下的連接都有效),默認(rèn)值是 0。
你可能會(huì)好奇,這 0 表示幾次?實(shí)際上當(dāng)為 0 時(shí),特指 8 次,從下面的內(nèi)核源碼可知:
如果 FIN_WAIT1 狀態(tài)連接很多,我們就需要考慮降低 tcp_orphan_retries 的值,當(dāng)重傳次數(shù)超過 tcp_orphan_retries 時(shí),連接就會(huì)直接關(guān)閉掉。
對(duì)于普遍正常情況時(shí),調(diào)低 tcp_orphan_retries 就已經(jīng)可以了。如果遇到惡意攻擊,F(xiàn)IN 報(bào)文根本無法發(fā)送出去,這由 TCP 兩個(gè)特性導(dǎo)致的:
首先,TCP 必須保證報(bào)文是有序發(fā)送的,F(xiàn)IN 報(bào)文也不例外,當(dāng)發(fā)送緩沖區(qū)還有數(shù)據(jù)沒有發(fā)送時(shí),F(xiàn)IN 報(bào)文也不能提前發(fā)送。
其次,TCP 有流量控制功能,當(dāng)接收方接收窗口為 0 時(shí),發(fā)送方就不能再發(fā)送數(shù)據(jù)。所以,當(dāng)攻擊者下載大文件時(shí),就可以通過接收窗口設(shè)為 0 ,這就會(huì)使得 FIN 報(bào)文都無法發(fā)送出去,那么連接會(huì)一直處于 FIN_WAIT1 狀態(tài)。
解決這種問題的方法,是調(diào)整 tcp_max_orphans 參數(shù),它定義了「孤兒連接」的最大數(shù)量:
當(dāng)進(jìn)程調(diào)用了 close
函數(shù)關(guān)閉連接,此時(shí)連接就會(huì)是「孤兒連接」,因?yàn)樗鼰o法在發(fā)送和接收數(shù)據(jù)。Linux 系統(tǒng)為了防止孤兒連接過多,導(dǎo)致系統(tǒng)資源長時(shí)間被占用,就提供了 tcp_max_orphans
參數(shù)。如果孤兒連接數(shù)量大于它,新增的孤兒連接將不再走四次揮手,而是直接發(fā)送 RST 復(fù)位報(bào)文強(qiáng)制關(guān)閉。
FIN_WAIT2 狀態(tài)的優(yōu)化
當(dāng)主動(dòng)方收到 ACK 報(bào)文后,會(huì)處于 FIN_WAIT2 狀態(tài),就表示主動(dòng)方的發(fā)送通道已經(jīng)關(guān)閉,接下來將等待對(duì)方發(fā)送 FIN 報(bào)文,關(guān)閉對(duì)方的發(fā)送通道。
這時(shí),如果連接是用 shutdown 函數(shù)關(guān)閉的,連接可以一直處于 FIN_WAIT2 狀態(tài),因?yàn)樗赡苓€可以發(fā)送或接收數(shù)據(jù)。但對(duì)于 close 函數(shù)關(guān)閉的孤兒連接,由于無法在發(fā)送和接收數(shù)據(jù),所以這個(gè)狀態(tài)不可以持續(xù)太久,而 tcp_fin_timeout 控制了這個(gè)狀態(tài)下連接的持續(xù)時(shí)長,默認(rèn)值是 60 秒:
它意味著對(duì)于孤兒連接(調(diào)用 close 關(guān)閉的連接),如果在 60 秒后還沒有收到 FIN 報(bào)文,連接就會(huì)直接關(guān)閉。
這個(gè) 60 秒不是隨便決定的,它與 TIME_WAIT 狀態(tài)持續(xù)的時(shí)間是相同的,后面我們?cè)趤碚f說為什么是 60 秒。
TIME_WAIT 狀態(tài)的優(yōu)化
TIME_WAIT 是主動(dòng)方四次揮手的最后一個(gè)狀態(tài),也是最常遇見的狀態(tài)。
當(dāng)收到被動(dòng)方發(fā)來的 FIN 報(bào)文后,主動(dòng)方會(huì)立刻回復(fù) ACK,表示確認(rèn)對(duì)方的發(fā)送通道已經(jīng)關(guān)閉,接著就處于 TIME_WAIT 狀態(tài)。在 Linux 系統(tǒng),TIME_WAIT 狀態(tài)會(huì)持續(xù) 60 秒后才會(huì)進(jìn)入關(guān)閉狀態(tài)。
TIME_WAIT 狀態(tài)的連接,在主動(dòng)方看來確實(shí)快已經(jīng)關(guān)閉了。然后,被動(dòng)方?jīng)]有收到 ACK 報(bào)文前,還是處于 LAST_ACK 狀態(tài)。如果這個(gè) ACK 報(bào)文沒有到達(dá)被動(dòng)方,被動(dòng)方就會(huì)重發(fā) FIN 報(bào)文。重發(fā)次數(shù)仍然由前面介紹過的 tcp_orphan_retries 參數(shù)控制。
TIME-WAIT 的狀態(tài)尤其重要,主要是兩個(gè)原因:
防止具有相同「四元組」的「舊」數(shù)據(jù)包被收到;
保證「被動(dòng)關(guān)閉連接」的一方能被正確的關(guān)閉,即保證最后的 ACK 能讓被動(dòng)關(guān)閉方接收,從而幫助其正常關(guān)閉;
原因一:防止舊連接的數(shù)據(jù)包
TIME-WAIT 的一個(gè)作用是防止收到歷史數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)錯(cuò)亂的問題。
假設(shè) TIME-WAIT 沒有等待時(shí)間或時(shí)間過短,被延遲的數(shù)據(jù)包抵達(dá)后會(huì)發(fā)生什么呢?
如上圖黃色框框服務(wù)端在關(guān)閉連接之前發(fā)送的
SEQ = 301
報(bào)文,被網(wǎng)絡(luò)延遲了。這時(shí)有相同端口的 TCP 連接被復(fù)用后,被延遲的
SEQ = 301
抵達(dá)了客戶端,那么客戶端是有可能正常接收這個(gè)過期的報(bào)文,這就會(huì)產(chǎn)生數(shù)據(jù)錯(cuò)亂等嚴(yán)重的問題。
所以,TCP 就設(shè)計(jì)出了這么一個(gè)機(jī)制,經(jīng)過 2MSL
這個(gè)時(shí)間,足以讓兩個(gè)方向上的數(shù)據(jù)包都被丟棄,使得原來連接的數(shù)據(jù)包在網(wǎng)絡(luò)中都自然消失,再出現(xiàn)的數(shù)據(jù)包一定都是新建立連接所產(chǎn)生的。
原因二:保證連接正確關(guān)閉
TIME-WAIT 的另外一個(gè)作用是等待足夠的時(shí)間以確保最后的 ACK 能讓被動(dòng)關(guān)閉方接收,從而幫助其正常關(guān)閉。
假設(shè) TIME-WAIT 沒有等待時(shí)間或時(shí)間過短,斷開連接會(huì)造成什么問題呢?
如上圖紅色框框客戶端四次揮手的最后一個(gè)
ACK
報(bào)文如果在網(wǎng)絡(luò)中被丟失了,此時(shí)如果客戶端TIME-WAIT
過短或沒有,則就直接進(jìn)入了CLOSE
狀態(tài)了,那么服務(wù)端則會(huì)一直處在LASE-ACK
狀態(tài)。當(dāng)客戶端發(fā)起建立連接的
SYN
請(qǐng)求報(bào)文后,服務(wù)端會(huì)發(fā)送RST
報(bào)文給客戶端,連接建立的過程就會(huì)被終止。
我們?cè)倩剡^頭來看看,為什么 TIME_WAIT 狀態(tài)要保持 60 秒呢?這與孤兒連接 FIN_WAIT2 狀態(tài)默認(rèn)保留 60 秒的原理是一樣的,因?yàn)檫@兩個(gè)狀態(tài)都需要保持 2MSL 時(shí)長。MSL 全稱是 Maximum Segment Lifetime,它定義了一個(gè)報(bào)文在網(wǎng)絡(luò)中的最長生存時(shí)間(報(bào)文每經(jīng)過一次路由器的轉(zhuǎn)發(fā),IP 頭部的 TTL 字段就會(huì)減 1,減到 0 時(shí)報(bào)文就被丟棄,這就限制了報(bào)文的最長存活時(shí)間)。
為什么是 2 MSL 的時(shí)長呢?這其實(shí)是相當(dāng)于至少允許報(bào)文丟失一次。比如,若 ACK 在一個(gè) MSL 內(nèi)丟失,這樣被動(dòng)方重發(fā)的 FIN 會(huì)在第 2 個(gè) MSL 內(nèi)到達(dá),TIME_WAIT 狀態(tài)的連接可以應(yīng)對(duì)。
為什么不是 4 或者 8 MSL 的時(shí)長呢?你可以想象一個(gè)丟包率達(dá)到百分之一的糟糕網(wǎng)絡(luò),連續(xù)兩次丟包的概率只有萬分之一,這個(gè)概率實(shí)在是太小了,忽略它比解決它更具性價(jià)比。
因此,TIME_WAIT 和 FIN_WAIT2 狀態(tài)的最大時(shí)長都是 2 MSL,由于在 Linux 系統(tǒng)中,MSL 的值固定為 30 秒,所以它們都是 60 秒。
雖然 TIME_WAIT 狀態(tài)有存在的必要,但它畢竟會(huì)消耗系統(tǒng)資源。如果發(fā)起連接一方的 TIME_WAIT 狀態(tài)過多,占滿了所有端口資源,則會(huì)導(dǎo)致無法創(chuàng)建新連接。
客戶端受端口資源限制:如果客戶端 TIME_WAIT 過多,就會(huì)導(dǎo)致端口資源被占用,因?yàn)槎丝诰?5536個(gè),被占滿就會(huì)導(dǎo)致無法創(chuàng)建新的連接;
服務(wù)端受系統(tǒng)資源限制:由于一個(gè) 四元組表示TCP連接,理論上服務(wù)端可以建立很多連接,服務(wù)端確實(shí)只監(jiān)聽一個(gè)端口 但是會(huì)把連接扔給處理線程,所以理論上監(jiān)聽的端口可以繼續(xù)監(jiān)聽。但是線程池處理不了那么多一直不斷的連接了。所以當(dāng)服務(wù)端出現(xiàn)大量 TIME_WAIT 時(shí),系統(tǒng)資源被占滿時(shí),會(huì)導(dǎo)致處理不過來新的連接;
另外,Linux 提供了 tcp_max_tw_buckets 參數(shù),當(dāng) TIME_WAIT 的連接數(shù)量超過該參數(shù)時(shí),新關(guān)閉的連接就不再經(jīng)歷 TIME_WAIT 而直接關(guān)閉:
當(dāng)服務(wù)器的并發(fā)連接增多時(shí),相應(yīng)地,同時(shí)處于 TIME_WAIT 狀態(tài)的連接數(shù)量也會(huì)變多,此時(shí)就應(yīng)當(dāng)調(diào)大 tcp_max_tw_buckets
參數(shù),減少不同連接間數(shù)據(jù)錯(cuò)亂的概率。
tcp_max_tw_buckets 也不是越大越好,畢竟內(nèi)存和端口都是有限的。
有一種方式可以在建立新連接時(shí),復(fù)用處于 TIME_WAIT 狀態(tài)的連接,那就是打開 tcp_tw_reuse 參數(shù)。但是需要注意,該參數(shù)是只用于客戶端(建立連接的發(fā)起方),因?yàn)槭窃谡{(diào)用 connect() 時(shí)起作用的,而對(duì)于服務(wù)端(被動(dòng)連接方)是沒有用的。
tcp_tw_reuse 從協(xié)議角度理解是安全可控的,可以復(fù)用處于 TIME_WAIT 的端口為新的連接所用。
什么是協(xié)議角度理解的安全可控呢?主要有兩點(diǎn):
只適用于連接發(fā)起方,也就是 C/S 模型中的客戶端;
對(duì)應(yīng)的 TIME_WAIT 狀態(tài)的連接創(chuàng)建時(shí)間超過 1 秒才可以被復(fù)用。
使用這個(gè)選項(xiàng),還有一個(gè)前提,需要打開對(duì) TCP 時(shí)間戳的支持(對(duì)方也要打開 ):
由于引入了時(shí)間戳,它能帶來了些好處:
我們?cè)谇懊嫣岬降?2MSL 問題就不復(fù)存在了,因?yàn)橹貜?fù)的數(shù)據(jù)包會(huì)因?yàn)闀r(shí)間戳過期被自然丟棄;
同時(shí),它還可以防止序列號(hào)繞回,也是因?yàn)橹貜?fù)的數(shù)據(jù)包會(huì)由于時(shí)間戳過期被自然丟棄;
老版本的 Linux 還提供了 tcp_tw_recycle 參數(shù),但是當(dāng)開啟了它,就有兩個(gè)坑:
Linux 會(huì)加快客戶端和服務(wù)端 TIME_WAIT 狀態(tài)的時(shí)間,也就是它會(huì)使得 TIME_WAIT 狀態(tài)會(huì)小于 60 秒,很容易導(dǎo)致數(shù)據(jù)錯(cuò)亂;
另外,Linux 會(huì)丟棄所有來自遠(yuǎn)端時(shí)間戳小于上次記錄的時(shí)間戳(由同一個(gè)遠(yuǎn)端發(fā)送的)的任何數(shù)據(jù)包。就是說要使用該選項(xiàng),則必須保證數(shù)據(jù)包的時(shí)間戳是單調(diào)遞增的。那么,問題在于,此處的時(shí)間戳并不是我們通常意義上面的絕對(duì)時(shí)間,而是一個(gè)相對(duì)時(shí)間。很多情況下,我們是沒法保證時(shí)間戳單調(diào)遞增的,比如使用了 NAT,LVS 等情況;
所以,不建議設(shè)置為 1 ,建議關(guān)閉它:
在 Linux 4.12 版本后,Linux 內(nèi)核直接取消了這一參數(shù)。
另外,我們可以在程序中設(shè)置 socket 選項(xiàng),來設(shè)置調(diào)用 close 關(guān)閉連接行為。
如果l_onoff
為非 0, 且l_linger
值為 0,那么調(diào)用close后,會(huì)立該發(fā)送一個(gè) RST 標(biāo)志給對(duì)端,該 TCP 連接將跳過四次揮手,也就跳過了 TIME_WAIT 狀態(tài),直接關(guān)閉。
但這為跨越 TIME_WAIT 狀態(tài)提供了一個(gè)可能,不過是一個(gè)非常危險(xiǎn)的行為,不值得提倡。
被動(dòng)方的優(yōu)化
當(dāng)被動(dòng)方收到 FIN 報(bào)文時(shí),內(nèi)核會(huì)自動(dòng)回復(fù) ACK,同時(shí)連接處于 CLOSE_WAIT 狀態(tài),顧名思義,它表示等待應(yīng)用進(jìn)程調(diào)用 close 函數(shù)關(guān)閉連接。
內(nèi)核沒有權(quán)利替代進(jìn)程去關(guān)閉連接,因?yàn)槿绻鲃?dòng)方是通過 shutdown 關(guān)閉連接,那么它就是想在半關(guān)閉連接上接收數(shù)據(jù)或發(fā)送數(shù)據(jù)。因此,Linux 并沒有限制 CLOSE_WAIT 狀態(tài)的持續(xù)時(shí)間。
當(dāng)然,大多數(shù)應(yīng)用程序并不使用 shutdown 函數(shù)關(guān)閉連接。所以,當(dāng)你用 netstat 命令發(fā)現(xiàn)大量 CLOSE_WAIT 狀態(tài)。就需要排查你的應(yīng)用程序,因?yàn)榭赡芤驗(yàn)閼?yīng)用程序出現(xiàn)了 Bug,read 函數(shù)返回 0 時(shí),沒有調(diào)用 close 函數(shù)。
處于 CLOSE_WAIT 狀態(tài)時(shí),調(diào)用了 close 函數(shù),內(nèi)核就會(huì)發(fā)出 FIN 報(bào)文關(guān)閉發(fā)送通道,同時(shí)連接進(jìn)入 LAST_ACK 狀態(tài),等待主動(dòng)方返回 ACK 來確認(rèn)連接關(guān)閉。
如果遲遲收不到這個(gè) ACK,內(nèi)核就會(huì)重發(fā) FIN 報(bào)文,重發(fā)次數(shù)仍然由 tcp_orphan_retries 參數(shù)控制,這與主動(dòng)方重發(fā) FIN 報(bào)文的優(yōu)化策略一致。
還有一點(diǎn)我們需要注意的,如果被動(dòng)方迅速調(diào)用 close 函數(shù),那么被動(dòng)方的 ACK 和 FIN 有可能在一個(gè)報(bào)文中發(fā)送,這樣看起來,四次揮手會(huì)變成三次揮手,這只是一種特殊情況,不用在意。
如果連接雙方同時(shí)關(guān)閉連接,會(huì)怎么樣?
由于 TCP 是雙全工的協(xié)議,所以是會(huì)出現(xiàn)兩方同時(shí)關(guān)閉連接的現(xiàn)象,也就是同時(shí)發(fā)送了 FIN 報(bào)文。
此時(shí),上面介紹的優(yōu)化策略仍然適用。兩方發(fā)送 FIN 報(bào)文時(shí),都認(rèn)為自己是主動(dòng)方,所以都進(jìn)入了 FIN_WAIT1 狀態(tài),F(xiàn)IN 報(bào)文的重發(fā)次數(shù)仍由 tcp_orphan_retries 參數(shù)控制。
接下來,雙方在等待 ACK 報(bào)文的過程中,都等來了 FIN 報(bào)文。這是一種新情況,所以連接會(huì)進(jìn)入一種叫做 CLOSING 的新狀態(tài),它替代了 FIN_WAIT2 狀態(tài)。接著,雙方內(nèi)核回復(fù) ACK 確認(rèn)對(duì)方發(fā)送通道的關(guān)閉后,進(jìn)入 TIME_WAIT 狀態(tài),等待 2MSL 的時(shí)間后,連接自動(dòng)關(guān)閉。
小結(jié)
針對(duì) TCP 四次揮手的優(yōu)化,我們需要根據(jù)主動(dòng)方和被動(dòng)方四次揮手狀態(tài)變化來調(diào)整系統(tǒng) TCP 內(nèi)核參數(shù)。
主動(dòng)方的優(yōu)化
主動(dòng)發(fā)起 FIN 報(bào)文斷開連接的一方,如果遲遲沒收到對(duì)方的 ACK 回復(fù),則會(huì)重傳 FIN 報(bào)文,重傳的次數(shù)由 tcp_orphan_retries
參數(shù)決定。
當(dāng)主動(dòng)方收到 ACK 報(bào)文后,連接就進(jìn)入 FIN_WAIT2 狀態(tài),根據(jù)關(guān)閉的方式不同,優(yōu)化的方式也不同:
如果這是 close 函數(shù)關(guān)閉的連接,那么它就是孤兒連接。如果
tcp_fin_timeout
秒內(nèi)沒有收到對(duì)方的 FIN 報(bào)文,連接就直接關(guān)閉。同時(shí),為了應(yīng)對(duì)孤兒連接占用太多的資源,tcp_max_orphans
定義了最大孤兒連接的數(shù)量,超過時(shí)連接就會(huì)直接釋放。反之是 shutdown 函數(shù)關(guān)閉的連接,則不受此參數(shù)限制;
當(dāng)主動(dòng)方接收到 FIN 報(bào)文,并返回 ACK 后,主動(dòng)方的連接進(jìn)入 TIME_WAIT 狀態(tài)。這一狀態(tài)會(huì)持續(xù) 1 分鐘,為了防止 TIME_WAIT 狀態(tài)占用太多的資源,tcp_max_tw_buckets
定義了最大數(shù)量,超過時(shí)連接也會(huì)直接釋放。
當(dāng) TIME_WAIT 狀態(tài)過多時(shí),還可以通過設(shè)置 tcp_tw_reuse
和 tcp_timestamps
為 1 ,將 TIME_WAIT 狀態(tài)的端口復(fù)用于作為客戶端的新連接,注意該參數(shù)只適用于客戶端。
被動(dòng)方的優(yōu)化
被動(dòng)關(guān)閉的連接方應(yīng)對(duì)非常簡單,它在回復(fù) ACK 后就進(jìn)入了 CLOSE_WAIT 狀態(tài),等待進(jìn)程調(diào)用 close 函數(shù)關(guān)閉連接。因此,出現(xiàn)大量 CLOSE_WAIT 狀態(tài)的連接時(shí),應(yīng)當(dāng)從應(yīng)用程序中找問題。
當(dāng)被動(dòng)方發(fā)送 FIN 報(bào)文后,連接就進(jìn)入 LAST_ACK 狀態(tài),在未等到 ACK 時(shí),會(huì)在 tcp_orphan_retries
參數(shù)的控制下重發(fā) FIN 報(bào)文。
03 TCP 傳輸數(shù)據(jù)的性能提升
在前面介紹的是三次握手和四次揮手的優(yōu)化策略,接下來主要介紹的是 TCP 傳輸數(shù)據(jù)時(shí)的優(yōu)化策略。
TCP 連接是由內(nèi)核維護(hù)的,內(nèi)核會(huì)為每個(gè)連接建立內(nèi)存緩沖區(qū):
如果連接的內(nèi)存配置過小,就無法充分使用網(wǎng)絡(luò)帶寬,TCP 傳輸效率就會(huì)降低;
如果連接的內(nèi)存配置過大,很容易把服務(wù)器資源耗盡,這樣就會(huì)導(dǎo)致新連接無法建立;
因此,我們必須理解 Linux 下 TCP 內(nèi)存的用途,才能正確地配置內(nèi)存大小。
滑動(dòng)窗口是如何影響傳輸速度的?
TCP 會(huì)保證每一個(gè)報(bào)文都能夠抵達(dá)對(duì)方,它的機(jī)制是這樣:報(bào)文發(fā)出去后,必須接收到對(duì)方返回的確認(rèn)報(bào)文 ACK,如果遲遲未收到,就會(huì)超時(shí)重發(fā)該報(bào)文,直到收到對(duì)方的 ACK 為止。
所以,TCP 報(bào)文發(fā)出去后,并不會(huì)立馬從內(nèi)存中刪除,因?yàn)橹貍鲿r(shí)還需要用到它。
由于 TCP 是內(nèi)核維護(hù)的,所以報(bào)文存放在內(nèi)核緩沖區(qū)。如果連接非常多,我們可以通過 free 命令觀察到 buff/cache
內(nèi)存是會(huì)增大。
如果 TCP 是每發(fā)送一個(gè)數(shù)據(jù),都要進(jìn)行一次確認(rèn)應(yīng)答。當(dāng)上一個(gè)數(shù)據(jù)包收到了應(yīng)答了, 再發(fā)送下一個(gè)。這個(gè)模式就有點(diǎn)像我和你面對(duì)面聊天,你一句我一句,但這種方式的缺點(diǎn)是效率比較低的。
所以,這樣的傳輸方式有一個(gè)缺點(diǎn):數(shù)據(jù)包的往返時(shí)間越長,通信的效率就越低。
要解決這一問題不難,并行批量發(fā)送報(bào)文,再批量確認(rèn)報(bào)文即刻。
然而,這引出了另一個(gè)問題,發(fā)送方可以隨心所欲的發(fā)送報(bào)文嗎?當(dāng)然這不現(xiàn)實(shí),我們還得考慮接收方的處理能力。
當(dāng)接收方硬件不如發(fā)送方,或者系統(tǒng)繁忙、資源緊張時(shí),是無法瞬間處理這么多報(bào)文的。于是,這些報(bào)文只能被丟掉,使得網(wǎng)絡(luò)效率非常低。
為了解決這種現(xiàn)象發(fā)生,TCP 提供一種機(jī)制可以讓「發(fā)送方」根據(jù)「接收方」的實(shí)際接收能力控制發(fā)送的數(shù)據(jù)量,這就是滑動(dòng)窗口的由來。
接收方根據(jù)它的緩沖區(qū),可以計(jì)算出后續(xù)能夠接收多少字節(jié)的報(bào)文,這個(gè)數(shù)字叫做接收窗口。當(dāng)內(nèi)核接收到報(bào)文時(shí),必須用緩沖區(qū)存放它們,這樣剩余緩沖區(qū)空間變小,接收窗口也就變小了;當(dāng)進(jìn)程調(diào)用 read 函數(shù)后,數(shù)據(jù)被讀入了用戶空間,內(nèi)核緩沖區(qū)就被清空,這意味著主機(jī)可以接收更多的報(bào)文,接收窗口就會(huì)變大。
因此,接收窗口并不是恒定不變的,接收方會(huì)把當(dāng)前可接收的大小放在 TCP 報(bào)文頭部中的窗口字段,這樣就可以起到窗口大小通知的作用。
發(fā)送方的窗口等價(jià)于接收方的窗口嗎?如果不考慮擁塞控制,發(fā)送方的窗口大小「約等于」接收方的窗口大小,因?yàn)榇翱谕ㄖ獔?bào)文在網(wǎng)絡(luò)傳輸是存在時(shí)延的,所以是約等于的關(guān)系。
從上圖中可以看到,窗口字段只有 2 個(gè)字節(jié),因此它最多能表達(dá) 65535 字節(jié)大小的窗口,也就是 64KB 大小。
這個(gè)窗口大小最大值,在當(dāng)今高速網(wǎng)絡(luò)下,很明顯是不夠用的。所以后續(xù)有了擴(kuò)充窗口的方法:在 TCP 選項(xiàng)字段定義了窗口擴(kuò)大因子,用于擴(kuò)大TCP通告窗口,使 TCP 的窗口大小從 2 個(gè)字節(jié)(16 位) 擴(kuò)大為 30 位,所以此時(shí)窗口的最大值可以達(dá)到 1GB(2^30)。
Linux 中打開這一功能,需要把 tcp_window_scaling 配置設(shè)為 1(默認(rèn)打開):
要使用窗口擴(kuò)大選項(xiàng),通訊雙方必須在各自的 SYN 報(bào)文中發(fā)送這個(gè)選項(xiàng):
主動(dòng)建立連接的一方在 SYN 報(bào)文中發(fā)送這個(gè)選項(xiàng);
而被動(dòng)建立連接的一方只有在收到帶窗口擴(kuò)大選項(xiàng)的 SYN 報(bào)文之后才能發(fā)送這個(gè)選項(xiàng)。
這樣看來,只要進(jìn)程能及時(shí)地調(diào)用 read 函數(shù)讀取數(shù)據(jù),并且接收緩沖區(qū)配置得足夠大,那么接收窗口就可以無限地放大,發(fā)送方也就無限地提升發(fā)送速度。
這是不可能的,因?yàn)榫W(wǎng)絡(luò)的傳輸能力是有限的,當(dāng)發(fā)送方依據(jù)發(fā)送窗口,發(fā)送超過網(wǎng)絡(luò)處理能力的報(bào)文時(shí),路由器會(huì)直接丟棄這些報(bào)文。因此,緩沖區(qū)的內(nèi)存并不是越大越好。
如果確定最大傳輸速度?
在前面我們知道了 TCP 的傳輸速度,受制于發(fā)送窗口與接收窗口,以及網(wǎng)絡(luò)設(shè)備傳輸能力。其中,窗口大小由內(nèi)核緩沖區(qū)大小決定。如果緩沖區(qū)與網(wǎng)絡(luò)傳輸能力匹配,那么緩沖區(qū)的利用率就達(dá)到了最大化。
問題來了,如何計(jì)算網(wǎng)絡(luò)的傳輸能力呢?
相信大家都知道網(wǎng)絡(luò)是有「帶寬」限制的,帶寬描述的是網(wǎng)絡(luò)傳輸能力,它與內(nèi)核緩沖區(qū)的計(jì)量單位不同:
帶寬是單位時(shí)間內(nèi)的流量,表達(dá)是「速度」,比如常見的帶寬 100 MB/s;
緩沖區(qū)單位是字節(jié),當(dāng)網(wǎng)絡(luò)速度乘以時(shí)間才能得到字節(jié)數(shù);
這里需要說一個(gè)概念,就是帶寬時(shí)延積,它決定網(wǎng)絡(luò)中飛行報(bào)文的大小,它的計(jì)算方式:
比如最大帶寬是 100 MB/s,網(wǎng)絡(luò)時(shí)延(RTT)是 10ms 時(shí),意味著客戶端到服務(wù)端的網(wǎng)絡(luò)一共可以存放 100MB/s * 0.01s = 1MB 的字節(jié)。
這個(gè) 1MB 是帶寬和時(shí)延的乘積,所以它就叫「帶寬時(shí)延積」(縮寫為 BDP,Bandwidth Delay Product)。同時(shí),這 1MB 也表示「飛行中」的 TCP 報(bào)文大小,它們就在網(wǎng)絡(luò)線路、路由器等網(wǎng)絡(luò)設(shè)備上。如果飛行報(bào)文超過了 1 MB,就會(huì)導(dǎo)致網(wǎng)絡(luò)過載,容易丟包。
由于發(fā)送緩沖區(qū)大小決定了發(fā)送窗口的上限,而發(fā)送窗口又決定了「已發(fā)送未確認(rèn)」的飛行報(bào)文的上限。因此,發(fā)送緩沖區(qū)不能超過「帶寬時(shí)延積」。
發(fā)送緩沖區(qū)與帶寬時(shí)延積的關(guān)系:
如果發(fā)送緩沖區(qū)「超過」帶寬時(shí)延積,超出的部分就沒辦法有效的網(wǎng)絡(luò)傳輸,同時(shí)導(dǎo)致網(wǎng)絡(luò)過載,容易丟包;
如果發(fā)送緩沖區(qū)「小于」帶寬時(shí)延積,就不能很好的發(fā)揮出網(wǎng)絡(luò)的傳輸效率。
所以,發(fā)送緩沖區(qū)的大小最好是往帶寬時(shí)延積靠近。
怎樣調(diào)整緩沖區(qū)大?。?/span>
在 Linux 中發(fā)送緩沖區(qū)和接收緩沖都是可以用參數(shù)調(diào)節(jié)的。設(shè)置完后,Linux 會(huì)根據(jù)你設(shè)置的緩沖區(qū)進(jìn)行動(dòng)態(tài)調(diào)節(jié)。
調(diào)節(jié)發(fā)送緩沖區(qū)范圍
先來看看發(fā)送緩沖區(qū),它的范圍通過 tcp_wmem 參數(shù)配置;
上面三個(gè)數(shù)字單位都是字節(jié),它們分別表示:
第一個(gè)數(shù)值是動(dòng)態(tài)范圍的最小值,4096 byte = 4K;
第二個(gè)數(shù)值是初始默認(rèn)值,87380 byte ≈ 86K;
第三個(gè)數(shù)值是動(dòng)態(tài)范圍的最大值,4194304 byte = 4096K(4M);
發(fā)送緩沖區(qū)是自行調(diào)節(jié)的,當(dāng)發(fā)送方發(fā)送的數(shù)據(jù)被確認(rèn)后,并且沒有新的數(shù)據(jù)要發(fā)送,就會(huì)把發(fā)送緩沖區(qū)的內(nèi)存釋放掉。
調(diào)節(jié)接收緩沖區(qū)范圍
而接收緩沖區(qū)的調(diào)整就比較復(fù)雜一些,先來看看設(shè)置接收緩沖區(qū)范圍的 tcp_rmem 參數(shù):
上面三個(gè)數(shù)字單位都是字節(jié),它們分別表示:
第一個(gè)數(shù)值是動(dòng)態(tài)范圍的最小值,表示即使在內(nèi)存壓力下也可以保證的最小接收緩沖區(qū)大小,4096 byte = 4K;
第二個(gè)數(shù)值是初始默認(rèn)值,87380 byte ≈ 86K;
第三個(gè)數(shù)值是動(dòng)態(tài)范圍的最大值,6291456 byte = 6144K(6M);
接收緩沖區(qū)可以根據(jù)系統(tǒng)空閑內(nèi)存的大小來調(diào)節(jié)接收窗口:
如果系統(tǒng)的空閑內(nèi)存很多,就可以自動(dòng)把緩沖區(qū)增大一些,這樣傳給對(duì)方的接收窗口也會(huì)變大,因而提升發(fā)送方發(fā)送的傳輸數(shù)據(jù)數(shù)量;
反正,如果系統(tǒng)的內(nèi)存很緊張,就會(huì)減少緩沖區(qū),這雖然會(huì)降低傳輸效率,可以保證更多的并發(fā)連接正常工作;
發(fā)送緩沖區(qū)的調(diào)節(jié)功能是自動(dòng)開啟的,而接收緩沖區(qū)則需要配置 tcp_moderate_rcvbuf 為 1 來開啟調(diào)節(jié)功能:
調(diào)節(jié) TCP 內(nèi)存范圍
接收緩沖區(qū)調(diào)節(jié)時(shí),怎么知道當(dāng)前內(nèi)存是否緊張或充分呢?這是通過 tcp_mem 配置完成的:
上面三個(gè)數(shù)字單位不是字節(jié),而是「頁面大小」,1 頁表示 4KB,它們分別表示:
當(dāng) TCP 內(nèi)存小于第 1 個(gè)值時(shí),不需要進(jìn)行自動(dòng)調(diào)節(jié);
在第 1 和第 2 個(gè)值之間時(shí),內(nèi)核開始調(diào)節(jié)接收緩沖區(qū)的大??;
大于第 3 個(gè)值時(shí),內(nèi)核不再為 TCP 分配新內(nèi)存,此時(shí)新連接是無法建立的;
一般情況下這些值是在系統(tǒng)啟動(dòng)時(shí)根據(jù)系統(tǒng)內(nèi)存數(shù)量計(jì)算得到的。根據(jù)當(dāng)前 tcp_mem 最大內(nèi)存頁面數(shù)是 177120,當(dāng)內(nèi)存為 (177120 * 4) / 1024K ≈ 692M 時(shí),系統(tǒng)將無法為新的 TCP 連接分配內(nèi)存,即 TCP 連接將被拒絕。
根據(jù)實(shí)際場景調(diào)節(jié)的策略
在高并發(fā)服務(wù)器中,為了兼顧網(wǎng)速與大量的并發(fā)連接,我們應(yīng)當(dāng)保證緩沖區(qū)的動(dòng)態(tài)調(diào)整的最大值達(dá)到帶寬時(shí)延積,而最小值保持默認(rèn)的 4K 不變即可。而對(duì)于內(nèi)存緊張的服務(wù)而言,調(diào)低默認(rèn)值是提高并發(fā)的有效手段。
同時(shí),如果這是網(wǎng)絡(luò) IO 型服務(wù)器,那么,調(diào)大 tcp_mem 的上限可以讓 TCP 連接使用更多的系統(tǒng)內(nèi)存,這有利于提升并發(fā)能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節(jié),而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設(shè)置 SO_SNDBUF 或者 SO_RCVBUF,這樣會(huì)關(guān)閉緩沖區(qū)的動(dòng)態(tài)調(diào)整功能。
小結(jié)
本節(jié)針對(duì) TCP 優(yōu)化數(shù)據(jù)傳輸?shù)姆绞?,做了一些介紹。
TCP 可靠性是通過 ACK 確認(rèn)報(bào)文實(shí)現(xiàn)的,又依賴滑動(dòng)窗口提升了發(fā)送速度也兼顧了接收方的處理能力。
可是,默認(rèn)的滑動(dòng)窗口最大值只有 64 KB,不滿足當(dāng)今的高速網(wǎng)絡(luò)的要求,要想要想提升發(fā)送速度必須提升滑動(dòng)窗口的上限,在 Linux 下是通過設(shè)置 tcp_window_scaling
為 1 做到的,此時(shí)最大值可高達(dá) 1GB。
滑動(dòng)窗口定義了網(wǎng)絡(luò)中飛行報(bào)文的最大字節(jié)數(shù),當(dāng)它超過帶寬時(shí)延積時(shí),網(wǎng)絡(luò)過載,就會(huì)發(fā)生丟包。而當(dāng)它小于帶寬時(shí)延積時(shí),就無法充分利用網(wǎng)絡(luò)帶寬。因此,滑動(dòng)窗口的設(shè)置,必須參考帶寬時(shí)延積。
內(nèi)核緩沖區(qū)決定了滑動(dòng)窗口的上限,緩沖區(qū)可分為:發(fā)送緩沖區(qū) tcp_wmem 和接收緩沖區(qū) tcp_rmem。
Linux 會(huì)對(duì)緩沖區(qū)動(dòng)態(tài)調(diào)節(jié),我們應(yīng)該把緩沖區(qū)的上限設(shè)置為帶寬時(shí)延積。發(fā)送緩沖區(qū)的調(diào)節(jié)功能是自動(dòng)打開的,而接收緩沖區(qū)需要把 tcp_moderate_rcvbuf 設(shè)置為 1 來開啟。其中,調(diào)節(jié)的依據(jù)是 TCP 內(nèi)存范圍 tcp_mem。
但需要注意的是,如果程序中的 socket 設(shè)置 SO_SNDBUF 和 SO_RCVBUF,則會(huì)關(guān)閉緩沖區(qū)的動(dòng)態(tài)整功能,所以不建議在程序設(shè)置它倆,而是交給內(nèi)核自動(dòng)調(diào)整比較好。
有效配置這些參數(shù)后,既能夠最大程度地保持并發(fā)性,也能讓資源充裕時(shí)連接傳輸速度達(dá)到最大值。
巨人的肩膀
[1] 系統(tǒng)性能調(diào)優(yōu)必知必會(huì).陶輝.極客時(shí)間.
[2] 網(wǎng)絡(luò)編程實(shí)戰(zhàn)專欄.盛延敏.極客時(shí)間.
[3] http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html
[4] http://blog.itpub.net/31559359/viewspace-2284113/
[5] https://blog.51cto.com/professor/1909022
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場,如有問題,請(qǐng)聯(lián)系我們,謝謝!