天天看点

记一次排查服务器线程数异常的过程:IdleConnectionEvictor导致线程数持续增加

记一次排查服务器线程数异常的过程

某机器最近突然线程数持续上升,现在已经达到了6000多,肯定是有问题的。对比该服务器和其他服务器上部署项目的不同,发现某机器上独有的项目是最近新上的A项目,并且当该项目重启之后,线程数会降低至1200。所以我们确定了是A项目出现了问题,开始针对该项目进行分析。

查看线程数的命令:

pstree -p | wc -l
           

pstree -p <pid> 查看该进程下的所有线程

1.查询tomcat的pid

ps aux |grep A项目
           

2.通过pid得到该项目的堆栈日志,并保存到1.txt文件中

jstack -F pid > 1.txt
           

3.分析堆栈日志,得出block的线程达到了5700,这种情况明显是异常的,一条完整的block日志记录如下:

Thread 11463: (state = BLOCKED)

- java.lang.Thread.sleep(long) @bci=0 (Compiled frame; information may be imprecise)

- org.apache.http.impl.client.IdleConnectionEvictor$1.run() @bci=16, line=66 (Interpreted frame)

- java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)
           

4.分析block

在这部分走了很大的弯路,因为A项目使用了我们之前没使用过的netty。所以在最开始猜想错误原因时就往netty的方向去想。导致浪费了很多时间。

分析blocked,如上,是出现在IdleConnectionEvictor的错误,这个类是httpclient包下的,本来看到这我就应该定位到跟netty没关系,但无奈对netty了解不够深入,没有确认到这一点。

可参考这篇文章: http://www.softyun.net/article/227520.html

大意是当我们使用httpclient时,往往会启动一个空闲链接回收的线程,代码如下:

this.httpClient = HttpClients.custom() .setMaxConnTotal(MAX_CONN_TOTAL) .setMaxConnPerRoute(MAX_CON_PER_ROUTE) .setDefaultCookieStore(cookieStore) .evictExpiredConnections() .evictIdleConnections(30, TimeUnit.SECONDS)
           

也就是说,每new一个httpclient,就会多创建一个idleconnection线程。当你的自定义的client类被GC回收时,是不会主动将这个线程也回收掉的。所以就导致该线程一直处于blocked状态。既不会有人调用,也不会有人销毁。

那我们怎么解决这个问题呢?

其实思路很简单,就是idleconnection线程只保存一份就可以了。

针对这个有两种做法,

(1) 将httpclient变为static

static {
    Registry<ConnectionSocketFactory> registry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", SSLConnectionSocketFactory.getSocketFactory()).build();
    PoolingHttpClientConnectionManager gcm = new PoolingHttpClientConnectionManager(registry);
    gcm.setMaxTotal(400);
    gcm.setDefaultMaxPerRoute(200);
    RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000).setSocketTimeout(30000).setConnectionRequestTimeout(5000).build();
    HttpClientBuilder httpClientBuilder = HttpClients.custom();
    httpClient = httpClientBuilder.setConnectionManager(gcm).setDefaultRequestConfig(requestConfig).setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(30000).build()).evictExpiredConnections().evictIdleConnections(30L, TimeUnit.SECONDS).build();
    defaultHeaders = new Header[]{new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"), new BasicHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"), new BasicHeader("Accept-Encoding", "gzip, deflate"), new BasicHeader("Accept-Language", "zh-CN,zh;q=0.9"), new BasicHeader("Connection", "keep-alive"), new BasicHeader("Upgrade-Insecure-Requests", "1")};
} 
           

(2) 每次都创建一个新的,但是当自定义的client类被回收时,手动将httpClient涉及的东西都关闭

public void destroy() {
    if (httpClient != null) {
        try {
            httpClient.close();
        } catch (IOException e) {
            logger.error("browserHttpClient close exception", e);
        }
    }
}

@Override
protected void finalize() throws Throwable {
    try {
        destroy();
    } finally {
        super.finalize();
    }
}
           

finalize相关的文章:https://blog.csdn.net/a4171175/article/details/90749839

关于使用finalize的争议,可能存在destroy方法执行不完的情况,但根据我们的实际应用,并没有出现大批量的idleconnection线程无法回收的问题。如果您有更好的处理方式,望指教。