generated from gc/template-java-maven
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:
부모
765d0e01c6
커밋
2eebf2c83e
@ -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)
|
||||||
|
|||||||
72
docs/schema/health_log_partition_migration.sql
Normal file
72
docs/schema/health_log_partition_migration.sql
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user