Aller au contenu

Planifier les tâches périodiques de Celery avec Beat et django-celery-beat

Faire tourner Celery Beat comme service systemd séparé, stocker la planification en base pour les modifications en direct, et empêcher les expéditions dupliquées dans un déploiement multi-hôtes.

Celery Beat est le planificateur qui transforme « envoyer le rapport quotidien à 06h00 » en « mettre cette tâche dans le broker toutes les 24 heures ». C’est un processus séparé, à instance unique — jamais deux — et il lui faut sa propre unité systemd, sa propre politique de redémarrage et des garde-fous explicites contre l’exécution de copies multiples. Cet article câble Beat avec django-celery-beat pour que les plannings vivent en base et soient modifiables sans déploiement, et ajoute un garde-fou d’instance unique pour les configurations multi-hôtes.

Comment vérifier

Confirmer que Beat est le seul planificateur en cours et que les dernières expéditions sont bien arrivées :

systemctl status celery-beat.service --no-pager | head
ps -eo pid,etime,cmd | grep '[c]elery.*beat' | head
redis-cli -h 127.0.0.1 -n 0 LLEN celery
journalctl -u celery-beat -n 100 --no-pager | grep -E 'Scheduler|Sending due task'
ls -la /var/lib/celery-beat/celerybeat-schedule*

Deux processus Beat sur des hôtes différents est la source la plus fréquente des bugs d’exécution dupliquée. La ligne ps doit retourner un PID, pas deux.

Ce qui se passe

Beat est un planificateur mono-processus qui se réveille au tic de planification, regarde la définition du planning, et expédie les tâches dues au broker. La planification elle-même se définit de trois façons : en code via app.conf.beat_schedule, dans un fichier celerybeat-schedule adossé à SQLite/fichier, ou en base via django-celery-beat. La troisième option est celle qui passe à l’échelle opérationnellement — les admins peuvent modifier les expressions cron dans l’admin Django sans déploiement, et il y a une source unique de vérité.

Beat ne doit pas tourner deux fois. Deux processus Beat expédieront la même tâche au même moment et vous enverrez deux copies du rapport quotidien. Il n’y a pas de coordination intégrée — la conception suppose qu’on en exécute exactement un. Dans un déploiement multi-hôtes, cela veut dire choisir un hôte et faire tourner Beat seulement sur cet hôte ; ou utiliser un outil comme redis-lock ou une chaîne systemd OnFailure= pour garantir un basculement mutuellement exclusif.

L’état que Beat garde entre les tics (horodatages de dernière exécution) vit soit dans un fichier soit dans une table de base. Avec django-celery-beat, la table django_celery_beat_periodictask détient le planning et django_celery_beat_periodictasks a une seule ligne dont last_update sert de drapeau « table sale » — Beat la sonde pour savoir quand recharger.

La procédure

  1. Installer django-celery-beat et appliquer les migrations :

    /opt/venvs/app/bin/pip install 'django-celery-beat==2.6.*'
    /opt/venvs/app/bin/python manage.py migrate django_celery_beat
  2. L’ajouter à INSTALLED_APPS et configurer Celery pour utiliser le scheduler base dans app/settings/prod.py :

    INSTALLED_APPS = [
        # ...
        "django_celery_beat",
    ]
    CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
  3. Créer /etc/systemd/system/celery-beat.service :

    [Unit]
    Description=Planificateur Celery Beat
    After=network.target redis-server.service postgresql.service
    Requires=redis-server.service
    
    [Service]
    Type=simple
    User=deploy
    Group=deploy
    WorkingDirectory=/var/www/app
    EnvironmentFile=/etc/celery-app/env
    StateDirectory=celery-beat
    StateDirectoryMode=0750
    ExecStart=/opt/venvs/app/bin/celery -A app.celery beat \
        --schedule=/var/lib/celery-beat/celerybeat-schedule \
        --pidfile=/run/celery-beat.pid \
        --loglevel=INFO
    Restart=on-failure
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target

    StateDirectory= est la façon dont systemd crée /var/lib/celery-beat/ avec le bon propriétaire — pas de danse mkdir plus chown manuel.

  4. Activer et démarrer :

    sudo systemctl daemon-reload
    sudo systemctl enable --now celery-beat.service
    journalctl -u celery-beat -n 40 --no-pager
  5. Ajouter un planning via l’admin Django ou par programmation :

    from django_celery_beat.models import CrontabSchedule, PeriodicTask
    sched, _ = CrontabSchedule.objects.get_or_create(
        minute="0", hour="6", day_of_week="*",
        day_of_month="*", month_of_year="*", timezone="UTC",
    )
    PeriodicTask.objects.get_or_create(
        crontab=sched,
        name="daily-report",
        task="app.tasks.send_daily_report",
    )
  6. Dans une configuration multi-hôtes, faire tourner Beat sur exactement un hôte et le garder avec un script de basculement pair. Le motif minimal est une coordination de style BindsTo= via un verrou Redis partagé acquis sur ExecStartPre= :

    ExecStartPre=/usr/local/bin/celery-beat-lock acquire
    ExecStopPost=/usr/local/bin/celery-beat-lock release

    celery-beat-lock est un petit script qui SETNX une clé avec un TTL et sort en non-zéro si elle existe déjà.

Pièges courants

  • Deux processus Beat sur des hôtes différents : chaque tâche se déclenche deux fois. Il n’y a pas d’« élection de leader » dans Beat — vous devez imposer l’instance unique via systemd, un outil HA ou un verrou pair.
  • Mélanger beat_schedule= en code et django-celery-beat en même temps : Beat ne lit que le scheduler configuré ; les entrées de l’autre source sont silencieusement ignorées, et les opérateurs perdent un après-midi à se demander pourquoi une édition de planning n’a pas pris.
  • Un décalage de fuseau entre TIME_ZONE de Django, timezone de Celery et le planning crontab — symptôme : des tâches qui se déclenchent N heures à côté. Tout figer en UTC et convertir à l’affichage.
  • Beat saute un tic en surcharge : si une tâche avec last_run_at=now() dure plus que son intervalle, le tic suivant arrive avant la fin du premier. Soit élargir l’intervalle, soit régler every_minute_run=False sur une tâche qui peut être saisonnée.
  • Oublier StateDirectory= : au redémarrage, Beat lit un fichier de planning vide et décide que chaque tâche est due « tout de suite », déclenchant le lot quotidien complet en une seconde. Utiliser la directive systemd pour que le fichier de planning survive à un redémarrage avec la bonne propriété.

Pour le côté worker de la pile, voir Celery avec un backend Redis. La pratique d’opérations infogérées de Stack Harbor fait tourner Beat comme service systemd dédié sur un hôte désigné par environnement, avec un script de basculement pair et des alertes qui se déclenchent si Beat est silencieux plus de 90 secondes — la panne Celery la plus courante pour laquelle nous sommes paginés.