TCP/IP TIME_WAIT狀態原理和服務端過多原因分析
TIME_WAIT狀態原理
----------------------------
通訊雙方建立TCP連線後,主動關閉連線的一方就會進入TIME_WAIT狀態。
客戶端主動關閉連線時,會發送最後一個ack後,然後會進入TIME_WAIT狀態,再停留2個MSL時間(後有MSL的解釋),進入CLOSED狀態。
下圖是以客戶端主動關閉連線為例,說明這一過程的。
TIME_WAIT狀態存在的理由
----------------------------
TCP/IP協議就是這樣設計的,是不可避免的。主要有兩個原因:
1)可靠地實現TCP全雙工連線的終止
TCP協議在關閉連線的四次握手過程中,最終的ACK是由主動關閉連線的一端(後面統稱A端)發出的,如果這個ACK丟失,對方(後面統稱B端)將重發出最終的FIN,因此A端必須維護狀態資訊(TIME_WAIT)允許它重發最終的ACK。如果A端不維持TIME_WAIT狀態,而是處於CLOSED 狀態,那麼A端將響應RST分節,B端收到後將此分節解釋成一個錯誤(在java中會丟擲connection reset的SocketException)。
因而,要實現TCP全雙工連線的正常終止,必須處理終止過程中四個分節任何一個分節的丟失情況,主動關閉連線的A端必須維持TIME_WAIT狀態 。
2)允許老的重複分節在網路中消逝
TCP分節可能由於路由器異常而“迷途”,在迷途期間,TCP傳送端可能因確認超時而重發這個分節,迷途的分節在路由器修復後也會被送到最終目的地,這個遲到的迷途分節到達時可能會引起問題。在關閉“前一個連線”之後,馬上又重新建立起一個相同的IP和埠之間的“新連線”,“前一個連線”的迷途重複分組在“前一個連線”終止後到達,而被“新連線”收到了。為了避免這個情況,TCP協議不允許處於TIME_WAIT狀態的連線啟動一個新的可用連線,因為TIME_WAIT狀態持續2MSL,就可以保證當成功建立一個新TCP連線的時候,來自舊連線重複分組已經在網路中消逝。
MSL時間
----------------------------
MSL就是maximum segment lifetime(最大分節生命期),這是一個IP資料包能在網際網路上生存的最長時間,超過這個時間IP資料包將在網路中消失 。MSL在RFC 1122上建議是2分鐘,而源自berkeley的TCP實現傳統上使用30秒。
TIME_WAIT狀態維持時間
----------------------------
TIME_WAIT狀態維持時間是兩個MSL時間長度,也就是在1-4分鐘。Windows作業系統就是4分鐘。
用於統計當前各種狀態的連線的數量的命令
---------------------------
#netstat -n | 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
對上述結果的解釋:
CLOSED:無連線是活動的或正在進行
LISTEN:伺服器在等待進入呼叫
SYN_RECV:一個連線請求已經到達,等待確認
SYN_SENT:應用已經開始,開啟一個連線
ESTABLISHED:正常資料傳輸狀態
FIN_WAIT1:應用說它已經完成
FIN_WAIT2:另一邊已同意釋放
ITMED_WAIT:等待所有分組死掉
CLOSING:兩邊同時嘗試關閉
TIME_WAIT:另一邊已初始化一個釋放
LAST_ACK:等待所有分組死掉
進一步論述這個問題:
===============================
--------------客戶端主動關閉連線-----------------------
注意一個問題,進入TIME_WAIT狀態的一般情況下是客戶端。
大多數伺服器端一般執行被動關閉,伺服器不會進入TIME_WAIT狀態。
當在伺服器端關閉某個服務再重新啟動時,伺服器是會進入TIME_WAIT狀態的。
舉例:
1.客戶端連線伺服器的80服務,這時客戶端會啟用一個本地的埠訪問伺服器的80,訪問完成後關閉此連線,立刻再次訪問伺服器的
80,這時客戶端會啟用另一個本地的埠,而不是剛才使用的那個本地埠。原因就是剛才的那個連線還處於TIME_WAIT狀態。
2.客戶端連線伺服器的80服務,這時伺服器關閉80埠,立即再次重啟80埠的服務,這時可能不會成功啟動,原因也是伺服器的連
接還處於TIME_WAIT狀態。
服務端提供服務時,一般監聽一個埠就夠了。例如Apach監聽80埠。
客戶端則是使用一個本地的空閒埠(大於1024),與服務端的Apache的80埠建立連線。
當通訊時使用短連線,並由客戶端主動關閉連線時,主動關閉連線的客戶端會產生TIME_WAIT狀態的連線,一個TIME_WAIT狀態的連線就佔用了一個本地埠。這樣在TIME_WAIT狀態結束之前,本地最多就能承受6萬個TIME_WAIT狀態的連線,就無埠可用了。
客戶端與服務端進行短連線的TCP通訊,如果在同一臺機器上進行壓力測試模擬上萬的客戶請求,並且迴圈與服務端進行短連線通訊,那麼這臺機器將產生4000個左右的TIME_WAIT socket,後續的短連線就會產生address already in use : connect的異常。
關閉的時候使用RST的方式,不進入 TIME_WAIT狀態,是否可行?
--------------服務端主動關閉連線------------------------------
服務端提供在服務時,一般監聽一個埠就夠了。例如Apach監聽80埠。
客戶端則是使用一個本地的空閒埠(大於1024),與服務端的Apache的80埠建立連線。
當通訊時使用短連線,並由服務端主動關閉連線時,主動關閉連線的服務端會產生TIME_WAIT狀態的連線。
由於都連線到服務端80埠,服務端的TIME_WAIT狀態的連線會有很多個。
假如server一秒鐘處理1000個請求,那麼就會積壓240秒*1000=24萬個TIME_WAIT的記錄,服務有能力維護這24萬個記錄。
大多數伺服器端一般執行被動關閉,伺服器不會進入TIME_WAIT狀態。
服務端為了解決這個TIME_WAIT問題,可選擇的方式有三種:
Ø 保證由客戶端主動發起關閉(即做為B端)
Ø 關閉的時候使用RST的方式
Ø 對處於TIME_WAIT狀態的TCP允許重用
一般Apache的配置是:
Timeout 30
KeepAlive On #表示伺服器端不會主動關閉連結
MaxKeepAliveRequests 100
KeepAliveTimeout 180
表示:Apache不會主動關閉連結,
兩種情況下Apache會主動關閉連線:
1、Apache收到了http協議頭中有客戶端要求Apache關閉連線資訊,如setRequestHeader("Connection", "close");
2、連線保持時間達到了180秒的超時時間,將關閉。
如果配置如下:
KeepAlive Off #表示伺服器端會響應完資料後主動關閉連結
--------------有代理時------------------------------
nginx代理使用了短連結的方式和後端互動,如果使用了nginx代理,那麼系統TIME_WAIT的數量會變得比較多,這是由於nginx代理使用了短連結的方式和後端互動的原因,使得nginx和後端的ESTABLISHED變得很少而TIME_WAIT很多。這不但發生在安裝nginx的代理伺服器上,而且也會使後端的app伺服器上有大量的TIME_WAIT。查閱TIME_WAIT資料,發現這個狀態很多也沒什麼大問題,但可能因為它佔用了系統過多的埠,導致後續的請求無法獲取埠而造成障礙。
對於大型的服務,一臺server搞不定,需要一個LB(Load Balancer)把流量分配到若干後端伺服器上,如果這個LB是以NAT方式工作的話,可能會帶來問題。假如所有從LB到後端Server的IP包的source address都是一樣的(LB的對內地址),那麼LB到後端Server的TCP連線會受限制,因為頻繁的TCP連線建立和關閉,會在server上留下TIME_WAIT狀態,而且這些狀態對應的remote address都是LB的,LB的source port撐死也就60000多個(2^16=65536,1~1023是保留埠,還有一些其他埠預設也不會用),每個LB上的埠一旦進入Server的TIME_WAIT黑名單,就有240秒不能再用來建立和Server的連線,這樣LB和Server最多也就能支援300個左右的連線。如果沒有LB,不會有這個問題,因為這樣server看到的remote address是internet上廣闊無垠的集合,對每個address,60000多個port實在是夠用了。
一開始我覺得用上LB會很大程度上限制TCP的連線數,但是實驗表明沒這回事,LB後面的一臺Windows Server 2003每秒處理請求數照樣達到了600個,難道TIME_WAIT狀態沒起作用?用Net Monitor和netstat觀察後發現,Server和LB的XXXX埠之間的連線進入TIME_WAIT狀態後,再來一個LB的XXXX埠的SYN包,Server照樣接收處理了,而是想像的那樣被drop掉了。翻書,從書堆裡面找出覆滿塵土的大學時代買的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中間提到一句,對於BSD-derived實現,只要SYN的sequence number比上一次關閉時的最大sequence number還要大,那麼TIME_WAIT狀態一樣接受這個SYN,難不成Windows也算BSD-derived?有了這點線索和關鍵字(BSD),找到這個post,在NT4.0的時候,還是和BSD-derived不一樣的,不過Windows Server 2003已經是NT5.2了,也許有點差別了。
做個試驗,用Socket API編一個Client端,每次都Bind到本地一個埠比如2345,重複的建立TCP連線往一個Server傳送Keep-Alive=false的HTTP請求,Windows的實現讓sequence number不斷的增長,所以雖然Server對於Client的2345埠連線保持TIME_WAIT狀態,但是總是能夠接受新的請求,不會拒絕。那如果SYN的Sequence Number變小會怎麼樣呢?同樣用Socket API,不過這次用Raw IP,傳送一個小sequence number的SYN包過去,Net Monitor裡面看到,這個SYN被Server接收後如泥牛如海,一點反應沒有,被drop掉了。
按照書上的說法,BSD-derived和Windows Server 2003的做法有安全隱患,不過至少這樣至少不會出現TIME_WAIT阻止TCP請求的問題,當然,客戶端要配合,保證不同TCP連線的sequence number要上漲不要下降。
-------------------------------------------
Q: 我正在寫一個unix server程式,不是daemon,經常需要在命令列上重啟它,絕大
多數時候工作正常,但是某些時候會報告"bind: address in use",於是重啟失
敗。
A: Andrew Gierth
server程式總是應該在呼叫bind()之前設定SO_REUSEADDR套接字選項。至於
TIME_WAIT狀態,你無法避免,那是TCP協議的一部分。
Q: 編寫 TCP/SOCK_STREAM 服務程式時,SO_REUSEADDR到底什麼意思?
A: 這個套接字選項通知核心,如果埠忙,但TCP狀態位於 TIME_WAIT ,可以重用
埠。如果埠忙,而TCP狀態位於其他狀態,重用埠時依舊得到一個錯誤資訊,
指明"地址已經使用中"。如果你的服務程式停止後想立即重啟,而新套接字依舊
使用同一埠,此時 SO_REUSEADDR 選項非常有用。必須意識到,此時任何非期
望資料到達,都可能導致服務程式反應混亂,不過這只是一種可能,事實上很不
可能。
一個套接字由相關五元組構成,協議、本地地址、本地埠、遠端地址、遠端端
口。SO_REUSEADDR 僅僅表示可以重用本地本地地址、本地埠,整個相關五元組
還是唯一確定的。所以,重啟後的服務程式有可能收到非期望資料。必須慎重使
用 SO_REUSEADDR 選項。