應(yīng)對(duì)高負(fù)載Linux服務(wù)中 TCP TIME-WAIT問題

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_recyclenet.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)求。

接下來我要給那些瞎逼逼的人上一課??

duty_calls

另外雖然從參數(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)圖:

tcp-state-diagram-v2.png

只有主動(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è)圖解
duplicate-segment.png

但是這樣的情況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)建就失敗了。
last-ack.png

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, and

  • net.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)行重建

last-ack-reuse.png

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 StevensUnix Network Programming中的一句話:

time-wait狀態(tài)是我們的好朋友是來幫助我們的(比如讓一些老的重復(fù)的網(wǎng)絡(luò)包過期掉)。相比極力避免折這種狀態(tài),我們更應(yīng)該充分理解它。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容