카테고리 없음

Celery 간헐적 태스크 실패

E58C 2026. 5. 19. 20:08

상황

cms에 음원을 등록할 때, 소스오디오 서비스에 음원 등록을 해야함. 소스오디오에 음원을 등록하면 나오는 source audio id 를 저장해야하는데 이 작업이 약 10-20분 소요. (바로 create 하는게 아니라 큐에 올리고 10분 뒤 큐에 있는 음악을 한번에 publish함)

source audio id 가 특정 서비스에 꼭 필요해서 정합성을 cms에서 맞춰줘야함.

오래 걸려서 비동기 + 웹훅으로 빼뒀음.

 

문제

.delay() 호출 후 간헐적으로 태스크가 실행되지 않음. 로컬에서 재현 실패(재현 불가능하다고 판단)

 

환경

- Django + Gunicorn (sync worker 4개)

- Celery 5.6.3

- Redis (브로커, result backend)

- ECS Fargate - django-app, celery-workerm celery-beat 동일 태스트 내에 존재

 

1단계 - 가설 수립

가설 1: Redis 에 태스크가 전달되지 않았다.

가설 2: Redis 에 전달은 됐지만 워커가 꺼내가지 못했다.

 

2단계 - 큐 상태 로깅 추가

.delay() 직후 Redis 상태 로깅

import redis

r = redis.from_url(os.getenv('CELERY_BROKER_URL'))
queue_len = r.llen('celery')
keys = r.keys('*celery*')
logger.info(f"[큐 상태] LLEN celery={queue_len}, keys={keys}")

 

3단계 - 가설 1 확정

실패한 케이스(task=abcd1234)에서:

- LLEN celery = 0

- GET celery-task-meta-abcd1234... = (nil)

- 워커 로그에 received 없음

성공한 케이스는 celery-task-meta-{task_id} 가 Redis에 정상 존재

결론: 태스크가 Redis 에 아예 들어간 적 없음. 가설 1 확정

 

원인

Celery는 Redis 브로커 연결은 내부적으로 Kombu 라이브러리를 통해 관리함. Kombu는 성능을 위해 Redis TCP 커넥션을 커넥션 풀에 캐싱해서 재사용함 (말을하지

gunicorn worker 프로세스가 idle 상태이거나, ECS 롤링 배포로 네트워크가 순간 흔들릴 때 중간 네트워크 장비(AWS)가 조용히 TCP 커넥션을 끊어버림. 근데 Kombu는 이걸 모르고 stale 커넥션을 .delay()에 사용

이 타이밍에:

.delay()→ Kombu publish → stale 커넥션으로 LPUSH 시도 → Kombu 버그로 예외 없이 조용히 실패 → 태스크 유실.

관련 이슈는 celery/kombu, celery/celery github에 존재함

 

https://github.com/celery/celery/discussions/7276

 

worker stops consuming tasks after redis reconnection on celery 5 · celery celery · Discussion #7276

I am experiencing an issue with celery==5.2.3 that I did not experience with celery 4.4.7 which I have recently migrated from. I am using redis (5.0.9) as the message broker. When I manually restar...

github.com

https://github.com/celery/kombu/issues/1588

 

Redis connection failure raises TypeError, bypasses retry · Issue #1588 · celery/kombu

If connecting to redis returns a timeout, kombu raises a TypeError instead of the timeout error and this bypasses any retry logic. > lambda: self._connect(), lambda error: self.disconnect(error) E ...

github.com

 

시도한 설정들

시도 기대한 효과
socket_keepalive 설정 TCP 레벨에서 커넥션이 살아있는지 주기적으로 확인
retry_on_timeout Redis 요청이 타임아웃 났을 때 자동으로 재시도하는 옵션
BROKER_POOL_LIMIT = None Kombu 커넥션 풀을 비활성화(풀 자체를 없애서 매번 새 커넥션을 만들게)
health_check_interval: 30 30초마다 커넥션 상태를 체크

 

결론 및 대응 방향

.delay() 자체가 간헐적으로 실패하는것을 인정. 실패 안 하게끔 고치는 것보다 실패해도 재처리가 되는 구조를 만들었습니다.

Redis 에 처리가 필요한 data의 unique한 값을 올리고 beat로 재처리했습니다.

정합성이 잘 잡힙니다.