1. 程式人生 > >HttpClient連線池丟擲大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查

HttpClient連線池丟擲大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查

今天解決了一個HttpClient的異常,汗啊,一個HttpClient使用稍有不慎都會是毀滅級別的啊。

裡面的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
				{
					in.close ();
				}
				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的區別。