項目中通過jsch中的sftp實現上傳下載文件。在壓測過程中,由於調用到sftp,下載文件不存在時,系統不斷拋出異常,內存飆升,逐漸把swap區也占滿,通過top監控未發現占用內存的進程,通過查找sshd進程,發現服務器多了很多sftp的進程沒有被關閉。

剛開始以為是sftp公共方法設計的有問題,每次創建連接都未釋放,下面是部分代碼片段
@Repository("SftpClient")
public class SftpClient {
private Logger logger = LoggerFactory.getLogger(SftpClient.class);
private ThreadLocal<Session> sessionLocal = new ThreadLocal<Session>();
private ThreadLocal<ChannelSftp> channelLocal = new ThreadLocal<ChannelSftp>();
//初始化連接
public SftpClient init() {
try {
String host = SFTP_HOST;
int port = Integer.valueOf(SFTP_PORT);
String userName = SFTP_USER_NAME;
String password = SFTP_USER_PASSWORD;
Integer timeout = Integer.valueOf(SFTP_TIMEOUT);
Integer aliveMax = Integer.valueOf(SFTP_ALIVEMAX);
// 創建JSch對象
JSch jsch = new JSch();
Session session = jsch.getSession(userName, host, port);
// 根據用戶名,主機ip,端口獲取一個Session對象
if (password != null) {
// 設置密碼
session.setPassword(password);
}
// 為Session對象設置properties
session.setConfig("StrictHostKeyChecking", "no");
if (timeout != null) {
// 設置timeout時間
session.setTimeout(timeout);
}
if (aliveMax != null) {
session.setServerAliveCountMax(aliveMax);
}
// 通過Session建立鏈接
session.connect();
// 打開SFTP通道
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
// 建立SFTP通道的連接
channel.connect();
channelLocal.set(channel);
sessionLocal.set(session);
logger.debug("SSH Channel connected.session={},channel={}", session, channel);
} catch (JSchException e) {
throw new SystemException(ImageExceptionCode.FTP_SEND_ERROR);
}
return this;
}
//斷開連接
public void disconnect() {
ChannelSftp channel = channelLocal.get();
Session session = sessionLocal.get();
//斷開sftp連接
if (channel != null) {
channel.disconnect();
logger.debug("SSH Channel disconnected.channel={}", channel);
}
//斷開sftp連接之后,再斷開session連接
if (session != null) {
session.disconnect();
logger.debug("SSH session disconnected.session={}", session);
}
channelLocal.remove();
sessionLocal.remove();
}
}
因為使用jsch的sftp有一個要注意的地方,當調用ChannelSftp.disconnect()斷開sftp的連接之后,該連接的session還存在,這時,需要顯式調用Session.disconnect()來斷掉session。程序設計的時候也考慮到這一點了,公共方法設計的是沒問題的,那么問題在哪呢?

調整日志級別,通過查看日志定位出問題所在,業務層在調用時,執行兩次init()創建了兩個sftp連接,但當遇到異常時,僅僅執行了一次disconnect()釋放了第二次創建的,第一個連接一直沒有得到釋放。
try { sftpClient.init(); for(XxxDto is:list){ /*********業務代碼,略*************/ try { /*********業務代碼,略*************/ try { /*********業務代碼,略*************/ } catch (Exception e) { throw new BusinessException(XxxExceptionCode.UNDER_USERID_FAIL); } /*********下載業務代碼,問題所在,此處拋出異常*************/ } catch (Exception e) { throw new BusinessException( XxxExceptionCode.FTP_DOWNLOAD_LOCAL_FAIL); } } } finally { sftpClient.disconnect(); }
下載業務代碼如下:
try { sftpClient.init(); /*************下載業務代碼,此處拋出異常被上層捕獲,該方法創建的連接被釋放,但上層的連接 enenenenene *****************/ } finally { sftpClient.disconnect(); }
業務層的代碼不規范,sftp連接在第一次初始化之后就不需要再在方法層里執行初始化了,這不但加劇了資源的消耗,而且由於在業務層有嵌套try,最里面拋出異常未將第一個連接釋放就再次執行初始化,導致未釋放的sftp原來越多。
將下載業務代碼里的init()和disconnect()方法去掉,把sftp的連接和斷開只交給上一層的業務層進行控制。
修改后的下載業務代碼如下:
/*************下載業務代碼,只專注業務 *****************/
附:問題排查過程中部分命令如下:
