Мониторинг периодических задач Celery beat
Celery beat — то, чем большинство проектов на Django и Python запускает периодические задачи: ночные отчёты, очистку, синхронизацию с внешними API. И потерять его незаметно очень легко. Beat — один процесс, который только отправляет задачи в брокер, а выполняют их воркеры. Если beat остановится, ничего не упадёт и в логах не будет ни одной ошибки. Задачи просто перестанут отправляться.
Что ломается
- Beat не запущен. Его забыли добавить в новый деплой, контейнер завершился, или из конфигурации supervisor пропала строка с
beat. Воркеры живы и простаивают. - Запущено два beat. Beat должен работать ровно в одном экземпляре. Запустите его в каждой реплике контейнера — и каждая задача уйдёт несколько раз.
- Задачи падают в воркерах. Исключение попадает в лог воркера. Если эти логи никто не собирает и не читает, его никто и не увидит.
- Задачи копятся. Воркеры заняты или лежат, очередь растёт, и задача на 3 часа ночи выполняется в полдень.
- Путаница с часовым поясом. С
enable_utcи безtimezoneвыражениеcrontab(hour=3)означает 3:00 по UTC.
Небольшой помощник для пингов
Положите этот декоратор рядом с задачами. Он сообщает о старте, успехе или ошибке каждого запуска. Неудачный пинг никогда не ломает задачу:
# monitoring.py import functools import urllib.request from celery.exceptions import Retry def _ping(url, body=None): try: data = body.encode()[:100_000] if body else None urllib.request.urlopen(url, data=data, timeout=10) except Exception: pass # мониторинг не должен ломать задачу def heartcron(url): def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): _ping(url + "/start") try: result = fn(*args, **kwargs) except Retry: raise # повтор — ещё не ошибка except Exception as e: _ping(url + "/fail", f"{type(e).__name__}: {e}") raise _ping(url) return result return wrapper return decorator
Оберните задачи
@app.task ставьте снаружи, чтобы Celery зарегистрировал уже обёрнутую функцию:
# tasks.py
from .celery import app
from .monitoring import heartcron
@app.task
@heartcron("https://ping.heartcron.com/<uuid>")
def nightly_report():
build_and_send_report()
И расписание с явно заданным часовым поясом:
# celery.py
from celery.schedules import crontab
app.conf.timezone = "Europe/Moscow"
app.conf.beat_schedule = {
"nightly-report": {
"task": "app.tasks.nightly_report",
"schedule": crontab(hour=3, minute=0),
},
}
В Heartcron создайте проверку с cron-выражением 0 3 * * *, тем же часовым поясом и запасом времени чуть больше обычной длительности задачи. Как перевести crontab() из Celery в cron? Его аргументы идут в порядке «минута, час, день недели, день месяца, месяц», а поля cron — «минута, час, день месяца, месяц, день недели». Проверить результат поможет расшифровщик cron-выражений.
Как выглядит каждый исход
| Ситуация | Что видит Heartcron |
|---|---|
| Задача выполнилась успешно | /start, затем успех с длительностью |
| Задача выбросила исключение | /fail с текстом ошибки, алерт сразу |
| Задача зависла | /start без успеха, алерт после запаса времени |
| Лежит beat, брокер или все воркеры | Ничего не приходит, алерт после запаса времени |
| Два beat отправляют задачу дважды | Два запуска в журнале там, где ждёте один |
Следите за самим beat
По задаче, которая выполняется раз в сутки, пропажу beat заметно только через сутки. Добавьте крошечную задачу раз в пять минут, которая только пингует. Она подтверждает, что работают и beat, и брокер, и хотя бы один воркер:
@app.task
@heartcron("https://ping.heartcron.com/<uuid>")
def heartbeat():
pass
app.conf.beat_schedule["heartbeat"] = {
"task": "app.tasks.heartbeat",
"schedule": 300.0,
}
Её проверке задайте период 5 минут и запас 5–10 минут.
Много задач без множества адресов
Читаемый адрес из ключа пингов проекта и короткого имени избавляет от копирования UUID для каждой задачи. Задайте проверке короткое имя, например sync-stripe, в кабинете или командой heartcron checks create:
@heartcron("https://ping.heartcron.com/<ping-key>/sync-stripe")
Обычные cron-задачи того же проекта, например management-команды Django, можно обернуть без изменений в коде: heartcron run cleanup -- python manage.py clearsessions с открытой утилитой heartcron.