This commit is contained in:
dean
2026-04-14 15:11:00 +09:00
parent 419b3ccdfc
commit 4fbfb31e97
3 changed files with 90 additions and 49 deletions

View File

@@ -3,6 +3,8 @@ package com.kamco.cd.training.common.service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@@ -13,19 +15,33 @@ import org.springframework.stereotype.Component;
public class GpuDmonReader { public class GpuDmonReader {
// ========================= // =========================
// GPU 사용률 저장소 // GPU 사용률 히스토리 저장소
// key: GPU index (0,1,2...) // key: GPU index (0,1,2...)
// value: 현재 GPU 사용률 (%) // value: 최근 WINDOW_SIZE 개의 sm 사용률 샘플 (1초 간격)
// ConcurrentHashMap → 멀티스레드 안전 // ConcurrentHashMap → 멀티스레드 안전 (deque 접근은 synchronized)
// ========================= // =========================
private final Map<Integer, Integer> gpuUtilMap = new ConcurrentHashMap<>(); private static final int WINDOW_SIZE = 10; // 10초 평균
private final Map<Integer, ArrayDeque<Integer>> historyMap = new ConcurrentHashMap<>();
// ========================= // =========================
// 외부 조회용 // 외부 조회용
// GPU 개수만큼 10초 평균값 반환
// SystemMonitorService에서 호출 // SystemMonitorService에서 호출
// ========================= // =========================
public Map<Integer, Integer> getGpuUtilMap() { public Map<Integer, Integer> getGpuUtilMap() {
return gpuUtilMap; Map<Integer, Integer> result = new HashMap<>();
historyMap.forEach(
(gpu, deque) -> {
synchronized (deque) {
int avg =
deque.isEmpty()
? 0
: (int) Math.round(deque.stream().mapToInt(Integer::intValue).average().orElse(0));
result.put(gpu, avg);
}
});
return result;
} }
// ========================= // =========================
@@ -76,10 +92,14 @@ public class GpuDmonReader {
// -s u → GPU utilization만 출력 // -s u → GPU utilization만 출력
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u"); ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
// stderr를 stdout에 합쳐서 소비 — 소비하지 않으면 stderr 버퍼가 가득 차
// nvidia-smi 프로세스 자체가 block되어 stdout도 멈춤
pb.redirectErrorStream(true);
Process process = pb.start();
// 프로세스 실행 후 stdout 읽기 // 프로세스 실행 후 stdout 읽기
try (BufferedReader br = try (BufferedReader br =
new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) { new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line; String line;
@@ -96,21 +116,24 @@ public class GpuDmonReader {
String[] parts = line.split("\\s+"); String[] parts = line.split("\\s+");
// 첫 번째 값이 GPU index인지 확인 // 첫 번째 값이 GPU index인지 확인
if (!parts[0].matches("\\d+")) continue; if (parts.length < 2 || !parts[0].matches("\\d+")) continue;
int index = Integer.parseInt(parts[0]); int index = Integer.parseInt(parts[0]);
try { try {
// 두 번째 값이 GPU 사용률 (sm) // 두 번째 값이 GPU 사용률 (sm), "-"이면 N/A (GPU 미활성)
int util = Integer.parseInt(parts[1]); int util = Integer.parseInt(parts[1]);
pushSample(index, util);
log.debug("[GpuDmon] gpu={} sm={}%", index, util);
// 최신 값 갱신 } catch (NumberFormatException e) {
gpuUtilMap.put(index, util); // "-" 등 숫자가 아닌 값 → GPU 비활성 상태이므로 0으로 기록
pushSample(index, 0);
} catch (Exception ignored) { log.debug("[GpuDmon] gpu={} sm=N/A (raw={}), stored as 0", index, parts[1]);
// 파싱 실패 시 무시
} }
} }
} finally {
process.destroy();
} }
// 여기까지 왔다는 건 dmon 프로세스 종료됨 // 여기까지 왔다는 건 dmon 프로세스 종료됨
@@ -118,6 +141,20 @@ public class GpuDmonReader {
throw new IllegalStateException("dmon stopped"); throw new IllegalStateException("dmon stopped");
} }
// =========================
// 샘플 추가 — 윈도우 초과 시 가장 오래된 값 제거
// =========================
private void pushSample(int gpuIndex, int util) {
ArrayDeque<Integer> deque =
historyMap.computeIfAbsent(gpuIndex, k -> new ArrayDeque<>(WINDOW_SIZE));
synchronized (deque) {
deque.addLast(util);
if (deque.size() > WINDOW_SIZE) {
deque.pollFirst();
}
}
}
// ========================= // =========================
// nvidia-smi 존재 여부 확인 // nvidia-smi 존재 여부 확인
// ========================= // =========================

View File

@@ -1,7 +1,9 @@
package com.kamco.cd.training.filemanager.dto; package com.kamco.cd.training.filemanager.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@@ -274,5 +276,13 @@ public class FileManagerDto {
@Schema(description = "디스크 사용률 (%)", example = "50.5") @Schema(description = "디스크 사용률 (%)", example = "50.5")
private Double usagePercentage; private Double usagePercentage;
@JsonFormatDttm
private ZonedDateTime lastModifiedDate;
public ZonedDateTime getLastModifiedDate() {
return ZonedDateTime.now();
}
} }
} }

View File

@@ -617,43 +617,37 @@ public class FileManagerService {
.build(); .build();
} }
// 디렉토리 사용량 계산 (재귀적으로 모든 파일 크기 합산) // 디렉토리 사용량 계산 — du -sb 사용 (walkFileTree 대비 수배 빠름)
final long[] usedSize = {0}; // fileCount/directoryCount는 du가 제공하지 않으므로 -1(미집계) 반환
final int[] fileCount = {0}; long usedSize = 0;
final int[] directoryCount = {0};
log.info("[StorageSpace] walkFileTree 시작 - path={}", directoryPath); log.info("[StorageSpace] du 시작 - path={}", directoryPath);
try { try {
Files.walkFileTree( ProcessBuilder duPb = new ProcessBuilder("du", "-sb", directoryPath);
directory, duPb.redirectErrorStream(true);
new SimpleFileVisitor<>() { Process duProcess = duPb.start();
@Override try (java.io.BufferedReader br =
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { new java.io.BufferedReader(
usedSize[0] += attrs.size(); new java.io.InputStreamReader(duProcess.getInputStream()))) {
fileCount[0]++; String line = br.readLine();
return FileVisitResult.CONTINUE; if (line != null) {
} // du -sb 출력 형식: "12345678\t/path/to/dir"
String[] parts = line.split("\t", 2);
@Override usedSize = Long.parseLong(parts[0].trim());
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { }
if (!dir.equals(directory)) { }
directoryCount[0]++; int exitCode = duProcess.waitFor();
}
return FileVisitResult.CONTINUE;
}
});
log.info( log.info(
"[StorageSpace] walkFileTree 완료 - fileCount={}, dirCount={}, usedSize={} bytes ({})", "[StorageSpace] du 완료 - exitCode={}, usedSize={} bytes ({})",
fileCount[0], exitCode,
directoryCount[0], usedSize,
usedSize[0], formatFileSize(usedSize));
formatFileSize(usedSize[0])); } catch (Exception e) {
} catch (IOException e) { log.error("[StorageSpace] du 실행 실패: {}", directoryPath, e);
log.error("디렉토리 용량 계산 중 오류 발생: {}", directoryPath, e);
return FileManagerDto.StorageSpaceRes.builder() return FileManagerDto.StorageSpaceRes.builder()
.directoryPath(directoryPath) .directoryPath(directoryPath)
.fileCount(0) .fileCount(-1)
.directoryCount(0) .directoryCount(-1)
.usedSize(0L) .usedSize(0L)
.usedSizeFormatted("0 B") .usedSizeFormatted("0 B")
.totalDiskSpace(0L) .totalDiskSpace(0L)
@@ -701,10 +695,10 @@ public class FileManagerService {
return FileManagerDto.StorageSpaceRes.builder() return FileManagerDto.StorageSpaceRes.builder()
.directoryPath(directoryPath) .directoryPath(directoryPath)
.fileCount(fileCount[0]) .fileCount(-1)
.directoryCount(directoryCount[0]) .directoryCount(-1)
.usedSize(usedSize[0]) .usedSize(usedSize)
.usedSizeFormatted(formatFileSize(usedSize[0])) .usedSizeFormatted(formatFileSize(usedSize))
.totalDiskSpace(totalDiskSpace) .totalDiskSpace(totalDiskSpace)
.totalDiskSpaceFormatted(formatFileSize(totalDiskSpace)) .totalDiskSpaceFormatted(formatFileSize(totalDiskSpace))
.freeSpace(freeSpace) .freeSpace(freeSpace)