首页
学习
活动
专区
圈层
工具
发布

SpringBoot 3 集成 Hutool OshiUtil:一行代码搞定 CPU、内存、磁盘监控

线上机器 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。别拿一把螺丝刀去拆发动机。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OXgkLYd1PNG15AexWWfOBxbQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券