一、問題描述
最近一直忙得很,好久沒寫博客。前兩天,微信收到個好友申請,說是想問問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連接就無法建立了,因為每個socket連接也算是一個文件描述符。
會報:Too many open files。
這里詳細說一下,linux上,每個進程,都有其自身的資源限制,比如最大可以打開的文件描述符數量。
比如下面,我查看了某個java進程的資源限制。
這部分,可以查看以下鏈接:
https://feichashao.com/ulimit_demo/
二、問題分析
我回想了一下,之前在博客園上是寫了一篇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這些,都有點關系,放這篇文章的話,主題就有點不集中,放下篇吧。
網絡上,我也發現有些差不多場景的文章: