jongkwan.dev
개발 · Essay №078

Celery 배포와 운영

Celery를 프로덕션에 올릴 때 필요한 배포 패턴, 워커 풀, prefetch 공정성, 스케일링, 모니터링, 보안 설정 정리

이종관2026년 4월 21일14 min read
Contents

프로덕션 Celery 운영은 브로커 신뢰성, 워커 가용성, 모니터링, 성능 튜닝을 함께 맞추는 작업입니다.

배포가 까다로운 이유

Celery는 메시지 큐(작업을 비동기로 주고받는 중간 저장소)에 작업을 쌓고, 워커가 꺼내 실행하는 분산 태스크 큐입니다. 단일 호스트 데모는 명령 한 줄로 뜨지만 프로덕션은 다릅니다. 브로커가 멈추면 대기 중인 작업이 사라지고, 워커가 과부하되면 응답이 밀립니다.

그래서 배포에서는 네 축을 함께 봅니다. 메시지 전달 신뢰성, 워커 가용성, 모니터링, 성능 튜닝입니다. 아래 설정값의 기본값은 Celery 공식 문서(docs.celeryq.dev) 기준입니다.

배포 패턴 세 가지

배포 환경이 단일 호스트인지 멀티 호스트인지에 따라 아래 패턴 중 하나를 고릅니다. 규모와 부하 변동성이 선택을 가릅니다.

패턴적합 환경스케일링비고
Docker Compose단일 호스트, 소규모수동 (replica 조정)구성이 가장 빠름
Kubernetes멀티 호스트, 가변 부하HPA 자동프로브·롤링 업데이트
systemdVM 직접 운영수동OS 통합, 외부 의존 없음

Docker Compose는 브로커, 결과 백엔드, 워커, 스케줄러를 한 파일로 묶습니다. 프로덕션 구성에서는 헬스체크와 재시작 정책, 볼륨 영속화를 함께 둡니다.

yaml
services:
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
      RABBITMQ_DEFAULT_VHOST: /celery
    healthcheck:
      test: rabbitmq-diagnostics -q ping
      interval: 10s
      timeout: 5s
      retries: 5
 
  worker:
    build: .
    command: celery -A myapp worker -l INFO -c 4 --max-tasks-per-child 1000 --max-memory-per-child 200000
    depends_on:
      rabbitmq:
        condition: service_healthy
    environment:
      CELERY_BROKER_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq//celery
      CELERY_RESULT_BACKEND: redis://:${REDIS_PASS}@redis:6379/1
    restart: unless-stopped

Kubernetes에서는 워커를 Deployment로 띄우고 헬스 프로브와 종료 유예 시간을 함께 지정합니다. liveness 프로브는 inspect ping으로 워커가 살아 있는지 확인합니다. readiness 프로브는 inspect active로 작업을 받을 준비가 됐는지 봅니다.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: celery-worker
spec:
  replicas: 3
  template:
    spec:
      terminationGracePeriodSeconds: 120
      containers:
      - name: worker
        image: myregistry/myapp:latest
        command: ["celery", "-A", "myapp", "worker"]
        args: ["-l", "INFO", "-c", "4", "--max-tasks-per-child", "1000"]
        env:
        - name: CELERY_BROKER_URL
          valueFrom:
            secretKeyRef:
              name: celery-secrets
              key: broker-url
        resources:
          requests: { memory: "256Mi", cpu: "500m" }
          limits: { memory: "512Mi", cpu: "1000m" }
        livenessProbe:
          exec:
            command: ["celery", "-A", "myapp", "inspect", "ping"]
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command: ["celery", "-A", "myapp", "inspect", "active"]
          initialDelaySeconds: 10
          periodSeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

워커는 종료 신호(SIGTERM)를 받으면 진행 중인 작업을 마친 뒤 멈추는 웜 셧다운(warm shutdown)을 합니다. 그래서 terminationGracePeriodSeconds는 가장 긴 작업의 실행 시간보다 길게 잡아야 합니다. 이 값이 작으면 쿠버네티스가 강제 종료 신호(SIGKILL)를 보내 작업이 중간에 끊깁니다.

VM에서 직접 돌릴 때는 systemd 유닛으로 등록합니다. Type=notify로 워커의 준비 완료를 systemd에 알리고, 실패 시 자동 재시작을 겁니다.

ini
# /etc/systemd/system/celery-worker.service
[Service]
Type=notify
User=celery
Group=celery
WorkingDirectory=/opt/myapp
Environment="CELERY_BROKER_URL=redis://localhost:6379/0"
ExecStart=/usr/local/bin/celery -A myapp worker \
    -l INFO -c 4 \
    --max-tasks-per-child=1000 \
    --max-memory-per-child=200000
Restart=on-failure
RestartSec=10

워커 풀과 동시성

워커는 작업을 어떻게 병렬 실행할지 풀(pool) 종류로 정합니다. 작업이 CPU를 쓰는지 I/O를 기다리는지에 따라 선택이 달라집니다.

CPU 집약I/O 집약동시성메모리
prefork최적좋음낮음~중간높음
eventlet부적합최적매우 높음낮음
gevent부적합최적매우 높음낮음
solo순차순차1낮음

기본값인 prefork는 작업마다 프로세스를 띄워 CPU 코어를 모두 씁니다. eventlet과 gevent는 그린 스레드(green thread, 사용자 공간에서 협력적으로 전환하는 경량 스레드)를 씁니다. 한 프로세스에서 수천 개의 I/O 대기를 동시에 다룹니다. 아래 기준으로 풀을 고릅니다.

text
I/O 집약 + 많은 동시 연결  → eventlet / gevent
CPU 집약 또는 메모리 누수 위험 → prefork (기본)
개발 / 테스트            → solo

동시성 수는 -c 옵션으로 정합니다. 부하가 변하면 --autoscale로 상한과 하한을 줘서 워커가 스스로 프로세스를 늘리고 줄이게 합니다.

bash
# CPU 작업: 코어 수만큼 prefork
celery -A myapp worker -l INFO -c 4
 
# I/O 작업: gevent로 동시성 1000
celery -A myapp worker -P gevent -c 1000
 
# 부하 변동: 최대 16, 최소 4 프로세스
celery -A myapp worker -l INFO --autoscale=16,4

eventlet과 gevent는 max_tasks_per_child를 지원하지 않습니다. 그래서 메모리 누수가 의심되면 별도 모니터링과 주기적 재시작이 필요합니다.

prefetch와 공정성

워커는 처리량을 위해 작업을 미리 당겨 옵니다. 이때 한 번에 당기는 개수는 동시성 × worker_prefetch_multiplier로 정해집니다. 기본 배수는 4입니다(Celery 공식 문서 기준).

짧은 작업이 많으면 미리 당기는 편이 빠릅니다. 문제는 긴 작업입니다. 긴 작업이 앞에 걸리면 같이 당겨 온 나머지가 그 뒤에서 길게 대기합니다.

작업 유형권장 배수이유
짧고 많은 작업4 (기본)당겨 두면 처리량이 높음
길고 무거운 작업1한 번에 하나만 받아 공정성 확보

긴 작업과 짧은 작업을 한 워커에 섞지 않는 편이 낫습니다. 큐를 나누고 워커도 큐별로 분리하면 한쪽 정체가 다른 쪽을 막지 않습니다.

bash
# 짧은 작업 큐: 동시성 높게
celery -A myapp worker -Q short -l INFO -c 8
 
# 긴 작업 큐: 동시성 낮게, prefetch 1, 타임아웃 부여
celery -A myapp worker -Q long -l INFO -c 2 \
    --time-limit=3600 --soft-time-limit=3500

긴 작업 워커에는 app.conf.worker_prefetch_multiplier = 1을 함께 둡니다. 큐 분리와 prefetch 1을 같이 써야 긴 작업이 다른 작업을 가두지 않습니다.

스케일링

확장은 세 방향으로 합니다. 워커 수를 늘리는 수평 확장, 동시성을 올리는 수직 확장, 큐를 쪼개는 분할입니다. 같은 큐를 소비하는 워커를 추가하면 처리량이 늘어납니다.

쿠버네티스에서는 수평 파드 오토스케일러(Horizontal Pod Autoscaler, HPA)로 부하에 맞춰 워커 파드를 자동으로 조절합니다. CPU와 메모리 사용률을 기준으로 최소·최대 replica 사이에서 움직입니다.

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: celery-worker-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: celery-worker
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

CPU 사용률만으로는 큐가 쌓이는 상황을 못 잡을 때가 있습니다. 대기 메시지 수가 중요한 워크로드라면 큐 길이를 별도 지표로 노출해 스케일 기준에 더하는 편이 정확합니다.

모니터링

Flower는 Celery의 표준 모니터링 웹 도구입니다. 워커 상태, 작업 실행 이력, 실시간 통계를 보여 주고, 작업 취소(revoke)나 rate limit 조정 같은 원격 제어도 합니다.

bash
# Flower 실행
celery -A myapp flower --port=5555

모니터링은 워커가 내보내는 이벤트를 소비해 동작합니다. 이벤트 전송을 켜야 Flower가 작업 흐름을 추적할 수 있습니다.

python
app.conf.worker_send_task_events = True
app.conf.task_send_sent_event = True
app.conf.task_track_started = True

장기 지표는 Flower의 /metrics 엔드포인트를 Prometheus가 수집합니다. 제어 명령은 inspect/control 응용 프로그래밍 인터페이스(API)로 보냅니다. 아래는 작업이 발행되어 모니터링까지 흘러가는 경로입니다.

다이어그램의 핵심은 두 가지입니다. 작업 흐름(프로듀서 → 브로커 → 워커 → 결과)과 관찰 흐름(워커 → 이벤트 → 모니터링)이 분리돼 있어, 모니터링이 멈춰도 작업 처리는 계속됩니다.

신뢰성과 보안 설정

워커가 작업을 받자마자 완료 신호를 보내면, 실행 중 크래시가 나도 작업이 사라집니다. task_acks_late=True는 완료 신호를 작업이 끝난 뒤로 미뤄 이런 유실을 막습니다. 대신 같은 작업이 다시 실행될 수 있으므로 멱등성(같은 입력을 여러 번 처리해도 결과가 같은 성질)을 보장해야 합니다.

python
app.conf.task_acks_late = True
app.conf.task_reject_on_worker_lost = True
app.conf.worker_prefetch_multiplier = 1

prefork 워커는 오래 돌면 메모리가 조금씩 늘 수 있습니다. 일정 횟수나 일정 메모리를 넘으면 자식 프로세스를 새로 띄워 누수를 끊습니다.

python
app.conf.worker_max_tasks_per_child = 1000      # 1000개 처리 후 재생성
app.conf.worker_max_memory_per_child = 200_000  # 200MB(KB 단위) 초과 시 재생성

보안은 직렬화에서 시작합니다. Pickle 직렬화는 임의 코드 실행 위험이 있으므로 JSON을 씁니다. 워커는 전용 사용자로 실행하고 root로 띄우지 않습니다.

python
app.conf.task_serializer = 'json'
app.conf.accept_content = ['json']
bash
# 전용 사용자로 실행
sudo -u celery celery -A myapp worker

메시지 위변조까지 막아야 한다면 암호화 서명을 켭니다. 키와 인증서로 메시지에 서명하고, 직렬화 형식을 auth로 바꿉니다. 브로커 연결 자체는 전송 계층 보안(Transport Layer Security, TLS)으로 암호화합니다.

python
from celery.security import setup_security
 
setup_security(
    app,
    key='/path/to/key.pem',
    cert='/path/to/cert.pem',
    store='/path/to/ca.pem',
    digest='sha256',
)
app.conf.task_serializer = 'auth'
app.conf.accept_content = ['auth']
app.conf.broker_use_ssl = True

정리

프로덕션 Celery는 배포 패턴 선택만으로 끝나지 않습니다. 신뢰성과 관찰성까지 함께 맞춰야 안정적으로 돌아갑니다. 배포는 환경에 따라 Docker Compose, Kubernetes, systemd 중에서 고릅니다. 워커는 작업 성격에 맞는 풀과 동시성으로 띄우고, 긴 작업은 큐를 나누고 prefetch를 1로 둬 짧은 작업을 가두지 않습니다.

모니터링은 Flower와 Prometheus로 이벤트를 수집합니다. 신뢰성은 acks_late와 멱등성으로, 보안은 JSON 직렬화와 전용 사용자, 메시지 서명으로 지킵니다. 배포 전에는 브로커 고가용성, 결과 백엔드, 모니터링, 알림, 보안 감사, 성능 기준선을 체크리스트로 한 번씩 확인하는 편이 안전합니다.