diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/util/ContainerMetricsUtil.java b/jeecg-boot-base-core/src/main/java/org/jeecg/util/ContainerMetricsUtil.java new file mode 100644 index 0000000..6cb616b --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/util/ContainerMetricsUtil.java @@ -0,0 +1,227 @@ +package org.jeecg.util; + +import lombok.extern.slf4j.Slf4j; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +public class ContainerMetricsUtil { + + // 缓存上一次 CPU 使用数据(用于计算使用率) + private static final Map LAST_CPU_USAGE = new ConcurrentHashMap<>(); + private static final Map LAST_CPU_TIME = new ConcurrentHashMap<>(); + + // 判断是否为 cgroup v2 + private static boolean isCgroupV2() { + return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers")); + } + + // 通用读取第一行 + private static String readFirstLine(String path) throws IOException { + java.nio.file.Path p = Paths.get(path); + if (!Files.exists(p)) { + throw new IOException("文件不存在: " + path); + } + List lines = Files.readAllLines(p); + if (lines.isEmpty()) { + throw new IOException("文件为空: " + path); + } + return lines.get(0).trim(); + } + + // ======================== + // 内存指标 + // ======================== + + public static long getMemoryLimitBytes() { + try { + if (isCgroupV2()) { + String max = readFirstLine("/sys/fs/cgroup/memory.max"); + if ("max".equals(max)) return -1; // 无限制 + return Long.parseLong(max); + } else { + long limit = Long.parseLong(readFirstLine("/sys/fs/cgroup/memory/memory.limit_in_bytes")); + // 过滤“无限制”的极大值(如 9223372036854771712) + return (limit > (1L << 40)) ? -1 : limit; + } + } catch (Exception e) { + log.debug("无法读取内存限制", e); + return -1; + } + } + + public static long getMemoryUsageBytes() { + try { + if (isCgroupV2()) { + return Long.parseLong(readFirstLine("/sys/fs/cgroup/memory.current")); + } else { + return Long.parseLong(readFirstLine("/sys/fs/cgroup/memory/memory.usage_in_bytes")); + } + } catch (Exception e) { + log.debug("无法读取内存使用量", e); + return -1; + } + } + + // ======================== + // CPU 指标 + // ======================== + + /** + * 获取 CPU 限制(核数),-1 表示无限制 + */ + public static double getCpuLimitCores() { + try { + if (isCgroupV2()) { + String cpuMax = readFirstLine("/sys/fs/cgroup/cpu.max"); + String[] parts = cpuMax.split(" "); + if ("max".equals(parts[0])) return -1; + long quota = Long.parseLong(parts[0]); + long period = Long.parseLong(parts[1]); + return (double) quota / period; + } else { + long quota = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")); + if (quota == -1) return -1; + long period = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpu.cfs_period_us")); + return (double) quota / period; + } + } catch (Exception e) { + log.debug("无法读取 CPU 限制", e); + return -1; + } + } + + /** + * 获取 CPU 使用率(百分比,单核基准) + * 首次调用返回 0,后续调用返回基于时间差的使用率 + */ + public static double getCpuUsagePercent() { + try { + long currentUsage; + if (isCgroupV2()) { + // 读取 /sys/fs/cgroup/cpu.stat + String content = readFirstLine("/sys/fs/cgroup/cpu.stat"); + String[] lines = content.split("\n"); + long usageUsec = -1; + for (String line : lines) { + if (line.startsWith("usage_usec")) { + String[] parts = line.trim().split("\\s+"); + if (parts.length >= 2) { + usageUsec = Long.parseLong(parts[1]); + break; + } + } + } + if (usageUsec == -1) { + log.warn("cgroup v2: 未找到 usage_usec,无法计算 CPU 使用率"); + return -1; + } + currentUsage = usageUsec * 1000L; // 转为纳秒 + } else { + // cgroup v1 + currentUsage = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpuacct.usage")); + } + + long currentTime = System.nanoTime(); + String key = isCgroupV2() ? "v2" : "v1"; + + Long lastUsage = LAST_CPU_USAGE.get(key); + Long lastTime = LAST_CPU_TIME.get(key); + + if (lastUsage == null || lastTime == null) { + LAST_CPU_USAGE.put(key, currentUsage); + LAST_CPU_TIME.put(key, currentTime); + return 0.0; + } + + long usageDelta = currentUsage - lastUsage; + long timeDeltaNs = currentTime - lastTime; + + if (timeDeltaNs <= 0) { + return 0.0; + } + + double cpuPercent = (usageDelta * 100.0) / timeDeltaNs; + + // 归一化(可选) + double limit = getCpuLimitCores(); + if (limit > 0 && limit < 1.0) { + cpuPercent = Math.min(cpuPercent / limit, 100.0); + } else { + cpuPercent = Math.min(cpuPercent, 100.0); + } + + LAST_CPU_USAGE.put(key, currentUsage); + LAST_CPU_TIME.put(key, currentTime); + + return Math.max(0.0, cpuPercent); + + } catch (Exception e) { + log.debug("计算 CPU 使用率失败", e); + return -1; + } + } + + // ======================== + // 综合方法:返回所有指标 + // ======================== + + public static Map getContainerMetrics() { + Map metrics = new HashMap<>(); + + // 内存 + long memLimit = getMemoryLimitBytes(); + long memUsage = getMemoryUsageBytes(); + metrics.put("memoryLimitBytes", memLimit > 0 ? memLimit : null); + metrics.put("memoryUsedBytes", memUsage > 0 ? memUsage : null); + if (memLimit > 0 && memUsage > 0) { + double memPercent = Math.round((memUsage * 100.0 / memLimit) * 100) / 100.0; + metrics.put("memoryPercent", memPercent); + } + + // CPU + double cpuLimit = getCpuLimitCores(); + double cpuUsage = getCpuUsagePercent(); + metrics.put("cpuLimitCores", cpuLimit > 0 ? cpuLimit : null); + metrics.put("cpuUsagePercent", cpuUsage >= 0 ? cpuUsage : null); + + // JVM 堆内存 + long heapUsed = java.lang.management.ManagementFactory.getMemoryMXBean() + .getHeapMemoryUsage().getUsed(); + metrics.put("jvmHeapUsedBytes", heapUsed); + + return metrics; + } + + // 辅助:格式化字节(仅用于日志,不放入返回值) + private static String formatBytes(long bytes) { + if (bytes <= 0) return "0 B"; + if (bytes < 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); + } + } + + // 便捷方法:记录日志 + 返回数据 + public static Map logAndCollectMetrics() { + Map m = getContainerMetrics(); + log.info( + "容器资源 | 内存: {} / {} ({}%) | CPU: {}% (限制: {}核) | JVM堆: {}", + m.get("memoryUsedBytes") != null ? formatBytes((Long) m.get("memoryUsedBytes")) : "N/A", + m.get("memoryLimitBytes") != null ? formatBytes((Long) m.get("memoryLimitBytes")) : "N/A", + m.get("memoryPercent"), + m.get("cpuUsagePercent"), + m.get("cpuLimitCores"), + formatBytes((Long) m.get("jvmHeapUsedBytes")) + ); + return m; + } +} \ No newline at end of file diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/util/SystemInfoUtil.java b/jeecg-boot-base-core/src/main/java/org/jeecg/util/SystemInfoUtil.java new file mode 100644 index 0000000..169b760 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/util/SystemInfoUtil.java @@ -0,0 +1,102 @@ +package org.jeecg.util; + +import com.sun.management.OperatingSystemMXBean; +import lombok.extern.slf4j.Slf4j; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +public class SystemInfoUtil { + + /** + * 打印当前 Java 进程在容器中的资源使用情况(CPU、内存等) + * 适用于 Docker/Kubernetes 环境(需 JDK 8u191+ 或 JDK 10+) + */ + /** + * 获取并可选打印当前 Java 进程在容器中的资源使用情况 + * + * @param logEnabled 是否同时输出日志(true=打印日志,false=仅返回数据) + * @return 包含资源指标的 Map + */ + public static Map getContainerMetrics(boolean logEnabled) { + Map metrics = new HashMap<>(); + try { + OperatingSystemMXBean osBean = (OperatingSystemMXBean) + ManagementFactory.getOperatingSystemMXBean(); + + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage heap = memoryBean.getHeapMemoryUsage(); + MemoryUsage nonHeap = memoryBean.getNonHeapMemoryUsage(); + + // CPU 使用率(百分比) + double processCpuLoad = osBean.getProcessCpuLoad(); + Double cpuPercent = (!Double.isNaN(processCpuLoad) && processCpuLoad >= 0) + ? Math.round(processCpuLoad * 100 * 100.0) / 100.0 // 保留两位小数 + : null; + metrics.put("cpuPercent", cpuPercent); + + // 容器内存(关键:受 Docker -m 限制) + long totalMem = osBean.getTotalPhysicalMemorySize(); // 容器总内存限制 + long freeMem = osBean.getFreePhysicalMemorySize(); + long usedMem = totalMem - freeMem; + metrics.put("memoryTotalBytes", totalMem > 0 ? totalMem : null); + metrics.put("memoryUsedBytes", totalMem > 0 ? usedMem : null); + Double memPercent = (totalMem > 0) + ? Math.round((usedMem * 100.0 / totalMem) * 100.0) / 100.0 + : null; + metrics.put("memoryPercent", memPercent); + + // JVM 内存 + metrics.put("jvmHeapUsedBytes", heap.getUsed()); + metrics.put("jvmNonHeapUsedBytes", nonHeap.getUsed()); + + // 进程运行时间(毫秒) + long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); + metrics.put("processUptimeMs", uptime); + + // 可选:格式化后的字符串(便于日志展示) + String cpuStr = cpuPercent != null ? String.format("%.2f%%", cpuPercent) : "N/A"; + String memTotalStr = formatBytes(totalMem); + String memUsedStr = formatBytes(usedMem); + String memPercentStr = memPercent != null ? String.format("%.1f%%", memPercent) : "N/A"; + String heapStr = formatBytes(heap.getUsed()); + String nonHeapStr = formatBytes(nonHeap.getUsed()); + + if (logEnabled) { + log.info( + "容器资源 | CPU: {} | 内存: {} / {} ({}) | JVM堆: {} | JVM非堆: {}", + cpuStr, memUsedStr, memTotalStr, memPercentStr, heapStr, nonHeapStr + ); + } + + } catch (Exception e) { + log.warn("获取容器指标失败", e); + // 即使失败也返回空 map(或可根据需求抛异常) + return new HashMap<>(); + } + return metrics; + } + + /** + * 快捷方法:获取指标并自动记录日志 + */ + public static Map logContainerMetrics() { + return getContainerMetrics(true); + } + + // 辅助方法:字节转易读格式(仅用于日志字符串,不放入返回 map) + private static String formatBytes(long bytes) { + if (bytes <= 0) return "0 B"; + if (bytes < 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); + } + } +} \ No newline at end of file diff --git a/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/task/MonitorSystemJob.java b/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/task/MonitorSystemJob.java new file mode 100644 index 0000000..98e5f44 --- /dev/null +++ b/jeecg-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/task/MonitorSystemJob.java @@ -0,0 +1,40 @@ +package org.jeecg.modules.system.task; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.util.ContainerMetricsUtil; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +@Slf4j +public class MonitorSystemJob { + + @XxlJob(value = "systemInfoJob") + public ReturnT systemInfoJob(String time) { + try { +// Map metrics = SystemInfoUtil.logContainerMetrics(); +// // 使用 Spring 的 JSON 工具转为标准 JSON +// String jsonResult = new ObjectMapper().writeValueAsString(metrics); +// +// return new ReturnT<>(jsonResult); + // 第一次调用:初始化 CPU 计算(返回 0%) + ContainerMetricsUtil.logAndCollectMetrics(); + + // 等待 1 秒以获取有效 CPU 使用率 + Thread.sleep(1000); + + // 第二次调用:获取真实 CPU 使用率 + Map metrics = ContainerMetricsUtil.logAndCollectMetrics(); + + // 转为 JSON 返回给 XXL-JOB + String json = new ObjectMapper().writeValueAsString(metrics); + return new ReturnT<>(ReturnT.SUCCESS_CODE, json); + } catch (Exception e) { + return ReturnT.FAIL; + } + } +}