背景
在生产过程中,经常需要SSH到远程服务器执行命令或文件上传。 存在以下几个问题:
- SSH Session需要频繁创建销毁,IO开销不容忽视
- 大部分服务器对于SSHD Session连接存在限制,不能无限制增加
针对以上的问题,我们抽取出我们的需求以及对应的实现方式:
- JAVA能够执行SSH操作。
- 能够控制每个服务器的SSHD Session的连接总数
- 能够实现SSH Session的复用,提升性能,减少IO开销
- 能够支持Springboot
🔥 我开发了一个支持SSH的SpringBoot Starter:ssh-spring-boot-starter 欢迎大家使用,提issue
实现
JAVA能够实现对SSH操作比较简单,JSch组件即可支持。自定义开发SpringBoot Starter即能够支持SpringBoot。
最核心的是能够实现SSH Session的池化,刚好业界有一个比较著名的对象池-- Apache Commons Pool2(下文将简称为ACP). 著名的Jedis和activemq都使用了该技术来实现对象池。
关于ACP源码以及原理,本文不赘述,大家可以Google,相关的文章和讲解非常多。本文只讲解使用。
首先第一步,因为我们需要针对每一个服务器来做Session池化,所以我们选用KeyedPooledObjectFactory<K,V>来做池化工厂。其中K表示Session的连接信息,V代表Session。
我们创建了一个SshSession类来作为对象池的Key,包含有SSH连接的最基本的信息,每个KEY都具有一个对象池。比如说A服务器B用户的SSH Session连接存在一个单独的对象池。
@Data
public class SshSession {
private String ip;
private int port;
private String account;
private String password;
@Override
public String toString() {
return "[" + account + "@" + ip + ":" + port + "]";
}
}
另外,对于对象池的Value,我并没有使用JSch的Session,而是使用了Session Wrapper,叫做SshSessionHolder,作为Session操作的封装,目的是为了后续新版本中能够将SSH实现做成SPI插件化。
public class SshSessionHolder {
private Session session;
private final SshSession sshHost;
private final String id;
public SshSessionHolder(SshSession sshHost) {
this.sshHost = sshHost;
this.id = UUID.randomUUID().toString();
this.session = null;
}
public void connect() throws JSchException {
this.connect(DEFAULT_CONNECT_TIMEOUT);
}
public void connect(int timeoutMills) throws JSchException {
JSch jSch = new JSch();
this.session = jSch.getSession(sshHost.getAccount(), sshHost.getIp(), sshHost.getPort());
this.session.setTimeout(timeoutMills);
this.session.setConfig("StrictHostKeyChecking", "no");
// other authorization methods can be considered in the future
this.session.setPassword(sshHost.getPassword());
this.session.connect();
logger.info("Connected to ssh session: {}, session id: {}", sshHost.toString(), id);
}
public void disconnect() {
if (session != null) {
session.disconnect();
}
}
public boolean isConnected() {
return session.isConnected();
}
public void keepAlive() throws Exception {
if (session != null) {
session.sendKeepAliveMsg();
}
}
public SshResponse execCommand(String command) {
......
}
}
紧接着,我们需要定义好对象池的基本实现工厂:
public class PoolSshSessionFactory extends BaseKeyedPooledObjectFactory<SshSession, SshSessionHolder> {
private static final Logger logger = LoggerFactory.getLogger(PoolSshSessionFactory.class);
@Override
public SshSessionHolder create(SshSession sessionHost) throws Exception {
SshSessionHolder pooledObject = new SshSessionHolder(sessionHost);
pooledObject.connect();
return pooledObject;
}
@Override
public PooledObject<SshSessionHolder> wrap(SshSessionHolder sshSessionHolder) {
return new DefaultPooledObject<>(sshSessionHolder);
}
@Override
public void destroyObject(SshSession key, PooledObject<SshSessionHolder> p,
DestroyMode destroyMode) {
logger.info("destroy session {}", p.getObject().toString());
p.getObject().disconnect();
}
@Override
public boolean validateObject(SshSession key, PooledObject<SshSessionHolder> p) {
if (p.getObject().isConnected()) {
try {
p.getObject().keepAlive();
return true;
} catch (Exception e) {
logger.error("Cannot send alive msg to session of {}", p.getObject().toString(), e);
}
}
return false;
}
}
最后就是对象池类了:
public class SshSessionPool {
private static final Logger logger = LoggerFactory.getLogger(SshSessionPool.class);
private volatile GenericKeyedObjectPool<SshSession, SshSessionHolder> sessionPool = null;
private GenericKeyedObjectPoolConfig<SshSessionHolder> poolConfig;
private AbandonedConfig abandonedConfig;
private SftpConfig sftpConfig;
public SshSessionPool(GenericKeyedObjectPoolConfig<SshSessionHolder> poolConfig, AbandonedConfig abandonedConfig,
SftpConfig sftpConfig) {
this.poolConfig = poolConfig;
this.abandonedConfig = abandonedConfig;
this.sftpConfig = sftpConfig;
}
public GenericKeyedObjectPool<SshSession, SshSessionHolder> getSessionPool() {
if (sessionPool == null) {
synchronized (SshSessionPool.class) {
if (sessionPool == null) {
sessionPool =
new GenericKeyedObjectPool<>(new PoolSshSessionFactory(), poolConfig, abandonedConfig);
}
}
}
return sessionPool;
}
public SshSessionHolder getSessionHolder(SshSession sessionHost) throws Exception {
logger.info("try to borrow a session:{}", sessionHost.toString());
SshSessionHolder holder = getSessionPool().borrowObject(sessionHost);
holder.setSftpConfig(sftpConfig);
return holder;
}
public void returnSshSessionHolder(SshSession sessionHost, SshSessionHolder sessionHolder) {
logger.info("return session:{}", sessionHost.toString());
getSessionPool().returnObject(sessionHost, sessionHolder);
}
public void printPoolStatus() {
Map<String, Integer> activeKeyMap = sessionPool.getNumActivePerKey();
for (Map.Entry<String, Integer> key : activeKeyMap.entrySet()) {
String hostName = key.getKey();
logger.info("Session Pool Stat: Key :{}, Active session count: {}, Total: {}", hostName, key.getValue(),
sessionPool.getMaxTotalPerKey());
}
logger.info("Session Pool Stat: Active session count: {}, Idle session : {}, Wait session: {} , Total: {}",
sessionPool.getNumActive(), sessionPool.getNumIdle(), sessionPool.getNumWaiters(),
sessionPool.getMaxTotal());
}
public void setPoolConfig(GenericKeyedObjectPoolConfig<SshSessionHolder> poolConfig) {
this.poolConfig = poolConfig;
}
public void setAbandonedConfig(AbandonedConfig abandonedConfig) {
this.abandonedConfig = abandonedConfig;
}
public SftpConfig getSftpConfig() {
return sftpConfig;
}
public void setSftpConfig(SftpConfig sftpConfig) {
this.sftpConfig = sftpConfig;
}
}
其实本来想对每部分都详细讲解以下,但是后来还是觉得这些东西实在就是ACP的样板代码,网上太多了,就不干扰大家的视线了,如果大家对于这部分存在疑问,欢迎在Github项目中提出issues.该仓库我会持续维护。