package cn.iocoder.yudao.module.mes.framework.qrexport;
|
|
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PreDestroy;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Component;
|
|
import java.lang.management.ManagementFactory;
|
import java.util.ArrayDeque;
|
import java.util.Deque;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* 系统 CPU 使用率监控
|
* <p>
|
* 用 JDK 自带的 {@code com.sun.management.OperatingSystemMXBean#getCpuLoad()} 取值,不引入额外依赖。
|
* 之所以要单独起采样线程,是因为该接口返回的是「距上次调用之间」的平均值:若只在提交导出时调用一次,
|
* 采样区间可能长达数小时,会把当前的高负载尖峰平滑掉,从而放过本该拒绝的请求。固定周期采样后,
|
* 提交闸门与运行节流读到的都是最近数秒的真实负载。
|
* <p>
|
* 三个必须处理的边界:
|
* <ol>
|
* <li>首次采样无效 —— 该接口需要前后两次采样才能算出结果,故取最近若干次的滑动平均,避免抖动误判;</li>
|
* <li>取不到值 —— 返回负数时视为「未知」,由调用方按放行处理,绝不能因为读不到指标就阻断业务;</li>
|
* <li>容器部署 —— 该指标反映的是宿主机而非容器配额,若部署在带 CPU limit 的容器中会失真,
|
* 此时需改为 cgroup 感知的口径。</li>
|
* </ol>
|
*/
|
@Slf4j
|
@Component
|
public class SystemCpuLoadMonitor {
|
|
/**
|
* 采样周期(秒):足够短以反映当前负载,足够长以避免采样本身带来开销
|
*/
|
private static final long SAMPLE_PERIOD_SECONDS = 2L;
|
|
/**
|
* 滑动平均窗口大小:取最近 3 次采样的均值,抹掉单次抖动
|
*/
|
private static final int WINDOW_SIZE = 3;
|
|
/**
|
* 采样线程名,便于线上排查
|
*/
|
private static final String THREAD_NAME = "mes-cpu-load-monitor";
|
|
/**
|
* 系统 CPU 探针;当前 JVM 不支持时保持为 {@code null}
|
*/
|
private final com.sun.management.OperatingSystemMXBean osBean = resolveOsBean();
|
|
private final Deque<Double> samples = new ArrayDeque<>(WINDOW_SIZE);
|
|
private ScheduledExecutorService executor;
|
|
@PostConstruct
|
public void start() {
|
if (osBean == null) {
|
log.warn("[start][当前 JVM 不支持系统 CPU 采样,大批量二维码导出的 CPU 闸门将失效(按放行处理)]");
|
return;
|
}
|
executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
Thread thread = new Thread(runnable, THREAD_NAME);
|
thread.setDaemon(true);
|
return thread;
|
});
|
executor.scheduleWithFixedDelay(this::sampleQuietly, SAMPLE_PERIOD_SECONDS, SAMPLE_PERIOD_SECONDS,
|
TimeUnit.SECONDS);
|
}
|
|
@PreDestroy
|
public void stop() {
|
if (executor != null) {
|
executor.shutdownNow();
|
}
|
}
|
|
/**
|
* 获取系统 CPU 使用率(最近若干次采样的滑动平均)
|
*
|
* @return 0~1 之间表示占用率;返回负数表示当前环境无法取值(调用方应按放行处理)
|
*/
|
public double getCpuLoad() {
|
synchronized (samples) {
|
if (samples.isEmpty()) {
|
// 采样线程尚未产出结果(如服务刚启动):同步取一次,拿不到就交给调用方按放行处理
|
double current = sample();
|
return current;
|
}
|
double sum = 0D;
|
for (Double value : samples) {
|
sum += value;
|
}
|
return sum / samples.size();
|
}
|
}
|
|
/**
|
* 是否可采集系统 CPU(不可采集时上层应跳过硬闸门)
|
*/
|
public boolean isAvailable() {
|
return osBean != null;
|
}
|
|
private void sampleQuietly() {
|
try {
|
double value = sample();
|
if (value < 0) {
|
return;
|
}
|
synchronized (samples) {
|
if (samples.size() >= WINDOW_SIZE) {
|
samples.pollFirst();
|
}
|
samples.addLast(value);
|
}
|
} catch (Exception e) {
|
// 采样失败不应影响任何业务,仅记录一次
|
log.warn("[sampleQuietly][系统 CPU 采样失败]", e);
|
}
|
}
|
|
/**
|
* 采样一次系统 CPU 使用率
|
*
|
* @return 0~1 表示占用率;负数表示不可用
|
*/
|
private double sample() {
|
if (osBean == null) {
|
return -1D;
|
}
|
double value = osBean.getCpuLoad();
|
// 该接口在不可用或首次采样时返回负数,统一按「未知」处理
|
return value < 0 ? -1D : Math.min(value, 1D);
|
}
|
|
private static com.sun.management.OperatingSystemMXBean resolveOsBean() {
|
java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
|
if (bean instanceof com.sun.management.OperatingSystemMXBean operatingSystemMXBean) {
|
return operatingSystemMXBean;
|
}
|
return null;
|
}
|
|
}
|