一、問題描述
最近一直忙得很,好久沒寫部落格。前兩天,微信收到個好友申請,說是想問問close_wait的事情。
找他問了些詳細資訊,大概瞭解到,他們後端服務是tomcat 7, jdk 7,centos,傳統的spring + hibernate + spring mvc 結構。
業務不清楚,客戶端主要是微信小程式。
目前的症狀就是,伺服器上有大量的close_wait狀態的連線,在他們的伺服器上執行 netstat 命令,如下圖:
從上圖看出,他們的伺服器ip為 172.18.206.252(反正是內網ip,不用打碼了吧),埠是443,那應該就是https服務了。
客戶端ip沒有重樣的,應該都是些全國各地的ip了。
close_wait的危害在於,在一個埠上開啟的檔案描述符超過一定數量,(在linux上預設是1024,可修改),新來的socket連線就無法建立了。
會報:Too many open files。
二、問題分析
我回想了一下,之前在部落格園上是寫了一篇close_wait相關的,叫:tcp連線出現close_wait狀態?可能是程式碼不夠健壯
在那篇文章裡,雖然我那也是服務端出現的,但是服務端其實是作為客戶端,去呼叫大資料服務。嚴格來說,和今天遇到的場景不一樣:
1、之前部落格裡的場景:服務端呼叫大資料,大資料關閉連線,(發起fin,伺服器回ack)。此時,因為程式碼不嚴謹,服務端沒有再向大資料發起close請求,
所以服務端與大資料的連線,在服務端上表現為close_wait。在大資料那邊,狀態應該是屬於FIN_WAIT_2。參考下圖:
2、這次遇到的場景:是作為小程式的客戶端訪問了伺服器,伺服器不知道為啥處於close_wait。
所以,一開始我也沒有思路,網上查了下,有人說是tomcat 的https有bug,更多的直接教你怎麼用運維手段解決。
後來,這個朋友提到,他們伺服器的一個介面,是會去呼叫微信伺服器,生成二維碼,而且,他們的監控顯示,該方法耗時較長。
這時,我想到一個問題是:如果服務端在處理過程中,耗時較長,(進入死迴圈、等鎖、下游服務響應慢等),假設20s才返回,
但是客戶端明顯不可能等那麼久,一般5-10s就超時了。超時了,客戶端發起fin,伺服器回ack,此時,
伺服器端應該就是close_wait。
在網上搜尋時,也發現網上其實有這方面案例了,比如:
我大概率估計,就是這個原因。但猜測只是猜測,還是要實踐一下。
三、驗證猜測
1、準備工作
我這邊的一臺開發伺服器是windows的,裝了wireshark,方便抓包,上面裝了tomcat 8.5,一會直接把war包丟進去跑就完了。
我的打算是,修改目前工程的一個controller介面,讓其睡眠30s再返回。 客戶端的話,我用了httpclient,寫了個測試類,直接去呼叫伺服器的controller介面。
然後,用netstat觀察該連線的狀態變化,同時,wireshark輔助檢視網路包的傳送情況。
controller程式碼如下:
客戶端程式碼:
pom.xml加上:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.3</version> </dependency>
工具類如下:
import com.alibaba.fastjson.JSON; import com.ceiec.base.common.utilities.AppConstants; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.time.StopWatch; import org.apache.http.HttpEntity; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; public final class MyHttpUtils { private static int TIMEOUT = 5000; private static final String APPLICATION_JSON = "application/json"; private static final String CONTENT_TYPE_TEXT_JSON = "text/json"; private static Logger logger = LoggerFactory.getLogger(MyHttpUtils.class); /** * POST方式提交請求 * * @param url 請求地址 * @param json JSON格式請求內容 * @throws IOException */ public static String doPost(String url, String json){ if (json == null) { HashMap<String, String> map = new HashMap<>(); json = JSON.toJSONString(map); } //計時 StopWatch timer = new StopWatch(); timer.start(); RequestConfig defaultRequestConfig = RequestConfig.custom().setSocketTimeout(TIMEOUT).setConnectTimeout(TIMEOUT).setConnectionRequestTimeout(TIMEOUT).build(); CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(defaultRequestConfig).build(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON); StringEntity stringEntity = new StringEntity(json, AppConstants.UTF8); stringEntity.setContentType(CONTENT_TYPE_TEXT_JSON); stringEntity.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, APPLICATION_JSON)); httpPost.setEntity(stringEntity); httpPost.setConfig(defaultRequestConfig); CloseableHttpResponse response = null; String responseContent = ""; try { response = httpClient.execute(httpPost); int status = response.getStatusLine().getStatusCode(); logger.debug("response status: " + status); if (status >= 200 && status < 300) { HttpEntity entity = response.getEntity(); if (entity == null) { return null; } responseContent = EntityUtils.toString(entity,"UTF-8"); return responseContent; } else { throw new ClientProtocolException("Unexpected response status: " + status); } } catch (Exception e) { logger.error("error occured.{}",e); throw new RuntimeException(e); } finally { timer.stop(); logger.info("doPost. requestUrl:{}, param:{},response:{},took {} ms", url,json,responseContent,timer.getTime()); IOUtils.closeQuietly(response); IOUtils.closeQuietly(httpClient); } } }
Test類:
public class Test { public static void main(String[] args) { MyHttpUtils.doPost("http://192.168.19.94:8080/CAD_WebService/getValue.do",null); } }
好了,在正式開始之前,說下MyHttpUtils中標紅的那個方法:RequestConfig.custom().setSocketTimeout(TIMEOUT) 這個setSocketTimeout表示設定等待伺服器端響應超時的時間,這裡設為5000,意為5s
2.測試驗證
這裡,準備就緒了,馬上開始,下面是我這邊的抓包:
這個圖,我們們先看抓包:
這個包,是在伺服器192.168.19.94上抓的。抓的是伺服器上8080埠,和我本地pc 10.15.4.46之間的網路包。
我們分析下:
序號為1/2/3的包: 三次握手,建立連線;
序號4的包:發起http請求,請求的controller方法,會睡眠30s
序號5的包:對序號4的包的ack。注意,此時時間為14:02:11
序號6的包:此時時間已經過去5s,客戶端等不及了,(就你猴急?),於是不耐煩了,老子不等了,發了個fin過來,要分手。
序號7的包:伺服器說:要分手?知道了。
此時伺服器在幹嘛,不好意思,要睡30s,這時才睡了5s,還沒醒。
說完了包,我們再看看那個cmd,裡面展示的是8080埠上的連線,可以看出來,此時該連線正處於close_wait狀態。
。。。
25s後。。。
25s後,伺服器終於醒了,睡得不錯,給客戶端發響應吧(序號8),但是呢,9號包可以看出來,我的開發機給伺服器回了rst。
意思就是:我不認識你。(因為前面我的開發機就提了分手。。。)
3.gif圖完整回放
由於是一邊寫一邊截圖的,所以有些圖等寫完了再想看的時候,已經沒有了。
下面截個完整的,這裡,把客戶端超時改為20s,方便檢視:
1、服務端視角,可以看到,超時前,為established,超時後,為close_wait。
2.客戶端視角
四、說到底,問題怎麼解決
扯了那麼多,伺服器出現大量close_wait,到底怎麼解決? 指標不治本的方法我就不說了,直接網上搜下,改改linux引數即可。
這篇部落格主要是治本,講了close_wait出現的一種情況:
服務端介面耗時較長,客戶端主動斷開了連線,此時,服務端就會出現 close_wait。
那怎麼解決呢?看看程式碼為啥耗時長吧。
另外,如果程式碼不規範的話,說不定在收到對方發起的fin後,自己根本就不會給人家發fin。(比如netty自己開發的框架那種)
沒啥好說的,檢查自己的程式碼吧,反正close_wait基本就是自己這邊的問題了。
如果覺得有點幫助,麻煩點個推薦哈。
ps:我這裡用chrome測過,用fiddler的composer也搞過,發現有些客戶端會一直等響應,過了很久才會主動去發fin,所以用了httpclient測試。
pps:tomcat在什麼情況會主動發起fin?其實我也想講講,因為和http的connection:keep-alive這些,都有點關係,放這篇文章的話,主題就有點不集中,放下篇吧。