0、引言
生產環境中,運行一段時間後,發現經常出現殘留着大量的TCP鏈接,如下圖所示,這些殘留的TCP鏈接佔用着系統的資源,一旦數量過大的話,會導致系統資源耗盡,甚至崩潰,急需排查解決。
2、TCP/IP的連接和斷開過程
2.1 三次握手建立連接
- 第一次握手:建立連接時,客戶端A發送SYN包(SYN=j)到服務器B,並進入SYN_SEND狀態,等待服務器B確認。
- 第二次握手:服務器B收到SYN包,必須發生一個ACK包,來確認客戶A的SYN(ACK=j+1),同時自己也發送一個SYN包(SYN=k),即SYN+ACK包,此時服務器B進入SYN_RECV狀態。
- 第三次握手:客戶端A收到服務器B的SYN+ACK包,向服務器B發送確認包ACK(ACK=k+1),此包發送完畢,客戶端A和服務器B進入ESTABLISHED狀態,完成三次握手(注意,主動打開方的最後一個ACK包中可能會攜帶了它要發送給服務端的數據)。
總結: 三次握手,其實就是主動打開方,發送SYN,表示要建立連接,然後被動打開方對此進行確認,表示可以,然後主動方收到確認之後,對確認進行確認;
2.2 四次揮手斷開連接
由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉,TCP的雙方都要向對方發送一次 FIN 包,並且要對方對次進行確認。根據兩次FIN包的發送和確認可以將四次揮手分爲兩個階段:
第一階段: 主要是主動閉方方發生FIN,被動方對它進行確認;
- 第一次揮手:主動關閉方,客戶端發送完數據之後,向服務器發送一個FIN(M)數據包,進入 FIN_WAIT1 狀態;被動關閉方服務器收到FIN(M)後,進入
CLOSE_WAIT
狀態; - 第二次揮手:服務端發生FIN(M)的確認包ACK(M+1),關閉服務器讀通道,進入 LAST_ACK 狀態;客戶端收到ACK(M+1)後,關閉客戶端寫通道,進入 FIN_WATI2狀態;此時客戶端仍能通過讀通道讀取服務器的數據,服務器仍能通過寫通道寫數據。
第二階段: 主要是被動關閉方發生FIN,主動方對它進行確認;
- 第三次揮手:服務器發送完數據,向客戶機發送一個FIN(N)數據包,狀態沒有變還是LAST_ACK;客戶端收到FIN(N)後,進入
TIME_WAIT
狀態 - 第四次揮手:客戶端返回對FIN(N)的確認段ACK(N+1),關閉客戶機讀通道(還是
TIME_WAIT
狀態);服務器收到ACK(N+1)後,關閉服務器寫通道,進入CLOSED狀態。
總結: 四次揮手,其本質就是:主動關閉方數據發生完成之後 發生FIN,表示我方數據發生完,要斷開連接,被動方對此進行確認;然後被動關閉方在數據發生完成之後 發生FIN,表示我方數據發生完成,要斷開連接,主動方對此進行確認;
3、CLOSE_WAIT
3.1 CLOSE_WAIT產生的原因
由上面的TCP四次揮手斷開連接的過程,可以知道 CLOSE_WAIT
是主動關閉方發生FIN之後,被動方收到 FIN 就進入了 CLOSE_WAIT
狀態,此時如果被動方沒有調用 close() 函數來關閉TCP連接,那麼被動方服務器就會一直處於 CLOSE_WAIT
狀態(等待調用close函數的狀態);
所以 CLOSE_WAIT
狀態很多的原因有兩點:
- 代碼中沒有寫關閉連接的代碼,也就是程序有bug;
- 該連接的業務代碼處理時間太長,代碼還在處理,對方已經發起斷開連接請求; 也就是客戶端因爲某種原因先於服務端發出了FIN信號,導致服務端被動關閉,若服務端不主動關閉socket發FIN給Client,此時服務端Socket會處於
CLOSE_WAIT
狀態(而不是 LAST_ACK 狀態)。
3.2 CLOSE_WAIT的特性
由於某種原因導致的 CLOSE_WAIT
,會維持一段時間。如果服務端程序因某個原因導致系統造成一堆 CLOSE_WAIT
消耗資源,那麼通常是等不到釋放那一刻,系統就已崩潰。TOMCAT失去響應等等。
如下圖(平臺使用的配置):
tcp_keepalive_time(7200):如果在該參數指定的秒數內,TCP連接一直處於空閒,則內核開始向客戶端發起對它的探測,看他是否還存活着;
tcp_keepalive_intvl(75):以該參數指定的秒數爲時間間隔,向客戶端發起對它的探測;
tcp_keepalive_probes(9):內核發起對客戶端探測的次數,如果都沒有得到相應,那麼就斷定客戶端不可達或者已關閉,內核就關閉該TCP連接,釋放相關資源;
所以 CLOSE_WAIT
狀態維持的秒數=tcp_keepalive_time+tcp_keepalive_intvl* tcp_keepalive_probes=7200+75*9=7875秒(約130分鐘)
3.3 CLOSE_WAIT的解決方法
-
若是系統的bug,那麼找到並修正bug;
-
修改TCP/IP的keepalive的相關參數來縮短
CLOSE_WAIT
狀態維持的時間;
修改方法:sysctl -w net.ipv4.tcp_keepalive_time=1800 sysctl -w net.ipv4.tcp_keepalive_probes=3 sysctl -w net.ipv4.tcp_keepalive_intvl=15 sysctl -p
修改會暫時生效,重新啓動服務器後,會還原成默認值。
修改之後,進行觀察一段時間,如果效果理想,那麼可以進行永久性修改:在文件
/etc/sysctl.conf
中的添加或者修改成下面的內容:net.ipv4.tcp_keepalive_time = 1800 net.ipv4.tcp_keepalive_probes = 3 net.ipv4.tcp_keepalive_intvl = 15
修改之後執行:
sysctl -p
使修改馬上生效。
3.4 數據庫連接池CLOSE_WAIT問題分析
假如連接池中的連接被數據庫關閉了,應用通過連接池getConnection時可能獲取到這些不可用的連接,且這些連接如果不被其他線程回收的話,它們不會被連接池被廢除,也不會重新被創建,佔用了連接池的名額,項目本身作爲服務端,數據庫鏈接被關閉,客戶端調用服務端就會出現大量的timeout,客戶端設置了超時時間,然而主動斷開,服務端必然出現 CLOSE_WAIT
。加大tomcat默認線程(server.tomcat.max-threads)只能緩解。
Tomcat 連接池中相關配置項的作用:
- testOnBorrow:true指明是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個
- testWhileIdle:默認false,建議設置爲true,指明連接是否被空閒連接回收器(如果有)進行檢驗,如果檢測失敗,則連接將被從池中去除。
- timeBetweenEvictionRunsMillis = “30000” 如果設置爲非正數,則不運行空閒連接回收器線程,每30秒運行一次空閒連接回收器
- minEvictableIdleTimeMillis = “1800000” 池中的連接空閒30分鐘後被回收,默認值就是30分鐘。
- numTestsPerEvictionRun=“3” 在每次空閒連接回收器線程(如果有)運行時檢查的連接數量,默認值就是3。
配置 timeBetweenEvictionRunsMillis = "30000"
後,每30秒運行一次空閒連接回收器(獨立線程)。並每次檢查3個連接,如果連接空閒時間超過30分鐘就銷燬。銷燬連接後,連接數量就少了,如果小於minIdle數量,就新建連接,維護數量不少於minIdle,過行了新老更替。
配置 testWhileIdle = "true"
表示每30秒,取出3條連接,使用validationQuery = “SELECT 1” 中的SQL進行測試 ,測試不成功就銷燬連接。銷燬連接後,連接數量就少了,如果小於minIdle數量,就新建連接。
4、TIME_WAIT
4.1 TIME_WAIT產生的原因
TCP連接是雙向的,所以在關閉連接的時候,兩個方向各自都需要關閉。先發FIN包的一方執行的是主動關閉;後發FIN包的一方執行的是被動關閉。主動關閉的一方會進入 TIME_WAIT
狀態,並且在此狀態停留2倍的MSL(報文最大生存時間)時長。平臺系統使用的是默認值60s。也就是說 TIME_WAIT
狀態需要維持120秒才能釋放。
在生產過程中,如果服務器使用短連接,那麼完成一次請求後會主動斷開連接,就會造成大量 TIME_WAIT
狀態。因此我們常常在系統中會採用長連接,減少建立連接的消耗,同時也減少 TIME_WAIT
的產生,但實際上即使使用長連接配置不當時,當 TIME_WAIT
的生產速度遠大於其消耗速度時,系統仍然會累計大量的 TIME_WAIT
狀態的連接。 TIME_WAIT
狀態連接過多就會造成一些問題。如果客戶端的 TIME_WAIT
連接過多,同時它還在不斷產生,將會導致客戶端端口耗盡,新的端口分配不出來,出現錯誤,tomcat也會進入假死狀態。如果服務器端的 TIME_WAIT
連接過多,可能會導致客戶端的請求連接失敗。
4.2 TIME_WAIT相關參數調優
查看當前系統的配置
tcp_tw_reuse:是否能夠重新啓用處於 TIME_WAIT
狀態的TCP連接用於新的連接;
tcp_tw_recycle:設置是否對 TIME_WAIT
狀態的TCP進行快速回收;
tcp_fin_timeout:主動關閉方TCP保持在FIN_WAIT_2狀態的時間。對方可能會一直不結束連接或不可預料的進程死亡。默認值爲60秒。
修改方法:
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=1
sysctl -w net.ipv4.tcp_fin_timeout=30
sysctl -p
修改會暫時生效,重新啓動服務器後,會還原成默認值。修改之後,進行觀察一段時間,如果效果理想,可以進行永久性修改:修改 /etc/sysctl.conf
:
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_fin_timeout=30
5、測試
查看各種狀態的網絡連接的數量,Linux 下使用命令:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
可以查看詳情,Linux 下使用命令:
netstat -na
某測試環境參數調整前:
某測試環境參數調整後: