본문 바로가기
Q&A2026년 7월 27일1분 읽기

node-cron 잡이 PM2 클러스터로 띄우니까 인스턴스 수만큼 중복 실행돼요

s
sched_minji
조회 656댓글 2

매일 새벽에 정산 메일 보내는 작업을 node-cron으로 걸어놨는데요, 얼마 전 트래픽 늘어서 PM2를 cluster 모드로 인스턴스 8개 띄웠더니 그 뒤로 정산 메일이 8통씩 갑니다.

당연한 얘긴가 싶기도 한데, cron 코드가 앱 코드 안에 있으니 인스턴스마다 스케줄러가 각자 돌아서 그런 것 같아요.

cron.schedule('0 4 * * *', () => {
  sendSettlementEmails()
})

인스턴스는 여러 개 유지하면서 이 작업만 딱 한 번 돌게 하는 정석적인 방법이 뭔가요? 인스턴스 하나에서만 돌게 하드코딩하는 건 좀 아닌 것 같고요.

댓글 2

김김영삼3개월 전
정확히 진단하셨어요. cron 스케줄러가 프로세스마다 각자 살아있으니 인스턴스 수만큼 실행됩니다. 이건 스케줄링의 근본 문제라 정석 패턴이 몇 개 있어요.

가장 흔한 임시방편은 PM2가 주는 인스턴스 번호로 거르는 겁니다. cluster 모드에서 process.env.NODE_APP_INSTANCE가 0,1,2...로 들어오니 if (process.env.NODE_APP_INSTANCE === '0') 일 때만 cron.schedule을 등록하는 거죠. 코드는 간단한데, 0번 인스턴스가 죽거나 재시작 중이면 그날 잡이 통째로 안 도는 위험이 있어서 완전한 해법은 아니에요.

제대로 하려면 스케줄링을 앱 프로세스에서 분리하는 방향입니다. 두 가지가 정석이에요.

첫째, 잡 큐. BullMQ 같은 걸 쓰면 repeatable job으로 스케줄을 Redis에 등록해두고, 여러 인스턴스가 다 worker로 붙어있어도 하나의 스케줄에서 잡 하나만 생성돼 그중 한 워커만 집어갑니다. 지금 상황엔 이게 제일 잘 맞아요. 이미 Redis 쓰고 계시면 도입 부담도 적고요.

둘째, 분산 락. 모든 인스턴스가 cron은 다 걸되, 잡 시작할 때 Redis에 SET key val NX EX로 락을 잡아서 성공한 놈만 실제 작업을 하고 나머지는 스킵하는 방식이에요. 하루짜리 잡이면 락 TTL을 넉넉히 주면 됩니다.

셋째, 아예 인프라 레벨로 빼는 것. 컨테이너 환경이면 쿠버네티스 CronJob으로 정산 작업을 별도 파드로 띄우거나, PM2라도 정산 스크립트를 앱과 분리된 단독 프로세스(fork 모드 1개)로 돌리는 겁니다. 스케줄 작업은 웹 서버 인스턴스랑 생명주기를 분리하는 게 장기적으로 제일 깔끔해요.

지금 Redis를 쓰시는지, 인프라가 순수 PM2인지 k8s인지에 따라 추천이 갈립니다. Redis 있으면 BullMQ repeatable, k8s면 CronJob으로 빼는 걸 우선 보세요.
ssched_minji(guest)3개월 전
아 NODE_APP_INSTANCE로 거르는 건 생각도 못 했네요. 지금은 Redis는 캐시로만 쓰고 있고 인프라는 순수 PM2예요. 우선 급한 대로 인스턴스 0번 조건으로 막아두고, BullMQ repeatable 쪽을 좀 더 알아봐야겠습니다. 방향 감사합니다.
Ctrl+Enter로 등록