业务需求:
需要在服务器AB之间同步数据文件,由于网络环境限制,B服务器只能单向连接A服务器,所以采用B服务器连接A服务器上的消息中间件,通知B服务器进行文件下载,在初期并发量不高的情况下采用单线程FTP下载,但是随着业务量增大,单线程无法满足下载需求,时常出现下载延迟的情况,所以需要一个FTP连接池以供多线程同时下载,但是目前没有比较官方的轮子,所以就参考开源代码基于Apache Commons Pool实现了一个FTP连接池。
BUG出现了:
运行过程中时常出现卡死,不输出任何日志,一段时间后kafka客户端报消费超时的异常。为了正常提供服务,就先把此同步程序停止,启动老版单线程程序,并将内存dump一份。
异常分析:
首先使用jstack输出jvm线程栈查看是否存在死锁,初次分析并没有发现异常(其实有),查询了各个线程所持有的锁,等待的锁,没有发现死锁,但是发现总共16个线程都卡在连接池获取连接的地方。
FTPClient ftpClient = getFtpClient();
try {
// 判断远程目录是否存在,卡住的地方
Assert.isTrue(isRemotePathExist(remoteDir), String.format("远程目录:[%s]不存在,无法获取远程文件", remoteDir));
// 下载文件到本地
ftpClient.retrieveFile(remoteDir + "/" + remoteFilename, localOutputStream);
} catch (Exception e) {
String errorMsg = String.format("下载远程文件:%s/%s到本地文件流失败!", remoteDir, remoteFilename);
throw new FTPClientUtilException(errorMsg, e);
} finally {
localOutputStream.flush();
localOutputStream.close();
returnFtpClient(ftpClient);
}
在下载文件时需要判断远程目录是否存在,而在这之前已经从连接池中获取了一个对象,如果此时连接池设置的最大数量小于线程池的二倍的话就会造成死锁,各个线程都在持有一个对象的情况下获取另一个对象。
解决办法:
- 调整线程池或连接池,使最大线程数的二倍小于连接池数量。
- 判断远程目录是否存在时不重新获取连接池,直接使用现有对象。