Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Celery и фоновые задачи в Python: практика

Celery на практике: брокеры, очереди, повторы и когда фоновые задачи не нужны

Как строить фоновые задачи на Celery: выбор брокера (Redis/RabbitMQ), очереди и приоритеты, идемпотентность и ретраи, мониторинг (Flower), Beat-расписания — и когда достаточно простых решений.

Прямой ответ

Celery нужен, когда работа дольше HTTP-запроса (письма, отчёты, интеграции), требует ретраев, или должна выполняться по расписанию/очереди вне веб-процесса. Минимальная рабочая схема: брокер (Redis — просто для старта, RabbitMQ — надёжнее для сложных маршрутов), задачи как идемпотентные функции с autoretry_for и экспоненциальным backoff, отдельные очереди под разную природу работы, мониторинг (Flower) с первого дня. И честная оговорка: для «отправить письмо» в маленьком проекте Celery — тяжёлая артиллерия (отдельный процесс-воркер, брокер, сериализация); иногда достаточно фоновой задачи внутри приложения или планировщика — см. статью про cron и фоновые задачи.

Архитектура: продюсер, брокер, воркер, backend

Веб-приложение кладёт задачу в брокер (task.delay(args) — быстрая сериализация сообщения, ответ клиенту сразу); воркер-процесс забирает и выполняет; результат (если нужен) — в result backend. Три следствия архитектуры. Первое: воркеры — отдельные процессы/контейнеры, их масштабируют независимо от веба (celery -A app worker -c 4 — конкурентность воркера под природу задач: процессы для CPU, потоки/gevent для I/O). Второе: аргументы задачи сериализуются (JSON по умолчанию) — передавайте id сущностей, а не ORM-объекты: «передал модель, а в воркере она detached» — классика (причина — в статье про сессии SQLAlchemy). Третье: брокер — инфраструктурная зависимость; локально его поднимает dev-стек (Redis — в статье про Redis в локальном стеке).

Задачи: идемпотентность и ретраи

Правило №1: задача обязана пережить повторный запуск без вреда (броker гарантирует «хотя бы раз», не «ровно раз»): проверка состояния перед действием («письмо уже отправлено? — выход»), идемпотентные операции, task_id как ключ дедупликации там, где нужно. Ретраи — декларативно: @app.task(autoretry_for=(ConnectionError, TimeoutError), retry_backoff=True, retry_backoff_max=600, max_retries=5) — экспоненциальный backoff с джиттером встроен; ретраить всё подряд нельзя — баг в коде от ретраев не лечится, а soft_time_limit/time_limit защищают от зависших задач, съедающих воркеров. Сериализация — JSON (pickle в проде запрещён: выполнение произвольного кода при компрометации брокера).

Очереди и маршрутизация

Одна очередь на всё — путь к тому, что минутный отчёт блокирует отправку писем. Практика: очереди по природе работы — default (быстрое), reports (долгое, низкий приоритет), integrations (внешние API, свои лимиты), воркеры слушают свои наборы (-Q reports,heavy); приоритеты — у RabbitMQ нативно, у Redis — порядком очередей в -Q. Отдельная тема — rate limiting на задачах, бьющих во внешние API (rate_limit='100/m'), чтобы воркеры не убивали чужой сервис и не ловили баны. Beat (встроенный планировщик) закрывает периодические задачи (celery -A app beat + beat_schedule), заменяя cron для внутриприложенческих расписаний — системный cron остаётся для ОС-уровня (разбор — в статье про cron).

Мониторинг и эксплуатация

«Задачи просто выполняются» — иллюзия до первого инцидента: очередь растёт, воркер молча умер, задача зависла. Минимум observability: Flower (веб-морда: очереди, воркеры, история задач, повторный запуск) или prometheus-экспортеры + алерты на длину очереди и возраст старейшей задачи (главный SLI «фоновая система жива»); логи воркеров — в общий стек (структурированные логи — в статье про логирование). Практические грабли: acks_late=True для долгих задач (подтверждение после выполнения — задача не теряется при смерти воркера) в паре с идемпотентностью; prefetch_multiplier=1 для тяжёлых задач (воркер не заглатывает пачку, которую не успеет); graceful shutdown воркеров при деплое (--warm-shutdown), иначе задачи рвутся на середине.

Когда Celery избыточен

Честные альтернативы по возрастанию сложности: фоновые задачи внутри процесса (asyncio-таски/потоки — см. статью про asyncio) — для лёгкой работы, переживающей рестарты приложения «не страшно»; планировщик БД (pg_cron) — для периодики вокруг данных; RQ — тот же Redis, в разы меньше концепций, если нужны просто очереди без Beat и canvas; APScheduler — внутрипроцессные расписания. Celery оправдан, когда появились: ретраи с backoff, несколько типов нагрузки с изоляцией, расписания, цепочки задач (chord/chain) или масштаб, где воркеры живут на отдельных машинах. Начинать с «малой» схемы и вырастать в Celery дешевле, чем стартовать с распределённой системы для трёх писем в день.

Практический сценарий: перенос отправки писем в очередь

Классическая миграция: эндпоинт регистрации отправляет приветственное письмо через SMTP и отвечает две секунды (SMTP медленный), при недоступности почтовика регистрация падает целиком. Переносим: задача send_welcome_email.delay(user_id) — аргументом id, не объект пользователя (сериализуемость и свежесть данных — воркер сам поднимет пользователя из БД и прочитает актуальный email); внутри задачи — идемпотентность по user_id (флаг «welcome отправлен» в таблице или дедуп-ключ в кеше — повторная доставка «хотя бы раз» не шлёт два письма). Настройка задачи: autoretry_for=(ConnectionError, SMTPServerDisconnected), retry_backoff=True, max_retries=5, soft_time_limit=60 — зависшая отправка не занимает воркер вечно. Очередь — отдельная email (воркер с -Q email, concurrency побольше — задача I/O-bound), тяжёлые отчёты уходят в reports и не блокируют письма. В эндпоинте после delay — немедленный ответ: регистрация больше не зависит от живости почтовика. Мониторинг: Flower показывает очередь email (растёт — почтовик лёг, алерт), история задач с ретраями видна. Тесты — task_always_eager=True (логика проверяется синхронно, SMTP мокается на границе), интеграционный прогон с реальным Redis — в testcontainers. Вся миграция — полчаса работы, а профиль задержки регистрации изменился качественно: две секунды превратились в пятьдесят миллисекунд.

FAQ

Redis или RabbitMQ для Celery? Redis — меньше эксплуатационных накладных, достаточно для большинства проектов (учитывая, что Redis у вас, скорее всего, уже есть под кеш). RabbitMQ — честные приоритеты, надёжная маршрутизация, подтверждения; выбирают при сложных очередях и высоких требованиях к доставке.

Как тестировать задачи Celery? task_always_eager=True в тестах — выполнение синхронно в процессе (проверяется логика, не инфраструктура); интеграция с реальным брокером — в testcontainers-прогонах. Сама логика задачи — обычная функция: выносите её из декоратора и тестируйте отдельно.

Нужен ли result backend? Только если результаты реально читаются (статус/значение). Для «сделать и забыть» backend — лишняя нагрузка и место утечек (результаты копятся); отключайте ignore_result=True или не настраивайте backend вовсе.

Почему задача не запускается локально? Чек-лист: брокер запущен и URL совпадает (redis://localhost:6379/0), воркер стартован с тем же приложением (-A), задача зарегистрирована (модуль импортирован), очередь в -Q воркера совпадает с очередью задачи. Девять из десяти «молча не работает» — воркер слушает не ту очередь.

Источники: документация Celery (introduction, tasks, routing) — ссылки в шапке.