von history_cleanup_every bis zum manuellen Cleanup
cron_schedule protokolliert jeden geplanten und ausgeführten Cron-Job in Magento und kann bei falscher Konfiguration innerhalb weniger Wochen Millionen Zeilen ansammeln. Wer die Statusübergänge der Tabelle versteht, history_cleanup_every richtig einstellt und bei Bedarf manuell nachhilft, verhindert langsame Cron-Läufe und Locking-Probleme in der Produktion.
Inhaltsverzeichnis
- 1. Wie cron_schedule funktioniert
- 2. Warum die Tabelle bloatet
- 3. history_cleanup_every und verwandte Konfiguration
- 4. Manuelle Cleanup-Queries für alte Einträge
- 5. Symptome einer aufgeblähten Tabelle
- 6. Analyse: welche Jobs die meisten Einträge erzeugen
- 7. Best-Practice-Konfiguration je Cron-Gruppe
- 8. Monitoring-Skript und Alerting
- 9. Vorbeugen: Indizes und Wartungsfenster
- 10. Zusammenfassung
- 11. FAQ
1. Wie cron_schedule funktioniert
Die Tabelle cron_schedule ist das Herzstück der Magento-Cron-Verarbeitung. Für jeden konfigurierten Cron-Job legt Magento im Voraus Zeilen mit dem Status pending an, jeweils mit job_code, scheduled_at und initial leerem executed_at. Sobald der Cron-Runner (bin/magento cron:run) einen fälligen Job findet, wechselt der Status zu running, nach Abschluss zu success oder bei einem Fehler zu error oder missed, falls das Zeitfenster verpasst wurde.
Diese Statusübergänge sind bewusst so gestaltet, dass cron_schedule gleichzeitig als Warteschlange und als Ausführungsprotokoll dient. Der Vorteil ist Nachvollziehbarkeit, jeder Job-Lauf lässt sich im Nachhinein über scheduled_at, executed_at und finished_at auswerten. Der Nachteil ist, dass ohne aktives Aufräumen jede einzelne dieser Zeilen dauerhaft in der Tabelle verbleibt, selbst nach erfolgreichem Abschluss des Jobs.
Bei einem typischen Magento-Shop mit mehreren Dutzend registrierten Cron-Jobs, die im Minutentakt geplant werden, erzeugt allein die Erzeugung neuer pending-Zeilen pro Tag mehrere Tausend neue Einträge in cron_schedule. Ohne Bereinigung ist ein Wachstum auf mehrere Millionen Zeilen innerhalb weniger Monate keine Ausnahme, sondern die Regel.
DESCRIBE cron_schedule;
-- +--------------+------------------+------+-----+---------+----------------+
-- | Field | Type | Null | Key | Default | Extra |
-- +--------------+------------------+------+-----+---------+----------------+
-- | schedule_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
-- | job_code | varchar(255) | NO | MUL | NULL | |
-- | status | varchar(7) | NO | MUL | pending | |
-- | messages | text | YES | | NULL | |
-- | created_at | timestamp | NO | | CURRENT | |
-- | scheduled_at | varchar(30) | NO | MUL | NULL | |
-- | executed_at | varchar(30) | YES | | NULL | |
-- | finished_at | varchar(30) | YES | | NULL | |
-- +--------------+------------------+------+-----+---------+----------------+
-- Current status distribution
SELECT status, COUNT(*) AS count FROM cron_schedule GROUP BY status;
2. Warum die Tabelle bloatet
Der Hauptgrund für Bloat in cron_schedule ist simpel: Magento erzeugt kontinuierlich neue Zeilen, aber die Standardkonfiguration löscht alte Zeilen nur mit einer bestimmten Verzögerung und nur, wenn der Bereinigungsjob selbst zuverlässig läuft. Fällt der Cron-Runner über längere Zeit aus, etwa weil ein Server-Wartungsfenster den Cron deaktiviert hat oder ein PHP-Fehler den Prozess crasht, sammeln sich pending-Einträge unkontrolliert an, ohne dass jemals eine Bereinigung stattfindet.
Ein zweiter, oft übersehener Grund ist eine zu aggressive schedule_ahead_for-Konfiguration oder Cron-Jobs, die mit sehr kurzen Intervallen (jede Minute oder häufiger) laufen. Jeder dieser Jobs erzeugt eigene Zeilen in cron_schedule, und bei Custom-Modulen, die eigene, sehr häufige Cron-Jobs registrieren, kann die Zeilenanzahl schneller wachsen, als der Standard-Cleanup nachkommt. Ein dritter Grund ist eine falsch konfigurierte oder deaktivierte history_cleanup_every-Einstellung, wodurch abgeschlossene Jobs zwar den Status success erhalten, aber nie tatsächlich gelöscht werden.
3. history_cleanup_every und verwandte Konfiguration
Magento steuert die Bereinigung von cron_schedule über mehrere Parameter in der crontab.xml-Gruppenkonfiguration beziehungsweise über die Admin-Einstellung unter System > Cron. Zentral sind history_cleanup_every (wie oft die Bereinigung läuft, in Minuten), history_success_lifetime (wie lange erfolgreiche Jobs aufbewahrt werden, in Minuten) und history_failure_lifetime (wie lange fehlgeschlagene Jobs aufbewahrt werden). Der Standardwert für history_success_lifetime liegt bei 2880 Minuten (2 Tage), was für die meisten Shops ausreichend ist, aber bei sehr hoher Job-Frequenz zu knapp bemessen sein kann.
Diese Werte lassen sich pro Cron-Gruppe (default, index, consumers) individuell in app/etc/env.php unter dem Schlüssel cron_configuration oder direkt über den Admin-Bereich anpassen, geprüft zum Beispiel mit bin/magento config:show cron_schedule/default/schedule_ahead_for. Eine häufige Fehlkonfiguration ist, dass Entwickler history_cleanup_every beim Debuggen auf einen sehr hohen Wert setzen, um Job-Historie länger einzusehen, und vergessen, den Wert danach zurückzusetzen. Das Ergebnis ist eine cron_schedule-Tabelle, die still und unbemerkt über Wochen wächst.
-- Verify how far back finished job history actually reaches
SELECT job_code, MIN(scheduled_at) AS oldest, MAX(scheduled_at) AS newest, COUNT(*) AS total
FROM cron_schedule
WHERE status = 'success'
GROUP BY job_code
ORDER BY total DESC
LIMIT 10;
4. Manuelle Cleanup-Queries für alte Einträge
Wenn cron_schedule bereits mehrere Millionen Zeilen erreicht hat, reicht das Zurücksetzen der Konfiguration allein nicht aus, weil der reguläre Cleanup-Job selbst nur begrenzte Batches pro Lauf verarbeitet und bei einer stark aufgeblähten Tabelle Wochen bräuchte, um den Rückstau abzuarbeiten. In diesem Fall ist ein manueller, einmaliger Cleanup per SQL der pragmatische Weg, wichtig ist dabei, in kontrollierten Batches zu löschen, um lange Locks auf einer produktiv genutzten Tabelle zu vermeiden.
Vor jedem manuellen Cleanup sollte der Cron-Prozess kurz pausiert werden (bin/magento cron:run darf während der Löschoperation nicht parallel schreiben), und es empfiehlt sich, zunächst nur wenige Tausend Zeilen pro DELETE-Anweisung zu entfernen und den Effekt auf die Systemlast zu beobachten, bevor größere Batches folgen.
-- Manual batched cleanup of old finished cron_schedule entries
-- Run repeatedly until affected rows reach 0, in small batches to avoid long locks
DELETE FROM cron_schedule
WHERE status IN ('success', 'missed')
AND scheduled_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 5000;
-- Separately clean up long-stuck error entries
DELETE FROM cron_schedule
WHERE status = 'error'
AND scheduled_at < DATE_SUB(NOW(), INTERVAL 60 DAY)
LIMIT 5000;
-- Reclaim disk space after a large cleanup (run during a maintenance window)
OPTIMIZE TABLE cron_schedule;
5. Symptome einer aufgeblähten Tabelle
Eine aufgeblähte cron_schedule-Tabelle zeigt sich zunächst indirekt: Cron-Läufe dauern spürbar länger, weil bin/magento cron:run bei jedem Aufruf eine Abfrage über alle pending-Zeilen ausführt, um fällige Jobs zu identifizieren. Ist die Tabelle nicht sauber indiziert oder enthält Millionen historischer Zeilen, verlangsamt sich diese Abfrage messbar, was in Summe zu verpassten (missed) Job-Ausführungen führt, weil der nächste Cron-Zyklus bereits beginnt, bevor der vorherige abgeschlossen ist.
Ein zweites Symptom ist Locking bei parallelen Zugriffen: Wenn mehrere Cron-Gruppen gleichzeitig gegen dieselbe stark gewachsene cron_schedule-Tabelle schreiben, steigt die Wahrscheinlichkeit für Lock-Wartezeiten und im Extremfall Deadlocks. In der Praxis äußert sich das oft als scheinbar zufällige Fehler in var/log/cron.log, die auf den ersten Blick nichts mit der Tabellengröße zu tun haben, aber bei genauerer Analyse auf Lock-Timeouts zurückzuführen sind.
6. Analyse: welche Jobs die meisten Einträge erzeugen
Bevor man cron_schedule pauschal bereinigt, lohnt sich eine kurze Analyse, welche job_code-Werte den größten Anteil an der Tabellengröße ausmachen. Häufig sind es nicht die Standard-Magento-Jobs, sondern Custom-Module oder Drittanbieter-Erweiterungen mit sehr kurzen Intervallen, die überproportional viele Zeilen erzeugen. Diese Analyse liefert oft die eigentliche Ursache, nicht nur ein Symptom, das man mit Löschen kaschiert.
Wird ein einzelner Job als Hauptverursacher identifiziert, lohnt sich die Prüfung, ob das konfigurierte Intervall tatsächlich notwendig ist. Ein Synchronisations-Job, der alle 60 Sekunden läuft, obwohl die zugrunde liegende Datenquelle sich nur alle 15 Minuten ändert, erzeugt unnötigen Overhead in cron_schedule und sollte auf ein realistisches Intervall gestreckt werden, statt die Symptome durch immer aggressiveres Löschen zu bekämpfen.
-- Identify the top job_code contributors to cron_schedule row count
SELECT job_code, COUNT(*) AS row_count,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM cron_schedule), 1) AS pct_of_total
FROM cron_schedule
GROUP BY job_code
ORDER BY row_count DESC
LIMIT 15;
7. Best-Practice-Konfiguration je Cron-Gruppe
Magento trennt Cron-Jobs standardmäßig in mehrere Gruppen: default für allgemeine Wartungsjobs, index für Reindex-Läufe und consumers für Message-Queue-Consumer. Jede Gruppe kann eine eigene history_cleanup_every-, history_success_lifetime- und history_failure_lifetime-Konfiguration haben, was gezieltes Tuning erlaubt: Die index-Gruppe mit potenziell sehr vielen kurzlebigen Jobs kann eine kürzere Aufbewahrungszeit vertragen als die default-Gruppe, in der Bestellstatus-Übergänge oder E-Mail-Versand-Jobs liegen, deren Historie für Support-Anfragen länger nützlich ist.
Eine bewährte Ausgangskonfiguration reduziert history_success_lifetime auf 1440 Minuten (24 Stunden) für hochfrequente Gruppen und belässt es bei 2880 Minuten (48 Stunden) für die default-Gruppe, kombiniert mit einem history_cleanup_every von 10 Minuten statt der Standardeinstellung, um Rückstände gar nicht erst entstehen zu lassen. Diese Werte sind Ausgangspunkte, keine Universallösung, die tatsächliche Job-Frequenz jedes Shops sollte die endgültige Konfiguration bestimmen.
8. Monitoring-Skript und Alerting
Ein einfaches Monitoring-Skript, das täglich per Cron läuft, prüft die Gesamtzeilenanzahl von cron_schedule sowie die Anzahl der pending-Einträge, die älter als eine erwartbare Zeitspanne sind. Überschreitet die Gesamtzeilenanzahl einen definierten Schwellenwert, oder gibt es überdurchschnittlich viele pending-Einträge, die eigentlich längst hätten ausgeführt werden sollen, deutet das auf einen hängenden oder inaktiven Cron-Runner hin, der sofortige Aufmerksamkeit braucht.
Ergänzend liefert eine Auswertung der error- und missed-Status über Zeit wertvolle Hinweise auf strukturelle Probleme, etwa wenn ein bestimmter Job systematisch fehlschlägt. Ein solches Monitoring lässt sich leicht in bestehende Alerting-Systeme integrieren, indem das Skript bei Überschreitung der Schwellenwerte eine E-Mail oder eine Nachricht an einen Monitoring-Kanal sendet.
#!/usr/bin/env bash
# cron-schedule-monitor.sh: alert on cron_schedule bloat or stuck runner
set -euo pipefail
THRESHOLD_ROWS=200000
THRESHOLD_STALE_PENDING=500
TOTAL_ROWS=$(mysql -N magento -e "SELECT COUNT(*) FROM cron_schedule")
STALE_PENDING=$(mysql -N magento -e "
SELECT COUNT(*) FROM cron_schedule
WHERE status = 'pending' AND scheduled_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
")
if (( TOTAL_ROWS > THRESHOLD_ROWS )); then
echo "[ALERT] cron_schedule has ${TOTAL_ROWS} rows, exceeds threshold" >&2
fi
if (( STALE_PENDING > THRESHOLD_STALE_PENDING )); then
echo "[ALERT] ${STALE_PENDING} stale pending jobs, cron runner may be stuck" >&2
fi
9. Vorbeugen: Indizes und Wartungsfenster
Magento legt auf cron_schedule standardmäßig Indizes auf job_code, status und scheduled_at an, die für die üblichen Abfragen ausreichen, solange die Tabelle nicht durch Bloat aus dem Ruder läuft. Bei sehr hoher Job-Frequenz kann ein zusätzlicher zusammengesetzter Index auf status und scheduled_at die Performance der Abfrage verbessern, mit der der Cron-Runner fällige Jobs sucht, weil MySQL dann direkt über beide Filterkriterien scannen kann statt nur über eines.
Vorbeugend hilft außerdem ein festes Wartungsfenster, in dem regelmäßig geprüft wird, ob history_cleanup_every noch aktiv und korrekt konfiguriert ist, besonders nach größeren Deployments oder Modul-Updates, die neue Cron-Jobs registrieren könnten. Ein Shop, der diese Prüfung als festen Bestandteil seines Deployment-Prozesses etabliert, verhindert, dass cron_schedule überhaupt erst in einen kritischen Zustand gerät.
Vergleich: Cleanup-Strategien für cron_schedule
| Strategie | Einsatz | Risiko | Wirkung |
|---|---|---|---|
| history_cleanup_every | Laufender Regelbetrieb | Gering | Kontinuierlich, verhindert Bloat |
| Manuelles Batch-DELETE | Bereits stark aufgeblähte Tabelle | Mittel bei falscher Batch-Größe | Sofortige Entlastung |
| Job-Intervall reduzieren | Einzelner Job als Hauptverursacher | Gering | Behebt Ursache statt Symptom |
| OPTIMIZE TABLE | Nach großem Cleanup | Sperrt Tabelle während Lauf | Speicherplatz freigeben |
Mironsoft
Magento-Cron-Konfiguration und Datenbankwartung
cron_schedule aus dem Ruder gelaufen?
Wir analysieren eure Cron-Jobs, identifizieren die Hauptverursacher, richten eine saubere history_cleanup_every-Konfiguration ein und bauen Monitoring gegen hängende Cron-Runner auf.
Cron-Audit
Job-Frequenz und Hauptverursacher der Tabellengröße identifizieren
Cleanup-Setup
history_cleanup_every je Cron-Gruppe sauber konfigurieren
Monitoring
Alerting gegen hängende oder deaktivierte Cron-Runner
10. Zusammenfassung
Die cron_schedule-Tabelle dient gleichzeitig als Warteschlange und als Ausführungsprotokoll für alle Magento-Cron-Jobs, was ohne aktives Aufräumen zu unkontrolliertem Wachstum führt. Die Standardkonfiguration über history_cleanup_every, history_success_lifetime und history_failure_lifetime reicht für die meisten Shops, sollte aber pro Cron-Gruppe an die tatsächliche Job-Frequenz angepasst werden, besonders bei hochfrequenten Custom-Jobs.
Ist die Tabelle bereits stark aufgebläht, hilft nur ein manueller, batched Cleanup per SQL, gefolgt von OPTIMIZE TABLE zur Speicherplatzfreigabe. Symptome wie langsame Cron-Läufe, verpasste Jobs und Deadlocks lassen sich mit einer Analyse der job_code-Verteilung auf die eigentliche Ursache zurückführen, statt nur das Symptom mit immer aggressiverem Löschen zu bekämpfen. Ein einfaches Monitoring-Skript, das Tabellengröße und veraltete pending-Einträge prüft, verhindert, dass sich das Problem unbemerkt wiederholt.
cron_schedule aufräumen: Das Wichtigste auf einen Blick
Konfiguration
history_cleanup_every, history_success_lifetime und history_failure_lifetime je Cron-Gruppe prüfen.
Manueller Cleanup
Batched DELETE mit LIMIT, gefolgt von OPTIMIZE TABLE zur Speicherplatzfreigabe.
Ursachenanalyse
job_code-Verteilung prüfen, oft ist ein Custom-Job mit zu kurzem Intervall die eigentliche Ursache.
Monitoring
Tägliches Skript gegen Tabellenwachstum und veraltete pending-Einträge einrichten.