Celery 배포와 운영
Celery를 프로덕션에 올릴 때 필요한 배포 패턴, 워커 풀, prefetch 공정성, 스케일링, 모니터링, 보안 설정 정리
Contents
프로덕션 Celery 운영은 브로커 신뢰성, 워커 가용성, 모니터링, 성능 튜닝을 함께 맞추는 작업입니다.
배포가 까다로운 이유
Celery는 메시지 큐(작업을 비동기로 주고받는 중간 저장소)에 작업을 쌓고, 워커가 꺼내 실행하는 분산 태스크 큐입니다. 단일 호스트 데모는 명령 한 줄로 뜨지만 프로덕션은 다릅니다. 브로커가 멈추면 대기 중인 작업이 사라지고, 워커가 과부하되면 응답이 밀립니다.
그래서 배포에서는 네 축을 함께 봅니다. 메시지 전달 신뢰성, 워커 가용성, 모니터링, 성능 튜닝입니다. 아래 설정값의 기본값은 Celery 공식 문서(docs.celeryq.dev) 기준입니다.
배포 패턴 세 가지
배포 환경이 단일 호스트인지 멀티 호스트인지에 따라 아래 패턴 중 하나를 고릅니다. 규모와 부하 변동성이 선택을 가릅니다.
| 패턴 | 적합 환경 | 스케일링 | 비고 |
|---|---|---|---|
| Docker Compose | 단일 호스트, 소규모 | 수동 (replica 조정) | 구성이 가장 빠름 |
| Kubernetes | 멀티 호스트, 가변 부하 | HPA 자동 | 프로브·롤링 업데이트 |
| systemd | VM 직접 운영 | 수동 | OS 통합, 외부 의존 없음 |
Docker Compose는 브로커, 결과 백엔드, 워커, 스케줄러를 한 파일로 묶습니다. 프로덕션 구성에서는 헬스체크와 재시작 정책, 볼륨 영속화를 함께 둡니다.
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-stoppedKubernetes에서는 워커를 Deployment로 띄우고 헬스 프로브와 종료 유예 시간을 함께 지정합니다. liveness 프로브는 inspect ping으로 워커가 살아 있는지 확인합니다. readiness 프로브는 inspect active로 작업을 받을 준비가 됐는지 봅니다.
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에 알리고, 실패 시 자동 재시작을 겁니다.
# /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 대기를 동시에 다룹니다. 아래 기준으로 풀을 고릅니다.
I/O 집약 + 많은 동시 연결 → eventlet / gevent
CPU 집약 또는 메모리 누수 위험 → prefork (기본)
개발 / 테스트 → solo동시성 수는 -c 옵션으로 정합니다. 부하가 변하면 --autoscale로 상한과 하한을 줘서 워커가 스스로 프로세스를 늘리고 줄이게 합니다.
# 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,4eventlet과 gevent는 max_tasks_per_child를 지원하지 않습니다. 그래서 메모리 누수가 의심되면 별도 모니터링과 주기적 재시작이 필요합니다.
prefetch와 공정성
워커는 처리량을 위해 작업을 미리 당겨 옵니다. 이때 한 번에 당기는 개수는 동시성 × worker_prefetch_multiplier로 정해집니다. 기본 배수는 4입니다(Celery 공식 문서 기준).
짧은 작업이 많으면 미리 당기는 편이 빠릅니다. 문제는 긴 작업입니다. 긴 작업이 앞에 걸리면 같이 당겨 온 나머지가 그 뒤에서 길게 대기합니다.
| 작업 유형 | 권장 배수 | 이유 |
|---|---|---|
| 짧고 많은 작업 | 4 (기본) | 당겨 두면 처리량이 높음 |
| 길고 무거운 작업 | 1 | 한 번에 하나만 받아 공정성 확보 |
긴 작업과 짧은 작업을 한 워커에 섞지 않는 편이 낫습니다. 큐를 나누고 워커도 큐별로 분리하면 한쪽 정체가 다른 쪽을 막지 않습니다.
# 짧은 작업 큐: 동시성 높게
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 사이에서 움직입니다.
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: 70CPU 사용률만으로는 큐가 쌓이는 상황을 못 잡을 때가 있습니다. 대기 메시지 수가 중요한 워크로드라면 큐 길이를 별도 지표로 노출해 스케일 기준에 더하는 편이 정확합니다.
모니터링
Flower는 Celery의 표준 모니터링 웹 도구입니다. 워커 상태, 작업 실행 이력, 실시간 통계를 보여 주고, 작업 취소(revoke)나 rate limit 조정 같은 원격 제어도 합니다.
# Flower 실행
celery -A myapp flower --port=5555모니터링은 워커가 내보내는 이벤트를 소비해 동작합니다. 이벤트 전송을 켜야 Flower가 작업 흐름을 추적할 수 있습니다.
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는 완료 신호를 작업이 끝난 뒤로 미뤄 이런 유실을 막습니다. 대신 같은 작업이 다시 실행될 수 있으므로 멱등성(같은 입력을 여러 번 처리해도 결과가 같은 성질)을 보장해야 합니다.
app.conf.task_acks_late = True
app.conf.task_reject_on_worker_lost = True
app.conf.worker_prefetch_multiplier = 1prefork 워커는 오래 돌면 메모리가 조금씩 늘 수 있습니다. 일정 횟수나 일정 메모리를 넘으면 자식 프로세스를 새로 띄워 누수를 끊습니다.
app.conf.worker_max_tasks_per_child = 1000 # 1000개 처리 후 재생성
app.conf.worker_max_memory_per_child = 200_000 # 200MB(KB 단위) 초과 시 재생성보안은 직렬화에서 시작합니다. Pickle 직렬화는 임의 코드 실행 위험이 있으므로 JSON을 씁니다. 워커는 전용 사용자로 실행하고 root로 띄우지 않습니다.
app.conf.task_serializer = 'json'
app.conf.accept_content = ['json']# 전용 사용자로 실행
sudo -u celery celery -A myapp worker메시지 위변조까지 막아야 한다면 암호화 서명을 켭니다. 키와 인증서로 메시지에 서명하고, 직렬화 형식을 auth로 바꿉니다. 브로커 연결 자체는 전송 계층 보안(Transport Layer Security, TLS)으로 암호화합니다.
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 직렬화와 전용 사용자, 메시지 서명으로 지킵니다. 배포 전에는 브로커 고가용성, 결과 백엔드, 모니터링, 알림, 보안 감사, 성능 기준선을 체크리스트로 한 번씩 확인하는 편이 안전합니다.