一段问题代码实验
在进行网络编程时,正确关闭资源是一件很重要的事。在高并发场景下,未正常关闭的资源数逐渐积累会导致系统资源耗尽,影响系统整体服务能力,但是这件重要的事情往往又容易被忽视。我们进行一个简单的实验,使用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都不会陌生,由于java.net中对于http访问只提供相对比较低级别的封装,使用起来很不方便,所以HttpClient作为Jakarta Commons的一个子项目出现在公众面前,为开发者提供了更友好的发起http连接的方式。然而目前进入Jakarta Commons HttpClient官网,会发现页面最顶部的“End of life”栏目,提示此项目已经停止维护了,它的功能已经被Apache HttpComponents的HttpClient和HttpCore所取代。
同为Apache基金会的项目,Apache HttpComponents提供了更多优秀特性,它总共由3个模块构成:HttpComponents Core、HttpComponents C