請說說你對TCP鏈接中time_wait狀態的理解javascript
解答:
先上TCP的狀態變遷圖html
這幅圖來自《TCP IP詳解卷1:協議 原書第2版中文》13.5 TCP狀態轉換圖前端
這幅圖來自《UNIX網絡編程,卷1:套接字聯網API》2.6.4 TCP狀態轉換圖java
1. time_wait狀態如何產生?
由上面的變遷圖,首先調用close()發起主動關閉的一方,在發送最後一個ACK以後會進入time_wait的狀態,也就說該發送方會保持2MSL時間以後纔會回到初始狀態。MSL值得是數據包在網絡中的最大生存時間。產生這種結果使得這個TCP鏈接在2MSL鏈接等待期間,定義這個鏈接的四元組(客戶端IP地址和端口,服務端IP地址和端口號)不能被使用。mysql
2.time_wait狀態產生的緣由linux
1)爲實現TCP全雙工鏈接的可靠釋放nginx
由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)爲使舊的數據包在網絡因過時而消失web
爲說明這個問題,咱們先假設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狀態。
TCP/IP詳解--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)客戶端發送一個帶SYN標誌的TCP報文到服務器。這是三次握手過程當中的報文1.
(2) 服務器端迴應客戶端的,這是三次握手中的第2個報文,這個報文同時帶ACK標誌和SYN標誌。所以它表示對剛纔客戶端SYN報文的迴應;同時又標誌SYN給客戶端,詢問客戶端是否準備好進行數據通信。
(3) 客戶必須再次迴應服務段一個ACK報文,這是報文段3.
二、鏈接終止協議(四次握手)
因爲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 {
至此,所有分析結束。通過查看 getMapNil 返回了nil,可是下面的判斷條件沒有進行回滾。
if nilMap == nil { o.Rollback()
總結
整個分析過程仍是廢了很多時間。最主要的是主觀意識太強,以爲運行了一年沒有出問題的爲何會忽然出問題?所以一開始是質疑 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的狀況的。
如今來講如何來解決這個問題。
解決思路很簡單,就是讓服務器可以快速回收和重用那些TIME_WAIT的資源。
下面來看一下咱們網管對/etc/sysctl.conf文件的修改:
- #對於一個新建鏈接,內核要發送多少個 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
修改完以後執行/sbin/sysctl -p讓參數生效。
這裏頭主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
這幾個參數。
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的開啓都是爲了回收處於TIME_WAIT狀態的資源。
net.ipv4.tcp_fin_timeout這個時間能夠減小在異常狀況下服務器從FIN-WAIT-2轉到TIME_WAIT的時間。
net.ipv4.tcp_keepalive_*一系列參數,是用來設置服務器檢測鏈接存活的相關配置。
2.服務器保持了大量CLOSE_WAIT狀態
休息一下,喘口氣,一開始只是打算說說TIME_WAIT和CLOSE_WAIT的區別,沒想到越挖越深,這也是寫博客總結的好處,總能夠有意外的收穫。
TIME_WAIT狀態能夠經過優化服務器參數獲得解決,由於發生TIME_WAIT的狀況是服務器本身可控的,要麼就是對方鏈接的異常,要麼就是本身沒有迅速回收資源,總之不是因爲本身程序錯誤致使的。
但 是CLOSE_WAIT就不同了,從上面的圖能夠看出來,若是一直保持在CLOSE_WAIT狀態,那麼只有一種狀況,就是在對方關閉鏈接以後服務器程 序本身沒有進一步發出ack信號。換句話說,就是在對方鏈接關閉以後,程序裏沒有檢測到,或者程序壓根就忘記了這個時候須要關閉鏈接,因而這個資源就一直 被程序佔着。我的以爲這種狀況,經過服務器內核參數也沒辦法解決,服務器對於程序搶佔的資源沒有主動回收的權利,除非終止程序運行。
在那邊日誌裏頭我舉了個場景,來講明CLOSE_WAIT和TIME_WAIT的區別,這裏從新描述一下:
服 務器A是一臺爬蟲服務器,它使用簡單的HttpClient去請求資源服務器B上面的apache獲取文件資源,正常狀況下,若是請求成功,那麼在抓取完 資源後,服務器A會主動發出關閉鏈接的請求,這個時候就是主動關閉鏈接,服務器A的鏈接狀態咱們能夠看到是TIME_WAIT。若是一旦發生異常呢?假設 請求的資源服務器B上並不存在,那麼這個時候就會由服務器B發出關閉鏈接的請求,服務器A就是被動的關閉了鏈接,若是服務器A被動關閉鏈接以後程序員忘了 讓HttpClient釋放鏈接,那就會形成CLOSE_WAIT的狀態了。
因此若是將大量CLOSE_WAIT的解決辦法總結爲一句話那就是:查代碼。由於問題出在服務器程序裏頭啊。
統計在一臺前端機上高峯時間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/
修改以後,再用
netstat -ae|grep mysql
tcp 0 0 aaaa:50408 192.168.12.13:mysql ESTABLISHED nobody 3224651
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進程的佔用率很快就降下來的,各網站訪問正常!!
以上只是暫時的解決方法,最後仔細巡查發現是前天新上線的一個系統,程序代碼中沒有使用mysql.colse(),才致使大量的mysql TIME_WAIT
若是你的服務器是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設置
描述:肯定 TCP/IP 可釋放已關閉鏈接並重用其資源前,必須通過的時間。
關閉和釋放之間的此時間間隔通稱 TIME_WAIT 狀態或兩倍最大段生命週期(2MSL)狀態。
此時間期間,從新打開到客戶機和服務器的鏈接的成本少於創建新鏈接。
減小此條目的值容許 TCP/IP 更快地釋放已關閉的鏈接,爲新鏈接提供更多資源。若是運行的應用程序須要快速釋放和建立新鏈接,並且因爲 TIME_WAIT 中存在不少鏈接,致使低吞吐量,則調整此參數。
如何查看或設置: 使用 regedit 命令訪問 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/ Services/TCPIP/Parameters 註冊表子鍵並建立名爲 TcpTimedWaitDelay 的新 REG_DWORD 值。
將此值設置爲十進制 30,其爲十六進制 0x0000001e。
該值將等待時間設置爲 30 秒。
中止並從新啓動系統。 缺省值:0xF0,它將等待時間設置爲 240 秒(4 分鐘)。
建議值:最小值爲 0x1E,它將等待時間設置爲 30 秒。
MaxUserPort 描述:肯定在應用程序從系統請求可用用戶端口時,TCP/IP 可指定的最高端口號。
如何查看或設置: 使用 regedit 命令訪問 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/ Services/TCPIP/Parameters 註冊表子鍵並建立名爲 MaxUserPort 的新 REG_DWORD 值。
中止並從新啓動系統。
缺省值:無 建議值:至少十進制 32768。
注:當在 Windows NT 或 Windows 2000 操做系統上調整 WebSphere Application Server 時,同時使用這兩個參數。
但願本站的知識能給您的工做、學習和生活帶來方便和樂趣!
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
這個是高併發服務端常見的一個問題,通常的作法是修改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: *
應答包:
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: *
應答包:
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。
一、 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鏈接的時候,來自鏈接先前化身的重複分組已經在網絡中消逝。
二、大量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協議的成功。
三、案列分析:
首先,根據一個查詢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
狀態描述:
View Code
命令解釋:
View Code
如何儘可能處理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套接字拖死。