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 使用率监控
*
* 用 JDK 自带的 {@code com.sun.management.OperatingSystemMXBean#getCpuLoad()} 取值,不引入额外依赖。
* 之所以要单独起采样线程,是因为该接口返回的是「距上次调用之间」的平均值:若只在提交导出时调用一次,
* 采样区间可能长达数小时,会把当前的高负载尖峰平滑掉,从而放过本该拒绝的请求。固定周期采样后,
* 提交闸门与运行节流读到的都是最近数秒的真实负载。
*
* 三个必须处理的边界:
*
* - 首次采样无效 —— 该接口需要前后两次采样才能算出结果,故取最近若干次的滑动平均,避免抖动误判;
* - 取不到值 —— 返回负数时视为「未知」,由调用方按放行处理,绝不能因为读不到指标就阻断业务;
* - 容器部署 —— 该指标反映的是宿主机而非容器配额,若部署在带 CPU limit 的容器中会失真,
* 此时需改为 cgroup 感知的口径。
*
*/
@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 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;
}
}