feat: health_log 일별 파티셔닝 + 인덱스 최적화

- PartitionService 범용화 (테이블명 파라미터) + 일별 파티션 메서드 추가
- PartitionManageScheduler에 health_log 일별 파티션 관리 추가 (7일 선행 생성, 90일 삭제)
- DataCleanupScheduler health_log DELETE 제거 (파티션 DROP으로 대체)
- SnpServiceHealthLog FK 제약 제거 (파티션 테이블 호환)
- 복합 인덱스 추가 (service_id+checked_at, daily_uptime 최적화)
- 마이그레이션 SQL 스크립트 추가
This commit is contained in:
HYOJIN 2026-04-13 09:27:46 +09:00
부모 765d0e01c6
커밋 2eebf2c83e
6개의 변경된 파일169개의 추가작업 그리고 21개의 파일을 삭제

파일 보기

@ -81,6 +81,8 @@ CREATE TABLE IF NOT EXISTS snp_service_health_log (
CREATE INDEX idx_snp_health_log_service ON snp_service_health_log (service_id); CREATE INDEX idx_snp_health_log_service ON snp_service_health_log (service_id);
CREATE INDEX idx_snp_health_log_checked ON snp_service_health_log (checked_at); CREATE INDEX idx_snp_health_log_checked ON snp_service_health_log (checked_at);
CREATE INDEX idx_health_log_svc_checked ON snp_service_health_log (service_id, checked_at DESC);
CREATE INDEX idx_health_log_daily_uptime ON snp_service_health_log (service_id, checked_at, current_status);
-- ----------------------------------------------------------- -- -----------------------------------------------------------
-- 5. snp_service_api (서비스 API) -- 5. snp_service_api (서비스 API)

파일 보기

@ -0,0 +1,72 @@
-- =============================================================
-- SNP Connection Monitoring - Health Log 파티션 마이그레이션
-- snp_service_health_log → 일별 Range 파티션 전환
-- 스키마: common
-- =============================================================
-- 1. 백업
CREATE TABLE common.snp_service_health_log_backup AS
SELECT * FROM common.snp_service_health_log;
-- 2. 기존 테이블 삭제
DROP TABLE common.snp_service_health_log;
-- 3. 파티션 테이블 생성
CREATE TABLE common.snp_service_health_log (
log_id BIGSERIAL,
service_id BIGINT NOT NULL,
previous_status VARCHAR(10),
current_status VARCHAR(10) NOT NULL,
response_time INTEGER,
error_message TEXT,
checked_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (log_id, checked_at)
) PARTITION BY RANGE (checked_at);
-- 4. 인덱스 (파티션에 자동 상속)
CREATE INDEX idx_snp_health_log_service ON common.snp_service_health_log (service_id);
CREATE INDEX idx_snp_health_log_checked ON common.snp_service_health_log (checked_at);
CREATE INDEX idx_health_log_svc_checked ON common.snp_service_health_log (service_id, checked_at DESC);
CREATE INDEX idx_health_log_daily_uptime ON common.snp_service_health_log (service_id, checked_at, current_status);
-- 5. 오늘 + 미래 7일 파티션 생성 (실행 시점에 맞게 날짜 수정)
-- 예시: 2026-04-13 실행 기준
CREATE TABLE common.snp_service_health_log_20260413
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-13') TO ('2026-04-14');
CREATE TABLE common.snp_service_health_log_20260414
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-14') TO ('2026-04-15');
CREATE TABLE common.snp_service_health_log_20260415
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-15') TO ('2026-04-16');
CREATE TABLE common.snp_service_health_log_20260416
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-16') TO ('2026-04-17');
CREATE TABLE common.snp_service_health_log_20260417
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-17') TO ('2026-04-18');
CREATE TABLE common.snp_service_health_log_20260418
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-18') TO ('2026-04-19');
CREATE TABLE common.snp_service_health_log_20260419
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-19') TO ('2026-04-20');
CREATE TABLE common.snp_service_health_log_20260420
PARTITION OF common.snp_service_health_log
FOR VALUES FROM ('2026-04-20') TO ('2026-04-21');
-- 6. 데이터 복원 (컬럼 명시)
INSERT INTO common.snp_service_health_log (
log_id, service_id, previous_status, current_status, response_time, error_message, checked_at
) SELECT
log_id, service_id, previous_status, current_status, response_time, error_message, checked_at
FROM common.snp_service_health_log_backup
WHERE checked_at >= '2026-04-13';
-- 7. 시퀀스 리셋
SELECT setval(pg_get_serial_sequence('common.snp_service_health_log', 'log_id'),
(SELECT COALESCE(MAX(log_id), 0) FROM common.snp_service_health_log));
-- 8. 백업 삭제 (확인 후)
-- DROP TABLE common.snp_service_health_log_backup;

파일 보기

@ -2,8 +2,10 @@ package com.gcsc.connection.monitoring.entity;
import com.gcsc.connection.service.entity.SnpService; import com.gcsc.connection.service.entity.SnpService;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
@ -28,7 +30,8 @@ public class SnpServiceHealthLog {
private Long logId; private Long logId;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "service_id", nullable = false) @JoinColumn(name = "service_id", nullable = false,
foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private SnpService service; private SnpService service;
@Column(name = "previous_status", length = 10) @Column(name = "previous_status", length = 10)

파일 보기

@ -1,38 +1,23 @@
package com.gcsc.connection.monitoring.scheduler; package com.gcsc.connection.monitoring.scheduler;
import com.gcsc.connection.monitoring.repository.SnpServiceHealthLogRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class DataCleanupScheduler { public class DataCleanupScheduler {
private final SnpServiceHealthLogRepository healthLogRepository;
@Value("${app.retention.health-log-days:90}")
private int healthLogRetentionDays;
/** /**
* 매일 02:00 실행 - 오래된 데이터 정리 * 매일 02:00 실행 - 오래된 데이터 정리
* Health log는 PartitionManageScheduler에서 파티션 DROP으로 관리
*/ */
@Scheduled(cron = "0 0 2 * * *") @Scheduled(cron = "0 0 2 * * *")
@Transactional
public void cleanupOldData() { public void cleanupOldData() {
log.info("데이터 정리 시작"); log.info("데이터 정리 시작");
// Health log는 PartitionManageScheduler에서 파티션 DROP으로 관리
// Health log 정리 log.info("데이터 정리 완료 (health_log는 파티션으로 관리)");
LocalDateTime healthCutoff = LocalDateTime.now().minusDays(healthLogRetentionDays);
int deletedHealthLogs = healthLogRepository.deleteOlderThan(healthCutoff);
log.info("Health log 정리 완료: {}건 삭제 (기준: {}일 이전)", deletedHealthLogs, healthLogRetentionDays);
log.info("데이터 정리 완료");
} }
} }

파일 보기

@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
@ -27,6 +28,10 @@ public class PartitionManageScheduler {
@Value("${app.retention.request-log-days:90}") @Value("${app.retention.request-log-days:90}")
private int retentionDays; private int retentionDays;
private static final String HEALTH_LOG_TABLE = "common.snp_service_health_log";
private static final String HEALTH_LOG_PREFIX = "snp_service_health_log_";
private static final int ADVANCE_DAYS = 7;
/** /**
* 매월 1일 00:00 실행 - 파티션 생성/삭제 * 매월 1일 00:00 실행 - 파티션 생성/삭제
*/ */
@ -64,6 +69,45 @@ public class PartitionManageScheduler {
log.info("파티션 관리 완료"); log.info("파티션 관리 완료");
} }
/**
* 매일 00:30 실행 - health_log 일별 파티션 관리
*/
@Scheduled(cron = "0 30 0 * * *")
public void manageHealthLogPartitions() {
if (!partitionEnabled) {
return;
}
log.info("Health log 파티션 관리 시작");
LocalDate today = LocalDate.now();
// 미래 7일 파티션 생성
for (int i = 0; i <= ADVANCE_DAYS; i++) {
partitionService.createDailyPartition(HEALTH_LOG_TABLE, today.plusDays(i));
}
// 보관기간 이전 파티션 삭제
LocalDate cutoff = today.minusDays(retentionDays);
List<String> existing = partitionService.getExistingPartitions(HEALTH_LOG_PREFIX);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd");
String cutoffStr = HEALTH_LOG_PREFIX + cutoff.format(fmt);
for (String partition : existing) {
if (partition.compareTo(cutoffStr) < 0) {
try {
LocalDate partDate = LocalDate.parse(
partition.replace(HEALTH_LOG_PREFIX, ""), fmt);
partitionService.dropDailyPartition(HEALTH_LOG_TABLE, partDate);
} catch (Exception e) {
log.warn("파티션 날짜 파싱 실패: {}", partition);
}
}
}
log.info("Health log 파티션 관리 완료");
}
private YearMonth parsePartitionYearMonth(String partitionName) { private YearMonth parsePartitionYearMonth(String partitionName) {
try { try {
String suffix = partitionName.replace("snp_api_request_log_", ""); String suffix = partitionName.replace("snp_api_request_log_", "");

파일 보기

@ -57,11 +57,53 @@ public class PartitionService {
} }
/** /**
* 기존 파티션 목록 조회 * 기존 파티션 목록 조회 (API 요청 로그용 - 하위 호환)
*/ */
public List<String> getExistingPartitions() { public List<String> getExistingPartitions() {
return getExistingPartitions(PARTITION_PREFIX);
}
/**
* 기존 파티션 목록 조회 (테이블 접두사 지정)
*/
public List<String> getExistingPartitions(String tablePrefix) {
String sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'common' AND tablename LIKE '" String sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'common' AND tablename LIKE '"
+ PARTITION_PREFIX + "%' ORDER BY tablename"; + tablePrefix + "%' ORDER BY tablename";
return jdbcTemplate.queryForList(sql, String.class); return jdbcTemplate.queryForList(sql, String.class);
} }
/**
* 일별 파티션 생성
*/
public void createDailyPartition(String tableName, LocalDate date) {
String partitionName = tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate nextDay = date.plusDays(1);
String sql = String.format(
"CREATE TABLE IF NOT EXISTS %s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')",
partitionName, tableName, date, nextDay);
try {
jdbcTemplate.execute(sql);
log.info("일별 파티션 생성 완료: {}", partitionName);
} catch (Exception e) {
log.error("일별 파티션 생성 실패: {}", partitionName, e);
}
}
/**
* 일별 파티션 삭제
*/
public void dropDailyPartition(String tableName, LocalDate date) {
String partitionName = tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String sql = String.format("DROP TABLE IF EXISTS %s", partitionName);
try {
jdbcTemplate.execute(sql);
log.info("일별 파티션 삭제 완료: {}", partitionName);
} catch (Exception e) {
log.error("일별 파티션 삭제 실패: {}", partitionName, e);
}
}
} }