基于ACP实现SSH Session池化的Spring Boot Starter

1,635 阅读3分钟

背景

在生产过程中,经常需要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). 著名的Jedisactivemq都使用了该技术来实现对象池。
关于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.该仓库我会持续维护。