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);
|
||||
|
||||
// 간소화 후 속도 재계산 (포인트 간 거리/시간 기반)
|
||||
recalculateSpeeds(track);
|
||||
|
||||
// 처음 5개 선박 상세 로그 (debug 레벨)
|
||||
if (simplifiedCount < 5) {
|
||||
log.debug("[CacheSimplify] vessel={} original={} -> DP={} -> distTime={} -> zoom={} (avg={} kn)",
|
||||
@ -139,6 +142,43 @@ public class CacheTrackSimplifier {
|
||||
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 대체) ──
|
||||
|
||||
private void applyDouglasPeucker(CompactVesselTrack track, double tolerance) {
|
||||
@ -412,6 +452,55 @@ public class CacheTrackSimplifier {
|
||||
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, 해리 단위) ──
|
||||
|
||||
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
|
||||
@ -40,9 +40,13 @@ public class DailyTrackCacheManager {
|
||||
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 DailyTrackCacheProperties cacheProperties;
|
||||
private final TrackMemoryBudgetManager memoryBudgetManager;
|
||||
private final CacheTrackSimplifier cacheTrackSimplifier;
|
||||
|
||||
// 날짜별 캐시 (D-1 ~ D-N)
|
||||
private final ConcurrentHashMap<LocalDate, DailyTrackData> cache = new ConcurrentHashMap<>();
|
||||
@ -56,10 +60,12 @@ public class DailyTrackCacheManager {
|
||||
public DailyTrackCacheManager(
|
||||
@Qualifier("queryDataSource") DataSource queryDataSource,
|
||||
DailyTrackCacheProperties cacheProperties,
|
||||
TrackMemoryBudgetManager memoryBudgetManager) {
|
||||
TrackMemoryBudgetManager memoryBudgetManager,
|
||||
CacheTrackSimplifier cacheTrackSimplifier) {
|
||||
this.queryDataSource = queryDataSource;
|
||||
this.cacheProperties = cacheProperties;
|
||||
this.memoryBudgetManager = memoryBudgetManager;
|
||||
this.cacheTrackSimplifier = cacheTrackSimplifier;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -337,6 +343,23 @@ public class DailyTrackCacheManager {
|
||||
|
||||
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 spatialIndex = buildSpatialIndex(tracks);
|
||||
estimatedMemory += tracks.size() * 100L; // 인덱스 오버헤드
|
||||
|
||||
@ -48,7 +48,7 @@ spring:
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000 # 커넥션 누수 감지 (60초)
|
||||
# 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
|
||||
data-source-properties:
|
||||
prepareThreshold: 3
|
||||
@ -68,7 +68,7 @@ spring:
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
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 크기 설정
|
||||
servlet:
|
||||
@ -283,8 +283,8 @@ app:
|
||||
cache:
|
||||
daily-track:
|
||||
enabled: true
|
||||
retention-days: 7 # D-1 ~ D-7 (오늘 제외)
|
||||
max-memory-gb: 6 # 최대 6GB (일 평균 ~720MB × 7일 = ~5GB)
|
||||
retention-days: 14 # D-1 ~ D-14 (2주, DP 간소화로 메모리 절감)
|
||||
max-memory-gb: 10 # 최대 10GB (DP 간소화 후 일 ~400MB × 14일 ≈ 6GB + 여유)
|
||||
warmup-async: true # 비동기 워밍업 (서버 시작 차단 없음)
|
||||
|
||||
# 항적 데이터 메모리 예산 (64GB JVM 기준)
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user