liu
10 小时以前 66517edf1de22c8203daa256abffddae43bae704
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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;
    }
 
}