1. 程式人生 > 其它 >記一次TIME_WAIT網路故障

記一次TIME_WAIT網路故障

最近發現一個PHP指令碼時常出現連不上伺服器的現象,除錯了一下,發現是TIME_WAIT狀態過多造成的,本文簡要介紹一下解決問題的過程。

遇到這類問題,我習慣於先用strace命令跟蹤了一下看看:

shell> strace php /path/to/file
EADDRNOTAVAIL (Cannot assign requested address)

從字面結果看似乎是網路資源相關問題。這裡順便介紹一點小技巧:在除錯的時候一般是從後往前看strace命令的結果,這樣更容易找到有價值的資訊。

檢視一下當前的網路連線情況,結果發現TIME_WAIT數非常大:

shell> netstat -ant | awk '
    {++s[$NF]} END {for(k in s) print k,s[k]}
'

補充一下,同netstat相比,ss要快很多:

shell> ss -ant | awk '
    {++s[$1]} END {for(k in s) print k,s[k]}
'

重複了幾次測試,結果每次出問題的時候,TIME_WAIT都等於28233,這真是一個魔法數字!實際原因很簡單,它取決於一個核心引數net.ipv4.ip_local_port_range:

shell> sysctl -a | grep port
net.ipv4.ip_local_port_range = 32768 61000

因為埠範圍是一個閉區間,所以實際可用的埠數量是:

shell> echo $((61000-32768+1))
28233

問題分析到這裡基本就清晰了,解決方向也明確了,內容所限,這裡就不說如何優化程式程式碼了,只是從系統方面來闡述如何解決問題,無非就是以下兩個方面:

首先是增加本地可用埠數量。這點可以用以下命令來實現:

shell> sysctl net.ipv4.ip_local_port_range="10240 61000"

其次是減少TIME_WAIT連線狀態。網路上已經有不少相關的介紹,大多是建議:

shell> sysctl net.ipv4.tcp_tw_reuse=1
shell> sysctl net.ipv4.tcp_tw_recycle=1

注:通過sysctl命令修改核心引數,重啟後會還原,要想持久化可以參考前面的方法。

這兩個選項在降低TIME_WAIT數量方面可以說是立竿見影,不過如果你覺得問題已經完美搞定那就錯了,實際上這樣可能會引入一個更復雜的網路故障。

關於核心引數的詳細介紹,可以參考官方文件。我們這裡簡要說明一下tcp_tw_recycle引數。它用來快速回收TIME_WAIT連線,不過如果在NAT環境下會引發問題。

RFC1323中有如下一段描述:

An additional mechanism could be added to the TCP, a per-hostcache of the last timestamp received from any connection.This value could then be used in thePAWSmechanism to rejectold duplicate segments from earlier incarnations of theconnection, if the timestamp clock can be guaranteed to haveticked at least once since the old connection was open. Thiswould require that the TIME-WAIT delay plus the RTT togethermust be at least one tick of the sender’s timestamp clock.Such an extension is not part of the proposal of this RFC.

大概意思是說TCP有一種行為,可以快取每個主機最新的時間戳,後續請求中如果時間戳小於快取的時間戳,即視為無效,相應的資料包會被丟棄。

Linux是否啟用這種行為取決於tcp_timestamps和tcp_tw_recycle,因為tcp_timestamps預設就是開啟的,所以當tcp_tw_recycle被開啟後,實際上這種行為就被激活了,當客戶端或服務端以NAT方式構建的時候就可能出現問題,下面以客戶端NAT為例來說明:

當多個客戶端通過NAT方式聯網並與服務端互動時,服務端看到的是同一個IP,也就是說對服務端而言這些客戶端實際上等同於一個,可惜由於這些客戶端的時間戳可能存在差異,於是乎從服務端的視角看,便可能出現時間戳錯亂的現象,進而直接導致時間戳小的資料包被丟棄。如果發生了此類問題,具體的表現通常是是客戶端明明發送的SYN,但服務端就是不響應ACK,我們可以通過下面命令來確認資料包不斷被丟棄的現象:

shell> netstat -s | grep timestamp
... packets rejects in established connections because of timestamp

安全起見,通常要禁止tcp_tw_recycle。說到這裡,大家可能會想到另一種解決方案:把tcp_timestamps設定為0,tcp_tw_recycle設定為1,這樣不就可以魚與熊掌兼得了麼?可惜一旦關閉了tcp_timestamps,那麼即便打開了tcp_tw_recycle,也沒有效果。

好在我們還有另一個核心引數tcp_max_tw_buckets(一般預設是180000)可用:

shell> sysctl net.ipv4.tcp_max_tw_buckets=100000

通過設定它,系統會將多餘的TIME_WAIT刪除掉,此時系統日誌裡可能會顯示:「TCP: time wait bucket table overflow」,不過除非不得已,否則不要輕易使用。

總體來說,這次網路故障本身並沒什麼高深之處,本不想羅羅嗦嗦寫這麼多,不過拔出蘿蔔帶出泥,在過程中牽扯的方方面面還是值得大家品味的,於是便有了這篇文字。

此條目由老王發表在Technical分類目錄,並貼了標籤。將固定連結加入收藏夾。

《記一次TIME_WAIT網路故障》上有23個想法

    1. hi,樓主
      我認為文章上有幾處錯誤:
      1.文章提到”對於後端伺服器來說,請求的源地址就是LVS的地址,加上埠會複用,所以從後端伺服器的角度看,原本不同客戶端的請求經過LVS的轉發,就可能會被認為是同一個連線” , 這裡對LVS的原理理解有誤。
      在 LVS下, 對於後端RealServer來說,他看到的請求報文IP地址都是公網的客戶端IP,而不是Load Balancer的。比如LVS-TUN模式就是將客戶端的ip包重新用IPIP協議打包後,轉發到RealServer,RealServer解包後處理後,就可以直接將回復返回給公網的客戶端(此時RealServer和LoadBalancer會共享同一個公網IP,因此可以直接回包),實際上就LVS相當於將客戶端與Load Balancer之間的TCP連結遷移到了LB選擇的RealServer 。具體可以參考:http://www.linuxvirtualserver.org/zh/lvs3.html
      如果是使用nginx這種應用層負載均衡的話,後端的處理server看到的報文的請求IP就是Nginx本身的IP

      2. 時間戳錯亂的原因搞錯了。
      限於實際的網路情況,很多使用者的客戶端沒有公網IP,只能依賴於NAT分享同一個公網IP, 這樣由於同一NAT下的不同機器的時間戳不一定保證同步,所以就導致同一個NAT過來的資料包的時間戳不能保證單調遞增。這樣就打破了RFC1323中PAWS方法依賴於對端時間戳單調遞增的要求。所以就表現為時間戳錯亂,導致丟棄時間戳較小的資料包,表現為packets rejects in established connections because of timestamp的資料不斷增加。所以,在LVS中的機器需要關閉tcp_tw_recycle 。