Apache HttpClient 连接池泄漏诊断思路

经常在线上看到一些应用直接因为连接池无法获得连接, 导致整个应用不在响应任何请求. 常见的有数据库连接池连接泄漏, Http 连接池泄漏. 对于这种连接泄漏的问题, 一般是应用没有考虑到某些特殊情况, 特殊异常的处理导致不能用完之后返回连接到连接池. 这里就针对 Apache HttpClient 连接池泄漏这种清楚, 分析一下基本的求解思路.

一般对于这种情况, 我们在 thread dump 里面都会看到如下的栈:

java.lang.Thread.State: WAITING (parking)
  at sun.misc.Unsafe.park(Native Method)
  - parking to wait for  <0x0000000671b45098> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2044)
  at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:393)
  at org.apache.http.pool.AbstractConnPool.access$300(AbstractConnPool.java:70)
  at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:253)
  - locked <0x0000000793606198> (a org.apache.http.pool.AbstractConnPool$2)
  at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:198)
  at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:304)
  at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:280)
       ...

我们可以看到这个线程停在了 PoolingHttpClientConnectionManager.leaseConnection 这个方法上, 正在请求一个连接.

如何确认连接被用完了呢? 如果这个线程一直卡在这个地方, 并且没有其他线程在使用同样的连接去访问同一个网址的请求, 基本可以确定连接被泄漏光了. 如果要完全确认的话, 可以在 heap dump 里面确认:

  1. 首先 确认这连接池里面 available 是空的
    connectionAvai.png
  2. 然后确认即便 available 是空的, leased 里面的每个连接除了连接池自己在引用它们, 没有其它引用(使用). 可以使用 MAT 的 Path to GC root 去它们的引用链;

那么对于这种泄漏的连接, 如何确认泄漏点呢? 只有找到泄漏点, 才能修复.
方法一:
如果你能确认这些泄漏大致发生的时间范围, 可以去找这段时间内的一些特别的出错日志. 一般情况下, 每泄漏一个连接, 就会出现一次特殊的错误, 所以如果有些错误正好能对上这些时间点, 同时发生的次数也和被泄漏连接的数量相当, 那么基本可以确认. 如果出错日志里面有整个出错的栈, 那么就更容易去找到问题发生的根本原因了.

方法二:
如果连接没有被返回到连接池, 那么这些连接应该也没有做一些清理打扫的工作, 那么这些被泄漏的连接里面应该还有蛛丝马迹能帮我们还原出错的部分场景.
找到那些被 leased 却没有正在被使用的连接, 查看他们的 outBuffer 和 inBuffer 你就能发现一些有用的信息:
inBuffer.png
inBuffer0.png

通过上面这些 outBuffer 和 inBuffer 里面, 我们看到了最后一次使用的这个连接时候的一些 HTTP 请求/回复的信息. 然在 route 里面的 URL 信息, 也能很容易看到. 那么我们就能针对这些请求/回复去查看代码的处理情况, 甚至去重现/Mock 方式重现这些信息的处理.

举个最近遇到的错误示例 (这里在不是 200 状态码的时候, 就不关掉连接, 导致连接泄漏):

public String makeSomeCall(final String query) throws Exception
    HttpPost httpPost = new HttpPost(env.getEndpoint());
    httpPost.setEntity(new StringEntity(query));
    CloseableHttpResponse response = client.execute(httpPost);
    int statusCode = response.getStatusLine().getStatusCode();
    if (statusCode == 200)
    {
        String responseString = EntityUtils.toString(response.getEntity());
        if (responseString.contains("error"))
        {
            throw new Exception("some error"));
        }
        return responseString;
    }
    else
    {
        throw new Exception("not 200");
    }

    return null;
}

标签: none

添加新评论