APScheduler를 FastAPI에 내장하지 않고 독립 Scheduler 서비스로 분리한 이유

초기 구현: APScheduler in FastAPI

처음에는 FastAPI 앱 내에 APScheduler를 내장해 APNs 알림 스케줄링을 처리했습니다.

# ❌ 초기 구현 — FastAPI startup에서 APScheduler 시작
from apscheduler.schedulers.asyncio import AsyncIOScheduler

@asynccontextmanager
async def lifespan(app: FastAPI):
    scheduler = AsyncIOScheduler()
    scheduler.add_job(send_pending_notifications, "interval", minutes=1)
    scheduler.start()
    yield
    scheduler.shutdown()

왜 분리했는가

세 가지 이유로 독립 서비스로 분리했습니다.

1. Kubernetes 수평 확장 문제

API 서버를 replica 2개로 확장하면 APScheduler가 두 인스턴스에서 동시에 실행됩니다. 같은 알림이 2번 전송되는 중복 실행 버그가 발생했습니다.

2. 메모리/CPU 격리

AI 브리핑 생성(OpenAI API 호출)이 스케줄러에 포함되면 API 서버의 응답 지연이 증가했습니다. 스케줄러를 별도 Pod로 분리해 리소스를 격리했습니다.

3. 독립 배포

알림 로직 버그 수정 시 API 서버 전체를 재배포할 필요 없이 스케줄러만 재배포할 수 있습니다.

독립 Scheduler 서비스 구조

# scheduler/main.py
import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from scheduler.core.job_queue_processor import JobQueueProcessor

async def main():
    processor = JobQueueProcessor()
    scheduler = AsyncIOScheduler(timezone="Asia/Seoul")

    # 1분마다 pending job 처리
    scheduler.add_job(
        processor.process_pending_jobs,
        "interval",
        minutes=1,
        max_instances=1  # 중복 실행 방지
    )

    # 매일 오전 6시 AI 브리핑 생성
    scheduler.add_job(
        processor.trigger_daily_briefings,
        "cron",
        hour=6, minute=0
    )

    scheduler.start()
    print("[Scheduler] 시작됨")
    try:
        await asyncio.Event().wait()
    finally:
        scheduler.shutdown()

if __name__ == "__main__":
    asyncio.run(main())

Kubernetes Deployment — replica 1 고정

# k8s/scheduler-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ivent-scheduler
spec:
  replicas: 1  # 반드시 1 — 중복 실행 방지
  selector:
    matchLabels:
      app: ivent-scheduler
  template:
    spec:
      containers:
      - name: scheduler
        image: ivent-scheduler:latest
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: ivent-secrets
              key: database-url

API 서버와 통신: DB Job Queue

API 서버는 DB에 Job을 INSERT하고, Scheduler는 주기적으로 DB를 폴링해 Job을 처리합니다. HTTP 직접 통신 대신 DB를 중간 버퍼로 사용해 느슨한 결합을 달성했습니다.

교훈

Kubernetes 환경에서 스케줄러는 반드시 독립 서비스로 분리하고 replica=1을 고정해야 합니다. APScheduler의 max_instances=1은 같은 프로세스 내 중복만 방지하고, 다중 Pod 중복은 막지 못합니다.

글쓴이

admin

https://ivent-admin.storyqbe.top/ 를 운영하고 있는 강프로입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다