Soweit so gut, doch manchmal ist es notwendig, dass bestimmte Berechnungen bzw. Operationen auf dem Server ausgeführt werden, ohne eine vorherige Interaktion eines Clients. Zum Beispiel müssten regelmäßig die Produktdaten mit dem PIM-System abgeglichen werden oder der Status der Bestellung im ERP-System müsste geprüft und ggf. mit dem eigenen Status abgeglichen werden. Würde man dies erst dann tun, wenn der Client – also aus der Sicht eines Online-Shops: ein Kunde – auf die Website kommt, dann würde die Berechnung der Seite vermutlich zu lange dauern und der Kunde verliert das Interesse. 

Ein häufig eingesetztes Mittel zur Lösung dieses Problems sind die s.g. Cronjobs. Nahezu alle Unix-basierten Systeme haben den Cron-Daemon installiert und da Webserver meistens mit Linux-Betriebssystemen betrieben werden, sind die Cronjobs das Mittel der Wahl für klassische Web-Anwendungen. Magento (sowie alle anderen gängigen Frameworks) stellt dabei eine eigene Konfigurationsmöglichkeit für die Entwickler bereit, sodass die Entwickler sich nicht um die Betriebssystem spezifischen Eigenheiten der Cron-Konfigurationen kümmern müssen. Ferner genügt es für den Betrieb lediglich einen Cronjob zu konfigurieren, die Magento-eigenen Logik kümmert sich um die Ausführung aller anwendungsspezifischer Prozesse.

Neben einer Reihe von Vorteilen für Betrieb und Entwicklung bietet dieses Vorgehen einige Nachteile. So kann es vorkommen, dass durch zahlreiche Extensions sehr viele Prozesse innerhalb eines Cronjobs ausgeführt werden und die Ausführungszeit dadurch die vorgegebene Zeitplanung überschreitet. Es entsteht also ein Prozess-Stau, obwohl die Serverkapazitäten durchaus ausreichend wären. Dieser Stau führt zu Deadlocks und damit Inkonsistenzen in der Datenbank oder dazu, dass bestimmte Prozesse ihren regulären Ausführungszeitpunkt verfehlen und somit nicht oder nicht korrekt ausgeführt werden. Ohne an dieser Stelle zu detailliert auf die technischen Probleme und Verstrickungen einzugehen: Das Resultat ist ein instabiles System mit zahlreichen, vermeintlich zufälligen Fehlern, inkonsistenten Daten und täglich neuen Überraschungen. Zu allem Übel sind die Fehler kaum reproduzierbar und die tickende Bombe sorgt für Frust sowohl bei dem Betreiber, als auch bei den Entwicklern des Shops.

 

Wie lassen sich diese Probleme vermeiden?

Cronjobs sind Prozesse, die zeitbasiert ausgelöst werden. Somit ist die Zeit die einzige Bedingung, die darüber entscheidet, ob der Befehl ausgeführt wird oder nicht. Server-Administratoren kennen das 3-Uhr-Problem wenn schlagartig das gesamte System unter einer hohen Last steht, weil diverse Programme ihr Backup starten. Daher haben wir für uns, die Limitierung der Bedingungen auf den Faktor Zeit, als eines der Hauptprobleme in Zusammenhang mit klassischen Cronjobs identifiziert. Schöner wäre es doch wenn man statt “Führe den Produktimport aus dem PIM-System um 04:15 täglich aus”, die Bedingung wie folgt formulieren könnte: “Führe den Produktimport aus dem PIM-System zwischen 03:30 Uhr und 05:00 Uhr täglich aus, wenn die Serverlast unter 60% liegt und keine anderen Import ausgeführt werden.” Es müssen also weitere Bedingungen überprüft und miteinander kombiniert werden können.

Ein weiteres Problem, sind die langlaufenden Prozesse. Die zuvor formulierte Bedingung würden auch dann nicht zu einem erfolgreichen Import der Produkte führen, wenn beispielsweise innerhalb der definierten Zeitspanne ein anderer Importprozess ausgeführt wird und somit unsere Ausführung blockiert. Betrachtet man solche langlaufenden Prozesse genauer, so stellt man fest, dass es in der Regel s.g. Prozess-Monolithen sind. Es sind oft Skripte, die eine Verkettung von vielen kleineren Prozessen beinhalten. Würde man also diese Prozesskette auch als solche offenlegen, würde man vermutlich feststellen, dass der Importprozess, welcher die Ausführung unseres Cron-Befehls verhindert hat, aus Teilschritten besteht. Dies Teilschritte sind beispielsweise “Daten beschaffen”, “Daten transformieren”, “Daten importieren”, “Re-Indexierung”, usw. Während der ersten beiden Teilschritte, hätte also unser Produktdaten-Import ausgeführt werden können und es war lediglich die monolithische Betrachtung des benachbarten Prozesses, die die Ausführung verhindert hat. Es müsste also möglich sein, die einzelnen Prozessschritte explizit in unseren Bedingungen zu adressieren.

 

Pipeline-Pattern als Lösung

Sind die Probleme erstmal identifiziert, so kann man sich auf die Suche nach einer Lösung machen. Unsere Suche hat uns zu einem Architektur Pattern geführt, welches den meisten Entwicklern sehr vertraut ist, jedoch eher aus der Perspektive eines Endnutzers. Das Pipeline-Pattern findet in den meisten Continuous Integration (CI) Tools, wie beispielsweise Jenkins oder Gitlab-CI, Anwendung. Dabei definieren die Entwickler eine Pipeline, mit der eine Software gebaut, getestet und ausgeliefert werden soll. Hier wird über verschiedene Bedingungen definiert wann welcher Arbeitsschritt ausgelöst werden darf. Zum Beispiel über Ereignisse (events), zeitliche Vorgaben oder durch manuelle Freigaben. In einer Kurzfassung lässt sich das Pattern wie folgt beschreiben: 

1. Man benötigt eine Deklaration der Pipelines

Die Pipelines ersetzen somit die Prozess-Monolithen und man definiert die einzelnen Schritte die zu einem Prozess gehören. Dabei lassen sich die einzelnen Schritte benennen und können somit in Relation zueinander und zu benachbarten Pipelines gebracht werden.

Wie es in Magento üblich ist, haben wir uns für eine XML-basierte Deklaration entschieden, da Magento hierzu bereits Mechanismen bietet, um modulübergreifend die Konfigurationen zusammenzuführen und auszulesen. Dies versetzt uns in die Lage bestehende Pipelines durch zusätzliche Module zu verändern bzw. zu erweitern.

Die folgende Abbildung zeigt eine exemplarische Konfiguration einer Import-Pipeline. Die Pipeline besteht aus Konditionen, über die definiert wird wann eine Pipeline gestartet werden soll bzw. darf, sowie mehreren Prozessschritten. Die Konditionen lauten in unserem Beispiel “IsExecutionTimeReached”, “HasImportFiles” und “NoImportProzessIsRunning”, die jeweils durch eine Klasse repräsentiert werden. Es lassen sich also nun zahlreiche Bedingungen miteinander verknüpfen. Die Prozessschritte haben einer Klasse für die eigentliche Businesslogik, sowie weiteren Konditionen über die, die einzelnen Schritte miteinander in Relation gebracht werden. Ferner können über die Konditionen zusätzliche Kontrollmechanismen geschaffen werden, wie beispielsweise sicherstellen, dass ein Prozessschritt im Fehlerfall wiederholt wird oder ähnliches. Die Relationen zwischen den Schritten sorgen dafür, dass die Schritte in der richtigen Reihenfolge abgearbeitet werden und ermöglichen es, dass voneinander unabhängige Schritte parallel ausgeführt werden können.

 

2. Man benötigt einen übergeordneten Kontrollprozess

Um die Bedingungen der Pipeline sowie der Prozessschritte zu kontrollieren, wird eine unabhängiger Prozess benötigt. Diesen Prozess haben wir in unserem Realisierung des Patterns “heartbeat” genannt. Der Heartbeat sorgt dafür, dass der Status aller Prozessschritte gegen die Bedingungen validiert wird und entscheidet dadurch, ob ein Schritt nun bereit für die Ausführung ist. Ferner werden auch die Bedingungen der Pipeline-Definitionen geprüft, woraufhin entschieden wird, ob eine neue Pipeline gestartet werden darf.

 

 

Dieser Kontrollprozess ist tatsächlich der einzige Cronjob, welcher für den Betrieb des Pipeline-Patterns benötigt wird. Wichtig ist dabei die Zuständigkeit dieses Cronjobs auf genau die zuvor genannten Funktionen zu beschränken. Die tatsächliche Ausführung der einzelnen Prozessschritte darf nicht durch den Heartbeat passieren, lediglich die Entscheidung über den Status. Und damit kommen wir zu …

3. Ausführung der Prozessschritte mit Hilfe von Runnern

Für die Prozessierung der einzelnen Schritte wird eine weitere Instanz benötigt. Klassischerweise wird diese Instanz in den gängigen CI-Tools als “Runner” bezeichnet. Der Runner ist ein persistent laufender Prozess, welcher darauf wartet vom Heartbeat eine Anweisung zu bekommen, einen Prozessschritt auszuführen. Es können auch mehrere Runner gleichzeitig aktiv sein, um parallele Verarbeitung zu ermöglichen. Ferner werden so, die Ressourcen des Servers optimal genutzt bzw. auf natürliche Weise eingeschränkt.

Für unsere Implementierung haben wir die bestehende Integration der RabbitMQ in Magento genutzt. Somit kommuniziert der Heartbeat über die Message-Queue mit den Runnern, die im Grunde einfache Consumer der Queue sind. Dieses Vorgehen ermöglicht uns zusätzlich die Möglichkeit serverübergreifend skalieren zu können, die Runner können auf dedizierten Servern laufen und belasten dadurch nicht die Ressourcen der Server, die für die Kundeninteraktion zuständig sind.

 

Fazit

Mit dem Pipeline-Pattern haben wir für uns eine perfekte Lösung gefunden, um mit den Problemen die durch komplexe Hintergrundprozesse entstehen, umzugehen. Die Integration des Pipeline Patterns im Rahmen von Pacemaker leistet uns seit vielen Jahren in zahlreichen Projekten gute Dienste und bildet die Basis für das Produkt Pacemaker Enterprise.