作者:牧原
TIME_WAIT是TCP連接配接關閉過程中的一個狀态,具體是這麼形成的:
1 主動關閉端A:發FIN,進入FIN-WAIT-1狀态,并等待......
2 被動關閉端P:收到FIN後必須立即發ACK,進入CLOSE_WAIT狀态,并等待......
3 主動關閉端A:收到ACK後進入FIN-WAIT-2狀态,并等待......
4 被動關閉端P:發FIN,進入LAST_ACK狀态,并等待......
5 主動關閉端A:收到FIN後必須立即發ACK,進入TIME_WAIT狀态,等待2MSL後結束Socket
6 被動關閉端P:收到ACK後結束Socket
是以,TIME_WAIT狀态是出現在主動發起連接配接關閉的一點,和是誰發起的連接配接無關,可以是client端,也可以是server端。
而從TIME_WAIT狀态到CLOSED狀态,有一個逾時設定,這個逾時設定是 2*MSL(RFC793定義了MSL為2分鐘,Linux設定成了30s)
為什麼需要TIME_WAIT?
主要有兩個原因:
1)為了確定兩端能完全關閉連接配接。
假設A伺服器是主動關閉連接配接方,B伺服器是被動方。如果沒有TIME_WAIT狀态,A伺服器發出最後一個ACK就進入關閉狀态,如果這個ACK對端沒有收到,對端就不能完成關閉。對端沒有收到ACK,會重發FIN,此時連接配接關閉,這個FIN也得不到ACK,而有TIME_WAIT,則會重發這個ACK,確定對端能正常關閉連接配接。
2)為了確定後續的連接配接不會收到“髒資料”
剛才提到主動端進入TIME_WAIT後,等待2MSL後CLOSE,這裡的MSL是指(maximum segment lifetime,我們核心一般是30s,2MSL就是1分鐘),網絡上資料包最大的生命周期。這是為了使網絡上由于重傳出現的old duplicate segment都消失後,才能建立參數(四元組,源IP/PORT,目标IP/PORT)相同的連接配接,如果等待時間不夠長,又建立好了一樣的連接配接,再收到old duplicate segment,資料就錯亂了。
TIME_WAIT 會導緻什麼問題
1) 建立連接配接失敗
TIME_WAIT到CLOSED,需要2MSL=60s的時間。這個時間非常長。每個連接配接在業務結束之後,需要60s的時間才能完全釋放。如果業務上采用的是短連接配接的方式,會導緻非常多的TIME_WAIT狀态的連接配接,會占用一些資源,主要是本地端口資源。
一台伺服器的本地可用端口是有限的,也就幾萬個端口,由這個參數控制:
sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 61000
當伺服器存在非常多的TIME_WAIT連接配接,将本地端口都占用了,就不能主動發起新的連接配接去連其他伺服器了。
這裡需要注意,是主動發起連接配接,又是主動發起關閉的一方才會遇到這個問題。
如果是server端主動關閉client端建立的連接配接産生了大量的TIME_WAIT連接配接,這是不會出現這個問題的。除非是其中涉及到的某個用戶端的TIME_WAIT連接配接都有好幾萬個了。
2)TIME_WAIT條目超出限制
這個限制,是由一個核心參數控制的:
sysctl net.ipv4.tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 5000
超出了這個限制會報一條INFO級别的核心日志,然後繼續關閉掉連接配接。并沒有什麼特别大的影響,隻是增加了剛才提到的收到髒資料的風險而已。
另外的風險就是,關閉掉TIME_WAIT連接配接後,剛剛發出的ACK如果對端沒有收到,重發FIN包出來時,不能正确回複ACK,隻是回複一個RST包,導緻對端程式報錯,說connection reset。
是以net.ipv4.tcp_max_tw_buckets這個參數是建議不要改小的,改小會帶來風險,沒有什麼收益,隻是表面上通過netstat看到的TIME_WAIT少了些而已,有啥用呢?
并且,建議是當遇到條目不夠,增加這個值,僅僅是浪費一點點記憶體而已。
如何解決time_wait?
1)最佳方案是應用改造長連接配接,但是一般不太适用
2)修改系統回收參數
設定以下參數
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1
設定該參數會帶來什麼問題?
如果這兩個參數同時開啟,會校驗源ip過來的包攜帶的timestamp是否遞增,如果不是遞增的話,則會導緻三次握手建聯不成功,具體表現為抓包的時候看到syn發出,server端不響應syn ack
通俗一些來講就是,一個區域網路有多個用戶端通路您,如果有用戶端的時間比别的用戶端時間慢,就會建聯不成功
治标不治本的方式:
放大端口範圍
放大time_wait的buckets
net.ipv4.tcp_max_tw_buckets = 180000
----------update 2020-02-22 關于tw_bucket之争------------
關于net.ipv4.tcp_max_tw_buckets到底要不要放大,目前雲上ecs多數是設定了5000,我個人淺見覺得小了
簡單來說 net.ipv4.tcp_max_tw_buckets的作用 是為了“優雅”的關閉連接配接
1,完整的關閉連接配接
2,避免有資料包重複
如果tw滿了會怎樣
TCP: time wait bucket table overflow
新核心
tw_bucket滿了的話,會影響established狀态的連接配接在finack的時候,直接進入closed狀态
老核心
tw_bucket滿了的話,會将tw_bucket裡面的time_wait按照一定的規則(如LRU),将一批time_Wait直接進入closed狀态 ,然後established狀态發送finack後進入time_wait
tw的開銷是什麼?
1,特别少量的記憶體
2,占用本地端口
tw放大的好與壞?
1,放大的話需要更多的記憶體開銷,但是幾乎可以忽略不計
2,占用更多的本地端口,需要适當的放大學地端口範圍,端口範圍經過簡單的測試,建議設定為tw的1.5倍
net.ipv4.ip_local_port_range
3,netstat 大量的掃描socket的時候(ss不會掃描,但是ss在slab記憶體特别高的時候,也有可能會引起抖動),極端情況下可能會引起性能抖動
4,tw放大,local_port_range放大,還可以配置複用以及快速回收等參數
5,使用快速回收可能會導緻snat時間戳遞增校驗問題,不遞增的話syn不響應
特殊場景的時候(本機會發起大量短連結的時候)
1, nginx結合php-fpm需要本地起端口,
2,nginx反代如(java ,容器等)
如下圖所示,
tcp_tw_reuse參數需要結合net.ipv4.tcp_timestamps = 1 一起來用
即 伺服器即做用戶端,也做server端的時候
tcp_tw_reuse參數用來設定是否可以在新的連接配接中重用TIME_WAIT狀态的套接字。注意,重用的是TIME_WAIT套接字占用的端口号,而不是TIME_WAIT套接字的記憶體等。這個參數對用戶端有意義,在主動發起連接配接的時候會在調用的inet_hash_connect()中會檢查是否可以重用TIME_WAIT狀态的套接字。如果你在伺服器段設定這個參數的話,則沒有什麼作用,因為伺服器端ESTABLISHED狀态的套接字和監聽套接字的本地IP、端口号是相同的,沒有重用的概念。但并不是說伺服器端就沒有TIME_WAIT狀态套接字。
是以 該類場景最終建議是
net.ipv4.tcp_tw_recycle = 0 關掉快速回收
net.ipv4.tcp_tw_reuse = 1 開啟tw狀态的端口複用(用戶端角色)
net.ipv4.tcp_timestamps = 1 複用需要timestamp校驗為1
net.ipv4.tcp_max_tw_buckets = 30000 放大bucket
net.ipv4.ip_local_port_range = 15000 65000 放大學地端口範圍
記憶體開銷測試
# ss -s
Total: 15254 (kernel 15288)
TCP: 15169 (estab 5, closed 15158, orphaned 0, synrecv 0, timewait 3/0), ports 0
Transport Total IP IPv6
* 15288 - -
RAW 0 0 0
UDP 5 4 1
TCP 11 11 0
INET 16 15 1
FRAG 0 0 0
15000個socket消耗30多m記憶體
----------萬惡的分割線-----------
關于 close_wait
如上所示,CLOSE_WAIT的狀态是 伺服器端/用戶端程式收到外部過來的FIN之後,響應了ACK包,之後就進入了 CLOSE_WAIT 狀态。一般來說,如果一切正常,稍後伺服器端/用戶端程式 需要發出 FIN 包,進而遷移到 LAST_ACK 狀态,收到對端過來的ACK後,完成TCP連接配接關閉的整個過程。
注:不管是伺服器還是用戶端,隻要是被動接收第一個FIN的那一方才會進入CLOSE_WAIT狀态