HttpClient Timeout waiting for connection from pool 問題解決方案


錯誤:org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool

前言 :
第一次看到這個錯誤, 上網找了下,有文章說是連接池不夠了。。。。 並沒有多想,立即將原有程序的 鏈接池擴容了3倍,然后單個路由 擴容了5倍。
問題解決, 以為找到了,答案。 但是 過了大約 幾天之后,再次出現該問題,當時就特別疑惑, 沒有擴容之前 程序已經運行了 將近兩年,並沒有發生任何錯誤,
現在 擴容了,竟然還報這個錯, 此時 分析方向 應該發生改變。

懷疑方向一:大量的Timeout 出現,是否 請求的域名有問題 ?
於是,手動 ping 了訪問域名,發現並沒有多慢. (疑問否決)
懷疑方向二:是否我們服務器出口IP 有問題, 請求 指定域名 超時?
於是: 我們新開了一台機器, 然后將 原有的兩台機器 停掉一台, 並且新開的機器 使用 全新的出口IP, 以區分於原有機器,后來發現 新卡的機器,沒有問題。
因此,我們大致定位了問題,就是 服務器的出口IP 有問題, 並不是程序的問題。 所以,解決方案就是 找運維 重新申請配置了機器.

下面 貼出 我連接池的 寫法, 做個小筆記 。
HttpClient 版本如下

       <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpclient</artifactId>  
            <version>4.3.4</version>  
        </dependency>  
        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpcore</artifactId>  
            <version>4.3.2</version>  
        </dependency>  
        <dependency>  
            <groupId>org.apache.httpcomponents</groupId>  
            <artifactId>httpmime</artifactId>  
            <version>4.3.1</version>  
        </dependency>  

連接池代碼如下:

import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;


public class HttpClientFactory {

    private static final Integer MAX_TOTAL = 300;             //連接池最大連接數
    private static final Integer MAX_PER_ROUTE = 50;          //單個路由默認最大連接數
    private static final Integer REQ_TIMEOUT =  5 * 1000;     //請求超時時間ms
    private static final Integer CONN_TIMEOUT = 5 * 1000;     //連接超時時間ms
    private static final Integer SOCK_TIMEOUT = 10 * 1000;    //讀取超時時間ms
    private static HttpClientConnectionMonitorThread thread;  //HTTP鏈接管理器線程

    public static HttpClientConnectionMonitorThread getThread() {
        return thread;
    }
    public static void setThread(HttpClientConnectionMonitorThread thread) {
        HttpClientFactory.thread = thread;
    }

    public static HttpClient createSimpleHttpClient(){
        SSLConnectionSocketFactory sf = SSLConnectionSocketFactory.getSocketFactory();
        return HttpClientBuilder.create()
                .setSSLSocketFactory(sf)
                .build();
    }
    
    public static HttpClient createHttpClient() {
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        poolingHttpClientConnectionManager.setMaxTotal(MAX_TOTAL);
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(REQ_TIMEOUT)
                .setConnectTimeout(CONN_TIMEOUT).setSocketTimeout(SOCK_TIMEOUT)
                .build();
        HttpClientFactory.thread=new HttpClientConnectionMonitorThread(poolingHttpClientConnectionManager); //管理 http連接池
        return HttpClients.custom().setConnectionManager(poolingHttpClientConnectionManager).setDefaultRequestConfig(requestConfig).build();
    }
}

以下 線程用來清理 連接池無效的鏈接 :

import java.util.concurrent.TimeUnit;
import org.apache.http.conn.HttpClientConnectionManager;

/**
 * <p>Description: 使用管理器,管理HTTP連接池 無效鏈接定期清理功能</p> 
 */
public class HttpClientConnectionMonitorThread extends Thread {

    private final HttpClientConnectionManager connManager;
    private volatile boolean shutdown;

    public HttpClientConnectionMonitorThread(HttpClientConnectionManager connManager) {
        super();
        this.setName("http-connection-monitor");
        this.setDaemon(true);
        this.connManager = connManager;
        this.start();
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000); // 等待5秒
                    // 關閉過期的鏈接
                    connManager.closeExpiredConnections();
                    // 選擇關閉 空閑30秒的鏈接
                    connManager.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
        }
    }
    
    /**
     * 方法描述: 停止 管理器 清理無效鏈接  (該方法當前暫時關閉) 
     * @author andy 2017年8月28日 下午1:45:18
     */
    @Deprecated
    public void shutDownMonitor() {
        synchronized (this) {
            shutdown = true;
            notifyAll();
        }
    }

}

HttpClient大並發下Timeout waiting for connection from pool 問題解決方案  http://blog.csdn.net/duxing_langzi/article/details/77772673

今天解決了一個HttpClient的異常,汗啊,一個HttpClient使用稍有不慎都會是毀滅級別的啊。
這里有之前因為route配置不當導致服務器異常的一個處理:http://blog.csdn.net/shootyou/article/details/6415248
里面的HttpConnectionManager實現就是我在這里使用的實現。

問題表現:
tomcat后台日志發現大量異常

org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection  

時間一長tomcat就無法繼續處理其他請求,從假死變成真死了。
linux運行:

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

發現CLOSE_WAIT的數量始終在400以上,一直沒降過。

問題分析:
一開始我對我的HttpClient使用過程深信不疑,我不認為異常是來自這里。
所以我開始從TCP的連接狀態入手,猜測可能導致異常的原因。以前經常遇到TIME_WAIT數過大導致的服務器異常,很容易解決,修改下sysctl就ok了。但是這次是CLOSE_WAIT,是完全不同的概念了。
關於TIME_WAIT和CLOSE_WAIT的區別和異常處理我會單獨起一篇文章詳細說說我的理解。

簡單來說CLOSE_WAIT數目過大是由於被動關閉連接處理不當導致的。
我說一個場景,服務器A會去請求服務器B上面的apache獲取文件資源,正常情況下,如果請求成功,那么在抓取完資源后服務器A會主動發出關閉連接的請求,這個時候就是主動關閉連接,連接狀態我們可以看到是TIME_WAIT。如果一旦發生異常呢?假設請求的資源服務器B上並不存在,那么這個時候就會由服務器B發出關閉連接的請求,服務器A就是被動的關閉了連接,如果服務器A被動關閉連接之后自己並沒有釋放連接,那就會造成CLOSE_WAIT的狀態了。
所以很明顯,問題還是處在程序里頭。

先看看我的HttpConnectionManager實現:

public class HttpConnectionManager { 

    private static HttpParams httpParams;
    private static ClientConnectionManager connectionManager;

    /**
     * 最大連接數
     */
    public final static int MAX_TOTAL_CONNECTIONS = 800;
    /**
     * 獲取連接的最大等待時間
     */
    public final static int WAIT_TIMEOUT = 60000;
    /**
     * 每個路由最大連接數
     */
    public final static int MAX_ROUTE_CONNECTIONS = 400;
    /**
     * 連接超時時間
     */
    public final static int CONNECT_TIMEOUT = 10000;
    /**
     * 讀取超時時間
     */
    public final static int READ_TIMEOUT = 10000;

    static {
        httpParams = new BasicHttpParams();
        // 設置最大連接數
        ConnManagerParams.setMaxTotalConnections(httpParams, MAX_TOTAL_CONNECTIONS);
        // 設置獲取連接的最大等待時間
        ConnManagerParams.setTimeout(httpParams, WAIT_TIMEOUT);
        // 設置每個路由最大連接數
        ConnPerRouteBean connPerRoute = new ConnPerRouteBean(MAX_ROUTE_CONNECTIONS);
        ConnManagerParams.setMaxConnectionsPerRoute(httpParams,connPerRoute);
        // 設置連接超時時間
        HttpConnectionParams.setConnectionTimeout(httpParams, CONNECT_TIMEOUT);
        // 設置讀取超時時間
        HttpConnectionParams.setSoTimeout(httpParams, READ_TIMEOUT);

        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

        connectionManager = new ThreadSafeClientConnManager(httpParams, registry);
    }

    public static HttpClient getHttpClient() {
        return new DefaultHttpClient(connectionManager, httpParams);
    }

}

看到沒MAX_ROUTE_CONNECTIONS 正好是400,跟CLOSE_WAIT非常接近啊,難道是巧合?繼續往下看。
然后看看調用它的代碼是什么樣的:

public static String readNet (String urlPath)
    {
        StringBuffer sb = new StringBuffer ();
        HttpClient client = null;
        InputStream in = null;
        InputStreamReader isr = null;
        try
        {
            client = HttpConnectionManager.getHttpClient();
            HttpGet get = new HttpGet();
            get.setURI(new URI(urlPath));
            HttpResponse response = client.execute(get);
            if (response.getStatusLine ().getStatusCode () != 200) {
                return null;
            }
            HttpEntity entity =response.getEntity();
            
            if( entity != null ){
                in = entity.getContent();
                .....
            }
            return sb.toString ();
            
        }
        catch (Exception e)
        {
            e.printStackTrace ();
            return null;
        }
        finally
        {
            if (isr != null){
                try
                {
                    isr.close ();
                }
                catch (IOException e)
                {
                    e.printStackTrace ();
                }
            }
            if (in != null){
                try
                {
                    <span style="color:#ff0000;">in.close ();</span>
                }
                catch (IOException e)
                {
                    e.printStackTrace ();
                }
            }
        }
    }

很簡單,就是個遠程讀取中文頁面的方法。值得注意的是這一段代碼是后來某某同學加上去的,看上去沒啥問題,是用於非200狀態的異常處理:

if (response.getStatusLine ().getStatusCode () != 200) {
    return null;
}

代碼本身沒有問題,但是問題是放錯了位置。如果這么寫的話就沒問題:

client = HttpConnectionManager.getHttpClient();
            HttpGet get = new HttpGet();
            get.setURI(new URI(urlPath));
            HttpResponse response = client.execute(get);
            
            HttpEntity entity =response.getEntity();
            
            if( entity != null ){
                in = entity.getContent();
            ..........
            }
            
            if (response.getStatusLine ().getStatusCode () != 200) {
                return null;
            }
            return sb.toString ();

看出毛病了吧。在這篇入門(HttpClient4.X 升級 入門 + http連接池使用)里頭我提到了HttpClient4使用我們常用的InputStream.close()來確認連接關閉,前面那種寫法InputStream in 根本就不會被賦值,意味着一旦出現非200的連接,這個連接將永遠僵死在連接池里頭,太恐怖了。。。所以我們看到CLOST_WAIT數目為400,因為對一個路由的連接已經完全被僵死連接占滿了。。。
其實上面那段代碼還有一個沒處理好的地方,異常處理不夠嚴謹,所以最后我把代碼改成了這樣:

public static String readNet (String urlPath)
    {
        StringBuffer sb = new StringBuffer ();
        HttpClient client = null;
        InputStream in = null;
        InputStreamReader isr = null;
        HttpGet get = new HttpGet();
        try
        {
            client = HttpConnectionManager.getHttpClient();
            get.setURI(new URI(urlPath));
            HttpResponse response = client.execute(get);
            if (response.getStatusLine ().getStatusCode () != 200) {
                get.abort();
                return null;
            }
            HttpEntity entity =response.getEntity();
            
            if( entity != null ){
                in = entity.getContent();
                ......
            }
            return sb.toString ();
            
        }
        catch (Exception e)
        {
            get.abort();
            e.printStackTrace ();
            return null;
        }
        finally
        {
            if (isr != null){
                try
                {
                    isr.close ();
                }
                catch (IOException e)
                {
                    e.printStackTrace ();
                }
            }
            if (in != null){
                try
                {
                    in.close ();
                }
                catch (IOException e)
                {
                    e.printStackTrace ();
                }
            }
        }
    }

顯示調用HttpGet的abort,這樣就會直接中止這次連接,我們在遇到異常的時候應該顯示調用,因為誰能保證異常是在InputStream in賦值之后才拋出的呢。

好了 ,分析完畢,明天准備總結下CLOSE_WAIT和TIME_WAIT的區別。

HttpClient連接池拋出大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查  http://blog.csdn.net/shootyou/article/details/6615051






免責聲明!

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



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