伺服器出現大量close_wait,我們來說說到底是怎麼回事?(以tomcat為例)

三國夢迴發表於2019-05-28

一、問題描述

最近一直忙得很,好久沒寫部落格。前兩天,微信收到個好友申請,說是想問問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。

 

在網上搜尋時,也發現網上其實有這方面案例了,比如:

一次服務端大量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這些,都有點關係,放這篇文章的話,主題就有點不集中,放下篇吧。

相關文章