HttpClient當HTTP連接的時候出現大量CLOSE_WAIT連接


三種狀態:

ESTABLISHED 表示正在進行網絡連接的數量
TIME_WAIT 表示表示等待系統主動關閉網絡連接的數量
CLOSE_WAIT 表示被動等待程序關閉的網絡連接數量

上篇文章給出了解決TIME_WAIT太多的方法,本篇文章以HttpClient為例說明解決大量CLOSE_WAIT狀態的方法。

HttpClient是大量使用的用於HTTP連接的包,首先需要說明的是HttpClient 3.x和4.x之間API差距很多,不過強烈建議使用4.x的版本。除此之外,4.x中每個x之間也有一些差別(比如一些棄用的類,新增加的類等),這里以4.2.3版本進行說明。

HttpClient使用的HTTP 1.1協議進行連接,相對於HTTP 1.0來說有一個持續連接的增強,為了充分利用持續連接的特性,在一次連接結束之后,即使將HttpResponse使用close方法關閉,並且將調用了HttpGet或HttpPost的releaseConnection方法,示例代碼如下:

HttpGet method = null;
HttpResponse response = null;
try {
    method = new HttpGet(url);
    response = client.execute(method);
} catch(Exception e) {

} finally {
    if(response != null) {
        EntityUtils.consumeQuietly(response.getEntity());
    }
    if(method != null) {
        method.releaseConnection();
    }
}

這個時候仍然發現連接處於CLOSE_WAIT狀態,這是因為HttpClient在執行close的時候,如果發現Response的Header中Connection是Keep-alive則連接不會關閉,以便下次請求相同網站的時候進行復用,這是產生CLOSE_WAIT連接的原因所在。

最簡單的一種解決方法在execute方法之前增加Connection: close頭信息,HTTP協議關於這個屬性的定義如下:

HTTP/1.1 defines the "close" connection option for the sender to signal that the connection will be closed after completion of the response. For example:
    Connection: close 

示例代碼如下:

HttpGet method = null;
HttpResponse response = null;
try {
    method = new HttpGet(url);
    method.setHeader(HttpHeaders.CONNECTION, "close");
    response = client.execute(method);
} catch(Exception e) {

} finally {
    if(response != null) {
        EntityUtils.consumeQuietly(response.getEntity());
    }
    if(method != null) {
        method.releaseConnection();
    }
}

當然,也有人建議每次請求之后關閉client,但這一點不符合HttpClient設計的原則——復用。如果每次連接完成之后就關閉連接,效率太低了。因此,需要使用PoolingClientConnectionManager,並且設置maxTotal(整個連接池里面最大連接數,默認為20)和defaultMaxPerRoute(每個主機的最大連接數,默認為2),另外client還有一個ClientPNames.CONN_MANAGER_TIMEOUT參數,用來設置當連接不夠獲取新連接等待的超時時間,默認和CoreConnectionPNames.CONNECTION_TIMEOUT相同。可以根據實際情況對PoolingClientConnectionManager進行設置,以達到效率最優。

還有一種情況也會造成大量CLOSE_WAIT連接,即HttpResponse的狀態碼不是200的時候,需要及時調用method.abort()方法對連接進行釋放

 

=====================================================

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
{
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賦值之后才拋出的呢。
————————————————


免責聲明!

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



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