lifecarelog
백엔드

크론 중복 실행, Postgres SKIP LOCKED와 advisory lock으로 막기

인스턴스를 늘렸더니 크론이 두 번 도는 문제를, 새 라이브러리 없이 Postgres 잠금 두 가지로 막은 방법을 정리했어요.

5분 읽기

크론(cron)이 정해진 시각에 한 번만 돌면 좋겠는데, 인스턴스를 2대로 늘리는 순간 같은 작업이 두 번 실행되는 걸 겪어 본 적 있나요? 저도 배치 하나를 두 서버에 똑같이 올려 두고, 정산 메일이 두 통씩 나가는 걸 뒤늦게 발견했어요.

원인은 단순해요. 스케줄러가 각 인스턴스에 따로 떠 있으니, 같은 시각에 같은 함수가 동시에 깨어나는 거죠. 애플리케이션 쪽에 플래그를 두는 방법도 있지만, 이미 Postgres를 쓰고 있다면 DB가 주는 잠금을 그대로 빌려 쓰는 게 제일 깔끔했어요.

방법 1 — advisory lock으로 "한 번만 실행" 가드

advisory lock은 테이블 행이 아니라 임의의 숫자 키에 거는 잠금이에요. bigint 키 1개(또는 int 2개)만 정하면 되고, 실제 데이터와 무관해요.

SELECT pg_try_advisory_lock(42);

pg_try_advisory_lock은 잠금을 못 잡으면 기다리지 않고 바로 false를 돌려줘요. 그래서 크론 진입부에서 true를 받은 인스턴스만 작업을 실행하고, false를 받은 쪽은 조용히 넘어가게 해요.

직접 붙여 보니 코드는 몇 줄 안 됐어요. 다만 세션 레벨 잠금(pg_advisory_lock)은 pg_advisory_unlock으로 직접 풀어 줘야 하고, 세션이 살아 있는 동안 계속 물려 있어요.

advisory lock(전역 싱글톤 가드) 대 SKIP LOCKED(작업 큐 분배)

방법 2 — SELECT FOR UPDATE SKIP LOCKED로 일감 나눠 잡기

할 일이 여러 건이고, 여러 워커가 서로 다른 건을 나눠 처리해야 한다면 SKIP LOCKED가 더 맞아요. Postgres 9.5부터 들어온 기능이에요.

SELECT id FROM job_queue
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;

이미 다른 트랜잭션이 잠근 행은 건너뛰고, 안 잠긴 행만 집어 와요. 테스트해 보면 워커 2대가 같은 행을 두고 싸우지 않고 각자 다른 일감을 가져가요.

언제 뭘 쓰나

돌려 보니 기준은 이렇게 갈렸어요.

  • 작업이 하나면 advisory lock
  • 일감이 여러 건이면 SKIP LOCKED
  • 커넥션 풀 쓰면 xact 버전

작업 자체를 한 번만 돌리고 싶으면 advisory lock, 큐에 쌓인 여러 건을 워커들이 나눠 처리해야 하면 SKIP LOCKED. 이 두 갈래로 나뉘어요.

한 가지 주의할 점은 커넥션 풀이에요. pgBouncer를 transaction 모드로 쓰면 세션 레벨 advisory lock이 예상과 다르게 동작할 수 있어요. 이럴 땐 트랜잭션이 끝나면 자동으로 풀리는 pg_advisory_xact_lock을 쓰는 편이 안전했어요. 적용해 봤더니 unlock을 깜빡해서 잠금이 남는 실수도 같이 사라졌고요.

정리

새 라이브러리를 붙이기 전에, 이미 쓰던 Postgres가 잠금 두 종류를 다 쥐고 있었어요. 저는 플랜씨(plan-c)의 계산 배치를 돌릴 때도 이 방식으로 중복을 막고 있어요. 하나만 돌릴 땐 advisory lock, 여러 건을 나눌 땐 SKIP LOCKED. 이 두 갈래만 기억해도 크론 중복은 거의 정리돼요.

#Postgres#크론 중복 실행#SKIP LOCKED

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.