diff --git a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java index ac8f554..c932e55 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java @@ -12,68 +12,144 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +/** + * Docker 工具类(连接池版本) + */ public class DockerUtil { - private static final int SSH_TIMEOUT = 3000; + private static final int SSH_TIMEOUT = 5000; + private static final int MAX_POOL_SIZE = 50; - private static String runCommand(MySftpAccounts account, String cmd) { - JSch jsch = new JSch(); - Session session = null; - ChannelExec channel = null; + /** 连接池:accountId -> Session */ + private static final Map sessionPool = new ConcurrentHashMap<>(); - try { - int port = account.getHostPort() == null ? 22 : account.getHostPort(); - session = jsch.getSession(account.getUsername(), account.getHostIp(), port); - session.setTimeout(SSH_TIMEOUT); + /** 被池化的 Session,包含创建时间和活跃标记 */ + private static class PooledSession { + Session session; + long createdAt; + volatile boolean inUse; // 防止并发误删 - // 认证 - if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) { - jsch.addIdentity("temp", account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null); + PooledSession(Session session) { + this.session = session; + this.createdAt = System.currentTimeMillis(); + this.inUse = false; + } + } + + /** + * 获取或创建 Session(线程安全,复用已有连接) + */ + private static PooledSession getSession(MySftpAccounts account) throws JSchException { + String key = account.getAccountId(); + + // 1. 检查现有连接:未使用 + 未断开 → 直接复用 + PooledSession pooled = sessionPool.get(key); + if (pooled != null && !pooled.inUse && pooled.session.isConnected()) { + // 超过30分钟没用的连接,视为过期,关闭重建 + if (System.currentTimeMillis() - pooled.createdAt > 30 * 60 * 1000) { + pooled.session.disconnect(); + pooled.session = null; } else { - session.setPassword(account.getPassword()); + return pooled; } + } - Hashtable config = new Hashtable<>(); - config.put("StrictHostKeyChecking", "no"); - session.setConfig(config); - session.connect(SSH_TIMEOUT); + // 2. 超过容量上限,先清理一个过期连接 + if (sessionPool.size() >= MAX_POOL_SIZE) { + sessionPool.entrySet().removeIf(e -> { + if (System.currentTimeMillis() - e.getValue().createdAt > 20 * 60 * 1000) { + e.getValue().session.disconnect(); + return true; + } + return false; + }); + } - // 执行命令 + // 3. 新建连接 + JSch jsch = new JSch(); + int port = account.getHostPort() == null ? 22 : account.getHostPort(); + Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port); + session.setTimeout(SSH_TIMEOUT); + + if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) { + jsch.addIdentity("key_" + key, + account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null); + } else { + session.setPassword(account.getPassword()); + } + + Hashtable config = new Hashtable<>(); + config.put("StrictHostKeyChecking", "no"); + session.setConfig(config); + session.connect(SSH_TIMEOUT); + + pooled = new PooledSession(session); + sessionPool.put(key, pooled); + return pooled; + } + + /** + * 执行单条命令(通过池化 Session) + */ + private static String runCommand(MySftpAccounts account, String cmd) { + PooledSession pooled = null; + ChannelExec channel = null; + try { + pooled = getSession(account); + pooled.inUse = true; + + Session session = pooled.session; channel = (ChannelExec) session.openChannel("exec"); + String command = account.getRootPath() != null && !account.getRootPath().isEmpty() - ? "cd " + account.getRootPath() + " && " + cmd - : cmd; + ? "cd " + account.getRootPath() + " && " + cmd + : cmd; channel.setCommand(command); InputStream in = channel.getInputStream(); - channel.connect(); + channel.connect(SSH_TIMEOUT); - // 读取输出 ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int len; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } - return out.toString(StandardCharsets.UTF_8).trim(); } catch (Exception e) { + // 连接异常,移除缓存,强制下次重建 + if (pooled != null) { + sessionPool.remove(account.getAccountId()); + if (pooled.session.isConnected()) { + pooled.session.disconnect(); + } + } return null; } finally { if (channel != null) channel.disconnect(); - if (session != null) session.disconnect(); + if (pooled != null) pooled.inUse = false; } } + // ────────────────────────────────────────────── + // 对外方法 + // ────────────────────────────────────────────── + + /** + * 列出容器(一次 SSH 连接获取所有信息) + */ public static List listContainers(MySftpAccounts accounts, boolean all) { List list = new ArrayList<>(); - String cmd = (all ? "docker ps -a" : "docker ps") - + " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\""; + String format = "\"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\""; + String cmd = (all ? "docker ps -a" : "docker ps") + " --format " + format; String output = runCommand(accounts, cmd); if (output == null || output.isBlank()) return list; + for (String line : output.split("\\R")) { if (line.isBlank()) continue; String[] arr = line.split("\\|"); @@ -112,7 +188,7 @@ public class DockerUtil { return res != null ? DockerResult.ok(res) : DockerResult.fail("获取日志失败"); } - public static DockerResult list(MySftpAccounts accounts, boolean all) { + public static DockerResult listRaw(MySftpAccounts accounts, boolean all) { String res = runCommand(accounts, all ? "docker ps -a" : "docker ps"); return res != null ? DockerResult.ok(res) : DockerResult.fail("获取列表失败"); } @@ -122,30 +198,60 @@ public class DockerUtil { return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败"); } - // 获取 CPU 使用率 - public static String getCpuUsage(MySftpAccounts accounts) { - // 1秒采样,输出纯数字百分比 - return runCommand(accounts, - "top -bn1 | grep 'Cpu(s)' | sed -n '1p' | awk '{printf \"%.1f\", 100 - $8}'"); - } - - // 获取内存使用率 - public static String getMemoryUsage(MySftpAccounts accounts) { - return runCommand(accounts, - "free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'"); - } - - // 获取磁盘使用率 - public static String getDiskUsage(MySftpAccounts accounts) { - return runCommand(accounts, - "df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'"); - } - + /** + * 系统状态:CPU + 内存 + 磁盘,三项一次 SSH 连接搞定 + */ public static SystemInfo systemInfo(MySftpAccounts accounts) { - SystemInfo systemInfo = new SystemInfo(); - systemInfo.setCpu(getCpuUsage(accounts)); - systemInfo.setMemory(getMemoryUsage(accounts)); - systemInfo.setDisk(getDiskUsage(accounts)); - return systemInfo; + String cmd = + "echo CPU:$(top -bn1 2>/dev/null | grep 'Cpu(s)' | awk '{printf \"%.1f\", 100-$8}') && " + + "echo MEM:$(free 2>/dev/null | grep Mem | awk '{printf \"%.1f\", $3/$2*100}') && " + + "echo DISK:$(df -h / 2>/dev/null | grep / | awk '{gsub(/%/,\"\"); print $5}')"; + + String output = runCommand(accounts, cmd); + SystemInfo info = new SystemInfo(); + if (output == null) { + info.setCpu("N/A"); + info.setMemory("N/A"); + info.setDisk("N/A"); + return info; + } + + for (String line : output.split("\\R")) { + if (line.startsWith("CPU:")) info.setCpu(line.substring(4).trim()); + else if (line.startsWith("MEM:")) info.setMemory(line.substring(4).trim()); + else if (line.startsWith("DISK:")) info.setDisk(line.substring(5).trim()); + } + return info; } -} \ No newline at end of file + + // ────────────────────────────────────────────── + // 连接管理 + // ────────────────────────────────────────────── + + /** + * 关闭指定账号的连接(账号删除或更新密码时调用) + */ + public static void closeSession(String accountId) { + PooledSession pooled = sessionPool.remove(accountId); + if (pooled != null && pooled.session.isConnected()) { + pooled.session.disconnect(); + } + } + + /** + * 清空所有连接池 + */ + public static void closeAll() { + sessionPool.values().forEach(p -> { + if (p.session.isConnected()) p.session.disconnect(); + }); + sessionPool.clear(); + } + + /** + * 获取当前池大小(调试用) + */ + public static int poolSize() { + return sessionPool.size(); + } +}