一段問題代碼實驗
在進行網路編程時,正確關閉資源是一件很重要的事。在高並發場景下,未正常關閉的資源數逐漸積累會導致系統資源耗盡,影響系統整體服務能力,但是這件重要的事情往往又容易被忽視。我們進行一個簡單的實驗,使用HttpClient-3.x編寫一個demo請求指定的url,看看如果不正確關閉資源會發生什麼事。
public String doGetAsString(String url) {
GetMethod getMethod = null;
String is = null;
InputStreamReader inputStreamReader = null;
BufferedReader br = null;
try {
HttpClient httpclient = new HttpClient();//問題標記①
getMethod = new GetMethod(url);
httpclient.executeMethod(getMethod);
if (HttpStatus.SC_OK == getMethod.getStatusCode()) {
......//對返回結果進行消費,代碼省略
}
return is;
} catch (Exception e) {
if (getMethod != null) {
getMethod.releaseConnection(); //問題標記②
}
} finally {
inputStreamReader.close();
br.close();
......//關閉流時的異常處理代碼省略
}
return null;
}
這段代碼邏輯很簡單, 先創建一個HttpClient對象,用url構建一個GetMethod對象,然後發起請求。但是用這段代碼並發地以極高的QPS去訪問外部的url,很快就會在日誌中看到「打開文件太多,無法打開文件」的錯誤,後續的http請求都會失敗。這時我們用lsof -p ${javapid}命令去查看java進程打開的文件數,發現達到了655350這麼多。
分析上面的代碼片段,發現存在以下2個問題:
(1)初始化方式不對。標記①直接使用new HttpClient()的方式來創建HttpClient,沒有顯示指定HttpClient connection manager,則構造函數內部默認會使用SimpleHttpConnectionManager,而SimpleHttpConnectionManager的默認參數中alwaysClose的值為false,意味著即使調用了releaseConnection方法,連接也不會真的關閉。
(2)在未使用連接池復用連接的情況下,代碼沒有正確調用releaseConnection。catch塊中的標記②是唯一調用了releaseConnection方法的代碼,而這段代碼僅在發生異常時才會走到,大部分情況下都走不到這裡,所以即使我們前面用正確的方式初始化了HttpClient,由於沒有手動釋放連接,也還是會出現連接堆積的問題。
可能有同學會有以下疑問:
1、明明是發起Http請求,為什麼會打開這麼多文件呢?為什麼是655350這個上限呢?2、正確的HttpClient使用姿勢是什麼樣的呢?這就涉及到linux系統中fd的概念。
什麼是fd
在linux系統中有「一切皆文件」的概念。打開和創建普通文件、Socket(套接字)、Pipeline(管道)等,在linux內核層面都需要新建一個文件描述符來進行狀態跟蹤和使用。我們使用HttpClient發起請求,其底層需要首先通過系統內核創建一個Socket連接,相應地就需要打開一個fd。
為什麼我們的應用最多隻能創建655350個fd呢?這個值是如何控制的,能否調整呢?事實上,linux系統對打開文件數有多個層面的限制:
1)限制單個Shell進程以及其派生子進程能打開的fd數量。用ulimit命令能查看到這個值。
2)限制每個user能打開的文件總數。具體調整方法是修改/etc/security/limits.conf文件,比如下圖中的紅框部分就是限制了userA用戶只能打開65535個文件,userB用戶只能打開655350個文件。由於我們的應用在伺服器上是以userB身份運行的,自然就受到這裡的限制,不允許打開多於655350個文件。
# /etc/security/limits.conf
#
#<domain> <type> <item> <value>
userA - nofile 65535
userB - nofile 655350
# End of file
3)系統層面允許打開的最大文件數限制,可以通過「cat /proc/sys/fs/file-max」查看。
前文demo代碼中錯誤的HttpClient使用方式導致連接使用完成後沒有成功斷開,連接長時間保持CLOSE_WAIT狀態,則fd需要繼續指向這個套接字信息,無法被回收,進而出現了本文開頭的故障。
再識HttpClient
我們的代碼中錯誤使用common-httpclient-3.x導致後續請求失敗,那這裡的common-httpclient-3.x到底是什麼東西呢?相信所有接觸過網路編程的同學對HttpClient都不會陌生,由於http://java.net中對於http訪問只提供相對比較低級別的封裝,使用起來很不方便,所以HttpClient作為Jakarta Commons的一個子項目出現在公眾面前,為開發者提供了更友好的發起http連接的方式。然而目前進入Jakarta Commons HttpClient官網,會發現頁面最頂部的「End of life」欄目,提示此項目已經停止維護了,它的功能已經被Apache HttpComponents的HttpClient和HttpCore所取代。
同為Apache基金會的項目,Apache HttpComponents提供了更多優秀特性,它總共由3個模塊構成:HttpComponents Core、HttpComponents Client、HttpComponents AsyncClient,分別提供底層核心網路訪問能力、同步連接介面、非同步連接介面。在大多數情況下我們使用的都是HttpComponents Client。為了與舊版的Commons HttpClient做區分,新版的HttpComponents Client版本號從4.x開始命名。
從源碼上來看,Jakarta Commons HttpClient和Apache HttpComponents Client雖然有很多同名類,但是兩者之間沒有任何關係。以最常使用到的HttpClient類為例,在commons-httpclient中它是一個類,可以直接發起請求;而在4.x版的httpClient中,它是一個介面,需要使用它的實現類。