Kubernetes MSA 환경에서 @Scheduled 배치가 3번씩 실행된 사고와 재발 방지
TL;DR
Kubernetes 기반 MSA(총 7개 서비스) 환경에서 일부 배치가 전용 배치 서버가 아닌 일반 서비스에서 @Scheduled로 실행되고 있었습니다.
@Scheduled는 인스턴스(=Pod)마다 독립적으로 실행되기 때문에, 해당 서비스가 3개 인스턴스로 운영 중이면 동일 배치가 3번 실행될 위험이 있었고, 실제로 배치가 3번 실행된 사실을 확인하여 사후조치 후 배치 서버로 이관하여 마무리했습니다.
1) 배경: “배치는 배치 서버에서”라는 전제가 깨져 있었습니다
현재 아키텍처는 Kubernetes 환경에서 MSA로 운영되고 있으며, 서버(서비스)가 총 7대(7개 서비스)로 구성되어 있습니다.
운영 점검 과정에서 “간단한 배치”라는 이유로 전용 배치 서버가 아닌 일반 서비스(웹/API 성격)에서 배치가 돌고 있는 케이스가 발견되었습니다.
문제는 여기서부터였습니다.
2) 문제: Spring @Scheduled는 “클러스터 단위”가 아니라 “인스턴스 단위”로 동작합니다
Spring의 @Scheduled는 기본적으로 애플리케이션 프로세스 내부 스케줄러입니다.
즉, Kubernetes에서 동일 서비스가 ReplicaSet으로 여러 Pod로 확장되어 있으면:
- Pod A:
@Scheduled실행 - Pod B:
@Scheduled실행 - Pod C:
@Scheduled실행
처럼 각 인스턴스가 각자 스케줄을 수행합니다.
인스턴스 수가 3대라면, 동일 배치가 3번 실행될 수 있는 구조적 위험을 내재하고 있었습니다.
3) 영향(리스크): “중복 실행”은 곧 데이터 정합성/비용/운영 리스크입니다
배치가 3번 실행되면 다음 문제가 즉시 발생할 수 있습니다.
- 데이터 중복 반영 (insert/update 중복)
- 외부 연동 중복 호출 (메일/푸시/정산/결제 후처리 등)
- 리소스 낭비 및 부하 증가 (DB/Redis/외부 API)
- “가끔씩만” 발생하는 비재현성 장애 (스케줄 타이밍/리더 변화 등)
특히 “간단한 배치”일수록 멱등성/락/보호장치가 약한 경우가 많아 더 위험합니다.
4) 발견 및 확인: 실제로 3번 실행된 사실을 확인
발견 즉시 해당 내용을 공유/보고하였고, 실행 로그 및 결과를 기반으로 실제로 배치가 3회 실행되었음을 확인했습니다.
이후 영향 범위를 점검하고 필요한 **사후조치(데이터 정리/중복 처리 등)**를 진행했습니다.
5) 조치: 배치를 전용 배치 서버로 이관하여 종료
근본적으로 “배치의 실행 위치”가 잘못되어 있었기 때문에, 해당 배치들을 **전용 배치 서버(또는 배치 전용 서비스/워크로드)**로 옮겨 운영하도록 정리했습니다.
결과적으로:
- 일반 서비스에서의
@Scheduled실행 제거 - 배치 서버로 이관 및 실행 통제
- 운영 리스크 해소
6) 재발 방지 체크리스트 (권장)
이번 케이스는 구조적인 패턴이어서, 한 번 정리하고 끝내기보다 “재발 방지 장치”가 중요합니다.
(1) 원칙 정리: 배치는 “배치 워크로드”로만 실행
- 배치 워크플로우나 Kubernetes CronJob으로 분리하거나
- 배치 전용 서비스/Deployment(replicas=1)로 격리하거나
- 최소한 락(Distributed Lock) + 멱등성을 필수로 적용
(2) @Scheduled 유지가 필요하다면: 분산락 필수
인스턴스가 늘어날 수 있는 서비스에서 @Scheduled를 유지해야 한다면,
아래 중 하나는 반드시 필요합니다.
- DB/Redis 기반 분산 락 (예: ShedLock 패턴)
- Kubernetes Leader Election 기반 단일 실행 보장
- 실행 자체를 CronJob/Queue Worker로 외부화
예) ShedLock을 통한 단일 실행(개념 예시)
@Scheduled(cron = "0 0/5 * * * *") // 5분마다
@SchedulerLock(name = "syncJob", lockAtMostFor = "PT10M", lockAtLeastFor = "PT1M")
public void syncJob() {
// 동일 시점에 여러 인스턴스가 떠도, 락을 가진 1개만 실행
}
(3) 배치는 “멱등성”을 기본값으로
락은 “동시 실행”을 막지만, 배포/재시작/장애 상황에서는 중복 실행이 발생할 수 있습니다. 따라서 배치 로직은 다음을 기본으로 갖추는 것이 안전합니다.
- 실행 키(예:
job_name + business_date) 기반 처리 이력 테이블 기록 - 이미 처리된 키면 skip
- 외부 호출은 idempotency key를 함께 사용
(4) 운영 가시성(Observability) 추가
“실행 횟수”가 바로 보이면, 이런 사고는 훨씬 빨리 잡힙니다.
- 배치 시작/종료 로그 표준화
job_name기준 메트릭(실행 횟수/실패 횟수/소요 시간)- “동일 job이 N분 내 2회 이상 실행되면 알림” 같은 룰
7) 마무리: Kubernetes에서는 ‘의도하지 않은 확장’이 곧 ‘중복 실행’입니다
Kubernetes 환경에서 인스턴스 수는 오토스케일링/배포/장애 복구로 쉽게 변합니다.
따라서 @Scheduled 기반 배치는 “단일 서버” 사고방식으로 두면 언제든지 재발할 수 있습니다.
이번 건은 배치를 배치 서버로 이관하면서 정리되었고, 추가로 분산락/멱등성/가시성까지 갖추면 같은 유형의 사고를 구조적으로 차단할 수 있습니다.