feat: L3 Daily 캐시 DP 사전 간소화 + 14일 확대
- CacheTrackSimplifier: simplifyDpOnly() (DP-only 간소화), recalculateSpeeds() (Haversine 속도 재계산) 추가 - DailyTrackCacheManager: loadDay() 시 DP 사전 간소화 적용 (tolerance=0.001, ~100m) - Daily 캐시 retention 7→14일, maxMemory 6→10GB - Query/Batch DataSource: work_mem 256MB, synchronous_commit off 세션 튜닝 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
c3a2ac3dea
커밋
0a0109fa7e
@ -117,6 +117,9 @@ public class CacheTrackSimplifier {
|
|||||||
|
|
||||||
track.setPointCount(afterZoom);
|
track.setPointCount(afterZoom);
|
||||||
|
|
||||||
|
// 간소화 후 속도 재계산 (포인트 간 거리/시간 기반)
|
||||||
|
recalculateSpeeds(track);
|
||||||
|
|
||||||
// 처음 5개 선박 상세 로그 (debug 레벨)
|
// 처음 5개 선박 상세 로그 (debug 레벨)
|
||||||
if (simplifiedCount < 5) {
|
if (simplifiedCount < 5) {
|
||||||
log.debug("[CacheSimplify] vessel={} original={} -> DP={} -> distTime={} -> zoom={} (avg={} kn)",
|
log.debug("[CacheSimplify] vessel={} original={} -> DP={} -> distTime={} -> zoom={} (avg={} kn)",
|
||||||
@ -139,6 +142,43 @@ public class CacheTrackSimplifier {
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── L3 캐시 저장용: DP-only 사전 간소화 ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DP(Douglas-Peucker)만 적용하는 사전 간소화 (L3 캐시 저장용).
|
||||||
|
* 방향 변화를 보존하여 어선 조업 패턴(원형, ㄹ자) 유지.
|
||||||
|
* 거리/시간 필터는 적용하지 않아 직선 구간만 제거.
|
||||||
|
*/
|
||||||
|
public void simplifyDpOnly(List<CompactVesselTrack> tracks, double dpTolerance) {
|
||||||
|
if (tracks == null || tracks.isEmpty()) return;
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
int totalOriginal = 0;
|
||||||
|
int totalAfter = 0;
|
||||||
|
int simplifiedCount = 0;
|
||||||
|
|
||||||
|
for (CompactVesselTrack track : tracks) {
|
||||||
|
if (track.getGeometry() == null || track.getGeometry().size() <= 2) continue;
|
||||||
|
|
||||||
|
int before = track.getGeometry().size();
|
||||||
|
totalOriginal += before;
|
||||||
|
|
||||||
|
applyDouglasPeucker(track, dpTolerance);
|
||||||
|
recalculateSpeeds(track);
|
||||||
|
track.setPointCount(track.getGeometry().size());
|
||||||
|
|
||||||
|
totalAfter += track.getGeometry().size();
|
||||||
|
simplifiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
if (simplifiedCount > 0) {
|
||||||
|
double reduction = (1 - (double) totalAfter / totalOriginal) * 100;
|
||||||
|
log.info("[DpPreSimplify] {} tracks, {} -> {} pts ({}% 감소), {}ms",
|
||||||
|
simplifiedCount, totalOriginal, totalAfter, Math.round(reduction), elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 1단계: Douglas-Peucker (ST_Simplify 대체) ──
|
// ── 1단계: Douglas-Peucker (ST_Simplify 대체) ──
|
||||||
|
|
||||||
private void applyDouglasPeucker(CompactVesselTrack track, double tolerance) {
|
private void applyDouglasPeucker(CompactVesselTrack track, double tolerance) {
|
||||||
@ -412,6 +452,55 @@ public class CacheTrackSimplifier {
|
|||||||
if (sampledSpd != null) track.setSpeeds(sampledSpd);
|
if (sampledSpd != null) track.setSpeeds(sampledSpd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 간소화 후 속도 재계산 ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 간소화된 포인트 간 속도 재계산.
|
||||||
|
* 간소화 후 남은 포인트에 대해 인접 좌표 간 Haversine 거리/시간차로 계산.
|
||||||
|
*/
|
||||||
|
private void recalculateSpeeds(CompactVesselTrack track) {
|
||||||
|
List<double[]> geometry = track.getGeometry();
|
||||||
|
List<String> timestamps = track.getTimestamps();
|
||||||
|
if (geometry == null || geometry.size() < 2 ||
|
||||||
|
timestamps == null || timestamps.size() != geometry.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = geometry.size();
|
||||||
|
List<Double> speeds = new ArrayList<>(size);
|
||||||
|
speeds.add(0.0); // 첫 포인트는 이전 포인트가 없으므로 0
|
||||||
|
|
||||||
|
for (int i = 1; i < size; i++) {
|
||||||
|
double[] prev = geometry.get(i - 1);
|
||||||
|
double[] curr = geometry.get(i);
|
||||||
|
try {
|
||||||
|
long prevTs = parseEpochSeconds(timestamps.get(i - 1));
|
||||||
|
long currTs = parseEpochSeconds(timestamps.get(i));
|
||||||
|
double timeDiffHours = (currTs - prevTs) / 3600.0;
|
||||||
|
if (timeDiffHours > 0) {
|
||||||
|
double distNm = calculateDistance(prev[1], prev[0], curr[1], curr[0]);
|
||||||
|
speeds.add(distNm / timeDiffHours); // knots
|
||||||
|
} else {
|
||||||
|
speeds.add(0.0);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
speeds.add(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track.setSpeeds(speeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseEpochSeconds(String tsStr) {
|
||||||
|
if (tsStr == null) throw new IllegalArgumentException("null timestamp");
|
||||||
|
if (tsStr.matches("\\d{10,}")) {
|
||||||
|
return Long.parseLong(tsStr);
|
||||||
|
}
|
||||||
|
return LocalDateTime.parse(tsStr, TIMESTAMP_FORMATTER)
|
||||||
|
.atZone(java.time.ZoneId.systemDefault())
|
||||||
|
.toEpochSecond();
|
||||||
|
}
|
||||||
|
|
||||||
// ── 거리 계산 (Haversine, 해리 단위) ──
|
// ── 거리 계산 (Haversine, 해리 단위) ──
|
||||||
|
|
||||||
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
|||||||
@ -40,9 +40,13 @@ public class DailyTrackCacheManager {
|
|||||||
NOT_STARTED, LOADING, PARTIAL, READY, DISABLED
|
NOT_STARTED, LOADING, PARTIAL, READY, DISABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** L3 사전 간소화 DP tolerance (~100m) — 항적 형상 유지하면서 직선 구간만 제거 */
|
||||||
|
private static final double L3_DP_TOLERANCE = 0.001;
|
||||||
|
|
||||||
private final DataSource queryDataSource;
|
private final DataSource queryDataSource;
|
||||||
private final DailyTrackCacheProperties cacheProperties;
|
private final DailyTrackCacheProperties cacheProperties;
|
||||||
private final TrackMemoryBudgetManager memoryBudgetManager;
|
private final TrackMemoryBudgetManager memoryBudgetManager;
|
||||||
|
private final CacheTrackSimplifier cacheTrackSimplifier;
|
||||||
|
|
||||||
// 날짜별 캐시 (D-1 ~ D-N)
|
// 날짜별 캐시 (D-1 ~ D-N)
|
||||||
private final ConcurrentHashMap<LocalDate, DailyTrackData> cache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<LocalDate, DailyTrackData> cache = new ConcurrentHashMap<>();
|
||||||
@ -56,10 +60,12 @@ public class DailyTrackCacheManager {
|
|||||||
public DailyTrackCacheManager(
|
public DailyTrackCacheManager(
|
||||||
@Qualifier("queryDataSource") DataSource queryDataSource,
|
@Qualifier("queryDataSource") DataSource queryDataSource,
|
||||||
DailyTrackCacheProperties cacheProperties,
|
DailyTrackCacheProperties cacheProperties,
|
||||||
TrackMemoryBudgetManager memoryBudgetManager) {
|
TrackMemoryBudgetManager memoryBudgetManager,
|
||||||
|
CacheTrackSimplifier cacheTrackSimplifier) {
|
||||||
this.queryDataSource = queryDataSource;
|
this.queryDataSource = queryDataSource;
|
||||||
this.cacheProperties = cacheProperties;
|
this.cacheProperties = cacheProperties;
|
||||||
this.memoryBudgetManager = memoryBudgetManager;
|
this.memoryBudgetManager = memoryBudgetManager;
|
||||||
|
this.cacheTrackSimplifier = cacheTrackSimplifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,6 +343,23 @@ public class DailyTrackCacheManager {
|
|||||||
|
|
||||||
estimatedMemory += tracks.size() * 200L; // 객체 오버헤드
|
estimatedMemory += tracks.size() * 200L; // 객체 오버헤드
|
||||||
|
|
||||||
|
// DP 사전 간소화: 직선 구간만 제거, 방향 변화(어선 조업 패턴) 보존
|
||||||
|
long memoryBeforeDp = estimatedMemory;
|
||||||
|
List<CompactVesselTrack> trackList = new ArrayList<>(tracks.values());
|
||||||
|
cacheTrackSimplifier.simplifyDpOnly(trackList, L3_DP_TOLERANCE);
|
||||||
|
|
||||||
|
// 간소화 후 메모리 재추정
|
||||||
|
estimatedMemory = trackList.stream()
|
||||||
|
.mapToLong(t -> t.getPointCount() * 40L)
|
||||||
|
.sum();
|
||||||
|
estimatedMemory += tracks.size() * 200L; // 객체 오버헤드
|
||||||
|
|
||||||
|
if (memoryBeforeDp > 0) {
|
||||||
|
long reduction = memoryBeforeDp > 0 ? Math.round((1 - (double) estimatedMemory / memoryBeforeDp) * 100) : 0;
|
||||||
|
log.info("[DailyLoadDay] {} DP pre-simplification: {}MB -> {}MB ({}% reduction, tolerance={})",
|
||||||
|
date, memoryBeforeDp / (1024 * 1024), estimatedMemory / (1024 * 1024), reduction, L3_DP_TOLERANCE);
|
||||||
|
}
|
||||||
|
|
||||||
// STRtree 공간 인덱스 빌드
|
// STRtree 공간 인덱스 빌드
|
||||||
STRtree spatialIndex = buildSpatialIndex(tracks);
|
STRtree spatialIndex = buildSpatialIndex(tracks);
|
||||||
estimatedMemory += tracks.size() * 100L; // 인덱스 오버헤드
|
estimatedMemory += tracks.size() * 100L; // 인덱스 오버헤드
|
||||||
|
|||||||
@ -48,7 +48,7 @@ spring:
|
|||||||
validation-timeout: 5000
|
validation-timeout: 5000
|
||||||
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
|
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
|
||||||
# PostGIS 함수를 위해 public 스키마를 search_path에 명시적으로 추가
|
# PostGIS 함수를 위해 public 스키마를 search_path에 명시적으로 추가
|
||||||
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public, pg_catalog;"
|
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public, pg_catalog; SET work_mem = '256MB'; SET synchronous_commit = 'off';"
|
||||||
statement-cache-size: 250
|
statement-cache-size: 250
|
||||||
data-source-properties:
|
data-source-properties:
|
||||||
prepareThreshold: 3
|
prepareThreshold: 3
|
||||||
@ -68,7 +68,7 @@ spring:
|
|||||||
idle-timeout: 600000
|
idle-timeout: 600000
|
||||||
max-lifetime: 1800000
|
max-lifetime: 1800000
|
||||||
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
|
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
|
||||||
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public;"
|
connection-init-sql: "SET TIME ZONE 'Asia/Seoul'; SET search_path TO signal, public; SET synchronous_commit = 'off';"
|
||||||
|
|
||||||
# Request 크기 설정
|
# Request 크기 설정
|
||||||
servlet:
|
servlet:
|
||||||
@ -283,8 +283,8 @@ app:
|
|||||||
cache:
|
cache:
|
||||||
daily-track:
|
daily-track:
|
||||||
enabled: true
|
enabled: true
|
||||||
retention-days: 7 # D-1 ~ D-7 (오늘 제외)
|
retention-days: 14 # D-1 ~ D-14 (2주, DP 간소화로 메모리 절감)
|
||||||
max-memory-gb: 6 # 최대 6GB (일 평균 ~720MB × 7일 = ~5GB)
|
max-memory-gb: 10 # 최대 10GB (DP 간소화 후 일 ~400MB × 14일 ≈ 6GB + 여유)
|
||||||
warmup-async: true # 비동기 워밍업 (서버 시작 차단 없음)
|
warmup-async: true # 비동기 워밍업 (서버 시작 차단 없음)
|
||||||
|
|
||||||
# 항적 데이터 메모리 예산 (64GB JVM 기준)
|
# 항적 데이터 메모리 예산 (64GB JVM 기준)
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user