PostgreSQL 스케줄러 오류, 영구 정지 문제 해결: pg_locks 연동 및 연결 관리
PostgreSQL 스케줄러의 영구 정지 문제를 pg_locks 연동 및 연결 관리로 해결한 경험을 공유합니다.
PostgreSQL 스케줄러 오류, 영구 정지 문제 해결: pg_locks 연동 및 연결 관리
스케줄러가 갑자기 멈추거나 오류를 뿜어내서 디버깅에 시간을 쏟고 있다면, 이 글이 도움이 될 수 있습니다. 기존에 잘 동작하던 스케줄러가 특정 상황에서 영구 정지되는 문제를 겪었는데, 결국 락 관리와 연결 처리 방식에서 원인을 찾았습니다.
시도와 함정
처음에는 스케줄러의 안정성을 높이기 위해 기존의 advisory lock 방식을 리스 테이블을 이용한 리더 선출 방식으로 변경해 봤습니다. 또한, 스케줄러 스스로 문제를 감지하고 복구할 수 있도록 싱글톤 패턴을 도입하여 자가 복구 기능을 추가했습니다.
하지만 여전히 락 관련 문제가 간헐적으로 발생했고, 스케줄러가 멈추는 현상이 완전히 사라지지 않았습니다. 디버깅을 위해 스케줄러가 사용하는 연결을 전용 asyncpg 연결로 분리하여 락 관련 문제를 더 깊이 파고들었습니다.
# 기존 advisory lock 방식 (예시) import psycopg2
conn = psycopg2.connect(...) cur = conn.cursor() cur.execute("SELECT pg_try_advisory_lock(123);") locked = cur.fetchone()[0] if locked: # 작업 수행 cur.execute("SELECT pg_advisory_unlock(123);") conn.close()
# 예상치 못한 에러 메시지 (실제 에러와 유사하게)
ERROR: deadlock detected
DETAIL: Process 12345 waits for Process 67890 on lock, which is held by Process 12345.
HINT: See server log for statement that blocked by other processes.
이 과정에서 스케줄러의 maintainer 로직과 PostgreSQL의 pg_locks 뷰를 연동하는 아이디어를 떠올렸습니다. pg_locks를 통해 현재 어떤 락이 누구에게 잡혀있는지 실시간으로 모니터링하고, 필요하다면 강한 참조(GC 방지)와 하트비트를 적용하여 락을 유지하거나 해제하는 방식을 시도했습니다.
원인
결국 문제는 복합적이었습니다. 기존 advisory lock 방식의 한계와 더불어, 스케줄러의 maintainer 로직이 락 관련 예외 상황을 제대로 처리하지 못하고 있었습니다. 특히, 락이 비정상적으로 해제되거나 예상치 못한 데드락이 발생했을 때 스케줄러가 복구되지 못하고 영구 정지되는 경우가 많았습니다. 또한, 디버깅 과정에서 불필요한 진단 로그가 과도하게 쌓여 시스템 부하를 증가시키는 것도 발견했습니다.
해결
락 관련 문제를 근본적으로 해결하기 위해 pg_locks 뷰를 적극적으로 활용했습니다. maintainer 로직을 pg_locks와 연동하여 현재 락 상태를 실시간으로 파악하고, 락이 예상치 못하게 풀리는 상황을 감지하면 즉시 재확보하거나 적절한 조치를 취하도록 수정했습니다.
# pg_locks 연동 및 강한 참조/하트비트 적용 (개념적 코드) import asyncio import asyncpgasync def monitor_locks(pool): while True: conn = await pool.acquire() try: # pg_locks에서 특정 스케줄러 관련 락 정보 조회 locks = await conn.fetch( "SELECT pid, granted, mode FROM pg_locks WHERE application_name = 'my_scheduler';" ) for lock in locks: if not lock['granted'] and lock['mode'] == 'ExclusiveLock': # 락이 부여되지 않은 경우, 재시도 또는 알림 print(f"Scheduler lock not granted for PID {lock['pid']}. Retrying...") # 여기에 락 재시도 로직 추가 pass # 하트비트 로직: 스케줄러가 살아있음을 알리는 쿼리 (예: pg_advisory_unlock) await conn.execute("SELECT pg_advisory_unlock(456);") # 예시 락 ID except Exception as e: print(f"Error monitoring locks: {e}") finally: await pool.release(conn) await asyncio.sleep(10) # 10초마다 락 상태 확인
async def run_scheduler(): # 전용 asyncpg 연결 풀 생성 pool = await asyncpg.create_pool(user='user', password='password', database='db', min_size=1, max_size=1) # 락 모니터링 비동기 작업 시작 asyncio.create_task(monitor_locks(pool))
# 스케줄러 메인 로직 while True: # ... 스케줄러 작업 수행 ... await asyncio.sleep(1)asyncio.run(run_scheduler())
또한, 스케줄러가 사용하는 데이터베이스 연결을 전용 asyncpg 연결 풀로 분리했습니다. 이를 통해 다른 작업과의 연결 경합을 줄이고, 락 관련 문제를 진단할 때 더 명확한 로그와 상태를 확인할 수 있었습니다. 불필요했던 상세 진단 로그는 과감히 제거하여 시스템 부하를 줄였습니다.
결과
- 스케줄러의 영구 정지 및 오류 발생 문제가 완전히 해결되었습니다.
- 전반적인 시스템 안정성이 크게 향상되었습니다.
- 락 관련 간헐적 문제가 사라져 데이터 일관성이 보장되었습니다.
- 불필요한 로그 제거로 시스템 부하가 감소했습니다.
정리 — 같은 함정 안 빠지려면
- [ ] 스케줄러나 백그라운드 작업에서 락 관련 문제가 발생한다면, PostgreSQL의
pg_locks뷰를 적극적으로 활용하여 현재 락 상태를 실시간으로 모니터링하세요. - [ ] 락이 비정상적으로 해제되거나 데드락이 발생할 경우를 대비하여, 락을 유지하거나 해제하는 로직에 강한 참조(GC 방지) 및 하트비트 메커니즘을 적용하는 것을 고려해 보세요.
- [ ] 복잡한 비동기 작업이나 데이터베이스 연결이 많은 경우, 전용 비동기 연결 풀을 사용하여 문제를 격리하고 진단을 용이하게 만드세요.
- [ ] 디버깅을 위해 추가했던 상세 로그가 시스템 부하를 유발하지 않는지 주기적으로 점검하고, 불필요한 로그는 제거하세요.
태그