8 Tipps für robuste Batch-Jobs

Auch im Zeitalter von Echtzeit-Webanwendungen und Software as a Service in der Cloud gibt es die Notwendigkeit zur stillen Batch-Verarbeitung im Hintergrund. Den Beweis dazu liefert der JSR 352: Batch Applications for the Java Platform. Als Teil der Enterprise Edition 7 liefert dieser Standard ein einheitliches Vorgehensmodell für die Auftragsverarbeitung mit Batch-Prozessen.

Das größte Problem mit Hintergrundprozessen ist, dass die Auswirkungen den Anwendern typischerweise erst auffallen, wenn sie seit längerer Zeit nicht mehr laufen. Da dabei teilweise Massen von Daten nicht verarbeitet werden, ist es nicht immer ganz einfach die ausgefallene Rechenzeit nachzuholen. In diesem Artikel liefern wir nützliche Hinweise, wie man Prozesse so entwickelt, dass sie im täglichen Produktivbetrieb robust und zuverlässig ihre Arbeit machen.

Was ist eigentlich ein Batch?

Für unsere Betrachtung lässt sich Batch als Stapel übersetzen. Die Stapel- oder Batchverarbeitung hat eine lange Historie in der EDV. Ein Stapel besteht dabei aus gleichartigen Aufträgen, die ohne Benutzerinteraktion nacheinander oder parallel abgearbeitet werden. Ein Batch erfüllt regelmäßige und geschäftskritische Aufgaben. Es gibt drei Arten einen Batch-Job zu starten:

  • Zeit: fest geplante Zeiten z.B. jede Minute, jeden Tag oder einmal im Monat
  • Ereignis: ausgelöst durch eine externe Abhängigkeit z.B. eine neue Datei im Dateisystem
  • Manuell: auf Anforderung eines Benutzers

Batch-Jobs eignen sich sehr gut für langlebige, rechenintensive Aufgaben und eine datenintensive Massenverarbeitung. Einzelne Aufträge lassen sich während der Ausführung stoppen, es können Checkpoints mit dem Erreichen von bestimmten Bedingungen gesetzt werden und Aufträge können partitioniert und parallel betrieben werden.

Im Umfeld des Enterprise Content Management finden sich häufig Batch-Jobs für die Archivierung, Formatkonvertierung (z.B. PDF-Generierung) und Integration von Ein- und Ausgangsschnittstellen. Das sind Operationen, die dem Anwender verborgen bleiben, aber für das Gesamtsystem essentiell sind. So ist jeder Batch Job ein Zahnrad, welches dafür sorgt, dass das gesamte Uhrwerk zusammenspielt.

![Runway: Einzelne Batch-Jobs und ihre Arbeitsergebnisse](http://res.cloudinary.com/ditemis/image/upload/v1436777090/ditemisblog/runway_batch_executions.png)

Mit unserem Produkt Runway helfen wir dabei, die Prozesse zu verwalten und im täglichen Betrieb den Überblick zu behalten. Entsprechende Management-Werkzeuge helfen aber nicht, wenn der Entwickler die betriebenen Prozesse nicht fehlertolerant implementiert. Mit den folgenden Tipps erleichtert ein Entwickler dem Betrieb die Arbeit und stellt die Robustheit der implementierten Batch-Jobs sicher.

1. Übergreifende Konsistenz im Prozessablauf

Die Verarbeitung von gleichartigen Prozessabläufen sollte mit den gleichen Komponenten erfolgen. Als Vorteil des objektorientierten Entwurf ist das sicherlich keine Neuigkeit. Abhängig von den erwarteten Anwendungsfällen können abstrakte Komponenten geschaffen werden, die für mehrere Prozesse wiederverwendet werden. Das erleichtert die Implementierung von neuen Prozessen und sorgt dafür, dass sich die Batch-Jobs in der Ausführung konsistent verhalten. Das gibt weniger Rückfragen durch den Betrieb und reduziert Komplexität sowie Wartungs- und Dokumentationsaufwand.

Ein Beispiel dafür sind Jobs, die auf Ereignisse aus dem Dateisystem warten. Wenn bestimmte Signaldateien in einem Verzeichnis erstellt werden, startet die Auftragsverarbeitung. Durch die Dateiendung an der Signaldatei kann der Status des Auftrags auch im Dateisystem verfolgt werden. Ein konsistentes Statusmodell mit Dateiendungen für Bereit-, Arbeits- und Fehlerstatus ist für alle Prozesse notwendig. Runway stellt dafür beispielsweise einen FileSystemSemaphore bereit, der einen Lock auf die Auftragsdatei setzt und die Dateiendung einheitlich über entsprechende Status-Methoden verändern lässt. So kann ohne großen Aufwand ein konsistenter Zustandsgraph für alle Jobs mit dateibasierter Auftragsverarbeitung sichergestellt werden.

2. Defensiv mit erwarteten Vorbedingungen umgehen

Jeder Entwickler trifft während der Implementierung implizit oder explizit Annahmen über den bestehenden Zustand an einer bestimmte Stelle im Programmablauf. In der Praxis existieren nur wenige echte Invarianten, die niemals von dem erwarten Wertebereich abweichen. Deshalb empfiehlt es sich fast immer die angenommenen Vorbedingungen explizit zu prüfen und dadurch zu bestätigen oder zu widerlegen.

Gerade bei der Übergabe von einem Job-Step zum nächsten ist es sinnvoll die übergebenen Daten auf Korrektheit zu prüfen. Sind alle benötigten Metadaten gesetzt? Entspricht das Dokument dem erwarteten Dokumentformat? Ist der Stream noch beschreibbar? Eine schnelle Analyse der Daten kann nicht schaden. So beugt man unerwarteten Exceptions vor und kann im Protokoll einen Hinweis darauf geben, wieso ein Auftrag nicht korrekt durchgeführt werden kann.

Insbesondere sollte man Annahmen sehr misstrauisch behandeln, wenn Operationen nur schwer rückgängig zu machen sind. Die Performance-Einbußen für den erweiterten Prüfaufwand können meistens problemlos in Kauf genommen werden. Bei zeitkritischen Prozessen sollte man die Prüfungen zumindest auf die Stellen mit den größten Risiken richten.

3. Punkte für einen ordentlichen Ausstieg einplanen

Abhängig vom Umfang der auszuführenden Operationen können einzelne Batch-Jobs sehr lange Laufzeiten haben. Falls die ausführende Umgebung innerhalb dieser Laufzeit ein Exit-Signal empfängt oder der Prozess gestoppt werden soll, muss der Job in der Lage sein, die Ausführung ordnungsgemäß zu beenden. Das setzt voraus, dass Ausstiegspunkte definiert werden, bei denen kein undefinierten Zustand zurückgelassen wird. Eine Ausstiegsprüfung eignet sich z.B. in der Schleifenbedingung, wenn im Schleifenkörper eine langlaufende Operation erwartet wird.

Als Erleichterung sollten die einzelnen Jobschritte möglichst schlank gestaltet werden, sodass praktisch in jedem Zwischenschritt neu begonnen werden kann. Das sorgt für ein robustes Wiederaufsetzen und verhindert die unschönen Überbleibsel fehlgeschlagener Ausführungen, die manuelle Arbeit und Nerven kosten.

4. Parallelität beachten

Wenn Jobs parallel ausgeführt werden, ist besonders darauf zu achten, dass die Jobs untereinander keine Abhängigkeit haben. Ansonsten können Race Conditions auftreten, die Fehlverhalten im betroffenen System verursachen, das schwer nachzuvollziehen und schwer zu rekonstruieren ist. Ein wichtiges Konstrukt dafür ist es, ein eindeutiges Merkmal zu identifizieren, ob ein Job bereits ausgeführt wird.

Als Beispiel ist vorstellbar, dass beim Erzeugen von Objekten mit einem Ordnungskriterium darauf zu achten ist, dass dieses Ordnungskriterium nicht redundant belegt ist. Wenn zwei Aufträge parallel ausgeführt werden, besteht das Risiko, dass eine vorgelagerte Prüfung von beiden Aufträgen positiv durchlaufen wird und dann beide Aufträge die Schreiboperation durchführen. Die Ausführungsumgebung kann anhand des Ordnungskriteriums gleichzeitig laufende Jobs identifizieren und diese entsprechend verzögern oder nacheinander ausführen.

Falls die Jobs in einer bestimmten Reihenfolge ausgeführt werden müssen, ist es am einfachsten auf die Vorteile von paralleler Ausführung zu verzichten und die Jobs in eine Warteschlange einzureihen und nacheinander auszuführen. Nur wenn die Performance ein kritischer Erfolgsfaktor ist, sollte die Komplexität von Parallelität in diesem Szenario in Kauf genommen werden.

5. Modularität sorgt für Testbarkeit

Für eine hohe Testbarkeit sollten Job-Steps als eigene Module betrachtet werden, die definierte Ein- und Ausgangsparameter besitzen. Jeder Schritt erhält dazu eine Art Initiator, der während der Ausführung mit Parametern aus dem Kontext befüllt wird. Im Testfall kann dieser Initiator je nach Bedarf losgelöst vom Rest des Batch-Jobs aufgerufen werden. Eine Hilfsfunktion, die die Ausgangselemente zurückgibt, hilft beim Prüfen der Assertions in Einheiten- oder Integrationstests.

Je abstrakter man einen Ausführungsschritt vom konkreten Ausführungskontext trennt, umso weniger Abhängigkeiten hat der dazugehörige Testfall, was das Ziel für UnitTests sein sollte. Falls dieser Abstraktionsgrad nicht erreicht werden kann, muss in der Testausführung der notwendige Ausführungskontext simuliert werden, wofür sich z.B. Arquillian (http://arquillian.org/) eignet.

6. Eindeutigkeit von Aufträgen

Für eine Nachverfolgbarkeit von einzelnen Aufträgen ist ein eindeutiges Identifikationsmerkmal hilfreich. Damit sollte ein Auftrag im Fehlerfall schnell auffindbar sein. Dafür bietet sich eine UUID an, die persistent für einen Job vergeben wird. Idealerweise wird diese kryptische UUID durch Merkmale ergänzt, die eine fachliche Bedeutung haben. Es fällt einer Person beispielsweise leichter über einen Dokumenttyp oder einen textuellen Betreff zu sprechen und darauf zu referenzieren als auf eine UUID.

7. Protokollierung für die Fehleranalyse

Eine Protokollierung mit Standard-Logmechanismen wie log4j o.ä. erleichtert eine automatische Auswertung und bietet eine Vielzahl an gewünschten Ausgabeformaten. Es sollten alle Loglevel ausgenutzt werden, sodass auch Ausgaben auf DEBUG-Level erfolgen können, um in ersten Tagen des Produktivbetriebs die ordnungsgemäße Ausführung zu bestätigen. So ist im Fehlerfall die volle Log-Info vorhanden. Für einen kurzen Einführungszeitraum ist dieser hohe Loglevel gerechtfertigt.

Es bietet sich an INFO-Logs am Beginn und Ende eines Job-Steps zu schreiben. Diese können auch in Vaterklassen umgesetzt werden, sodass die Nachrichten konsistent sind und nicht vergessen werden. ERROR- oder FATAL-Logeinträge müssen den vollen Exception-Stack protokollieren, um den Entwickler im Fehlerfall zu unterstützen.

Die Logausgaben sollten nach Auftragskennung filterbar sein. Es fällt andernfalls schwer, bei paralleler Jobausführung einen Überblick über den Auftragszustand zu behalten, da die Logeinträge miteinander vermischt werden.

8. Fehlertoleranz mit Verzweigungen

In vielen Fällen ist es besser einen Job mit Lücken oder Annahmen durchzuführen anstatt ihn als fehlerhaft zu beenden, wenn Informationen fehlen. Ob Jobs in unterschiedlichen Endzuständen zugelassen werden, hängt von dem Anspruch an die Datenqualität ab und vor allem, ob Fehler oder lückenhafte Einträge in nachgelagerten Systemen korrigiert werden können. Es gilt abzuwägen, welche Aufträge noch zugelassen werden können und welche zu einer Ablehnung führen.

Wenn man sich für verschiedene Endzustände entscheiden kann, ist es sinnvoll, diese Verzweigung explizit als separate Schritte umzusetzen und so unterschiedliche Prozessabläufe zu modellieren. Durch Quality Gates am Anfang bzw. Ende eines Job-Steps können bestimmte Ausführungsstränge durch Konfigurationen aktiviert oder deaktiviert werden. So kann der Grad der Fehlertoleranz abhängig von der Reife des gesamten Informationsflusses gesteuert werden.