hello
This commit is contained in:
@@ -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 존재 여부 확인
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
usedSize = Long.parseLong(parts[0].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
|
|
||||||
if (!dir.equals(directory)) {
|
|
||||||
directoryCount[0]++;
|
|
||||||
}
|
}
|
||||||
return FileVisitResult.CONTINUE;
|
int exitCode = duProcess.waitFor();
|
||||||
}
|
|
||||||
});
|
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user