Celery 아키텍처와 메시지 라이프사이클
Producer·Broker·Worker·결과 백엔드 네 역할과, 메시지 한 건이 발행돼 실행되기까지의 내부 흐름
Contents
Celery에서 메시지 한 건은 프로듀서의 발행부터 워커의 실행까지 정해진 단계를 거칩니다.
네 가지 핵심 역할
Celery는 메시지 기반 분산 작업 큐입니다. 프로듀서(Producer)가 작업을 메시지로 만들어 브로커에 발행하면, 워커(Worker)가 그 메시지를 꺼내 실행합니다. 이런 작업 큐 패턴에서는 발행하는 쪽과 실행하는 쪽이 직접 연결되지 않습니다.
다이어그램의 화살표는 메시지가 한 방향으로 흐르는 경로입니다. 각 역할이 맡는 일은 아래 표와 같습니다.
| 역할 | 하는 일 |
|---|---|
| Producer (프로듀서) | delay()·apply_async()로 태스크를 메시지로 발행 |
| Broker (브로커) | 메시지를 큐에 보관하고 워커에 전달 |
| Worker (워커) | 큐에서 메시지를 꺼내 태스크 코드를 실행 |
| Result Backend (결과 백엔드) | 태스크 상태와 반환값 저장 (선택) |
| Beat (비트) | 주기적 태스크를 정해진 시각에 큐로 발행 |
프로듀서는 워커의 상태를 모른 채 메시지만 발행합니다. 워커는 브로커에서 자율적으로 메시지를 소비합니다. 그래서 같은 큐를 보는 워커를 늘리면 처리량이 수평으로 확장됩니다.
메시지 한 건의 처리 흐름
하나의 태스크 호출은 클라이언트 측 발행과 워커 측 수신으로 나뉩니다. 클라이언트는 라우팅, 직렬화, 발행을 차례로 거칩니다. 워커는 수신, Request 생성, 실행, 확인 응답 순으로 처리합니다.
이 흐름에서 클라이언트는 발행 직후 제어를 돌려받습니다. 결과가 필요하면 나중에 결과 백엔드를 조회합니다.
프로토콜 v2 메시지 구조
프로토콜 v2(protocol version 2)는 Celery가 메시지를 구성하는 직렬화 규약입니다. 메시지를 properties, headers, body 세 부분으로 나눕니다. 헤더는 본문을 역직렬화하지 않고도 읽을 수 있습니다.
message = {
'properties': {
'correlation_id': task_id, # task_id와 동일
'content_type': 'application/json', # 직렬화 포맷
'content_encoding': 'utf-8',
},
'headers': { # 본문을 풀지 않고 접근
'lang': 'py',
'task': 'myapp.tasks.add', # 정규화된 태스크 이름
'id': task_id,
'root_id': root_id, # 워크플로우 추적용
'retries': 0, # 재시도 횟수
'eta': None, # 예약 실행 시각
'expires': 600, # 만료(초)
},
'body': (
args_tuple, # 위치 인자
kwargs_dict, # 키워드 인자
{'callbacks': [], 'errbacks': [], 'chain': [], 'chord': None},
),
}헤더와 본문의 분리가 v2의 핵심 변화입니다. 브로커와 모니터링 도구는 본문을 풀지 않고 헤더만으로 라우팅과 추적을 처리합니다. 헤더의 task는 어떤 함수를 실행할지, id는 결과를 어디에 매칭할지 가리킵니다.
브로커와 AMQP 모델
브로커가 메시지를 큐에 넣고 꺼내는 내부 모델은 AMQP(Advanced Message Queuing Protocol)를 따릅니다. 프로듀서는 메시지를 Exchange로 보내고, Exchange는 Binding 규칙에 따라 Queue로 분배합니다.
Exchange 타입이 분배 방식을 정합니다. direct는 routing_key가 정확히 맞는 큐로, topic은 와일드카드 패턴으로, fanout은 모든 큐로 메시지를 보냅니다.
RabbitMQ는 이 AMQP 모델을 네이티브로 구현한 브로커입니다. Redis는 Kombu(Celery의 메시징 추상화 라이브러리)를 통해 같은 시맨틱을 흉내 냅니다. Redis에서 Queue는 리스트(LPUSH/BRPOP)로, QoS는 visibility timeout으로 대체됩니다.
| 선택 기준 | 권장 브로커 |
|---|---|
| 대규모·높은 신뢰성 | RabbitMQ |
| 중소 규모·운영 간소 | Redis |
| AWS 전용 | SQS |
| Canvas·Chord 사용 | Redis 또는 RabbitMQ + Redis 백엔드 |
표는 일반적인 선택 기준이며 절대적이지 않습니다. 라우팅과 신뢰성이 중요하면 RabbitMQ가, 구성의 단순함이 우선이면 Redis가 무난합니다.
워커의 수신과 부트스텝
워커는 시작하자마자 메시지를 받지 않습니다. 부트스텝(bootstep) 의존성 그래프인 Blueprint가 구성 요소를 순서대로 켭니다. 메시지 수신은 마지막 Consumer 안의 Tasks 단계에서 시작됩니다.
Worker Blueprint의 마지막 Consumer 단계가 아래 Consumer Blueprint로 펼쳐집니다.
Tasks 단계에서 kombu.Consumer가 큐를 구독하고 on_message 콜백을 겁니다. 메시지가 오면 헤더의 태스크 이름으로 전략을 찾아 Request를 만들고 풀로 넘깁니다.
def on_message(body, message):
task_name = message.headers['task'] # 1. 헤더에서 태스크 이름
strategy = strategies[task_name] # 2. 태스크별 전략 조회
request = strategy( # 3. 디코딩 + Request 생성
message,
accept=consumer.accept,
timeout=worker.pool_timeout,
)
dispatch_to_pool(request) # 4. 실행 풀로 디스패치strategy는 태스크마다 미리 컴파일해 둔 처리 함수입니다. Request는 헤더와 본문을 풀어 args, kwargs, eta 같은 실행 정보를 담은 객체입니다. 이 객체가 풀에 들어가면 실제 함수가 호출됩니다.
워커 풀 선택
실제 태스크를 실행하는 주체는 워커 풀입니다. 풀 종류에 따라 동시성 모델과 적합한 작업이 달라집니다.
| 풀 | CPU 집약 | I/O 집약 | 동시성 | 메모리 |
|---|---|---|---|---|
| prefork | 최적 | 좋음 | 낮음~중간 | 높음 |
| gevent | 부적합 | 최적 | 매우 높음 | 낮음 |
| eventlet | 부적합 | 최적 | 매우 높음 | 낮음 |
| solo | 순차 | 순차 | 1 | 낮음 |
CPU를 많이 쓰거나 메모리 누수가 걱정되면 prefork가 안전합니다. 외부 호출이 잦은 I/O 작업은 gevent나 eventlet이 적은 메모리로 높은 동시성을 냅니다. 개발과 디버깅에는 단일 프로세스인 solo가 편합니다.
Ack 전략과 prefetch
메시지 손실 여부는 확인 응답(ack) 시점에서 갈립니다. 기본값은 early ack로, 워커가 메시지를 받자마자 브로커에 ack를 보냅니다. 이때 실행 도중 워커가 죽으면 그 작업은 사라집니다.
task_acks_late=True로 바꾸면 실행이 끝난 뒤 ack를 보냅니다. 워커가 중간에 죽으면 메시지가 큐에 남아 다른 워커가 다시 실행합니다. 다만 같은 작업이 두 번 실행될 수 있으므로 멱등한 태스크에만 안전합니다.
prefetch는 워커가 한 번에 미리 예약하는 메시지 수입니다. worker_prefetch_multiplier가 클수록 처리량은 늘지만 작업이 한 워커에 몰립니다. 긴 작업에서는 1로 낮춰 고르게 분배합니다.
app.conf.update(
task_acks_late=True, # 실행 완료 후 ack
task_reject_on_worker_lost=True, # 워커 사망 시 메시지 재큐
worker_prefetch_multiplier=1, # 한 번에 1건만 예약
)이 세 설정을 함께 쓰면 워커가 죽어도 작업이 살아남고, 큐가 한쪽으로 쏠리지 않습니다.
정리
Celery의 동작은 프로듀서·브로커·워커·결과 백엔드라는 네 역할과, 그 사이를 오가는 메시지 한 건의 처리 흐름으로 요약됩니다. 클라이언트는 프로토콜 v2로 메시지를 구성해 Exchange에 발행하고, 브로커는 AMQP 모델에 따라 큐에 보관합니다. 워커는 Blueprint 순서로 부팅한 뒤 Consumer로 메시지를 받아 풀에서 실행합니다.
신뢰성은 ack 전략과 prefetch, 재시도 설정의 조합으로 맞춥니다. 브로커는 라우팅과 신뢰성이 중요하면 RabbitMQ, 단순함이 우선이면 Redis를 고르면 됩니다. 더 깊은 설정값은 공식 문서(docs.celeryq.dev)의 구성 레퍼런스에서 확인할 수 있습니다.