lifecarelog
백엔드

재배포마다 사라지는 FastAPI BackgroundTasks, 큐 없이 살리는 법

Railway 재배포 때마다 FastAPI BackgroundTasks가 증발하는 이유와, Celery 없이 Postgres 테이블 하나로 잡을 지키는 설계를 정리했어요.

5분 읽기

재배포마다 사라지는 FastAPI BackgroundTasks, 큐 없이 살리는 법

배포 버튼을 눌렀을 뿐인데 돌아가던 작업이 조용히 사라진 적 없나요? 저도 Railway에 올린 FastAPI 서버를 운영하면서 직접 겪었어요. 발송 대기 중이던 잡이 흔적도 없이 증발했는데, 에러 로그조차 남지 않았어요.

왜 사라질까요

FastAPI의 BackgroundTasks는 큐가 아니에요. 응답을 보낸 뒤 같은 프로세스 안에서 함수를 이어서 실행하는 목록일 뿐이라, 작업 상태가 전부 메모리에만 있어요.

Railway는 재배포할 때 새 컨테이너를 띄우고 기존 컨테이너를 내려요. 이때 실행 중이거나 대기 중이던 작업은 어디에도 기록되지 않은 채 사라져요. 크래시나 재시작도 마찬가지예요.

큐 없이 버티는 설계: 테이블이 곧 큐

Celery와 Redis를 붙이기 전에, Postgres 테이블 하나로 옮겨 봤더니 대부분의 경우 충분했어요.

핵심은 3단계예요.

요청 수신jobs 테이블 INSERT폴러가 5초마다 조회·실행상태 갱신
  1. API 핸들러는 작업을 실행하지 않고 jobs 테이블에 INSERT만 해요. 컬럼은 id, payload, status, attempts, run_at 5개면 시작할 수 있어요.
  2. lifespan에서 띄운 asyncio 루프가 5초 간격으로 pending 상태 행을 집어 가요.
  3. 성공하면 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에 뭘 맡겨 뒀는지 한번 살펴보세요.

#FastAPI#Railway#백그라운드잡

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

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

서비스 살펴보기

관련 글

댓글

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