最近监控发现服务运行一段时间之后,线程数会缓慢升高,于是通过jstack命令将线程堆栈信息打印出来,根据线程名称进行统计发现Connection evictor线程居然有两百多个,如下:
通过排查该线程是用来清理httpclient过期连接的,每生成一个Apache Httpclient对象就会对应生成一个Connection evictor线程,该线程不会主动释放,除非手动调用httpclient.close(),该线程才会关闭。
系统中共有几十处创建httpclient的地方,远远达不到两百多个,因此该情况肯定是不正常的,那么到底是哪个地方在不断的创建httpclient呢?通过思考和代码审查,并未发现异常的地方,测试环境因流量较少或未触发相关逻辑,并未出现线程数缓慢升高的情况。因此问题变得棘手起来,使用HttpClient有几十处且第三方jar包也有使用HttpClient的情况,范围太广,实在无从下手。
那么我们是否可以从线程创建的源头下手呢?如找到该线程创建的源头,然后将堆栈信息打印出来,这样的话我们就能够知道到底是哪些地方在创建该线程。
说干就干。
首先,我们需要在源码中找到创建该线程的地方,该线程是由HttpClientBuilder创建,在HttpClientBuilder的build方法中有如下代码:
if (!this.connManagerShared) {
if (closeablesCopy == null) {
closeablesCopy = new ArrayList<Closeable>(1);
}
final HttpClientConnectionManager cm = connManagerCopy;
if (evictExpiredConnections || evictIdleConnections) {
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
connectionEvictor.shutdown();
try {
connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
} catch (final InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
});
connectionEvictor.start();
}
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
cm.shutdown();
}
});
}
该方法在每次创建HttpClient的时候就会创建一个IdleConnectionEvictor对象,该对象就会启动一个名称为Connection evictor的线程。
找到了创建线程的地方,我们就可以通过埋点的方式监控线程的生成情况。从Github中下载自己项目中所使用的HttpClient对应版本的源码,找到HttpClientBuilder源码,
首先,我们需要添加如下的堆栈打印方法,并将打印的堆栈保存到文件之中:
public static void printStackTrace() {
// 使用PrintWriter打印堆栈跟踪信息
try (PrintWriter writer = new PrintWriter(new FileWriter("logs/stacktrace.txt", true))) {
writer.println("-----------------stack-begin------------------");
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
writer.println(element);
}
writer.println("-----------------stack-end------------------");
} catch (Exception e) {
e.printStackTrace(); // 如果发生异常,打印异常信息
}
}
然后,我们需要在build方法中引入该方法:
public CloseableHttpClient build() {
//获取当前线程的堆栈跟踪信息
printStackTrace();
//接下来是源码中的代码,不要动
.....
}
之后,我们需要修改maven坐标,对修改之后的jar包进行编译,并上传到私服。
修改pom文件,引入修改之后的jar包,并排除项目中对老的httpclient jar包依赖,使我们引入的新jar包生效。因本次修改不会对代码产生任何影响,直接更新线上一台服务器进行排查。注意:上线之前要在测试环境测试,堆栈打印是否生效,多测试,避免产生问题。
最后,等服务器产生堆栈日志,下载下来分析就好了,这样就能通过堆栈信息轻松定位到了生成HttpClient最多的代码,并进行修改,如此就能轻松解决了。
如果感觉有收获,麻烦点赞+关注!