12k
All articles

Cron-Jobs durch systemd-Timer ersetzen

Ersetzen Sie Cron-Jobs durch systemd-Timer: Erstellen Sie Service- und Timer-Units, prüfen Sie OnCalendar-Zeitpläne, holen Sie Läufe nach und sehen Sie Protokolle ein.

OpenReplay Team
OpenReplay Team
Cron-Jobs durch systemd-Timer ersetzen

Um einen Cron-Job durch einen systemd-Timer zu ersetzen, schreiben Sie zwei Unit-Dateien: eine .service-Datei, die das Skript ausführt, und eine .timer-Datei, die festlegt, wann das geschieht. Anschließend aktivieren Sie den Timer mit systemctl enable --now.

Wenn Sie schon einmal um 9 Uhr morgens /var/log/backup.log geöffnet haben und nicht erkennen konnten, ob der Lauf der vergangenen Nacht funktioniert hat, wissen Sie bereits, warum viele Admins von Cron wegmigrieren. Diese Anleitung überführt eine reale Crontab-Zeile in einen Timer:

30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Jeder Abschnitt ersetzt einen Teil dieser Zeile. Aus dem Zeitplan wird OnCalendar=, aus der Umleitung wird das Journal, und aus „Ist er gelaufen?“ wird systemctl status. Hintergrundinformationen zu Cron selbst finden Sie unter How to automate cron jobs. Die Befehle funktionieren auf jeder aktuellen systemd-basierten Distribution.

Das Wichtigste in Kürze

  • Ein systemd-Timer ist eine .timer-Unit, deren [Timer]-Abschnitt festlegt, wann ein zugehöriger .service ausgeführt wird. Der Service definiert, was ausgeführt wird, und der Timer ist die Unit, die Sie aktivieren.
  • Der Service, den ein Timer auslöst, benötigt keinen [Install]-Abschnitt. Der Timer bindet sich über WantedBy=timers.target selbst in den Bootvorgang ein.
  • systemd-analyze calendar prüft, ob ein OnCalendar=-Ausdruck korrekt geparst wird, und gibt den nächsten Auslösezeitpunkt aus.
  • Persistent=true führt einen Job, der verpasst wurde, während der Rechner ausgeschaltet war, sofort aus, sobald der Timer wieder aktiviert wird.
  • systemd-Timer haben kein eingebautes Pendant zu Crons MAILTO. Für Benachrichtigungen bei Fehlern verwenden Sie OnFailure=.

Was Cron nicht kann

Cron startet Befehle nach einem Zeitplan und tut darüber hinaus wenig. Die folgenden Lücken sind die üblichen Gründe, einen Job zu migrieren:

  • Kein Logging außer per Mail versandter Ausgabe. Entweder leiten Sie die Ausgabe von Hand in eine Datei um, oder Sie verlassen sich auf MAILTO. Timer-Jobs schreiben automatisch ins Journal (siehe „Logs und Status des letzten Laufs auslesen“).
  • Keine Abhängigkeiten zwischen Jobs. Timer-Jobs sind gewöhnliche Units und können daher von anderen systemd-Units abhängen.
  • Nichts läuft, wenn der Rechner zum geplanten Zeitpunkt ausgeschaltet war. Persistent=true holt den verpassten Lauf nach (siehe den Abschnitt zu Persistent).
  • Keine einfache Möglichkeit, das Ergebnis der letzten Nacht zu prüfen. systemctl status zeigt den letzten Lauf und seinen Exit-Code.

Einen systemd-Timer von Anfang bis Ende erstellen

Ein funktionierender Timer besteht aus zwei Dateien in /etc/systemd/system/ sowie einem Befehl: sudo systemctl enable --now backup.timer. Beginnen Sie mit dem Skript.

#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail

SRC=/srv/app/data
DEST=/var/backups/app

tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete

set -euo pipefail sorgt dafür, dass das Skript mit einem Exit-Code ungleich null beendet wird, sobald ein Befehl fehlschlägt – und genau diesen Exit-Code zeichnet systemd auf. Machen Sie das Skript ausführbar und legen Sie das Zielverzeichnis an. Falls Ihnen die Berechtigungsbits nicht vertraut sind, erklärt der Linux File Permissions Guide sie im Detail.

sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app

Als Nächstes der Service, /etc/systemd/system/backup.service:

[Unit]
Description=Nightly tarball of /srv/app/data

[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh

Type=oneshot in systemd.service(5) eignet sich für Skripte, die bis zum Ende durchlaufen. systemd betrachtet die Unit erst dann als gestartet, wenn das Skript beendet ist. Sofern Sie nicht RemainAfterExit= hinzufügen, wechselt die Unit danach direkt wieder in den Zustand „inactive“ und wird nie als „active“ angezeigt. Ersetzen Sie deploy durch ein vorhandenes Nicht-Root-Konto, das SRC lesen und in DEST schreiben darf. Ohne User= läuft der Job als root. Der Service hat keinen [Install]-Abschnitt und keinen Verweis auf den Timer. Die Timer-Seite im Arch Wiki erklärt den Grund: Der Timer ist die Unit, die Sie aktivieren, und er startet den Service mit dem passenden Namen.

Nun der Timer, /etc/systemd/system/backup.timer:

[Unit]
Description=Run backup.service daily at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

timers.target ist ein spezielles Target, über das systemd beim Booten alle Timer startet, die laufen sollen. Laden Sie systemd neu, führen Sie den Job einmal manuell aus und planen Sie ihn dann ein:

sudo systemctl daemon-reload
sudo systemctl start backup.service     # test run now; returns when the script exits
systemctl status backup.service         # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer

Da jeder zeitgesteuerte Job ein gewöhnlicher Service ist, können Sie ihn mit systemctl start backup.service jederzeit bei Bedarf ausführen und debuggen, ohne bis 02:30 Uhr warten zu müssen. Für sich allein legt enable nur die Symlinks an. Der Schalter --now in systemctl(1) startet den Timer zusätzlich. list-timers akzeptiert ein Unit-Muster, und --all bezieht auch inaktive Timer mit ein. Zwischen den Läufen wird der Service als „inactive“ angezeigt. Das ist normal.

OnCalendar im Vergleich zur fünfteiligen Cron-Zeile

Führen Sie systemd-analyze calendar für jeden OnCalendar=-Ausdruck aus, bevor er in eine Timer-Datei wandert. Der Befehl bestätigt, dass der Ausdruck korrekt geparst wird, und gibt den nächsten Auslösezeitpunkt aus. Die Grammatik in systemd.time(7) lautet DayOfWeek Year-Month-Day Hour:Minute:Second. Jedes Datums- oder Zeitfeld kann * (passt auf alles), eine kommagetrennte Liste von Werten oder ein mit .. geschriebener Bereich sein. Den Wochentag können Sie komplett weglassen. Wenn Sie ihn angeben, verwenden Sie englische Tagesnamen wie Mon oder Monday, nicht *.

Gängige Cron-Zeitpläne lassen sich wie folgt auf OnCalendar=-Ausdrücke abbilden. Das Linux Cron Cheatsheet behandelt die Cron-Seite dieser Tabelle:

ZeitplancronOnCalendar
Täglich um 02:30 Uhr30 2 * * **-*-* 02:30:00
Werktags um 22:30 Uhr30 22 * * 1-5Mon..Fri 22:30
Sonntags um 03:00 Uhr0 3 * * 0Sun *-*-* 03:00:00
Am 1. des Monats, Mitternacht0 0 1 * **-*-01 00:00:00
Alle 15 Minuten*/15 * * * **:00/15

Die Cron-Zeile 30 22 * * 1-5 wird zu OnCalendar=Mon..Fri 22:30, da die Cron-Wochentagsnummern 1 bis 5 für Montag bis Freitag stehen. Das Suffix /15 bedeutet „alle 15 Einheiten wiederholen“. Validieren Sie einen Ausdruck mit systemd-analyze calendar:

systemd-analyze calendar "Mon..Fri 22:30" --iterations=3

Beispielausgabe (die Datumsangaben sind Platzhalter):

  Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
    Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
       (in UTC): Mon YYYY-MM-DD 20:30:00 UTC
       From now: 5h 12min left
   Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
   Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST

Die Zeile „Normalized form“ zeigt die Kurzform, erweitert auf die vollständige Grammatik. Die Zeile (in UTC) erscheint nur, wenn die lokale Zeit nicht UTC ist. Versionen vor systemd 253 bezeichnen die zusätzlichen Läufe als Iter. #2 statt Iteration #2. --iterations= erfordert systemd 242 oder neuer.

Ein Hinweis zur Genauigkeit: AccuracySec eines Timers ist standardmäßig auf eine Minute eingestellt, sodass ein Job für 02:30:00 Uhr nicht garantiert exakt zu dieser Sekunde startet. Wenn Sie das präziseste Timing benötigen, das systemd bietet, fügen Sie dem [Timer]-Abschnitt AccuracySec=1us hinzu.

Persistent=true: Das Laptop-Feature

Mit Persistent=true startet ein Timer, der seinen geplanten Lauf verpasst hat, weil der Rechner ausgeschaltet war, seinen Service sofort, sobald der Timer wieder aktiviert wird. In der Regel ist das beim nächsten Booten der Fall. Klassisches Cron bietet nichts Vergleichbares. Ist der Laptop um 02:30 Uhr ausgeschaltet, überspringt Cron den Job. Für das Nachholen verpasster Jobs gibt es das separate Tool anacron (auf Systemen der RHEL-Familie wird es mit cronie ausgeliefert), es plant jedoch nur in ganzen Tagen und nicht zu einer bestimmten Uhrzeit. systemd.timer(5) speichert, wann jeder persistente Timer zuletzt ausgelöst wurde. Persistent= hat bei Timern ohne OnCalendar= keine Wirkung, und auch der nachgeholte Lauf wartet eine etwaige RandomizedDelaySec=-Verzögerung ab.

Monotone Timer wie OnBootSec= und OnUnitActiveSec= verhalten sich anders. Sie zählen nicht weiter, während sich das System im Ruhezustand befindet, sofern nicht WakeSystem= gesetzt ist. Verwenden Sie für Jobs, die an eine Uhrzeit gebunden sind, OnCalendar= zusammen mit Persistent=true.

Logs und Status des letzten Laufs auslesen

Um zu prüfen, ob der Job der letzten Nacht erfolgreich war, führen Sie systemctl status backup.service aus. Der Befehl zeigt, wann der Service zuletzt gelaufen ist, ob er erfolgreich war oder fehlgeschlagen ist, sowie den Exit-Code des Prozesses. journalctl(1) zeigt alles an, was das Skript ausgegeben hat:

systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"

--since akzeptiert yesterday, today, absolute Zeitstempel sowie relative Offsets mit vorangestelltem -. Beachten Sie, dass yesterday Mitternacht zu Beginn des gestrigen Tages bedeutet; die Ausgabe enthält also auch die Einträge von heute.

Es lohnt sich, einen fehlgeschlagenen systemd-Timer-Job einmal bewusst zu provozieren. Ändern Sie das Skript so, dass es SRC=/srv/app/missing enthält, und führen Sie es dann aus:

sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20

tar schlägt fehl, set -e lässt das Skript mit einem Exit-Code ungleich null enden, und die Unit landet im Zustand „failed“, wobei der Exit-Code in ihrem Status erscheint. Das Journal enthält die eigentliche Fehlermeldung von tar. Setzen Sie SRC anschließend wieder zurück.

Wann ist Cron weiterhin eine gute Wahl?

Für einen einzelnen Job ohne Abhängigkeiten ist Cron weiterhin völlig ausreichend – und einfacher. Ein Job ist eine einzige Zeile in einer Crontab, Cron ist nahezu überall verfügbar, und ein einzelner nächtlicher Job ohne Abhängigkeiten läuft damit tadellos. Timer kosten Sie zwei Dateien und ein paar systemctl-Befehle. Zudem haben sie kein eingebautes Pendant zu MAILTO. Um bei einem fehlgeschlagenen Job benachrichtigt zu werden, verweisen Sie mit der OnFailure=-Direktive des Service auf eine Unit, die die Benachrichtigung versendet.

Fazit

Der Umstieg von Cron auf systemd-Timer bringt Ihnen drei Dinge, die klassisches Cron nicht bieten kann: einen Job, den Sie jederzeit ausführen und inspizieren können, eine Ausgabe, die ohne Umleitungen im Journal landet, und nachgeholte Läufe nach Ausfallzeiten. Wählen Sie den Crontab-Job aus, um den Sie sich am häufigsten Sorgen machen. Schreiben Sie dafür Service und Timer, validieren Sie den Zeitplan mit systemd-analyze calendar, testen Sie ihn mit systemctl start und löschen Sie die Crontab-Zeile, sobald systemctl status einen erfolgreichen geplanten Lauf meldet.

FAQs

Kann ich einen systemd-Timer als normaler Benutzer ohne Root-Rechte ausführen?

Ja. Legen Sie die .service- und .timer-Dateien in ~/.config/systemd/user/ ab und verwalten Sie sie mit systemctl --user, zum Beispiel systemctl --user enable --now backup.timer. Standardmäßig wird die systemd-Instanz des Benutzers beendet, sobald dessen letzte Login-Sitzung geschlossen wird – und damit lösen auch die Timer nicht mehr aus. Führen Sie sudo loginctl enable-linger gefolgt vom Benutzernamen aus, damit die Benutzerinstanz beim Booten startet und auch ohne Sitzung weiterläuft.

Was ist der Unterschied zwischen AccuracySec und RandomizedDelaySec?

AccuracySec gibt systemd Spielraum, Timer-Ereignisse zu bündeln und die CPU seltener aufzuwecken. Der Timer kann zu einem beliebigen Zeitpunkt zwischen der geplanten Zeit und AccuracySec später auslösen; standardmäßig beträgt dieses Fenster 1 Minute. RandomizedDelaySec wirkt der Bündelung entgegen. Es verschiebt jeden Lauf um einen zufälligen Betrag bis zum eingestellten Wert (standardmäßig 0), sodass sich Timer mit demselben Zeitplan verteilen, statt gleichzeitig auszulösen. Wenn Sie beide setzen, wird die zufällige Verzögerung angewendet, bevor systemd das Ereignis mit anderen zusammenfasst.

Wie plane ich mit systemd einen einmaligen Job, ohne Unit-Dateien zu schreiben?

Verwenden Sie systemd-run mit einer Timer-Option, zum Beispiel sudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.sh. Dadurch wird ein transientes Paar aus Timer und Service erzeugt. Nur der Timer startet sofort; der Service läuft, sobald der Timer abläuft. Flags wie --on-active= und --on-boot= erzeugen monotone transiente Timer. Transiente Units überstehen keinen Neustart, daher gehören wiederkehrende Jobs weiterhin in Unit-Dateien.

Muss die Timer-Datei denselben Namen tragen wie der Service, den sie auslöst?

Nein, aber das ist die Standardeinstellung. Ein Timer startet den Service mit demselben Namen ohne Suffix, backup.timer startet also backup.service. Um eine anders benannte Unit auszulösen, setzen Sie Unit= im [Timer]-Abschnitt, zum Beispiel Unit=nightly-backup.service. Der Wert darf keine andere .timer-Unit sein. Dennoch empfiehlt systemd.timer(5), dem Timer und der von ihm gestarteten Unit denselben Basisnamen zu geben.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.