time_wait狀態產生的原因,危害,如何避免
請說說你對TCP連接中time_wait狀態的理解
解答:
先上TCP的狀態變遷圖
這幅圖來自《TCP IP詳解卷1:協議 原書第2版中文》13.5 TCP狀態轉換圖
這幅圖來自《UNIX網絡編程,卷1:套接字聯網API》2.6.4 TCP狀態轉換圖
1. time_wait狀態如何產生?
由上面的變遷圖,首先調用close()發起主動關閉的一方,在發送最后一個ACK之后會進入time_wait的狀態,也就說該發送方會保持2MSL時間之后才會回到初始狀態。MSL值得是數據包在網絡中的最大生存時間。產生這種結果使得這個TCP連接在2MSL連接等待期間,定義這個連接的四元組(客戶端IP地址和端口,服務端IP地址和端口號)不能被使用。
2.time_wait狀態產生的原因
1)為實現TCP全雙工連接的可靠釋放
由TCP狀態變遷圖可知,假設發起主動關閉的一方(client)最后發送的ACK在網絡中丟失,由於TCP協議的重傳機制,執行被動關閉的一方(server)將會重發其FIN,在該FIN到達client之前,client必須維護這條連接狀態,也就說這條TCP連接所對應的資源(client方的local_ip,local_port)不能被立即釋放或重新分配,直到另一方重發的FIN達到之后,client重發ACK后,經過2MSL時間周期沒有再收到另一方的FIN之后,該TCP連接才能恢復初始的CLOSED狀態。如果主動關閉一方不維護這樣一個TIME_WAIT狀態,那么當被動關閉一方重發的FIN到達時,主動關閉一方的TCP傳輸層會用RST包響應對方,這會被對方認為是有錯誤發生,然而這事實上只是正常的關閉連接過程,並非異常。
2)為使舊的數據包在網絡因過期而消失
為說明這個問題,我們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP連接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我們先關閉,接着很快以相同的四元組建立一條新連接。本文前面介紹過,TCP連接由四元組唯一標識,因此,在我們假設的情況中,TCP協議棧是無法區分前后兩條TCP連接的不同的,在它看來,這根本就是同一條連接,中間先釋放再建立的過程對其來說是“感知”不到的。這樣就可能發生這樣的情況:前一條TCP連接由local peer發送的數據到達remote peer后,會被該remot peer的TCP傳輸層當做當前TCP連接的正常數據接收並向上傳遞至應用層(而事實上,在我們假設的場景下,這些舊數據到達remote peer前,舊連接已斷開且一條由相同四元組構成的新TCP連接已建立,因此,這些舊數據是不應該被向上傳遞至應用層的),從而引起數據錯亂進而導致各種無法預知的詭異現象。作為一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種情況的發生,這正是TIME_WAIT狀態存在的第2個原因。
3)總結
具體而言,local peer主動調用close后,此時的TCP連接進入TIME_WAIT狀態,處於該狀態下的TCP連接不能立即以同樣的四元組建立新連接,即發起active close的那方占用的local port在TIME_WAIT期間不能再被重新分配。由於TIME_WAIT狀態持續時間為2MSL,這樣保證了舊TCP連接雙工鏈路中的舊數據包均因過期(超過MSL)而消失,此后,就可以用相同的四元組建立一條新連接而不會發生前后兩次連接數據錯亂的情況。
3.time_wait狀態如何避免
首先服務器可以設置SO_REUSEADDR套接字選項來通知內核,如果端口忙,但TCP連接位於TIME_WAIT狀態時可以重用端口。在一個非常有用的場景就是,如果你的服務器程序停止后想立即重啟,而新的套接字依舊希望使用同一端口,此時SO_REUSEADDR選項就可以避免TIME_WAIT狀態。
1. 實際問題
初步查看發現,無法對外新建TCP連接時,線上服務器存在大量處於TIME_WAIT狀態的TCP連接(最多的一次為單機10w+,其中引起報警的那個模塊產生的TIME_WAIT約2w),導致其無法跟下游模塊建立新TCP連接。
TIME_WAIT涉及到TCP釋放連接過程中的狀態遷移,也涉及到具體的socket api對TCP狀態的影響,下面開始逐步介紹這些概念。
2. TCP狀態遷移
面向連接的TCP協議要求每次peer間通信前建立一條TCP連接,該連接可抽象為一個4元組(four-tuple,有時也稱socket pair):(local_ip, local_port, remote_ip,remote_port),這4個元素唯一地代表一條TCP連接。
1)TCP Connection Establishment
TCP建立連接的過程,通常又叫“三次握手”(three-way handshake),可用下圖來示意:
可對上圖做如下解釋:
a. client向server發送SYN並約定初始包序號(sequence number)為J;
b. server發送自己的SYN並表明初始包序號為K,同時,針對client的SYNJ返回ACKJ+1(注:J+1表示server期望的來自該client的下一個包序為J+1);
c. client收到來自server的SYN+ACK后,發送ACKK+1,至此,TCP建立成功。
其實,在TCP建立時的3次握手過程中,還要通過SYN包商定各自的MSS,timestamp等參數,這涉及到協議的細節,本文旨在拋磚引玉,不再展開。
2)TCPConnection Termination
與建立連接的3次握手相對應,釋放一條TCP連接時,需要經過四步交互(又稱“四次揮手”),如下圖所示:
可對上圖做如下解釋:
a. 連接的某一方先調用close()發起主動關閉(active close),該api會促使TCP傳輸層向remotepeer發送FIN包,該包表明發起active close的application不再發送數據(特別注意:這里“不再發送數據”的承諾是從應用層角度來看的,在TCP傳輸層,還是要將該application對應的內核tcp send buffer中當前尚未發出的數據發到鏈路上)。
remote peer收到FIN后,需要完成被動關閉(passive close),具體分為兩步:
b. 首先,在TCP傳輸層,先針對對方的FIN包發出ACK包(主要ACK的包序是在對方FIN包序基礎上加1);
c. 接着,應用層的application收到對方的EOF(end-of-file,對方的FIN包作為EOF傳給應用層的application)后,得知這條連接不會再有來自對方的數據,於是也調用close()關閉連接,該close會促使TCP傳輸層發送FIN。
d. 發起主動關閉的peer收到remote peer的FIN后,發送ACK包,至此,TCP連接關閉。
注意1:TCP連接的任一方均可以首先調用close()以發起主動關閉,上圖以client主動發起關閉做說明,而不是說只能client發起主動關閉。
注意2:上面給出的TCP建立/釋放連接的過程描述中,未考慮由於各種原因引起的重傳、擁塞控制等協議細節,感興趣的同學可以查看各種TCP RFC Documents ,比如TCP RFC793。
3)TCP StateTransition Diagram
上面介紹了TCP建立、釋放連接的過程,此處對TCP狀態機的遷移過程做總體說明。將TCP RFC793中描述的TCP狀態機遷移圖摘出如下(下圖引用自這里):
TCP狀態機共含11個狀態,狀態間在各種socket apis的驅動下進行遷移,雖然此圖看起來錯綜復雜,但對於有一定TCP網絡編程經驗的同學來說,理解起來還是比較容易的。限於篇幅,本文不准備展開詳述,想了解具體遷移過程的新手同學,建議閱讀《Linux Network Programming Volume1》第2.6節。
3. TIME_WAIT狀態
經過前面的鋪墊,終於要講到與本文主題相關的內容了。 ^_^
從TCP狀態遷移圖可知,只有首先調用close()發起主動關閉的一方才會進入TIME_WAIT狀態,而且是必須進入(圖中左下角所示的3條狀態遷移線最終均要進入該狀態才能回到初始的CLOSED狀態)。
從圖中還可看到,進入TIME_WAIT狀態的TCP連接需要經過2MSL才能回到初始狀態,其中,MSL是指Max
Segment Lifetime,即數據包在網絡中的最大生存時間。每種TCP協議的實現方法均要指定一個合適的MSL值,如RFC1122給出的建議值為2分鍾,又如Berkeley體系的TCP實現通常選擇30秒作為MSL值。這意味着TIME_WAIT的典型持續時間為1-4分鍾。
TIME_WAIT狀態存在的原因主要有兩點:
1)為實現TCP這種全雙工(full-duplex)連接的可靠釋放
參考本文前面給出的TCP釋放連接4次揮手示意圖,假設發起active close的一方(圖中為client)發送的ACK(4次交互的最后一個包)在網絡中丟失,那么由於TCP的重傳機制,執行passiveclose的一方(圖中為server)需要重發其FIN,在該FIN到達client(client是active close發起方)之前,client必須維護這條連接的狀態(盡管它已調用過close),具體而言,就是這條TCP連接對應的(local_ip, local_port)資源不能被立即釋放或重新分配。直到romete peer重發的FIN達到,client也重發ACK后,該TCP連接才能恢復初始的CLOSED狀態。如果activeclose方不進入TIME_WAIT以維護其連接狀態,則當passive close方重發的FIN達到時,active close方的TCP傳輸層會以RST包響應對方,這會被對方認為有錯誤發生(而事實上,這是正常的關閉連接過程,並非異常)。
2)為使舊的數據包在網絡因過期而消失
為說明這個問題,我們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP連接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我們先關閉,接着很快以相同的四元組建立一條新連接。本文前面介紹過,TCP連接由四元組唯一標識,因此,在我們假設的情況中,TCP協議棧是無法區分前后兩條TCP連接的不同的,在它看來,這根本就是同一條連接,中間先釋放再建立的過程對其來說是“感知”不到的。這樣就可能發生這樣的情況:前一條TCP連接由local peer發送的數據到達remote peer后,會被該remot peer的TCP傳輸層當做當前TCP連接的正常數據接收並向上傳遞至應用層(而事實上,在我們假設的場景下,這些舊數據到達remote peer前,舊連接已斷開且一條由相同四元組構成的新TCP連接已建立,因此,這些舊數據是不應該被向上傳遞至應用層的),從而引起數據錯亂進而導致各種無法預知的詭異現象。作為一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種情況的發生,這正是TIME_WAIT狀態存在的第2個原因。
具體而言,local peer主動調用close后,此時的TCP連接進入TIME_WAIT狀態,處於該狀態下的TCP連接不能立即以同樣的四元組建立新連接,即發起active close的那方占用的local port在TIME_WAIT期間不能再被重新分配。由於TIME_WAIT狀態持續時間為2MSL,這樣保證了舊TCP連接雙工鏈路中的舊數據包均因過期(超過MSL)而消失,此后,就可以用相同的四元組建立一條新連接而不會發生前后兩次連接數據錯亂的情況。
另一比較深入的說法
TIME_WAIT狀態的存在有兩個理由:(1)讓4次握手關閉流程更加可靠;4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。(2)防止lost duplicate對后續新建正常鏈接的傳輸造成破壞。lost duplicate在實際的網絡中非常常見,經常是由於路由器產生故障,路徑無法收斂,導致一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,因此這個包有兩種命運,要么最后TTL變為0,在網絡中消失;要么TTL在變為0之前路由器路徑收斂,它憑借剩余的TTL跳數終於到達目的地。但非常可惜的是TCP通過超時重傳機制在早些時候發送了一個跟它一模一樣的包,並先於它達到了目的地,因此它的命運也就注定被TCP協議棧拋棄。另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,則會對我們的傳輸造成致命的錯誤。大家都知道TCP是流式的,所有包到達的順序是不一致的,依靠序列號由TCP協議棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000, len=1000, 則tcp認為這個lost duplicate合法,並存放入了receive buffer,導致傳輸出現錯誤。通過一個2MSL TIME_WAIT狀態,確保所有的lost duplicate都會消失掉,避免對新連接造成錯誤。
Q: 編寫 TCP/SOCK_STREAM 服務程序時,SO_REUSEADDR到底什么意思?
A: 這個套接字選項通知內核,如果端口忙,但TCP狀態位於 TIME_WAIT ,可以重用端口。如果端口忙,而TCP狀態位於其他狀態,重用端口時依舊得到一個錯誤信息, 指明"地址已經使用中"。如果你的服務程序停止后想立即重啟,而新套接字依舊 使用同一端口,此時 SO_REUSEADDR 選項非常有用。必須意識到,此時任何非期 望數據到達,都可能導致服務程序反應混亂,不過這只是一種可能,事實上很不可能。
------------------------------------
TCP狀態以及握手詳解
1、建立連接協議(三次握手)
(1)客戶端發送一個帶SYN標志的TCP報文到服務器。這是三次握手過程中的報文1.
(2) 服務器端回應客戶端的,這是三次握手中的第2個報文,這個報文同時帶ACK標志和SYN標志。因此它表示對剛才客戶端SYN報文的回應;同時又標志SYN給客戶端,詢問客戶端是否准備好進行數據通訊。
(3) 客戶必須再次回應服務段一個ACK報文,這是報文段3.
2、連接終止協議(四次握手)
由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
(1) TCP客戶端發送一個FIN,用來關閉客戶到服務器的數據傳送(報文段4)。
(2) 服務器收到這個FIN,它發回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。
(3) 服務器關閉客戶端的連接,發送一個FIN給客戶端(報文段6)。
(4) 客戶段發回ACK報文確認,並將確認序號設置為收到序號加1(報文段7)。
CLOSED: 這個沒什么好說的了,表示初始狀態。
LISTEN: 這個也是非常容易理解的一個狀態,表示服務器端的某個SOCKET處於監聽狀態,可以接受連接了。
SYN_RCVD: 這個狀態表示接受到了SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫,基本上用netstat你是很難看到這種狀態的,除非你特意寫了一個客戶端測試程序,故意將三次TCP握手過程中最后一個ACK報文不予發送。因此這種狀態時,當收到客戶端的ACK報文后,它會進入到ESTABLISHED狀態。
SYN_SENT: 這個狀態與SYN_RCVD遙想呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,因此也隨即它會進入到了SYN_SENT狀態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。
ESTABLISHED:這個容易理解了,表示連接已經建立了。
FIN_WAIT_1: 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是:FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連接,向對方發送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方回應ACK報文后,則進入到FIN_WAIT_2狀態,當然在實際的正常情況下,無論對方何種情況下,都應該馬上回應ACK報文,所以FIN_WAIT_1狀態一般是比較難見到的,而FIN_WAIT_2狀態還有時常常可以用netstat看到。
FIN_WAIT_2:上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半連接,也即有一方要求close連接,但另外還告訴對方,我暫時還有點數據需要傳送給你,稍后再關閉連接。
TIME_WAIT: 表示收到了對方的FIN報文,並發送出了ACK報文,就等2MSL后即可回到CLOSED可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。
CLOSING: 這種狀態比較特殊,實際情況中應該是很少見,屬於一種比較罕見的例外狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?其實細想一下,也不難得出結論:那就是如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。
CLOSE_WAIT: 這種狀態的含義其實是表示在等待關閉。怎么理解呢?當對方close一個SOCKET后發送FIN報文給自己,你系統毫無疑問地會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正需要考慮的事情是察看你是否還有數據發送給對方,如果沒有的話,那么你也就可以close這個SOCKET,發送FIN報文給對方,也即關閉連接。所以你在CLOSE_WAIT狀態下,需要完成的事情是等待你去關閉連接。
LAST_ACK: 這個狀態還是比較容易好理解的,它是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,也即可以進入到CLOSED可用狀態了。
https://cloud.tencent.com/developer/articles/107?q=hot
近日遇到一個線上服務 socket 資源被不斷打滿的情況。通過各種工具分析線上問題,定位到問題代碼。這里對該問題發現、修復過程進行一下復盤總結。
先看兩張圖。一張圖是服務正常時監控到的 socket 狀態,另一張當然就是異常啦!

圖一:正常時監控

圖二:異常時監控
從圖中的表現情況來看,就是從 04:00 開始,socket 資源不斷上漲,每個谷底時重啟后恢復到正常值,然后繼續不斷上漲不釋放,而且每次達到峰值的間隔時間越來越短。
重啟后,排查了日志,沒有看到 panic ,此時也就沒有進一步檢查,真的以為重啟大法好。
情況說明
該服務使用Golang開發,已經上線正常運行將近一年,提供給其它服務調用,主要底層資源有DB/Redis/MQ。
為了后續說明的方便,將服務的架構圖進行一下說明。

圖三:服務架構
架構是非常簡單。
問題出現在早上 08:20 左右開始的,報警收到該服務出現 504,此時第一反應是該服務長時間沒有重啟(快兩個月了),可能存在一些內存泄漏,沒有多想直接進行了重啟。也就是在圖二第一個谷底的時候,經過重啟服務恢復到正常水平(重啟真好用,開心)。
將近 14:00 的時候,再次被告警出現了 504 ,當時心中略感不對勁,但由於當天恰好有一場大型促銷活動,因此先立馬再次重啟服務。直到后續大概過了1小時后又開始告警,連續幾次重啟后,發現需要重啟的時間間隔越來越短。此時發現問題絕不簡單。這一次重啟真的解決不了問題老,因此立馬申請機器權限、開始排查問題。下面的截圖全部來源我的重現demo,與線上無關。
發現問題
出現問題后,首先要進行分析推斷、然后驗證、最后定位修改。根據當時的表現是分別進行了以下猜想。
ps:后續截圖全部來源自己本地復現時的截圖
推斷一
socket 資源被不斷打滿,並且之前從未出現過,今日突然出現,懷疑是不是請求量太大壓垮服務
經過查看實時 qps 后,放棄該想法,雖然量有增加,但依然在服務器承受范圍(遠遠未達到壓測的基准值)。
推斷二
兩台機器故障是同時發生,重啟一台,另外一台也會得到緩解,作為獨立部署在兩個集群的服務非常詭異
有了上面的的依據,推出的結果是肯定是該服務依賴的底層資源除了問題,要不然不可能獨立集群的服務同時出問題。
由於監控顯示是 socket 問題,因此通過 netstat 命令查看了當前tcp鏈接的情況(本地測試,線上實際值大的多)
/go/src/hello # netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' LISTEN 2 CLOSE_WAIT 23 # 非常異常 TIME_WAIT 1
發現絕大部份的鏈接處於 CLOSE_WAIT 狀態,這是非常不可思議情況。然后用 netstat -an
命令進行了檢查。

圖四:大量的CLOSE_WAIT
CLOSED 表示socket連接沒被使用。 LISTENING 表示正在監聽進入的連接。 SYN_SENT 表示正在試着建立連接。 SYN_RECEIVED 進行連接初始同步。 ESTABLISHED 表示連接已被建立。 CLOSE_WAIT 表示遠程計算器關閉連接,正在等待socket連接的關閉。 FIN_WAIT_1 表示socket連接關閉,正在關閉連接。 CLOSING 先關閉本地socket連接,然后關閉遠程socket連接,最后等待確認信息。 LAST_ACK 遠程計算器關閉后,等待確認信號。 FIN_WAIT_2 socket連接關閉后,等待來自遠程計算器的關閉信號。 TIME_WAIT 連接關閉后,等待遠程計算器關閉重發。
然后開始重點思考為什么會出現大量的mysql連接是 CLOSE_WAIT 呢?為了說清楚,我們來插播一點TCP的四次揮手知識。
TCP四次揮手
我們來看看 TCP 的四次揮手是怎么樣的流程:

圖五:TCP四次揮手
用中文來描述下這個過程:
Client: 服務端大哥,我事情都干完了,准備撤了
,這里對應的就是客戶端發了一個FIN
Server:知道了,但是你等等我,我還要收收尾
,這里對應的就是服務端收到 FIN 后回應的 ACK
經過上面兩步之后,服務端就會處於 CLOSE_WAIT 狀態。過了一段時間 Server 收尾完了
Server:小弟,哥哥我做完了,撤吧
,服務端發送了FIN
Client:大哥,再見啊
,這里是客戶端對服務端的一個 ACK
到此服務端就可以跑路了,但是客戶端還不行。為什么呢?客戶端還必須等待 2MSL 個時間,這里為什么客戶端還不能直接跑路呢?主要是為了防止發送出去的 ACK 服務端沒有收到,服務端重發 FIN 再次來詢問,如果客戶端發完就跑路了,那么服務端重發的時候就沒人理他了。這個等待的時間長度也很講究。
Maximum Segment Lifetime 報文最大生存時間,它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄
這里一定不要被圖里的 client/server 和項目里的客戶端服務器端混淆,你只要記住:主動關閉的一方發出 FIN 包(Client),被動關閉(Server)的一方響應 ACK 包,此時,被動關閉的一方就進入了 CLOSE_WAIT 狀態。如果一切正常,稍后被動關閉的一方也會發出 FIN 包,然后遷移到 LAST_ACK 狀態。
既然是這樣, TCP 抓包分析下:
/go # tcpdump -n port 3306 # 發生了 3次握手 11:38:15.679863 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [S], seq 4065722321, win 29200, options [mss 1460,sackOK,TS val 2997352 ecr 0,nop,wscale 7], length 0 11:38:15.679923 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [S.], seq 780487619, ack 4065722322, win 28960, options [mss 1460,sackOK,TS val 2997352 ecr 2997352,nop,wscale 7], length 0 11:38:15.679936 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 1, win 229, options [nop,nop,TS val 2997352 ecr 2997352], length 0 # mysql 主動斷開鏈接 11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL負載均衡器發送fin包給我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回復ack給它 ... ... # 本來還需要我發送fin給他,但是我沒有發,所以出現了close_wait。那這是什么緣故呢?
src > dst: flags data-seqno ack window urgent options src > dst 表明從源地址到目的地址flags 是TCP包中的標志信息,S 是SYN標志, F(FIN), P(PUSH) , R(RST) "."(沒有標記)data-seqno 是數據包中的數據的順序號ack 是下次期望的順序號window 是接收緩存的窗口大小urgent 表明數據包中是否有緊急指針options 是選項
結合上面的信息,我用文字說明下:MySQL負載均衡器 給我的服務發送 FIN 包,我進行了響應,此時我進入了 CLOSE_WAIR 狀態,但是后續作為被動關閉方的我,並沒有發送 FIN,導致我服務端一直處於 CLOSE_WAIR 狀態,無法最終進入 CLOSED 狀態。
那么我推斷出現這種情況可能的原因有以下幾種:
- 負載均衡器 異常退出了,
這基本是不可能的,他出現問題絕對是大面積的服務報警,而不僅僅是我一個服務
- MySQL負載均衡器 的超時設置的太短了,導致業務代碼還沒有處理完,MySQL負載均衡器 就關閉tcp連接了
這也不太可能,因為這個服務並沒有什么耗時操作,當然還是去檢查了負載均衡器的配置,設置的是60s。
- 代碼問題,MySQL 連接無法釋放
目前看起來應該是代碼質量問題,加之本次數據有異常,觸發到了以前某個沒有測試到的點,目前看起來很有可能是這個原因
查找錯誤原因
由於代碼的業務邏輯並不是我寫的,我擔心一時半會看不出來問題,所以直接使用 perf
把所有的調用關系使用火焰圖給繪制出來。既然上面我們推斷代碼中沒有釋放mysql連接。無非就是:
- 確實沒有調用close
- 有耗時操作(火焰圖可以非常明顯看到),導致超時了
- mysql的事務沒有正確處理,例如:rollback 或者 commit
由於火焰圖包含的內容太多,為了讓大家看清楚,我把一些不必要的信息進行了折疊。

圖六:有問題的火焰圖
火焰圖很明顯看到了開啟了事務,但是在余下的部分,並沒有看到 Commit 或者是Rollback 操作。這肯定會操作問題。然后也清楚看到出現問題的是:
MainController.update 方法內部,話不多說,直接到 update 方法中去檢查。發現了如下代碼:
func (c *MainController) update() (flag bool) { o := orm.NewOrm() o.Using("default") o.Begin() nilMap := getMapNil() if nilMap == nil {// 這里只檢查了是否為nil,並沒有進行rollback或者commit return false } nilMap[10] = 1 nilMap[20] = 2 if nilMap == nil && len(nilMap) == 0 { o.Rollback() return false } sql := "update tb_user set name=%s where id=%d" res, err := o.Raw(sql, "Bug", 2).Exec() if err == nil { num, _ := res.RowsAffected() fmt.Println("mysql row affected nums: ", num) o.Commit() return true } o.Rollback() return false }
至此,全部分析結束。經過查看 getMapNil 返回了nil,但是下面的判斷條件沒有進行回滾。
if nilMap == nil { o.Rollback()// 這里進行回滾 return false }
總結
整個分析過程還是廢了不少時間。最主要的是主觀意識太強,覺得運行了一年沒有出問題的為什么會突然出問題?因此一開始是質疑 SRE、DBA、各種基礎設施出了問題(人總是先懷疑別人)。導致在這上面費了不少時間。
理一下正確的分析思路:
- 出現問題后,立馬應該檢查日志,確實日志沒有發現問題;
- 監控明確顯示了socket不斷增長,很明確立馬應該使用
netstat
檢查情況看看是哪個進程的鍋; - 根據
netstat
的檢查,使用tcpdump
抓包分析一下為什么連接會被動斷開(TCP知識非常重要); - 如果熟悉代碼應該直接去檢查業務代碼,如果不熟悉則可以使用
perf
把代碼的調用鏈路打印出來; - 不論是分析代碼還是火焰圖,到此應該能夠很快定位到問題。
那么本次到底是為什么會出現 CLOSE_WAIR 呢?大部分同學應該已經明白了,我這里再簡單說明一下:
由於那一行代碼沒有對事務進行回滾,導致服務端沒有主動發起close。因此 MySQL負載均衡器 在達到 60s 的時候主動觸發了close操作,但是通過tcp抓包發現,服務端並沒有進行回應,這是因為代碼中的事務沒有處理,因此從而導致大量的端口、連接資源被占用。在貼一下揮手時的抓包數據:
# mysql 主動斷開鏈接
11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL負載均衡器發送fin包給我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回復ack給它
希望此文對大家排查線上問題有所幫助。為了便於幫助大家理解,下面附上正確情況下的火焰圖與錯誤情況下的火焰圖。大家可以自行對比。
- 正確情況下的火焰圖 : https://dayutalk.cn/img/right.svg
- 錯誤情況的火焰圖 : https://dayutalk.cn/img/err.svg
我參考的一篇文章對這種情況提出了兩個思考題,我覺得非常有意義,大家自己思考下:
- 為什么一台機器幾百個 CLOSE_WAIR 就導致不可繼續訪問?我們不是經常說一台機器有 65535 個文件描述符可用嗎?
- 為什么我有負載均衡,而兩台部署服務的機器確幾乎同時出了 CLOSE_WAIR ?
來自:http://blog.csdn.net/shootyou/article/details/6622226
http://blog.csdn.net/shootyou/article/details/6615051
在服務器的日常維護過程中,會經常用到下面的命令:
- netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
它會顯示例如下面的信息:
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
常用的三個狀態是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。
具體每種狀態什么意思,其實無需多說,看看下面這種圖就明白了,注意這里提到的服務器應該是業務請求接受處理的一方:
這么多狀態不用都記住,只要了解到我上面提到的最常見的三種狀態的意義就可以了。一般不到萬不得已的情況也不會去查看網絡狀態,如果服務器出了異常,百分之八九十都是下面兩種情況:
1.服務器保持了大量TIME_WAIT狀態
2.服務器保持了大量CLOSE_WAIT狀態
因為linux分配給一個用戶的文件句柄是有限的(可以參考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT兩種狀態如果一直被保持,那么意味着對應數目的通道就一直被占着,而且是“占着茅坑不使勁”,一旦達到句柄數上限,新的請求就無法被處理了,接着就是大量Too Many Open Files異常,tomcat崩潰。。。
下 面來討論下這兩種情況的處理方法,網上有很多資料把這兩種情況的處理方法混為一談,以為優化系統內核參數就可以解決問題,其實是不恰當的,優化系統內核參 數解決TIME_WAIT可能很容易,但是應對CLOSE_WAIT的情況還是需要從程序本身出發。現在來分別說說這兩種情況的處理方法:
1.服務器保持了大量TIME_WAIT狀態
這種情況比較常見,一些爬蟲服務器或者WEB服務器(如果網管在安裝的時候沒有做內核參數優化的話)上經常會遇到這個問題,這個問題是怎么產生的呢?
從 上面的示意圖可以看得出來,TIME_WAIT是主動關閉連接的一方保持的狀態,對於爬蟲服務器來說他本身就是“客戶端”,在完成一個爬取任務之后,他就 會發起主動關閉連接,從而進入TIME_WAIT的狀態,然后在保持這個狀態2MSL(max segment lifetime)時間之后,徹底關閉回收資源。為什么要這么做?明明就已經主動關閉連接了為啥還要保持資源一段時間呢?這個是TCP/IP的設計者規定 的,主要出於以下兩個方面的考慮:
1.防止上一次連接中的包,迷路后重新出現,影響新連接(經過2MSL,上一次連接中所有的重復包都會消失)
2. 可靠的關閉TCP連接。在主動關閉方發送的最后一個 ack(fin) ,有可能丟失,這時被動方會重新發fin, 如果這時主動方處於 CLOSED 狀態 ,就會響應 rst 而不是 ack。所以主動方要處於 TIME_WAIT 狀態,而不能是 CLOSED 。另外這么設計TIME_WAIT 會定時的回收資源,並不會占用很大資源的,除非短時間內接受大量請求或者受到攻擊。
關於MSL引用下面一段話:
- MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你可以想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,因此我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其他連線使用。
再引用網絡資源的一段話:
- 值 得一說的是,對於基於TCP的HTTP協議,關閉TCP連接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可 想而知,對於訪 問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鍾接收1000個請求,那么就會積壓 240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。當然現代操作系統都會用快速的查找算法來管理這些 TIME_WAIT,所以對於新的 TCP連接請求,判斷是否hit中一個TIME_WAIT不會太費時間,但是有這么多狀態要維護總是不好。
- HTTP協議1.1版規定default行為是Keep-Alive,也就是會重用TCP連接傳輸多個 request/response,一個主要原因就是發現了這個問題。
也就是說HTTP的交互跟上面畫的那個圖是不一樣的,關閉連接的不是客戶端,而是服務器,所以web服務器也是會出現大量的TIME_WAIT的情況的。
- #對於一個新建連接,內核要發送多少個 SYN 連接請求才決定放棄,不應該大於255,默認值是5,對應於180秒左右時間
- net.ipv4.tcp_syn_retries=2
- #net.ipv4.tcp_synack_retries=2
- #表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改為300秒
- net.ipv4.tcp_keepalive_time=1200
- net.ipv4.tcp_orphan_retries=3
- #表示如果套接字由本端要求關閉,這個參數決定了它保持在FIN-WAIT-2狀態的時間
- net.ipv4.tcp_fin_timeout=30
- #表示SYN隊列的長度,默認為1024,加大隊列長度為8192,可以容納更多等待連接的網絡連接數。
- net.ipv4.tcp_max_syn_backlog = 4096
- #表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉
- net.ipv4.tcp_syncookies = 1
- #表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉
- net.ipv4.tcp_tw_reuse = 1
- #表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉
- net.ipv4.tcp_tw_recycle = 1
- ##減少超時前的探測次數
- net.ipv4.tcp_keepalive_probes=5
- ##優化網絡設備接收隊列
- net.core.netdev_max_backlog=3000
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
統計在一台前端機上高峰時間TCP連接的情況,統計命令:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
結果:
除了ESTABLISHED,可以看到連接數比較多的幾個狀態是:FIN_WAIT1, TIME_WAIT, CLOSE_WAIT, SYN_RECV和LAST_ACK;下面的文章就這幾個狀態的產生條件、對系統的影響以及處理方式進行簡單描述。
發現存在大量TIME_WAIT狀態的連接
tcp 0 0 127.0.0.1:3306 127.0.0.1:41378 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:41379 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39352 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39350 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:35763 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39372 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39373 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:41176 TIME_WAIT
通過調整內核參數解決
vi /etc/sysctl.conf
編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后執行/sbin/sysctl -p讓參數生效。
net.ipv4.tcp_syncookies = 1表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
net.ipv4.tcp_tw_reuse = 1表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout修改系統默認的TIMEOUT時間
修改之后,再用命令查看TIME_WAIT連接數
netstat -ae|grep “TIME_WAIT” |wc –l
發現大量的TIME_WAIT 已不存在,mysql進程的占用率很快就降下來的,網站訪問正常。
不過很多時候,出現大量的TIME_WAIT狀態的連接,往往是因為網站程序代碼中沒有使用mysql.colse(),才導致大量的mysql TIME_WAIT.
根據TCP協議定義的3次握手斷開連接規定,發起socket主動關閉的一方 socket將進入TIME_WAIT狀態,TIME_WAIT狀態將持續2個MSL(Max Segment Lifetime),在Windows下默認為4分鍾,即240秒,TIME_WAIT狀態下的socket不能被回收使用. 具體現象是對於一個處理大量短連接的服務器,如果是由服務器主動關閉客戶端的連接,將導致服務器端存在大量的處於TIME_WAIT狀態的socket, 甚至比處於Established狀態下的socket多的多,嚴重影響服務器的處理能力,甚至耗盡可用的socket,停止服務. TIME_WAIT是TCP協議用以保證被重新分配的socket不會受到之前殘留的延遲重發報文影響的機制,是必要的邏輯保證.
在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,添加名為TcpTimedWaitDelay的
DWORD鍵,設置為60,以縮短TIME_WAIT的等待時間
http://kerry.blog.51cto.com/172631/105233/
tcp 0 0 aaaa:50417 192.168.12.13:mysql ESTABLISHED nobody 3224673
tcp 0 0 aaaa:50419 192.168.12.13:mysql ESTABLISHED nobody 3224675
發現大量的TIME_WAIT 已不存在,mysql進程的占用率很快就降下來的,各網站訪問正常!!
如果你的服務器是Windows平台,可以修改下面的注冊表鍵值:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
"TcpTimedWaitDelay"=dword:0000001e
此值是TIME_WAIT狀態的最長時間。缺省為240秒,最低為30秒,最高為300秒。建議為30秒。
注釋:
(
1,TCP結束的過程如下:
Server Client
-------------- FIN --------------> server: fin_wait_1
<------------- ACK --------------- client: close_wait server:fin_wait_2
<------------- FIN --------------- client發出fin之后就關閉
-------------- ACK -------------> server發出ack后進入time_wait狀態
Time_Wait的默認時間是2倍的MLS,就是240秒鍾。MLS是TCP片在網上的最長存活時間。
TIME_Wait的主要作用是保證關閉的TCP端口不立即被使用。因為當網絡存在延遲時,可能當某個端口被關閉后,網絡中還有一些重傳的TCP片在發向這個端口,如果這個端口立即建立新的TCP連接,則可能會有影響。所以使用2倍的MSL時間來限制這個端口立即被使用。
現在的問題在於,4分鍾的時間有點長。
因此,Time_wait的影響,我想,首先每個TCP連接都各自有個數據結構,叫TCP Control Block.Time_wait的時候這個數據結構沒有被釋放。所以當有太多的TCP連接時,內存可能會被占用很多。
2,To ValorZ:TIME_WAIT狀態也稱為2MSL等待狀態,而不是2MLS,筆誤吧!
每個TCP報文在網絡內的最長時間,就稱為MSL(Maximum Segment Lifetime),它的作用和IP數據包的TTL類似。
RFC793指出,MSL的值是2分鍾,但是在實際的實現中,常用的值有以下三種:30秒,1分鍾,2分鍾。
注意一個問題,進入TIME_WAIT狀態的一般情況下是客戶端,大多數服務器端一般執行被動關閉,不會進入TIME_WAIT狀態,當在服務器端關閉某個服務再重新啟動時,它是會進入TIME_WAIT狀態的。
舉例:
1.客戶端連接服務器的80服務,這時客戶端會啟用一個本地的端口訪問服務器的80,訪問完成后關閉此連接,立刻再次訪問服務器的80,這時客戶端會啟用另一個本地的端口,而不是剛才使用的那個本地端口。原因就是剛才的那個連接還處於TIME_WAIT狀態。
2.客戶端連接服務器的80服務,這時服務器關閉80端口,立即再次重啟80端口的服務,這時可能不會成功啟動,原因也是服務器的連接還處於TIME_WAIT狀態。
windows
TcpTimedWaitDelay和MaxUserPort設置
http://blog.csdn.net/gzh0222/article/details/8491178
1. 實際問題
初步查看發現,無法對外新建TCP連接時,線上服務器存在大量處於TIME_WAIT狀態的TCP連接(最多的一次為單機10w+,其中引起報警的那個模塊產生的TIME_WAIT約2w),導致其無法跟下游模塊建立新TCP連接。
TIME_WAIT涉及到TCP釋放連接過程中的狀態遷移,也涉及到具體的socket api對TCP狀態的影響,下面開始逐步介紹這些概念。
2. TCP狀態遷移
面向連接的TCP協議要求每次peer間通信前建立一條TCP連接,該連接可抽象為一個4元組(four-tuple,有時也稱socket pair):(local_ip, local_port, remote_ip,remote_port),這4個元素唯一地代表一條TCP連接。
1)TCP Connection Establishment
TCP建立連接的過程,通常又叫“三次握手”(three-way handshake),可用下圖來示意:
可對上圖做如下解釋:
a. client向server發送SYN並約定初始包序號(sequence number)為J;
b. server發送自己的SYN並表明初始包序號為K,同時,針對client的SYNJ返回ACKJ+1(注:J+1表示server期望的來自該client的下一個包序為J+1);
c. client收到來自server的SYN+ACK后,發送ACKK+1,至此,TCP建立成功。
其實,在TCP建立時的3次握手過程中,還要通過SYN包商定各自的MSS,timestamp等參數,這涉及到協議的細節,本文旨在拋磚引玉,不再展開。
2)TCPConnection Termination
與建立連接的3次握手相對應,釋放一條TCP連接時,需要經過四步交互(又稱“四次揮手”),如下圖所示:
可對上圖做如下解釋:
a. 連接的某一方先調用close()發起主動關閉(active close),該api會促使TCP傳輸層向remotepeer發送FIN包,該包表明發起active close的application不再發送數據(特別注意:這里“不再發送數據”的承諾是從應用層角度來看的,在TCP傳輸層,還是要將該application對應的內核tcp send buffer中當前尚未發出的數據發到鏈路上)。
remote peer收到FIN后,需要完成被動關閉(passive close),具體分為兩步:
b. 首先,在TCP傳輸層,先針對對方的FIN包發出ACK包(主要ACK的包序是在對方FIN包序基礎上加1);
c. 接着,應用層的application收到對方的EOF(end-of-file,對方的FIN包作為EOF傳給應用層的application)后,得知這條連接不會再有來自對方的數據,於是也調用close()關閉連接,該close會促使TCP傳輸層發送FIN。
d. 發起主動關閉的peer收到remote peer的FIN后,發送ACK包,至此,TCP連接關閉。
注意1:TCP連接的任一方均可以首先調用close()以發起主動關閉,上圖以client主動發起關閉做說明,而不是說只能client發起主動關閉。
注意2:上面給出的TCP建立/釋放連接的過程描述中,未考慮由於各種原因引起的重傳、擁塞控制等協議細節,感興趣的同學可以查看各種TCP RFC Documents ,比如TCP RFC793。
3)TCP StateTransition Diagram
上面介紹了TCP建立、釋放連接的過程,此處對TCP狀態機的遷移過程做總體說明。將TCP RFC793中描述的TCP狀態機遷移圖摘出如下(下圖引用自這里):
TCP狀態機共含11個狀態,狀態間在各種socket apis的驅動下進行遷移,雖然此圖看起來錯綜復雜,但對於有一定TCP網絡編程經驗的同學來說,理解起來還是比較容易的。限於篇幅,本文不准備展開詳述,想了解具體遷移過程的新手同學,建議閱讀《Linux Network Programming Volume1》第2.6節。
3. TIME_WAIT狀態
經過前面的鋪墊,終於要講到與本文主題相關的內容了。 ^_^
從TCP狀態遷移圖可知,只有首先調用close()發起主動關閉的一方才會進入TIME_WAIT狀態,而且是必須進入(圖中左下角所示的3條狀態遷移線最終均要進入該狀態才能回到初始的CLOSED狀態)。
從圖中還可看到,進入TIME_WAIT狀態的TCP連接需要經過2MSL才能回到初始狀態,其中,MSL是指Max
Segment Lifetime,即數據包在網絡中的最大生存時間。每種TCP協議的實現方法均要指定一個合適的MSL值,如RFC1122給出的建議值為2分鍾,又如Berkeley體系的TCP實現通常選擇30秒作為MSL值。這意味着TIME_WAIT的典型持續時間為1-4分鍾。
TIME_WAIT狀態存在的原因主要有兩點:
1)為實現TCP這種全雙工(full-duplex)連接的可靠釋放
參考本文前面給出的TCP釋放連接4次揮手示意圖,假設發起active close的一方(圖中為client)發送的ACK(4次交互的最后一個包)在網絡中丟失,那么由於TCP的重傳機制,執行passiveclose的一方(圖中為server)需要重發其FIN,在該FIN到達client(client是active close發起方)之前,client必須維護這條連接的狀態(盡管它已調用過close),具體而言,就是這條TCP連接對應的(local_ip, local_port)資源不能被立即釋放或重新分配。直到romete peer重發的FIN達到,client也重發ACK后,該TCP連接才能恢復初始的CLOSED狀態。如果activeclose方不進入TIME_WAIT以維護其連接狀態,則當passive close方重發的FIN達到時,active close方的TCP傳輸層會以RST包響應對方,這會被對方認為有錯誤發生(而事實上,這是正常的關閉連接過程,並非異常)。
2)為使舊的數據包在網絡因過期而消失
為說明這個問題,我們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP連接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我們先關閉,接着很快以相同的四元組建立一條新連接。本文前面介紹過,TCP連接由四元組唯一標識,因此,在我們假設的情況中,TCP協議棧是無法區分前后兩條TCP連接的不同的,在它看來,這根本就是同一條連接,中間先釋放再建立的過程對其來說是“感知”不到的。這樣就可能發生這樣的情況:前一條TCP連接由local peer發送的數據到達remote peer后,會被該remot peer的TCP傳輸層當做當前TCP連接的正常數據接收並向上傳遞至應用層(而事實上,在我們假設的場景下,這些舊數據到達remote peer前,舊連接已斷開且一條由相同四元組構成的新TCP連接已建立,因此,這些舊數據是不應該被向上傳遞至應用層的),從而引起數據錯亂進而導致各種無法預知的詭異現象。作為一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種情況的發生,這正是TIME_WAIT狀態存在的第2個原因。
具體而言,local peer主動調用close后,此時的TCP連接進入TIME_WAIT狀態,處於該狀態下的TCP連接不能立即以同樣的四元組建立新連接,即發起active close的那方占用的local port在TIME_WAIT期間不能再被重新分配。由於TIME_WAIT狀態持續時間為2MSL,這樣保證了舊TCP連接雙工鏈路中的舊數據包均因過期(超過MSL)而消失,此后,就可以用相同的四元組建立一條新連接而不會發生前后兩次連接數據錯亂的情況。
另一比較深入的說法
TIME_WAIT狀態的存在有兩個理由:(1)讓4次握手關閉流程更加可靠;4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。(2)防止lost duplicate對后續新建正常鏈接的傳輸造成破壞。lost duplicate在實際的網絡中非常常見,經常是由於路由器產生故障,路徑無法收斂,導致一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,因此這個包有兩種命運,要么最后TTL變為0,在網絡中消失;要么TTL在變為0之前路由器路徑收斂,它憑借剩余的TTL跳數終於到達目的地。但非常可惜的是TCP通過超時重傳機制在早些時候發送了一個跟它一模一樣的包,並先於它達到了目的地,因此它的命運也就注定被TCP協議棧拋棄。另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,則會對我們的傳輸造成致命的錯誤。大家都知道TCP是流式的,所有包到達的順序是不一致的,依靠序列號由TCP協議棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000, len=1000, 則tcp認為這個lost duplicate合法,並存放入了receive buffer,導致傳輸出現錯誤。通過一個2MSL TIME_WAIT狀態,確保所有的lost duplicate都會消失掉,避免對新連接造成錯誤。
Q: 編寫 TCP/SOCK_STREAM 服務程序時,SO_REUSEADDR到底什么意思?
A: 這個套接字選項通知內核,如果端口忙,但TCP狀態位於 TIME_WAIT ,可以重用
端口。如果端口忙,而TCP狀態位於其他狀態,重用端口時依舊得到一個錯誤信息,
指明"地址已經使用中"。如果你的服務程序停止后想立即重啟,而新套接字依舊
使用同一端口,此時 SO_REUSEADDR 選項非常有用。必須意識到,此時任何非期
望數據到達,都可能導致服務程序反應混亂,不過這只是一種可能,事實上很不
可能。
TIME_WAIT問題
TIME_WAIT
這個是高並發服務端常見的一個問題,一般的做法是修改sysctl的參數來解決。但是,做為一個有追求的程序猿,你需要多問幾個為什么,為什么會出現TIME_WAIT?出現這個合理嗎?
我們需要先回顧下tcp的知識,請看下面的狀態轉換圖(圖片來自「The TCP/IP Guide」):
因為TCP連接是雙向的,所以在關閉連接的時候,兩個方向各自都需要關閉。先發FIN包的一方執行的是主動關閉;后發FIN包的一方執行的是被動關閉。主動關閉的一方會進入TIME_WAIT狀態,並且在此狀態停留兩倍的MSL時長。
修改sysctl的參數,只是控制TIME_WAIT的數量。你需要很明確的知道,在你的應用場景里面,你預期是服務端還是客戶端來主動關閉連接的。一般來說,都是客戶端來主動關閉的。
nginx在某些情況下,會主動關閉客戶端的請求,這個時候,返回值的connection為close。我們看兩個例子:
http 1.0協議
請求包:
GET /hello HTTP/1.0 User-Agent: curl/7.37.1 Host: 127.0.0.1 Accept: */* Accept-Encoding: deflate, gzip
應答包:
HTTP/1.1 200 OK Date: Wed, 08 Jul 2015 02:53:54 GMT Content-Type: text/plain Connection: close Server: 360 web server hello world
對於http 1.0協議,如果請求頭里面沒有包含connection,那么應答默認是返回Connection: close,也就是說nginx會主動關閉連接。
user agent
請求包:
POST /api/heartbeat.json HTTP/1.1 Content-Type: application/x-www-form-urlencoded Cache-Control: no-cache User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT) Accept-Encoding: gzip, deflate Accept: */* Connection: Keep-Alive Content-Length: 0
應答包:
HTTP/1.1 200 OK Date: Mon, 06 Jul 2015 09:35:34 GMT Content-Type: text/plain Transfer-Encoding: chunked Connection: close Server: 360 web server Content-Encoding: gzip
這個請求包是http1.1的協議,也聲明了Connection: Keep-Alive,為什么還會被nginx主動關閉呢?問題出在User-Agent,nginx認為終端的瀏覽器版本太低,不支持keep alive,所以直接close了。
在我們應用的場景下,終端不是通過瀏覽器而是后台請求的,而我們也沒法控制終端的User-Agent,那有什么方法不讓nginx主動去關閉連接呢?可以用keepalive_disable這個參數來解決。這個參數並不是字面的意思,用來關閉keepalive,而是用來定義哪些古代的瀏覽器不支持keepalive的,默認值是MSIE6。
keepalive_disable none;
修改為none,就是認為不再通過User-Agent中的瀏覽器信息,來決定是否keepalive。
1、 time_wait的作用:
TIME_WAIT狀態存在的理由: 1)可靠地實現TCP全雙工連接的終止 在進行關閉連接四次揮手協議時,最后的ACK是由主動關閉端發出的,如果這個最終的ACK丟失,服務器將重發最終的FIN, 因此客戶端必須維護狀態信息允許它重發最終的ACK。如果不維持這個狀態信息,那么客戶端將響應RST分節,服務器將此分節解釋成一個錯誤(在java中會拋出connection reset的SocketException)。 因而,要實現TCP全雙工連接的正常終止,必須處理終止序列四個分節中任何一個分節的丟失情況,主動關閉的客戶端必須維持狀態信息進入TIME_WAIT狀態。 2)允許老的重復分節在網絡中消逝 TCP分節可能由於路由器異常而“迷途”,在迷途期間,TCP發送端可能因確認超時而重發這個分節,迷途的分節在路由器修復后也會被送到最終目的地,這個原來的迷途分節就稱為lost duplicate。 在關閉一個TCP連接后,馬上又重新建立起一個相同的IP地址和端口之間的TCP連接,后一個連接被稱為前一個連接的化身(incarnation),那么有可能出現這種情況,前一個連接的迷途重復分組在前一個連接終止后出現,從而被誤解成從屬於新的化身。 為了避免這個情況,TCP不允許處於TIME_WAIT狀態的連接啟動一個新的化身,因為TIME_WAIT狀態持續2MSL,就可以保證當成功建立一個TCP連接的時候,來自連接先前化身的重復分組已經在網絡中消逝。
2、大量TIME_WAIT造成的影響:
在高並發短連接的TCP服務器上,當服務器處理完請求后立刻主動正常關閉連接。這個場景下會出現大量socket處於TIME_WAIT狀態。如果客戶端的並發量持續很高,此時部分客戶端就會顯示連接不上。
我來解釋下這個場景。主動正常關閉TCP連接,都會出現TIMEWAIT。
為什么我們要關注這個高並發短連接呢?有兩個方面需要注意:
1. 高並發可以讓服務器在短時間范圍內同時占用大量端口,而端口有個0~65535的范圍,並不是很多,刨除系統和其他服務要用的,剩下的就更少了。
2. 在這個場景中,短連接表示“業務處理+傳輸數據的時間 遠遠小於 TIMEWAIT超時的時間”的連接。
這里有個相對長短的概念,比如取一個web頁面,1秒鍾的http短連接處理完業務,在關閉連接之后,這個業務用過的端口會停留在TIMEWAIT狀態幾分鍾,而這幾分鍾,其他HTTP請求來臨的時候是無法占用此端口的(占着茅坑不拉翔)。單用這個業務計算服務器的利用率會發現,服務器干正經事的時間和端口(資源)被掛着無法被使用的時間的比例是 1:幾百,服務器資源嚴重浪費。(說個題外話,從這個意義出發來考慮服務器性能調優的話,長連接業務的服務就不需要考慮TIMEWAIT狀態。同時,假如你對服務器業務場景非常熟悉,你會發現,在實際業務場景中,一般長連接對應的業務的並發量並不會很高。
綜合這兩個方面,持續的到達一定量的高並發短連接,會使服務器因端口資源不足而拒絕為一部分客戶服務。同時,這些端口都是服務器臨時分配,無法用SO_REUSEADDR選項解決這個問題。
關於time_wait的反思:
存在即是合理的,既然TCP協議能盛行四十多年,就證明他的設計合理性。所以我們盡可能的使用其原本功能。 依靠TIME_WAIT狀態來保證我的服務器程序健壯,服務功能正常。 那是不是就不要性能了呢?並不是。如果服務器上跑的短連接業務量到了我真的必須處理這個TIMEWAIT狀態過多的問題的時候,我的原則是盡量處理,而不是跟TIMEWAIT干上,非先除之而后快。 如果盡量處理了,還是解決不了問題,仍然拒絕服務部分請求,那我會采取負載均衡來抗這些高並發的短請求。持續十萬並發的短連接請求,兩台機器,每台5萬個,應該夠用了吧。一般的業務量以及國內大部分網站其實並不需要關注這個問題,一句話,達不到時才需要關注這個問題的訪問量。
小知識點:
TCP協議發表:1974年12月,卡恩、瑟夫的第一份TCP協議詳細說明正式發表。當時美國國防部與三個科學家小組簽定了完成TCP/IP的協議,結果由瑟夫領銜的小組捷足先登,首先制定出了通過詳細定義的TCP/IP協議標准。當時作了一個試驗,將信息包通過點對點的衛星網絡,再通過陸地電纜
,再通過衛星網絡,再由地面傳輸,貫串歐洲和美國,經過各種電腦系統,全程9.4萬公里竟然沒有丟失一個數據位,遠距離的可靠數據傳輸證明了TCP/IP協議的成功。
3、案列分析:
首先,根據一個查詢TCP連接數,來說明這個問題。
netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}' LAST_ACK 14 SYN_RECV 348 ESTABLISHED 70 FIN_WAIT1 229 FIN_WAIT2 30 CLOSING 33 TIME_WAIT 18122
狀態描述:

命令解釋:

如何盡量處理TIMEWAIT過多?
編輯內核文件/etc/sysctl.conf,加入以下內容:
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉; net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。 net.ipv4.tcp_fin_timeout 修改系默認的 TIMEOUT 時間
然后執行 /sbin/sysctl -p 讓參數生效.
/etc/sysctl.conf是一個允許改變正在運行中的Linux系統的接口,它包含一些TCP/IP堆棧和虛擬內存系統的高級選項,修改內核參數永久生效。
簡單來說,就是打開系統的TIMEWAIT重用和快速回收。
如果以上配置調優后性能還不理想,可繼續修改一下配置:
vi /etc/sysctl.conf net.ipv4.tcp_keepalive_time = 1200 #表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改為20分鍾。 net.ipv4.ip_local_port_range = 1024 65000 #表示用於向外連接的端口范圍。缺省情況下很小:32768到61000,改為1024到65000。 net.ipv4.tcp_max_syn_backlog = 8192 #表示SYN隊列的長度,默認為1024,加大隊列長度為8192,可以容納更多等待連接的網絡連接數。 net.ipv4.tcp_max_tw_buckets = 5000 #表示系統同時保持TIME_WAIT套接字的最大數量,如果超過這個數字,TIME_WAIT套接字將立刻被清除並打印警告信息。 默認為180000,改為5000。對於Apache、Nginx等服務器,上幾行的參數可以很好地減少TIME_WAIT套接字數量,但是對於 Squid,效果卻不大。此項參數可以控制TIME_WAIT套接字的最大數量,避免Squid服務器被大量的TIME_WAIT套接字拖死。