시스템 사용율 모니터링 기능 테스트 #163
@@ -1,21 +1,8 @@
|
|||||||
package com.kamco.cd.training.common.dto;
|
package com.kamco.cd.training.common.dto;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MonitorDto {
|
public class MonitorDto {
|
||||||
|
|
||||||
public int cpu; // 30초 평균 (%)
|
public int cpu; // CPU 사용률 (%)
|
||||||
public String memory; // "3.2/16GB"
|
public String memory; // "사용/전체"
|
||||||
public List<Gpu> gpus = new ArrayList<>();
|
public int gpu; // 🔥 전체 GPU 평균 (%)
|
||||||
|
|
||||||
public static class Gpu {
|
|
||||||
public int index;
|
|
||||||
public int util;
|
|
||||||
|
|
||||||
public Gpu(int index, int util) {
|
|
||||||
this.index = index;
|
|
||||||
this.util = util;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,31 +12,18 @@ import org.springframework.stereotype.Component;
|
|||||||
@Log4j2
|
@Log4j2
|
||||||
public class GpuDmonReader {
|
public class GpuDmonReader {
|
||||||
|
|
||||||
// =========================
|
// GPU index → 현재 util
|
||||||
// GPU 사용률 저장소
|
|
||||||
// - key: GPU index (0,1,2...)
|
|
||||||
// - value: 현재 GPU 사용률 (%)
|
|
||||||
// - ConcurrentHashMap → 멀티스레드 안전
|
|
||||||
// =========================
|
|
||||||
private final Map<Integer, Integer> gpuUtilMap = new ConcurrentHashMap<>();
|
private final Map<Integer, Integer> gpuUtilMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// nvidia-smi dmon 프로세스
|
// 외부 조회
|
||||||
// - 스트리밍으로 GPU 상태를 계속 받아옴
|
|
||||||
// =========================
|
|
||||||
private volatile Process process;
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 외부에서 GPU 사용률 조회
|
|
||||||
// =========================
|
// =========================
|
||||||
public Map<Integer, Integer> getGpuUtilMap() {
|
public Map<Integer, Integer> getGpuUtilMap() {
|
||||||
return gpuUtilMap;
|
return gpuUtilMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Bean 초기화 시 실행
|
// 시작
|
||||||
// - 별도 스레드에서 dmon 실행
|
|
||||||
// - 메인 스레드 block 방지
|
|
||||||
// =========================
|
// =========================
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void start() {
|
public void start() {
|
||||||
@@ -46,61 +33,52 @@ public class GpuDmonReader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread t = new Thread(this::runWithRestart, "gpu-dmon-thread");
|
Thread t = new Thread(this::runLoop, "gpu-dmon-thread");
|
||||||
t.setDaemon(true); // 서버 종료 시 같이 종료
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// dmon 실행 + 자동 재시작 루프
|
// 무한 루프 (자동 복구)
|
||||||
// - dmon이 죽어도 계속 재시작
|
|
||||||
// =========================
|
// =========================
|
||||||
private void runWithRestart() {
|
private void runLoop() {
|
||||||
|
|
||||||
boolean firstError = true;
|
boolean firstError = true;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
log.info("Starting nvidia-smi dmon...");
|
log.info("Starting nvidia-smi dmon...");
|
||||||
runDmon();
|
runDmon();
|
||||||
firstError = true; // 정상 실행되면 초기화
|
firstError = true;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
if (firstError) {
|
if (firstError) {
|
||||||
log.error("nvidia-smi not available. GPU monitoring disabled.", e);
|
log.error("nvidia-smi dmon failed.", e);
|
||||||
firstError = false;
|
firstError = false;
|
||||||
} else {
|
} else {
|
||||||
log.warn("dmon retry...");
|
log.warn("dmon retry...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
sleep(5000);
|
||||||
Thread.sleep(5000); // 5초 후에 시작
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// nvidia-smi dmon 실행 및 출력 파싱
|
// dmon 실행
|
||||||
// =========================
|
// =========================
|
||||||
private void runDmon() throws Exception {
|
private void runDmon() throws Exception {
|
||||||
|
|
||||||
// GPU utilization만 출력 (-s u)
|
|
||||||
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
|
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
|
||||||
|
|
||||||
process = pb.start();
|
try (BufferedReader br =
|
||||||
|
new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) {
|
||||||
|
|
||||||
// dmon은 stdout으로 계속 데이터를 뿌림 (스트리밍)
|
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
||||||
String line;
|
String line;
|
||||||
|
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) {
|
||||||
|
|
||||||
// 디버깅 로그
|
|
||||||
// log.info("RAW: [{}]", line);
|
|
||||||
|
|
||||||
// 헤더 제거
|
|
||||||
if (line.startsWith("#")) continue;
|
if (line.startsWith("#")) continue;
|
||||||
|
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
@@ -108,50 +86,25 @@ public class GpuDmonReader {
|
|||||||
|
|
||||||
String[] parts = line.split("\\s+");
|
String[] parts = line.split("\\s+");
|
||||||
|
|
||||||
// GPU index 확인
|
|
||||||
if (!parts[0].matches("\\d+")) continue;
|
if (!parts[0].matches("\\d+")) continue;
|
||||||
|
|
||||||
int index = Integer.parseInt(parts[0]);
|
int index = Integer.parseInt(parts[0]);
|
||||||
|
|
||||||
int util = 0;
|
|
||||||
try {
|
try {
|
||||||
util = Integer.parseInt(parts[1]); // sm 값
|
int util = Integer.parseInt(parts[1]);
|
||||||
} catch (Exception e) {
|
gpuUtilMap.put(index, util);
|
||||||
continue;
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
gpuUtilMap.put(index, util);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 여기 도달했다는 건 dmon 프로세스가 종료된 상태
|
// dmon 종료되면 예외 던져서 재시작
|
||||||
int exitCode = process.waitFor();
|
|
||||||
log.warn("dmon exited. code={}", exitCode);
|
|
||||||
|
|
||||||
// 상위 루프에서 재시작하도록 예외 발생
|
|
||||||
throw new IllegalStateException("dmon stopped");
|
throw new IllegalStateException("dmon stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// dmon 프로세스 살아있는지 확인
|
// util
|
||||||
// =========================
|
// =========================
|
||||||
public boolean isAlive() {
|
|
||||||
return process != null && process.isAlive();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// dmon 강제 재시작
|
|
||||||
// - 기존 프로세스 종료 → runWithRestart에서 자동 재시작
|
|
||||||
// =========================
|
|
||||||
public void restart() {
|
|
||||||
try {
|
|
||||||
if (process != null && process.isAlive()) {
|
|
||||||
process.destroy();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNvidiaAvailable() {
|
private boolean isNvidiaAvailable() {
|
||||||
try {
|
try {
|
||||||
Process p = new ProcessBuilder("which", "nvidia-smi").start();
|
Process p = new ProcessBuilder("which", "nvidia-smi").start();
|
||||||
@@ -160,4 +113,11 @@ public class GpuDmonReader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sleep(long ms) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(ms);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,54 +16,29 @@ import org.springframework.stereotype.Service;
|
|||||||
@Log4j2
|
@Log4j2
|
||||||
public class SystemMonitorService {
|
public class SystemMonitorService {
|
||||||
|
|
||||||
// =========================
|
|
||||||
// CPU 이전값 (누적값 → delta 계산용)
|
|
||||||
// =========================
|
|
||||||
private long prevIdle = 0;
|
private long prevIdle = 0;
|
||||||
private long prevTotal = 0;
|
private long prevTotal = 0;
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 최근 30초 히스토리
|
|
||||||
// - CPU: 30개 (1초 * 30)
|
|
||||||
// - GPU: GPU별 30개
|
|
||||||
// =========================
|
|
||||||
private final Deque<Double> cpuHistory = new ArrayDeque<>();
|
private final Deque<Double> cpuHistory = new ArrayDeque<>();
|
||||||
private final Map<Integer, Deque<Integer>> gpuHistory = new java.util.HashMap<>();
|
private final Map<Integer, Deque<Integer>> gpuHistory = new java.util.HashMap<>();
|
||||||
|
|
||||||
// =========================
|
|
||||||
// GPU dmon reader (스트리밍)
|
|
||||||
// =========================
|
|
||||||
private final GpuDmonReader gpuReader;
|
private final GpuDmonReader gpuReader;
|
||||||
|
|
||||||
// =========================
|
|
||||||
// 캐시 (API 응답용)
|
|
||||||
// - 매 요청마다 계산하지 않기 위해 사용
|
|
||||||
// - volatile로 동시성 안전 보장
|
|
||||||
// =========================
|
|
||||||
private volatile MonitorDto cached = new MonitorDto();
|
private volatile MonitorDto cached = new MonitorDto();
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// 1초마다 실행되는 수집 스케줄러
|
// 1초 수집
|
||||||
// =========================
|
// =========================
|
||||||
@Scheduled(fixedRate = 1000)
|
@Scheduled(fixedRate = 1000)
|
||||||
public void collect() {
|
public void collect() {
|
||||||
// 디버깅용
|
|
||||||
// log.info("collect instance = {}", this);
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// =====================
|
// CPU
|
||||||
// 1. CPU 수집
|
|
||||||
// =====================
|
|
||||||
double cpu = readCpu();
|
double cpu = readCpu();
|
||||||
|
|
||||||
cpuHistory.add(cpu);
|
cpuHistory.add(cpu);
|
||||||
|
|
||||||
// 30개 유지 (rolling window)
|
|
||||||
if (cpuHistory.size() > 30) cpuHistory.poll();
|
if (cpuHistory.size() > 30) cpuHistory.poll();
|
||||||
|
|
||||||
// =====================
|
// GPU
|
||||||
// 2. GPU (dmon 데이터 사용)
|
|
||||||
// =====================
|
|
||||||
Map<Integer, Integer> gpuMap = gpuReader.getGpuUtilMap();
|
Map<Integer, Integer> gpuMap = gpuReader.getGpuUtilMap();
|
||||||
|
|
||||||
for (Map.Entry<Integer, Integer> entry : gpuMap.entrySet()) {
|
for (Map.Entry<Integer, Integer> entry : gpuMap.entrySet()) {
|
||||||
@@ -76,10 +51,7 @@ public class SystemMonitorService {
|
|||||||
if (q.size() > 30) q.poll();
|
if (q.size() > 30) q.poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
updateCache();
|
||||||
// 3. 캐시 업데이트
|
|
||||||
// =====================
|
|
||||||
updateCache(gpuMap);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("collect error", e);
|
log.error("collect error", e);
|
||||||
@@ -87,7 +59,7 @@ public class SystemMonitorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// CPU 사용률 계산 (/proc/stat)
|
// CPU
|
||||||
// =========================
|
// =========================
|
||||||
private double readCpu() throws Exception {
|
private double readCpu() throws Exception {
|
||||||
|
|
||||||
@@ -95,8 +67,7 @@ public class SystemMonitorService {
|
|||||||
|
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader("/proc/stat"))) {
|
try (BufferedReader br = new BufferedReader(new FileReader("/proc/stat"))) {
|
||||||
|
|
||||||
String line = br.readLine();
|
String[] p = br.readLine().split("\\s+");
|
||||||
String[] p = line.split("\\s+");
|
|
||||||
|
|
||||||
long user = Long.parseLong(p[1]);
|
long user = Long.parseLong(p[1]);
|
||||||
long nice = Long.parseLong(p[2]);
|
long nice = Long.parseLong(p[2]);
|
||||||
@@ -124,10 +95,6 @@ public class SystemMonitorService {
|
|||||||
if (totalDiff == 0) return 0;
|
if (totalDiff == 0) return 0;
|
||||||
|
|
||||||
return (1.0 - (double) idleDiff / totalDiff) * 100;
|
return (1.0 - (double) idleDiff / totalDiff) * 100;
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("CPU read fail", e);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +103,11 @@ public class SystemMonitorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Memory (/proc/meminfo)
|
// Memory
|
||||||
// - 현재값 (평균 아님)
|
|
||||||
// =========================
|
// =========================
|
||||||
private String readMemory() throws Exception {
|
private String readMemory() throws Exception {
|
||||||
|
|
||||||
// linux 환경일때만 실행
|
if (!isLinux()) return "N/A";
|
||||||
if (!isLinux()) {
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"));
|
BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"));
|
||||||
|
|
||||||
@@ -164,7 +127,6 @@ public class SystemMonitorService {
|
|||||||
|
|
||||||
long used = total - available;
|
long used = total - available;
|
||||||
|
|
||||||
// kB → GB
|
|
||||||
double usedGB = used / (1024.0 * 1024);
|
double usedGB = used / (1024.0 * 1024);
|
||||||
double totalGB = total / (1024.0 * 1024);
|
double totalGB = total / (1024.0 * 1024);
|
||||||
|
|
||||||
@@ -172,43 +134,39 @@ public class SystemMonitorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// 캐시 업데이트
|
// 캐시 업데이트 (🔥 핵심)
|
||||||
// =========================
|
// =========================
|
||||||
private void updateCache(Map<Integer, Integer> gpuMap) throws Exception {
|
private void updateCache() throws Exception {
|
||||||
|
|
||||||
MonitorDto dto = new MonitorDto();
|
MonitorDto dto = new MonitorDto();
|
||||||
|
|
||||||
// =====================
|
// CPU 평균
|
||||||
// CPU 평균 (30초)
|
|
||||||
// =====================
|
|
||||||
dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
||||||
|
|
||||||
// =====================
|
// Memory
|
||||||
// Memory (현재값)
|
|
||||||
// =====================
|
|
||||||
dto.memory = readMemory();
|
dto.memory = readMemory();
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// GPU 평균 (30초)
|
// GPU 평균 (🔥 여기 중요)
|
||||||
// =====================
|
// =====================
|
||||||
for (Map.Entry<Integer, Deque<Integer>> entry : gpuHistory.entrySet()) {
|
int sum = 0;
|
||||||
|
int gpuCount = 0;
|
||||||
|
|
||||||
int index = entry.getKey();
|
for (Deque<Integer> q : gpuHistory.values()) {
|
||||||
Deque<Integer> q = entry.getValue();
|
|
||||||
|
|
||||||
int avg = (int) q.stream().mapToInt(i -> i).average().orElse(0);
|
int avgPerGpu = (int) q.stream().mapToInt(i -> i).average().orElse(0);
|
||||||
|
|
||||||
dto.gpus.add(new MonitorDto.Gpu(index, avg));
|
sum += avgPerGpu;
|
||||||
|
gpuCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
dto.gpu = (gpuCount == 0) ? 0 : sum / gpuCount;
|
||||||
// 캐시 교체 (atomic)
|
|
||||||
// =====================
|
|
||||||
this.cached = dto;
|
this.cached = dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// 외부 조회 (Controller에서 호출)
|
// 조회
|
||||||
// =========================
|
// =========================
|
||||||
public MonitorDto get() {
|
public MonitorDto get() {
|
||||||
return cached;
|
return cached;
|
||||||
|
|||||||
Reference in New Issue
Block a user