HttpClient4 TIME_WAIT和CLOSE_WAIT


  最近,公司的接口服務器(客戶端,向外發送數據)頻繁出現了connect timeout 以及readtime out 的情況,經過運維平台檢測,並沒有網絡延時的情況。於是,開始懷疑連接池出了問題。

  使用linux命令: netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'  可以清楚的看到tcp各個狀態下的連接數。

  

  如圖: CLOSE_WAIT 數目大的驚人,問題就出在了這里:這個級別的TIME_WAIT是沒有問題的, linux的句柄數(https://blog.csdn.net/shootyou/article/details/6579139)有限,大量的CLOSE_WAIT占去了過多的連接數,導致其他連接異常。

  據查:

  tcp連接三次握手,四次揮手。

  

    這其中,我們比較關注的狀態有三個: ESTABLISHED 表示正在通信,TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。

    其中:ESTABLISHED  無需多言,  TIME_WAIT  的存在是:

  1. 防止上一次連接中的包,迷路后重新出現,影響新連接(經過2MSL,上一次連接中所有的重復包都會消失)
  2.  可靠的關閉TCP連接。在主動關閉方發送的最后一個 ack(fin) ,有可能丟失,這時被動方會重新發fin, 如果這時主動方處於 CLOSED 狀態 ,就會響應 rst 而不是 ack。所以主動方要處於 TIME_WAIT 狀態,而不能是 CLOSED 。另外這么設計TIME_WAIT 會定時的回收資源,並不會占用很大資源的,除非短時間內接受大量請求或者受到攻擊。  

  

值 得一說的是,對於基於TCP的HTTP協議,關閉TCP連接的是Server端,這樣,Server端會進入TIME_WAIT狀態,可 想而知,對於訪 問量大的Web Server,會存在大量的TIME_WAIT狀態,假如server一秒鍾接收1000個請求,那么就會積壓 240*1000=240,000個 TIME_WAIT的記錄,維護這些狀態給Server帶來負擔。當然現代操作系統都會用快速的查找算法來管理這些 TIME_WAIT,所以對於新的 TCP連接請求,判斷是否hit中一個TIME_WAIT不會太費時間,但是有這么多狀態要維護總是不好。  
HTTP協議1.1版規定default行為是Keep-Alive,也就是會重用TCP連接傳輸多個 request/response,一個主要原因就是發現了這個問題。  

 

    也就是說HTTP的交互跟上面畫的那個圖是不一樣的,關閉連接的不是客戶端,而是服務器,所以web服務器也是會出現大量的TIME_WAIT的情況的。這也說清楚了,為什么我的客戶端服務器會出現大量CLOSE_WAIT的情況。

    然后,我又在windows 端實時測試(netstat同樣適用),的確,我所使用的HttpClient4 在每次接口發送完畢都會產生一個CLOSE_WAIT。這是為什么呢,使用連接池,是為了連接可以復用,而現在的情況確是相反的。

    經查,HttpClient4為了連接復用使用的都是長連接,Response的Header中默認Connection是Keep-alive即連接不會關閉,以便下次請求相同網站的時候進行復用,這是產生CLOSE_WAIT連接的原因所在。所以我猜測HttpClient4 故意留住CLOSE_WAIT

復用該連接。不會復用? 是不是我的對象創建的有問題?於是改了單例:

 

    

public class HttpApacheClient {

    private static class SingletonHandler{
        private static HttpApacheClient singleton = new HttpApacheClient();
    }

    private HttpApacheClient(){}

    public static HttpApacheClient getInstance(){
        return SingletonHandler.singleton;
    }

    private static Logger logger = LoggerFactory.getLogger(HttpApacheClient.class);



    private final HttpClientConnectionManager manager = builderPoolConnectionManager() ;  //定義連接池管理變量
    ResponseHandler<String> responseHandler = new ResponseHandler<String>() {
        @Override
        public String handleResponse(final HttpResponse response)
                throws ClientProtocolException, IOException {
            int status = response.getStatusLine().getStatusCode();
            if (status >= 200 && status < 300) {
                HttpEntity entity = response.getEntity();
                if (entity == null) throw new BusinessException(RESULT_LOG_ERROR);
                String content =  EntityUtils.toString(entity);
                logger.info("the log back :"+content);
                JSONObject jsonObject = JSON.parseObject(content);
                Integer returnStatus = (Integer)jsonObject.get("status");
                if (returnStatus !=200) throw new BusinessException(RESULT_LOG_ERROR,content);
                return content;
            } else {
                throw new ClientProtocolException("Unexpected response status: " + status);
            }
        }
    };


    public HttpClientConnectionManager builderPoolConnectionManager(){
        final SSLContext context = SSLContexts.createSystemDefault();
        final HostnameVerifier verifier = new DefaultHostnameVerifier() ;
        //自定義注冊器,既可以發送http請求,也可以發送https請求
        final Registry<ConnectionSocketFactory> register = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http" , PlainConnectionSocketFactory.INSTANCE)
                .register("https" , new SSLConnectionSocketFactory(context ,verifier))
                .build() ;
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(register);
        poolingHttpClientConnectionManager.setMaxTotal(200);  //設置連接池的最大連接數
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(poolingHttpClientConnectionManager.getMaxTotal());  //一個路由的最大連接數
        return poolingHttpClientConnectionManager ;
    }

    public HttpClient buildHttpClient(){
        RequestConfig config = RequestConfig.custom()
                .setConnectionRequestTimeout(3000)  //從池中獲取請求的時間
                .setConnectTimeout(2000)   //連接到服務器的時間
                .setSocketTimeout(5000).build(); //讀取信息時間

        CloseableHttpClient build = HttpClients.custom()
                .setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE)
                .setDefaultRequestConfig(config)
                .setConnectionManagerShared(true)
                .setConnectionManager(manager)
                .build();
        return build ;
    }

    public static String postJson(String url, List<NameValuePair> params) throws Exception {
        String urlHead = (String) BeanHelper.getConfig("url");
        url = urlHead+url;
        logger.info("url "+url+";"+params);
        HttpApacheClient utils = getInstance();
        HttpClient httpClient = null;
        HttpPost httpPost = null;
        try {
            httpClient = utils.buildHttpClient();
            httpPost = new HttpPost(url);
            httpPost.setEntity(new UrlEncodedFormEntity(params,"UTF-8"));
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            return httpClient.execute(httpPost, utils.responseHandler);
        }catch (Exception e){
            if (httpPost!=null) httpPost.abort();//異常時 關閉連接
            throw  e;
        }
    }

    }
}

  問題得以解決。

  水比較深,未完待續。

 


免責聲明!

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



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