解決:HttpClient導致應用出現過多Close_Wait的問題


最近發現一個問題,在服務器上通過netstat命令發現有大量的Close_Wait長時間存在,甚至有時候數量接近1000:

查看服務器參數(etc/sysctl.conf):

net.ipv4.tcp_keepalive_time 網管已經修改成1200。

參數值還可以改小,但似乎是治標不治本,出現這種問題,肯定是某個地方的程序本身存在問題。

根據ip及端口信息,不難發現是什么地方除問題了,項目中有涉及到圖片上傳,於是找到圖片上傳的代碼,結果發現代碼非常簡單,一行上傳權限初始化代碼,一行CDN官方提供的一個靜態方法,之后就是處理響應結果的代碼了。代碼少且簡單,上傳調用代碼沒什么問題,那么問題可能出在CDN官方提供的jar包了,好在CDN有提供源碼,於是查看源碼,源碼中使用apache 的是httpClient包,調用代碼大致如下:

        String response = "";
        HttpPost httpPost = null;
        CloseableHttpResponse ht = null;
        String startTime = formatter.format(new Date());//請求時間
        String endTime = "-";
        String statusCode = "-";
        String contentLength = "-";
        String contentType = "-";
        try {
            httpPost = new HttpPost(url);
            List<NameValuePair> paramsList = new ArrayList<NameValuePair>();

            if (file != null) {
                MultipartEntityBuilder mEntityBuilder = MultipartEntityBuilder.create();

                BandwithLimiterFileBody fileBody = new BandwithLimiterFileBody(file, null, "application/octet-stream", null, BaseBlockUtil.maxRate, progressNotifier);
                mEntityBuilder.addPart("file", fileBody);
                mEntityBuilder.addTextBody("desc", file.getName());

                if (params != null && params.size() > 0) {
                    for (String name : params.keySet()) {
                        mEntityBuilder.addTextBody(name, params.get(name), ContentType.create("text/plain", Charset.forName("UTF-8")));
                    }
                }

                httpPost.setEntity(mEntityBuilder.build());
            } else if (params != null && params.size() > 0) {
                for (String name : params.keySet()) {
                    paramsList.add(new BasicNameValuePair(name, params.get(name)));
                }
                HttpEntity he = new UrlEncodedFormEntity(paramsList, "utf-8");
                httpPost.setEntity(he);
            }

            if (headMap != null && headMap.size() > 0) {
                for (String name : headMap.keySet()) {
                    httpPost.addHeader(name, headMap.get(name));
                }
            }
            if(!httpPost.containsHeader("User-Agent"))
                httpPost.addHeader("User-Agent", Config.VERSION_NO);
            CloseableHttpClient hc = HttpClients.createDefault();
            RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(30000).setConnectTimeout(30000).build();//設置請求和傳輸超時時間
            httpPost.setConfig(requestConfig);
            ht = hc.execute(httpPost);
            endTime = formatter.format(new Date());

            Header[] headerArr = ht.getAllHeaders();
            for (Header header : headerArr) {
                BufferedHeader bh = (BufferedHeader) header;
                if (bh.getBuffer().toString().contains("Content-Length")) {
                    contentLength = bh.getValue();
                } else if (bh.getBuffer().toString().contains("Content-Type")) {
                    contentType = bh.getValue();
                }
            }
            HttpEntity het = ht.getEntity();
            InputStream is = het.getContent();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf8"));
            String readLine;
            while ((readLine = br.readLine()) != null) {
                response = response + readLine;
            }

            is.close();
            br.close();
            int status = ht.getStatusLine().getStatusCode();
            statusCode = String.valueOf(status);
            if (status == 200) {
                if (!new JsonValidator().validate(response)) {
                    response = EncodeUtils.urlsafeDecodeString(response);
                }
            }
            return new HttpClientResult(status, response);
        } catch (Exception e) {
            statusCode = "500";
            endTime = formatter.format(new Date());
            throw new HttpClientException(e);
        } finally {
            if (httpPost != null) {
                httpPost.releaseConnection();
            }
            if (ht != null) {
                try {
                    ht.close();
                } catch (IOException ignored) {
                }
            }
            writeHttpLog(startTime, url, "-", (null != params ? params.get("token") : "-"), (null != file ? file.getName() : "-"), "-", "-", endTime, statusCode, contentType, contentLength, response);
        }

 查看TCP協議端口狀態說明 , 如果一直保持在CLOSE_WAIT狀態,那么只有一種情況,就是在對方關閉連接之后服務器程序自己沒有進一步發出ack信號。因此要解決這個問題大致有以下幾種方案:

a、實例化httpClient 時,使用alwaysClose 的SimpleHttpConnectionManager

  通常默認情況實例化httpClient 的時候使用的是無參構造的SimpleHttpConnectionManager,因此可替換為帶參構造:

new HttpClient(new SimpleHttpConnectionManager(true));

b、在method.releaseConnection() 之后 通過獲取HttpConnectionManager,進行關閉(getConnectionManager方法在httpclient 4.3之后已經標記為過期,后期可能會移除該方法):

hc.getConnectionManager().shutdown();

c、在method.releaseConnection() 之后 通過獲取HttpConnectionManager 調用closeIdleConnections方法進行關閉,(getConnectionManager方法在httpclient 4.3之后已經標記為過期,后期可能會移除該方法):

hc.getConnectionManager().releaseConnection(conn, validDuration, timeUnit);

d、通過設置header由服務端自動關閉.

method.setHeader("Connection", "close"); 

HTTP協議中有關於這個屬性的定義: 
HTTP/1.1 defines the "close" connection option for the sender to signal that the connection will be closed after completion of the response. For example:
      Connection: close 

e、使用MultiThreadedHttpConnectionManager 進行復用,但同時也必須在合適的地方進行關閉處理;

f、請求之后未收到響應信息時,調用method.abort()進行處理

可參考:http://wiki.apache.org/HttpComponents/FrequentlyAskedConnectionManagementQuestions

    http://www.softlab.ntua.gr/facilities/documentation/unix/unix-socket-faq/unix-socket-faq-2.html#ss2.7 

    http://hc.apache.org/httpclient-legacy/tutorial.html

    TCP/IP詳解卷一

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM