线上机器 CPU 飙到 90%,接口还没开始慢,报警先炸了。
这种监控我一般不喜欢一上来就接 Prometheus、Grafana、Node Exporter。不是它们不好,是有些小系统、内部工具、单机服务,先把 CPU、内存、磁盘打出来就够用。
SpringBoot 3 里集成 Hutool 的OshiUtil,这事确实很省。
真正拿系统信息,一行代码就行:
GlobalMemory memory = OshiUtil.getMemory();
但真放到项目里,不能只写这一行。否则你最多是“能看”,不是“能用”。
依赖先加上。
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.35</version>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.6.5</version>
这里多说一句,OshiUtil底层用的是 OSHI。只引 Hutool 有时候代码能编译,运行到系统信息采集时不一定舒服,所以我一般会把oshi-core明确放进去,省得环境一换开始扯皮。
先写一个返回对象,别直接把 OSHI 原对象往外吐。那玩意字段太杂,接口一暴露,前端看了也烦。
public record MachineSnapshot(
double cpuUsedPercent,
long memoryTotalMb,
long memoryUsedMb,
double memoryUsedPercent,
List<DiskSnapshot> disks
) {
}
public record DiskSnapshot(
String name,
String mount,
long totalGb,
long usedGb,
double usedPercent
) {
}
采集逻辑我会单独放一个 Service。这个类别写太花,监控代码最怕“封装得很优雅,排查时看不懂”。
import cn.hutool.system.oshi.OshiUtil;
import org.springframework.stereotype.Service;
import oshi.hardware.CentralProcessor;
import oshi.hardware.GlobalMemory;
import oshi.software.os.OSFileStore;
import java.util.ArrayList;
import java.util.List;
@Service
publicclass MachineWatchService {
public MachineSnapshot snapshot() {
double cpu = readCpuUsedPercent();
GlobalMemory memory = OshiUtil.getMemory();
long total = memory.getTotal();
long available = memory.getAvailable();
long used = total - available;
List<DiskSnapshot> disks = new ArrayList<>();
for (OSFileStore store : OshiUtil.getOsFileStores()) {
long diskTotal = store.getTotalSpace();
long diskFree = store.getUsableSpace();
if (diskTotal <= 0) {
continue;
}
long diskUsed = diskTotal - diskFree;
disks.add(new DiskSnapshot(
store.getName(),
store.getMount(),
toGb(diskTotal),
toGb(diskUsed),
percent(diskUsed, diskTotal)
));
}
returnnew MachineSnapshot(
cpu,
toMb(total),
toMb(used),
percent(used, total),
disks
);
}
private double readCpuUsedPercent() {
CentralProcessor processor = OshiUtil.getProcessor();
long[] prevTicks = processor.getSystemCpuLoadTicks();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return0D;
}
return round(processor.getSystemCpuLoadBetweenTicks(prevTicks) * 100);
}
private long toMb(long bytes) {
return bytes / 1024 / 1024;
}
private long toGb(long bytes) {
return bytes / 1024 / 1024 / 1024;
}
private double percent(long used, long total) {
if (total <= 0) {
return0D;
}
return round(used * 100.0 / total);
}
private double round(double value) {
return Math.round(value * 100.0) / 100.0;
}
}
CPU 这里要注意一下。
不少人会直接取一个 CPU load,然后发现数值一会儿 0,一会儿又很怪。CPU 使用率不是内存那种“当前值”,它通常要靠两次 tick 差值算出来。
所以我这里停了 300ms。
这 300ms 看着有点土,但现场排障时我宁愿它土一点,也不想拿一个忽高忽低的 CPU 指标去误判。
再补一个接口。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
publicclass MachineWatchController {
privatefinal MachineWatchService watchService;
public MachineWatchController(MachineWatchService watchService) {
this.watchService = watchService;
}
@GetMapping("/internal/machine/snapshot")
public MachineSnapshot snapshot() {
return watchService.snapshot();
}
}
启动后访问:
curl http://127.0.0.1:8080/internal/machine/snapshot
大概能看到这种结果:
{
"cpuUsedPercent": 18.34,
"memoryTotalMb": 16023,
"memoryUsedMb": 10441,
"memoryUsedPercent": 65.16,
"disks": [
{
"name": "Local Disk",
"mount": "/",
"totalGb": 200,
"usedGb": 143,
"usedPercent": 71.5
}
]
}
到这一步,只能叫“能查”。
我更习惯再加一个定时巡检日志。很多线上问题不是人主动点接口发现的,是日志里先露头。
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
publicclass MachineWatchJob {
privatefinal MachineWatchService watchService;
public MachineWatchJob(MachineWatchService watchService) {
this.watchService = watchService;
}
@Scheduled(fixedDelay = 30_000)
public void watch() {
MachineSnapshot snapshot = watchService.snapshot();
if (snapshot.cpuUsedPercent() >= 80) {
log.warn("machine cpu high, used={}%", snapshot.cpuUsedPercent());
}
if (snapshot.memoryUsedPercent() >= 85) {
log.warn("machine memory high, used={}%, usedMb={}, totalMb={}",
snapshot.memoryUsedPercent(),
snapshot.memoryUsedMb(),
snapshot.memoryTotalMb());
}
for (DiskSnapshot disk : snapshot.disks()) {
if (disk.usedPercent() >= 90) {
log.warn("machine disk high, mount={}, used={}%, usedGb={}, totalGb={}",
disk.mount(),
disk.usedPercent(),
disk.usedGb(),
disk.totalGb());
}
}
}
}
别忘了开定时任务。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class WatchApplication {
public static void main(String[] args) {
SpringApplication.run(WatchApplication.class, args);
}
}
这里有个坑,容器环境里尤其明显。
你在 Docker 里跑 SpringBoot,OshiUtil看到的可能是宿主机信息,也可能受 cgroup 限制影响。这个要看运行环境和 JDK、OSHI 版本。线上排查时别看到“内存 64G”就高兴,先确认这是不是容器自己的限制。
我一般会顺手打一下 JVM 内存,跟机器内存放在一起看。
Runtime rt = Runtime.getRuntime();
long jvmMax = rt.maxMemory() / 1024 / 1024;
long jvmTotal = rt.totalMemory() / 1024 / 1024;
long jvmFree = rt.freeMemory() / 1024 / 1024;
log.info("jvm memory, maxMb={}, totalMb={}, freeMb={}", jvmMax, jvmTotal, jvmFree);
因为很多时候机器内存还很富裕,Java 进程自己已经快顶到-Xmx了。
这俩不是一回事。
机器内存高,可能是整台机器有别的进程在吃。JVM 内存高,才更像是自己应用里对象堆积、缓存没控住、批量任务太猛。
磁盘也一样。
接口里看到/快满了,不代表一定是业务文件。先看日志目录,再看临时目录,再看 dump 文件。
常见命令我会直接贴到运维备注里:
df -h
du -sh /data/app/logs/*
du -sh /tmp/*
ls -lh *.hprof
这个小监控适合放在内部系统里,不适合裸奔暴露到公网。
至少加个路径隔离,比如/internal/**,再配网关白名单。机器信息虽然不算密码,但也不该谁都能看。
OshiUtil的好处就是快,CPU、内存、磁盘这些东西不用自己调系统命令,不用区分 Linux、Windows,也不用写一堆解析脚本。
但它不是完整监控平台。
小项目、内部工具、排障接口,用它很舒服。
真到了多实例、历史趋势、报警收敛、故障自愈,那还是该接 Prometheus 就接 Prometheus。别拿一把螺丝刀去拆发动机。