今天解決了一個HttpClient的異常,汗啊,一個HttpClient使用稍有不慎都會是毀滅級別的啊。java
這裏有以前由於route配置不當致使服務器異常的一個處理:http://blog.csdn.net/shootyou/article/details/6415248linux
裏面的HttpConnectionManager實現就是我在這裏使用的實現。apache
問題表現:tomcat
tomcat後臺日誌發現大量異常服務器
[plain] view plain copyapp
print?tcp
- org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection
時間一長tomcat就沒法繼續處理其餘請求,從假死變成真死了。url
linux運行:spa
[plain] view plain copy.net
print?
- 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實現:
[java] view plain copy
print?
- 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很是接近啊,難道是巧合?繼續往下看。
而後看看調用它的代碼是什麼樣的:
[java] view plain copy
print?
- 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狀態的異常處理:
[java] view plain copy
print?
- if (response.getStatusLine ().getStatusCode () != 200) {
- return null;
- }
代碼自己沒有問題,可是問題是放錯了位置。若是這麼寫的話就沒問題:
[java] view plain copy
print?
- 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,由於對一個路由的鏈接已經徹底被僵死鏈接佔滿了。。。
其實上面那段代碼還有一個沒處理好的地方,異常處理不夠嚴謹,因此最後我把代碼改爲了這樣:
[java] view plain copy
print?
- 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的區別。