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