Verteiltes Job-Scheduling: der Zeitplan ist der leichte Teil
Cron-Ausdrücke zu parsen ist gelöst. Schwierig wird es, wenn ein Runner mitten im Job verschwindet. Was wir beim Bau von Croniq über verwaiste Ausführungen, Identitätswechsel und Kalender-Gating gelernt haben.
Wer einen Job-Scheduler baut, unterschätzt zuerst, welcher Teil davon schwer ist. Cron-Ausdrücke zu parsen und den nächsten Ausführungszeitpunkt zu berechnen ist ein gelöstes Problem mit Bibliotheken in jeder Sprache. Der schwere Teil beginnt danach: wenn mehrere Maschinen Arbeit übernehmen und eine davon ausfällt.
Wir bauen mit Croniq einen verteilten Scheduler in Rust, aktuell bei v0.31.0, Apache-2.0. Die interessanten Probleme der letzten Releases hatten alle nichts mit Zeitplänen zu tun.
Pull statt Push
Die erste Architekturentscheidung war, dass Runner sich Arbeit holen, statt sie zugewiesen zu bekommen. Runner fragen den Server per HTTP-Long-Poll, ob etwas für sie da ist. Der Server kennt die Fähigkeiten jedes Runners und gibt nur passende Jobs heraus.
Das kostet etwas Latenz und spart eine Menge Betrieb. Runner brauchen keine erreichbare Adresse, keine eingehenden Firewall-Regeln und keine Registrierung im Voraus. Man startet einen weiteren Container und er nimmt Arbeit an. Skalierung wird damit zu einer Frage, wie viele Prozesse laufen, nicht wie die Verteilung konfiguriert ist.
Was passiert, wenn ein Runner verschwindet
Hier liegt die eigentliche Arbeit. Ein Runner nimmt einen Job an, markiert ihn als beansprucht, und stirbt dann. Der Prozess ist weg, der Datensatz sagt "läuft". Ohne Gegenmaßnahme bleibt dieser Job für immer in diesem Zustand und blockiert bei Singleton-Jobs jede weitere Ausführung.
Die Lösung braucht drei Teile, die zusammenspielen müssen:
- Verwaiste Ausführungen einsammeln. Beanspruchte Ausführungen ohne lebendigen Runner müssen erkannt und freigegeben werden, damit ein anderer Runner sie übernehmen kann.
- Identität beim Neustart übernehmen. Kommt derselbe Runner zurück, soll er seine alte Identität wieder einnehmen statt als Neuling aufzutreten. Sonst wächst die Liste der Runner mit jedem Neustart.
- Flattern erkennen. Ein Runner, der im Minutentakt verschwindet und zurückkommt, ist ein Symptom. Diese Übernahmen werden protokolliert und die Zähler dazu liegen auf dem Metrics-Endpunkt, damit ein Alarm darauf reagieren kann.
Der dritte Punkt ist der, den man beim ersten Entwurf vergisst. Ein System, das sich selbst repariert, ohne das zu melden, versteckt genau die Information, die man im Betrieb braucht.
Kalender-Gating hat eine unauffällige Falle
Jobs können an einen Kalender gebunden werden: nur an Werktagen, nicht an Feiertagen, nur in bestimmten Zeiträumen. Die naive Umsetzung berechnet den nächsten Zeitpunkt nach dem Zeitplan und verwirft ihn, wenn der Kalender ihn verbietet.
Das führt dazu, dass ein Job über eine längere Sperrzeit seine Kandidaten aufbraucht und danach gar nicht mehr feuert. Richtig ist, den Auslöser auf den nächsten erlaubten Zeitpunkt vorzurücken, statt zu verwerfen. Der Unterschied fällt im Test nicht auf, weil Testkalender kurze Sperren haben. Er fällt über Weihnachten auf.
Wann sich so etwas lohnt
Ein eigener Scheduler ist selten die richtige Antwort. Wer eine Handvoll Cronjobs auf einem Server hat, braucht keinen. Sinnvoll wird es in einem schmalen Bereich: etwa zwanzig bis zweihundert geplante Jobs, verteilt über mehrere Maschinen, ohne eigenes Plattform-Team, das eine große Workflow-Engine betreiben will.
Croniq deckt genau diesen Bereich ab: ein Binary oder ein Container, SQLite als Standard-Backend, ein Dashboard für Jobs und Ausführungsprotokolle. Der Punkt ist weniger das Werkzeug als die Erkenntnis dahinter. Bei verteilter Ausführung steckt der Aufwand nicht im Zeitplan, sondern in der Frage, was das System tut, wenn ein Teil davon nicht mehr antwortet.