재배포마다 사라지는 FastAPI BackgroundTasks, 큐 없이 살리는 법
Railway 재배포 때마다 FastAPI BackgroundTasks가 증발하는 이유와, Celery 없이 Postgres 테이블 하나로 잡을 지키는 설계를 정리했어요.
재배포마다 사라지는 FastAPI BackgroundTasks, 큐 없이 살리는 법
배포 버튼을 눌렀을 뿐인데 돌아가던 작업이 조용히 사라진 적 없나요? 저도 Railway에 올린 FastAPI 서버를 운영하면서 직접 겪었어요. 발송 대기 중이던 잡이 흔적도 없이 증발했는데, 에러 로그조차 남지 않았어요.
왜 사라질까요
FastAPI의 BackgroundTasks는 큐가 아니에요. 응답을 보낸 뒤 같은 프로세스 안에서 함수를 이어서 실행하는 목록일 뿐이라, 작업 상태가 전부 메모리에만 있어요.
Railway는 재배포할 때 새 컨테이너를 띄우고 기존 컨테이너를 내려요. 이때 실행 중이거나 대기 중이던 작업은 어디에도 기록되지 않은 채 사라져요. 크래시나 재시작도 마찬가지예요.
큐 없이 버티는 설계: 테이블이 곧 큐
Celery와 Redis를 붙이기 전에, Postgres 테이블 하나로 옮겨 봤더니 대부분의 경우 충분했어요.
핵심은 3단계예요.
- API 핸들러는 작업을 실행하지 않고 jobs 테이블에 INSERT만 해요. 컬럼은 id, payload, status, attempts, run_at 5개면 시작할 수 있어요.
- lifespan에서 띄운 asyncio 루프가 5초 간격으로 pending 상태 행을 집어 가요.
- 성공하면 done으로 바꾸고, 실패하면 attempts를 올리며 run_at을 뒤로 미뤄요.
상태가 DB에 있으니 재배포로 프로세스가 죽어도, 새 컨테이너의 폴러가 남은 작업을 그대로 이어받아요.
동시 실행이 걱정된다면
배포 전환 중엔 구 컨테이너와 새 컨테이너가 잠깐 같이 떠 있을 수 있어요. 같은 잡을 두 번 집는 건 PostgreSQL 9.5부터 지원하는 SKIP LOCKED가 막아 줘요.
SELECT id, payload FROM jobs
WHERE status = 'pending' AND run_at <= now()
ORDER BY run_at LIMIT 10
FOR UPDATE SKIP LOCKED;직접 테스트해 보면 두 프로세스를 동시에 돌려도 각자 다른 행만 잠그고 가져가요. 재시도는 최대 3회로 잡고, 그래도 실패한 잡은 failed로 남겨 두면 다음 날 확인할 수 있어요.
저도 플랜엘(plan-l)처럼 외부 데이터를 받아 오는 서비스를 혼자 운영하다 보니, 워커 서버를 따로 두는 대신 이 방식으로 관리 지점을 줄였어요. 적용해 봤더니 배포가 두렵지 않게 된 게 가장 큰 변화였어요.
진짜 큐가 필요해지는 시점
분당 수백 건 이상 쌓이거나 잡 처리 서버를 API와 분리해 늘려야 한다면, 그때 Redis 기반 큐로 넘어가면 돼요. 그 전까지 테이블 방식은 마이그레이션 파일 1개로 시작하는 작은 도구예요.
상태를 메모리에 두면 배포가 무서워지고, DB에 두면 배포는 그냥 버튼이 돼요. 다음 배포 전에 BackgroundTasks에 뭘 맡겨 뒀는지 한번 살펴보세요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.