HttpClient連接池拋出大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查


今天解決了一個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的區別。


免責聲明!

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



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