From 80fd2bda3eac088272271ca8a3a929f21483d878 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 19 Mar 2026 16:52:26 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9C=A8=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/training/common/dto/MonitorDto.java | 2 +- .../common/service/GpuDmonReader.java | 59 +++++--- .../common/service/SystemMonitorService.java | 126 ++++++++++++------ 3 files changed, 128 insertions(+), 59 deletions(-) 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 0ffd7d9..95ddba9 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 @@ -3,6 +3,6 @@ package com.kamco.cd.training.common.dto; public class MonitorDto { public int cpu; // CPU 사용률 (%) - public String memory; // "사용/전체" + public long[] 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 f9ccf80..77d7eae 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,98 +12,114 @@ import org.springframework.stereotype.Component; @Log4j2 public class GpuDmonReader { - // GPU index → 현재 util + // ========================= + // GPU 사용률 저장소 + // key: GPU index (0,1,2...) + // value: 현재 GPU 사용률 (%) + // ConcurrentHashMap → 멀티스레드 안전 + // ========================= private final Map gpuUtilMap = new ConcurrentHashMap<>(); // ========================= - // 외부 조회 + // 외부 조회용 + // SystemMonitorService에서 호출 // ========================= public Map getGpuUtilMap() { return gpuUtilMap; } // ========================= - // 시작 + // Bean 초기화 시 실행 + // - 별도 스레드에서 GPU 모니터링 시작 + // - 메인 스레드 block 방지 // ========================= @PostConstruct public void start() { + // nvidia-smi 없는 환경이면 GPU 모니터링 비활성화 if (!isNvidiaAvailable()) { log.warn("nvidia-smi not found. GPU monitoring disabled."); return; } + // 데몬 스레드로 실행 (서버 종료 시 자동 종료) Thread t = new Thread(this::runLoop, "gpu-dmon-thread"); t.setDaemon(true); t.start(); } // ========================= - // 무한 루프 (자동 복구) + // 무한 루프 + // - dmon 실행 + // - 죽으면 자동 재시작 // ========================= private void runLoop() { - boolean firstError = true; - while (true) { try { - log.info("Starting nvidia-smi dmon..."); - runDmon(); - firstError = true; - + runDmon(); // GPU 사용률 수집 시작 } catch (Exception e) { - - if (firstError) { - log.error("nvidia-smi dmon failed.", e); - firstError = false; - } else { - log.warn("dmon retry..."); - } + // dmon 프로세스 종료되면 여기로 들어옴 + log.warn("dmon restart..."); } + // 5초 대기 후 재시작 sleep(5000); } } // ========================= - // dmon 실행 + // nvidia-smi dmon 실행 + // - GPU 사용률 스트리밍으로 계속 수신 // ========================= private void runDmon() throws Exception { + // -s u → GPU utilization만 출력 ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u"); + // 프로세스 실행 후 stdout 읽기 try (BufferedReader br = new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) { String line; + // dmon은 계속 출력됨 (스트리밍) while ((line = br.readLine()) != null) { + // 헤더 제거 (#로 시작) if (line.startsWith("#")) continue; line = line.trim(); if (line.isEmpty()) continue; + // 공백 기준 분리 String[] parts = line.split("\\s+"); + // 첫 번째 값이 GPU index인지 확인 if (!parts[0].matches("\\d+")) continue; int index = Integer.parseInt(parts[0]); try { + // 두 번째 값이 GPU 사용률 (sm) int util = Integer.parseInt(parts[1]); + + // 최신 값 갱신 gpuUtilMap.put(index, util); + } catch (Exception ignored) { + // 파싱 실패 시 무시 } } } - // dmon 종료되면 예외 던져서 재시작 + // 여기까지 왔다는 건 dmon 프로세스 종료됨 + // → runLoop에서 재시작하도록 예외 발생 throw new IllegalStateException("dmon stopped"); } // ========================= - // util + // nvidia-smi 존재 여부 확인 // ========================= private boolean isNvidiaAvailable() { try { @@ -114,6 +130,9 @@ public class GpuDmonReader { } } + // ========================= + // sleep 유틸 + // ========================= private void sleep(long ms) { try { Thread.sleep(ms); 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 318231a..a8c2690 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 @@ -6,6 +6,7 @@ import java.io.FileReader; import java.util.ArrayDeque; import java.util.Deque; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.scheduling.annotation.Scheduled; @@ -16,41 +17,75 @@ import org.springframework.stereotype.Service; @Log4j2 public class SystemMonitorService { + // ========================= + // CPU 이전값 (delta 계산용) + // - /proc/stat은 누적값이기 때문에 + // - 이전 값과 비교해서 사용률 계산 + // ========================= 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<>(); + // key: GPU index + // value: 최근 30개 사용률 + private final Map> gpuHistory = new ConcurrentHashMap<>(); + + // ========================= + // GPU 데이터 제공 (dmon reader) + // ========================= private final GpuDmonReader gpuReader; + // ========================= + // 캐시 (API 응답용) + // - 매 요청마다 계산하지 않기 위해 사용 + // - volatile → 멀티스레드 안전하게 최신값 유지 + // ========================= private volatile MonitorDto cached = new MonitorDto(); // ========================= - // 1초 수집 + // 1초마다 수집 // ========================= @Scheduled(fixedRate = 1000) public void collect() { try { - // CPU + // ===================== + // 1. CPU 수집 + // ===================== double cpu = readCpu(); + cpuHistory.add(cpu); + + // 30개 유지 (rolling window) if (cpuHistory.size() > 30) cpuHistory.poll(); - // GPU + // ===================== + // 2. GPU 수집 + // ===================== Map gpuMap = gpuReader.getGpuUtilMap(); for (Map.Entry entry : gpuMap.entrySet()) { + int index = entry.getKey(); int util = entry.getValue(); + // GPU별 히스토리 생성 및 추가 gpuHistory.computeIfAbsent(index, k -> new ArrayDeque<>()).add(util); + // 30개 유지 Deque q = gpuHistory.get(index); if (q.size() > 30) q.poll(); } + // ===================== + // 3. 캐시 업데이트 + // ===================== updateCache(); } catch (Exception e) { @@ -59,7 +94,9 @@ public class SystemMonitorService { } // ========================= - // CPU + // CPU 사용률 계산 + // - /proc/stat 사용 + // - 이전값과의 차이로 계산 (delta 방식) // ========================= private double readCpu() throws Exception { @@ -80,6 +117,7 @@ public class SystemMonitorService { long total = user + nice + system + idle + iowait + irq + softirq; long idleAll = idle + iowait; + // 최초 실행 시 기준값만 저장 if (prevTotal == 0) { prevTotal = total; prevIdle = idleAll; @@ -94,79 +132,91 @@ public class SystemMonitorService { if (totalDiff == 0) return 0; + // CPU 사용률 (%) return (1.0 - (double) idleDiff / totalDiff) * 100; } } + // ========================= + // Linux 환경 체크 + // ========================= private boolean isLinux() { return System.getProperty("os.name").toLowerCase().contains("linux"); } // ========================= - // Memory + // Memory 조회 (/proc/meminfo) + // - OS 값 그대로 사용 (kB) + // - [사용량, 전체] // ========================= - private String readMemory() throws Exception { + private long[] readMemory() throws Exception { - if (!isLinux()) return "N/A"; + if (!isLinux()) return new long[] {0, 0}; - BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo")); + try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) { - long total = 0; - long available = 0; + long total = 0; + long available = 0; - String line; - while ((line = br.readLine()) != null) { - if (line.startsWith("MemTotal")) { - total = Long.parseLong(line.replaceAll("\\D+", "")); - } else if (line.startsWith("MemAvailable")) { - available = Long.parseLong(line.replaceAll("\\D+", "")); + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("MemTotal")) { + total = Long.parseLong(line.replaceAll("\\D+", "")); + } else if (line.startsWith("MemAvailable")) { + available = Long.parseLong(line.replaceAll("\\D+", "")); + } } + + long used = total - available; + + return new long[] {used, total}; } - - br.close(); - - long used = total - available; - - double usedGB = used / (1024.0 * 1024); - double totalGB = total / (1024.0 * 1024); - - return String.format("%.1f/%.0fGB", usedGB, totalGB); } // ========================= - // 캐시 업데이트 (🔥 핵심) + // 캐시 업데이트 + // - CPU: 30초 평균 + // - GPU: 전체 샘플 평균 + // - Memory: 현재값 // ========================= private void updateCache() throws Exception { MonitorDto dto = new MonitorDto(); - // CPU 평균 + // ===================== + // CPU 평균 (30초) + // ===================== dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0); - // Memory + // ===================== + // Memory (kB 그대로) + // ===================== dto.memory = readMemory(); // ===================== - // GPU 평균 (🔥 여기 중요) + // GPU 평균 (🔥 전체 샘플 기준) // ===================== int sum = 0; - int gpuCount = 0; + int count = 0; for (Deque q : gpuHistory.values()) { - - int avgPerGpu = (int) q.stream().mapToInt(i -> i).average().orElse(0); - - sum += avgPerGpu; - gpuCount++; + for (int v : q) { + sum += v; + count++; + } } - dto.gpu = (gpuCount == 0) ? 0 : sum / gpuCount; + dto.gpu = (count == 0) ? 0 : sum / count; + // ===================== + // 캐시 교체 (atomic) + // ===================== this.cached = dto; } // ========================= - // 조회 + // 외부 조회 + // - Controller에서 호출 // ========================= public MonitorDto get() { return cached; -- 2.49.1