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 java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2;
@@ -13,19 +15,33 @@ import org.springframework.stereotype.Component;
public class GpuDmonReader {
// =========================
// GPU 사용률 저장소
// GPU 사용률 히스토리 저장소
// key: GPU index (0,1,2...)
// value: 현재 GPU 사용률 (%)
// ConcurrentHashMap → 멀티스레드 안전
// value: 최근 WINDOW_SIZE 개의 sm 사용률 샘플 (1초 간격)
// 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에서 호출
// =========================
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만 출력
ProcessBuilder pb = new ProcessBuilder("nvidia-smi", "dmon", "-s", "u");
// stderr를 stdout에 합쳐서 소비 — 소비하지 않으면 stderr 버퍼가 가득 차
// nvidia-smi 프로세스 자체가 block되어 stdout도 멈춤
pb.redirectErrorStream(true);
Process process = pb.start();
// 프로세스 실행 후 stdout 읽기
try (BufferedReader br =
new BufferedReader(new InputStreamReader(pb.start().getInputStream()))) {
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
@@ -96,21 +116,24 @@ public class GpuDmonReader {
String[] parts = line.split("\\s+");
// 첫 번째 값이 GPU index인지 확인
if (!parts[0].matches("\\d+")) continue;
if (parts.length < 2 || !parts[0].matches("\\d+")) continue;
int index = Integer.parseInt(parts[0]);
try {
// 두 번째 값이 GPU 사용률 (sm)
// 두 번째 값이 GPU 사용률 (sm), "-"이면 N/A (GPU 미활성)
int util = Integer.parseInt(parts[1]);
pushSample(index, util);
log.debug("[GpuDmon] gpu={} sm={}%", index, util);
// 최신 값 갱신
gpuUtilMap.put(index, util);
} catch (Exception ignored) {
// 파싱 실패 시 무시
} catch (NumberFormatException e) {
// "-" 등 숫자가 아닌 값 → GPU 비활성 상태이므로 0으로 기록
pushSample(index, 0);
log.debug("[GpuDmon] gpu={} sm=N/A (raw={}), stored as 0", index, parts[1]);
}
}
} finally {
process.destroy();
}
// 여기까지 왔다는 건 dmon 프로세스 종료됨
@@ -118,6 +141,20 @@ public class GpuDmonReader {
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 존재 여부 확인
// =========================

View File

@@ -1,7 +1,9 @@
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 java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -274,5 +276,13 @@ public class FileManagerDto {
@Schema(description = "디스크 사용률 (%)", example = "50.5")
private Double usagePercentage;
@JsonFormatDttm
private ZonedDateTime lastModifiedDate;
public ZonedDateTime getLastModifiedDate() {
return ZonedDateTime.now();
}
}
}

View File

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