고가용성(HA) 및 유사 무중단 서비스를 요구하는 고객사 환경에 맞춰 복합 이중화 클러스터링 엔진을 설치했습니다.
그 결과 스케줄링 중복 등록 현상과 특정 노드로의 작업 쏠림 현상이 동시에 발생했습니다.
기존에 적용되어 있던 Time-based Scheduler와 Quartz의 메커니즘을 면밀히 분석하고, 역할(등록/실행) 분리와 쿼리 기반 균등 분배를 통해 문제를 해결한 과정을 공유합니다.
두 스케줄러 모두 시간 기반(Time-based)으로 작동하지만, 트리거를 관리하고 실행하는 주체가 근본적으로 다릅니다.
기존 엔진에는 단순 반복 작업에는 Spring Scheduler가, DB 영속성이 필요한 작업에는 Quartz가 혼용되어 있었습니다.
Spring Scheduler
애플리케이션 내부 메모리(RAM)에 의존하여 가볍고 빠르지만, 이중화 시 각 노드가 독립적으로 실행되어 중복 문제가 발생합니다.
문제가 발생한 기능에 이 방식이 적용되어 있는 것을 확인했습니다.
Quartz Scheduler
JDBC JobStore를 통해 공유 DB에 스케줄 정보를 관리하므로 서버 재시작이나 분산 환경에서도 안정적인 영속성을 보장합니다.
그러나 기본 클러스터링 방식 자체에도 한계가 존재했습니다.
여러 대의 엔진이 하나의 서비스를 안정적으로 유지하기 위한 Quartz의 기본 HA 구성을 분석했습니다.
HA 원리
모든 노드가 동일한 QRTZ_LOCKS, QRTZ_TRIGGERS 테이블을 공유합니다. 특정 시간이 되면 각 노드가 DB Lock을 시도하고, 가장 먼저 Lock을 획득한 노드만이 해당 Job을 실행합니다.
Fail-over
Lock을 획득한 노드에 장애가 발생하면 다른 노드가 자동으로 작업을 인계받습니다. 이것이 Quartz가 제공하는 기본적인 고가용성입니다.
문제점 — Greedy Locking
그러나 실제 운영 환경에서 이 방식은 '선착순 독식' 구조로 동작했습니다.
네트워크 지연이 적거나 리소스가 여유로운 특정 노드가 매번 Lock을 선점하여 모든 작업을 가져가는 쏠림 현상이 발생했습니다.
나머지 노드들은 사실상 유휴 상태로 전락하여 클러스터링의 의미가 퇴색되었습니다.
기본 클러스터링의 한계를 극복하기 위해 애플리케이션 레벨에서 데이터 분할(Partitioning) 전략을 설계하여 적용했습니다.
3-1. 등록/실행 역할 분리 (Master-Worker)
스케줄 등록(Upsert)은 단일 Master 엔진에서만 수행하도록 분리했습니다.
이를 통해 여러 노드가 동시에 동일한 스케줄을 등록하려다 발생하는 Race Condition(Read-Update 충돌) 문제를 근본적으로 차단했습니다.
3-2. Modulo 연산 기반 균등 분배
Quartz의 기본 클러스터링 Lock 메커니즘 대신, 각 Worker 엔진이 처리할 데이터 범위를 쿼리조건으로 명확히 나누었습니다.
예를 들어 4대의 Worker가 있다면 WHERE ID % 4 = 0, WHERE ID % 4 = 1 등의 조건 분기를 통해 각 노드가 정확히 전체 작업량의 25%씩만 처리하도록 유도했습니다.
또는, 등록시 특정 name으로 indexing 처리를 하고 통계 테이블을 만들어서 분할하는 방법도 사용할 수 있습니다.
3-3. 리소스 최적화 효과
4대의 서버가 균등하게 리소스를 소모하도록 유도하여 특정 서버의 과부하 없이 시스템 전체 효율을 극대화했습니다.
Time-based의 한계:
동일 시간에 다수의 노드가 깨어나는 환경에서는 단순한 상태 체크 로직이 통하지 않습니다. 분산 환경이라면 처음부터 '동시 접근'을 전제로 아키텍처를 설계해야 합니다.
HA ≠ LB:
Quartz의 기본 HA 기능은 '가용성(Availability)'에 집중되어 있습니다. 만약 목적이 '부하 분산(Load Balancing)'이라면 위와 같이 데이터 레이어에서의 파티셔닝 전략이 훨씬 효과적입니다.
실무 적용 결과
이중화 환경에서 스케줄링 중복 실행 현상이 100% 제거되었으며, 4대 노드의 CPU 사용률 편차가 기존 70% 이상에서 5% 이내로 평준화되었습니다.
결론:
이번 사례를 통해 단순히 이중화 인프라를 구성하는 것만으로는 '고가용성'과 '부하 분산'이 동시에 달성되지 않는다는 것을 체감했습니다.
Quartz가 제공하는 기본 클러스터링은 Fail-over를 통한 가용성 확보에는 탁월하지만, 균등한 부하 분산까지 보장하지는 않습니다.
결국 프레임워크의 기능을 맹신하기보다 실제 동작 메커니즘을 깊이 이해하고, 시스템의 요구사항에 맞춰 애플리케이션 레벨에서 능동적으로 보완하는 것이 시니어 개발자의 핵심 역량이라는 점을 다시 한번 실감했습니다.