通过FD耗尽实验谈谈使用HttpClient的正确姿势

一段问题代码实验

在进行网络编程时,正确关闭资源是一件很重要的事。在高并发场景下,未正常关闭的资源数逐渐积累会导致系统资源耗尽,影响系统整体服务能力,但是这件重要的事情往往又容易被忽视。我们进行一个简单的实验,使用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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值