https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux#fn-netfilter
一句話總結(jié)| 不要開啟 net.ipv4.tcp_tw_recycle
在Linux內(nèi)核文檔上,關(guān)于 net.ipv4.tcp_tw_recycle 和 net.ipv4.tcp_tw_reuse 都語焉不詳。
針對(duì)recycle:能夠快速回收TIME-WAIT狀態(tài)的sockets,默認(rèn)不開啟。沒有專家建議不應(yīng)改動(dòng)
針對(duì)reuse:能夠復(fù)用處于TIME-WAIT狀態(tài)的sockets,默認(rèn)不開啟。沒有專家建議不應(yīng)改動(dòng)
官方文檔的缺失導(dǎo)致了市面上很多調(diào)優(yōu)指南上都建議開啟這兩個(gè)參數(shù),以減少TIME-WAIT狀態(tài)的鏈接。但是在 tcp(7) manual page上卻提到,這兩個(gè)參數(shù)的開啟造成了一個(gè)十分難以發(fā)現(xiàn)的隱患:在NAT設(shè)備后的多個(gè)服務(wù)器可能會(huì)無法處理正常請(qǐng)求。
接下來我要給那些瞎逼逼的人上一課??

另外雖然從參數(shù)的的名稱上有ipv4的字樣,但是該參數(shù)同樣適用于ipv6。所以謹(jǐn)記我們是在處理Linux tcp協(xié)議族的事情,如果從抓包的角度分析那就可能需要調(diào)整一下了。
TIME-WAIT 狀態(tài)
讓我們簡單復(fù)習(xí)一下TIME-WAIT狀態(tài)。它到底是什么?我們看一下TCP狀態(tài)流轉(zhuǎn)圖:

只有主動(dòng)關(guān)閉鏈接的一方會(huì)進(jìn)入到TIME-WAIT的狀態(tài),被動(dòng)關(guān)閉的一方通常都會(huì)快速關(guān)閉。
目的
TIME-WAIT狀態(tài)存在有兩個(gè)目的:
- 最被大家熟知的是出于可能存在數(shù)據(jù)包發(fā)送后有延遲,然后被后來創(chuàng)建的一個(gè)連接處理了。當(dāng)然序列號(hào)( sequence number)也是在特定范圍的才能接收,這讓問題發(fā)生的概率降低了,但是問題仍然存在。RFC?1337解釋了當(dāng)TIME-WAIT不存在的時(shí)候的情況。以下是一個(gè)圖解

但是這樣的情況TIME-WAIT只保持一個(gè)MSL就可以了
- 另外一個(gè)目的是確保遠(yuǎn)端連接已經(jīng)關(guān)閉。如果最后一個(gè)ACK丟失,此時(shí)被動(dòng)關(guān)閉端處于LAST-ACK狀態(tài),如果不存在time-wait 狀態(tài)的話,一個(gè)創(chuàng)建新鏈接的請(qǐng)求可能會(huì)發(fā)送到LAST-ACK狀態(tài)的連接上,此時(shí)端上會(huì)返回RST,新鏈接的創(chuàng)建就失敗了。

RFC?793 要求time-wait狀態(tài)必要持續(xù)2 MSL,Linux上是不可調(diào)的,內(nèi)核寫死60s。
https://tools.ietf.org/html/rfc1185
The proper time to delay the final close step is not really
related to the MSL; it depends instead upon the RTO for the
FIN segments and therefore upon the RTT of the path.*
Although there is no formal upper-bound on RTT, common
network engineering practice makes an RTT greater than 1
minute very unlikely. Thus, the 4 minute delay in TIME-WAIT
state works satisfactorily to provide a reliable full-duplex
TCP close. Note again that this is independent of MSL
enforcement and network speed.
有人建議把這個(gè)參數(shù)設(shè)置成可調(diào)節(jié)的,但是基于保持time-wait狀態(tài)是有益的這一前提下,該提議被否決了。
問題
為什么這個(gè)狀態(tài)在處理海量連接的服務(wù)器上會(huì)變的十分惱人呢。以下是其中的三個(gè)問題:
無法創(chuàng)建新鏈接
socket 占用內(nèi)存
額外的CPU使用
連接表卡槽
一個(gè)處于time-wait狀態(tài)的連接大概會(huì)在連接表中存在一分鐘,在這個(gè)時(shí)間內(nèi)具有相同源地址源端口目標(biāo)地址目標(biāo)端口的連接是不能創(chuàng)建的。
對(duì)于一個(gè)web服務(wù)器來說,外網(wǎng)地址和端口往往都是固定的。如果是一個(gè)7層負(fù)載均衡后的web服務(wù)。源地址同樣是固定的。在linux上客戶端的端口范圍大概是30000個(gè)(可以通過 net.ipv4.ip_local_port_range 參數(shù)更改),也就是說負(fù)載服務(wù)器和web服務(wù)器每分鐘只能建立30000個(gè),QPS大概是500
客戶端的time-wait現(xiàn)象很容易定位,connect()的方法調(diào)用會(huì)拋出EADDRNOTAVAIL的異常,并且也會(huì)有對(duì)應(yīng)的日志記錄。但是在服務(wù)端就變得十分復(fù)雜了,只能通過命令行的方式看當(dāng)前那些ip 端口被占用了。
如果是客戶端端口不夠用,通過 net.ipv4.ip_local_port_range 調(diào)大端口的可用范圍
如果是服務(wù)端的端口不夠用,添加額外的端口
如果是客戶端的IP不夠用, 通過round-robin 進(jìn)行客戶端的負(fù)載均衡
如果是服務(wù)端的ip不夠用,配置多個(gè)服務(wù)端ip
內(nèi)存
如果需要處理超多的連接,這些socket額外處于open狀態(tài)一分鐘會(huì)耗點(diǎn)內(nèi)存,舉個(gè)例子來說,如果每秒鐘要處理10000個(gè)請(qǐng)求的話,time-wait狀態(tài)的請(qǐng)求就會(huì)有60*10000個(gè)。那需要消耗的內(nèi)存大概是多少呢?實(shí)際上也沒有那么多
首先,從應(yīng)用的角度考慮,一個(gè)處于time-wait狀態(tài)的socket不會(huì)消耗任何的內(nèi)存。因?yàn)閟ocket已經(jīng)被關(guān)閉了。從內(nèi)核的角度考慮,socket主要以三種不同的形式存在內(nèi)存中。
hash table of connections set of lists of connections hash table of bound ports
參考linux的代碼,40000個(gè)入站連接大概需要10m的內(nèi)存,40000個(gè)出站的連接大概需要2.5M。
CUP
從CPU的角度看,找到一個(gè)可用的端口確實(shí)是略昂貴的。 inet_csk_get_port() function 方法需要加鎖迭代的方式找到可用的端口。 如果在TIME-WAIT狀態(tài)下有很多出站連接(如與memcached服務(wù)器的短暫連接),則此哈希表中的大量條目通常不會(huì)成為問題:連接通常共享相同的配置文件,該功能將很快 找到一個(gè)可用端口,因?yàn)樗错樞虻鼈儭?/p>
其他的解決辦法
如果上面的回答仍然沒有解決你的問題,還有三個(gè)方法可以嘗試。
disable socket lingering,
net.ipv4.tcp_tw_reuse, andnet.ipv4.tcp_tw_recycle.
Socket lingering
當(dāng)close() 被調(diào)用的時(shí)候,內(nèi)核緩沖區(qū)的數(shù)據(jù)會(huì)被在后臺(tái)繼續(xù)發(fā)送然后socket最終會(huì)轉(zhuǎn)為time-wait狀態(tài)。應(yīng)用程序可以立即繼續(xù)工作,并假設(shè)最終將安全地傳遞所有數(shù)據(jù)。
應(yīng)用程序可以選擇禁用這個(gè)功能,稱之為Socket lingering。它有以下兩種特性:
1.余下的數(shù)據(jù)會(huì)被直接拋棄,而且不進(jìn)行四次揮手的操作,連接直接發(fā)送RST并且直接關(guān)閉。不存在TINE-WAIt狀態(tài)的情況。
2.第二個(gè)場景下,如果發(fā)送緩存中仍然有未發(fā)送的數(shù)據(jù),進(jìn)程在調(diào)用close()后會(huì)休眠直到數(shù)據(jù)完全發(fā)完并且已經(jīng)通知到了對(duì)端,或者配置的延遲時(shí)間過期了。還有另外一個(gè)場景,線程并不阻塞并且持續(xù)的發(fā)送數(shù)據(jù)包,如果成功發(fā)完就正經(jīng)關(guān)閉,然后連接變?yōu)閠ime-wait狀態(tài)。如果沒發(fā)完過期了,就直接發(fā)RST然后拋棄剩余的包。
開啟改參數(shù)并不能適用于所有的場景,在HAProxy or Nginx 的服務(wù)器上可以在確認(rèn)合適的情況下開啟。
net.ipv4.tcp_tw_reuse
time-wait狀態(tài)的存在避免了延遲包被無關(guān)的連接處理的情況,但是在特定場景下,我們可以認(rèn)為新的連接不會(huì)處理老連接的包。
RFC?1323 提供了一系列在高并發(fā)的情況下的提升性能的擴(kuò)展。這其中就包含兩個(gè)四字節(jié)的timestamp fields字段,前兩個(gè)用于記錄發(fā)送包的時(shí)間戳,后兩個(gè)記錄從遠(yuǎn)端收到的最新的時(shí)間戳。
開啟了 net.ipv4.tcp_tw_reuse 參數(shù)后,如果linux收到一個(gè)明顯比上個(gè)連接最后一個(gè)包時(shí)間晚的請(qǐng)求,那linux就會(huì)復(fù)用這個(gè)處于time-wait狀態(tài)的連接。大概一秒鐘后time-wait的連接就能繼續(xù)使用
這樣能確保合法么?time-wait狀態(tài)是為了避免收到一個(gè)其他連接請(qǐng)求的包,但是因?yàn)?strong>timestamp fields字段的存在,類似的包會(huì)因?yàn)槌瑫r(shí)而被丟棄掉。
另外一個(gè)原則是為了確保對(duì)端不會(huì)因?yàn)樽詈笠粋€(gè)ACK包的丟失而導(dǎo)致連接一直處于LAST-ACK狀態(tài)。對(duì)端會(huì)一直發(fā)送FIN包知道以下幾種場景:
1:主動(dòng)放棄(并且關(guān)閉連接)
2:收到了期望的ACK
3:收到RST包
如果FIN包被及時(shí)接收了,本端連接仍然保持在time-wait狀態(tài),然后ACK包也會(huì)照常發(fā)送。
一旦新的連接代替了time-wait的坑位,新鏈接的SYN包會(huì)被返回FIN,然后FIN包會(huì)被應(yīng)答RST。然后對(duì)端就脫離了LAST-ACK狀態(tài)了。連接就會(huì)馬上進(jìn)行重建

net.ipv4.tcp_tw_recycle
此機(jī)制還依賴于timestamp選項(xiàng),但會(huì)影響傳入和傳出連接。這在服務(wù)器通常首先關(guān)閉連接時(shí)更實(shí)用。
TIME-WAIT狀態(tài)計(jì)劃更快到期:它將在RTO(從RTT及其方差計(jì)算的重傳超時(shí))之后被刪除。 你可以通過ss命令找到活動(dòng)連接的相應(yīng)值。
參數(shù)開啟后,linux會(huì)記錄請(qǐng)求包最新的時(shí)間戳,然后老的都刪除。但是遠(yuǎn)程主機(jī)是一個(gè)NAT設(shè)備,NAT后的不同主機(jī)的時(shí)間戳可能是亂序的,所以就會(huì)導(dǎo)致丟包的問題。
Linux?4.10 后,linux隨機(jī)生成時(shí)間戳的偏移量,所以這兩個(gè)參數(shù)就廢了。在4.12后這個(gè)參數(shù)被移除了。
總結(jié)一哈
通用的解決方案毫無疑問是添加足夠的可用連接數(shù)量,這樣就不會(huì)因?yàn)閠ime-wait焦頭爛額。
在服務(wù)端,不要開啟 net.ipv4.tcp_tw_recycle 參數(shù),開啟net.ipv4.tcp_tw_reuse 參數(shù)對(duì)入棧連接也沒什么用。
在客戶端,開啟net.ipv4.tcp_tw_reuse 也算一個(gè)相對(duì)合理的方法。開啟net.ipv4.tcp_tw_recycle也沒什么大用。
制訂協(xié)議的時(shí)候,不要讓客戶端先關(guān)閉連接。這樣客戶端就不用處理time-wait狀態(tài)的連接了。服務(wù)端處理這些問題更合理的。
最后引用一句 W. Richard Stevens 在Unix Network Programming中的一句話:
time-wait狀態(tài)是我們的好朋友是來幫助我們的(比如讓一些老的重復(fù)的網(wǎng)絡(luò)包過期掉)。相比極力避免折這種狀態(tài),我們更應(yīng)該充分理解它。