池化设计之SSH连接池设计

24 阅读4分钟

前言

池化设计的思想在软件行业使用很广泛,但是做为基层程序员一般使用最多的可能就是数据库连接池了,最近有项目使用ssh连接来频繁获取主机的资源使用情况,因此也考虑通过池化思想来复用连接,避免频繁创建连接。

连接池的实现

1、抽象连接池接口


/**
 * @program: lifeguard
 * @description: 连接池接口
 * @author: Cheng Zhi
 * @create: 2024-01-15 14:10
 **/
public interface IPool<T> extends Closeable {

    /**
     * 获取连接
     * @return
     * @throws Exception
     */
    public T poll();

    /**
     * 归还连接
     * @param conn
     */
    public void offer(T conn);

    /**
     * 查询状态
     * @return
     */
    public PoolStatus getStatus();

    /**
     * 获得数据源
     * @return
     */
    Object getDatasource();

    /**
     * 关闭多余的连接
     */
    public void closeConnectionTillMin();

    /**
     * 关闭连接池,包括释放其中全部的连接
     */
    void close();
}

2、连接池实现

package com.ai.lifeguard.common.ssh;

import com.ai.lifeguard.common.IPool;
import com.ai.lifeguard.common.PoolReleaseWorker;
import com.ai.lifeguard.common.PoolStatus;
import com.ai.lifeguard.utils.CommonSshClient;
import org.apache.sshd.client.SshClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: lifeguard
 * @description: ssh连接池
 * @author: Cheng Zhi
 * @create: 2024-01-15 17:29
 **/
public class SshConnectionPool implements IPool<SshConnection> {

    Logger logger = LoggerFactory.getLogger(SshConnectionPool.class);

    private SshClient sshClient;

    private SshSource sshSource;
    /**
     * 最大连接数
     */
    private int max;

    /**
     * 最小连接数
     */
    private int min;

    /**
     * 被取走的连接数
     */
    private AtomicInteger used = new AtomicInteger();

    /**
     * 空闲连接数
     */
    private final BlockingQueue<SshConnection> freeConns;

    /**
     * 连接池是否被关闭
     */
    private boolean isClosed = false;

    public SshConnectionPool(int min, int max, SshSource sshSource) {
        this.min = min;
        this.max = max;
        this.sshSource = sshSource;
        this.sshClient = CommonSshClient.getSshClient();
        this.freeConns = new LinkedBlockingQueue<SshConnection>(max);
        PoolReleaseWorker.getInstance().addPool(this);
    }

    @Override
    public SshConnection poll() {
        try {
            SshConnection conn = freeConns.poll();
            if(conn!=null){
                used.incrementAndGet(); // 计数器自增
                conn = ensureOpen(conn);
                return conn;
            }
            if (used.get() < max) {// 尝试用新连接
                used.incrementAndGet(); // 必须立刻累加计数器,否则并发的线程会立刻抢先创建对象,从而超出连接池限制
                conn = new SshConnection(sshClient.connect(sshSource.getUserName(), sshSource.getHost(), sshSource.getPort()).verify(sshSource.getTimeOut(), TimeUnit.MILLISECONDS).getSession(), this);
                conn.addPasswordIdentity(sshSource.getPassWord());
                if (!conn.auth().verify(sshSource.getTimeOut(), TimeUnit.MILLISECONDS).isSuccess()) {
                    conn.close();
                    throw new IllegalArgumentException("ssh auth failed.");
                }
            } else {
                used.incrementAndGet(); // 提前计数,并发下为了严格阻止连接池超出上限,必须这样做
                conn = freeConns.poll(5000000000L, TimeUnit.NANOSECONDS);// 5秒
                if (conn == null) {
                    used.decrementAndGet();// 回滚计数器
                    throw new RuntimeException("No connection avaliable now." + getStatus());
                }
                conn = ensureOpen(conn);
            }
            return conn;
        } catch (Exception e) {
            logger.error("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 检查连接是否可用,如果不可以则新建
     * @param conn
     * @return
     * @throws RuntimeException
     */
    private SshConnection ensureOpen(SshConnection conn) throws RuntimeException {
        boolean closed = true;
        try {
            closed = conn.isClosed();
        } catch (Exception e) {
            conn.closePhysical();
        }
        if (closed) {
            try {
                conn = new SshConnection(sshClient.connect(sshSource.getUserName(), sshSource.getHost(), sshSource.getPort()).verify(sshSource.getTimeOut(), TimeUnit.MILLISECONDS).getSession(), this);
                conn.addPasswordIdentity(sshSource.getPassWord());
            } catch (IOException e) {
                logger.error("ssh会话创建:", e);
            }
            return conn;
        } else {
            return conn;
        }
    }


    @Override
    public void offer(SshConnection conn) {
        if (isClosed) {
            // 如果连接池已经被关闭,该连接意味着无家可归,应及时关闭
            conn.closePhysical();
        }
        boolean success = freeConns.offer(conn);
        if (!success) {
            // 归还连接失败,关闭连接
            conn.closePhysical();
        }
        used.decrementAndGet();
    }

    @Override
    public PoolStatus getStatus() {
        int used = this.used.get();
        int free = freeConns.size();
        return new PoolStatus(max, min, used + free, used, free);
    }

    @Override
    public SshSource getDatasource() {
        return this.sshSource;
    }

    @Override
    public void closeConnectionTillMin() {
        if (freeConns.size() > min) {
            SshConnection conn;
            while (freeConns.size() > min && (conn = freeConns.poll()) != null) {
                logger.debug("释放空闲线程:" + conn.toString());
                conn.closePhysical();
            }
        }
    }

    @Override
    public void close() {
        max = 0;
        min = 0;
        closeConnectionTillMin();
        PoolReleaseWorker.getInstance().removePool(this);
        this.isClosed = true;
    }

    @Override
    public void closePhysical() {
        close();
    }

    @Override
    protected void finalize() throws Throwable {
        this.close();
        super.finalize();
    }
}

连接池自动缩容

池,一般都会设计成可以自动根据使用需求来进行自动扩容和缩容,扩容的实现一般相对简单,比如上面的代码中当前连接不够用且没有达到指定的最大值时即可继续扩容。而缩容则相对要复杂一些,因为连接池本身并不知道什么时候是缩容的最佳时机,如果在归还连接时将立即将超出核心连接数的多余连接关闭的话,由于此时并不能确定连接够不够用,如果不够用还要继续扩容,这样的话连接池的设计就没有意义了,因此这里新开一个线程专门进行线程的收缩和资源释放。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * @program: lifeguard
 * @description: 负责释放大于核心连接数的连接
 * @author: Cheng Zhi
 * @create: 2024-01-15 17:13
 **/
public class PoolReleaseWorker extends Thread {

    Logger logger = LoggerFactory.getLogger(PoolReleaseWorker.class);

    private static PoolReleaseWorker prw = new PoolReleaseWorker();

    /**
     * 保存所有的连接池,可以是jdbc连接池,也可以是ssh连接池
     */
    private final ConcurrentLinkedQueue<IPool<?>> pools = new ConcurrentLinkedQueue<IPool<?>>();

    private boolean alive = true;

    /**
     * 清除多余连接线程运行间隔时间
     */
    private static final int SLEEP_TIME = 60 * 1000;

    private PoolReleaseWorker() {
        super("pool-release-worker");
        setDaemon(true);
    }

    public void addPool(IPool<?> ip) {
        pools.add(ip);
        synchronized (this) {
            if (!isAlive() && alive) {
                start();
            }
        }
    }

    public void removePool(IPool<?> ip) {
        pools.remove(ip);

    }

    public static PoolReleaseWorker getInstance() {
        return prw;
    }

    public void close() {
        this.alive = false;
    }

    @Override
    public void run() {
        ThreadUtils.doSleep(SLEEP_TIME);
        try {
            while (alive) {
                for (IPool<?> pool : pools) {
                    try {
                        pool.closeConnectionTillMin();
                    } catch (Exception e) {
                        logger.error("release connecton pool error", e);
                    }
                }
                ThreadUtils.doSleep(SLEEP_TIME);
            }
        } catch (Exception e) {
            logger.error("释放连接异常", e);
        }
    }
}

总结

之前写过一篇介绍jdbc连接池的文章,最近正好用到ssh连接,复用之前的设计,如上就是一个ssh连接池的设计,其中sshConnection主要是利用org.apache.sshd.client.session.ClientSession包装一下设计成的,实践证明ssh也可以使用连接池来管控。