Das Ziel des Artikels ist es dar zu stellen, wie mit Hilfe von JMS und JBoss Cache eine einfache Architektur zur Verteilung von Arbeitsaufträgen implementiert werden kann.
Dabei möchte ich davon ausgehen, dass es eine zentrale Stelle gibt, von der aus Aufträge in das System kommen. Diese werden in einer Datenbank gespeichert und sollen von mehreren Instanzen bearbeitet werden.
Dabei kommt es zu verschiedenen Fragestellungen, die beantwortet werden müssen: zum Beispiel dem des Deadlocks (also der Ressourcensperrung), der Frage, wie sichergestellt wird, dass ein Auftrag nicht von zwei Instanzen gleichzeitig bearbeitet wird und vieles mehr.
Diese, relativ klassische, Fragestellung lässt sich unter Verwendung von klassischen JEE Prinzipien sehr einfach lösen. Dabei kommen folgende Bausteine zum Einsatz:
- Hibernate – für das Auslesen von Nachrichten aus der Datenbank
- JMS – ein Messaging Standard
- JBoss Cache – ein Caching System, um die Last auf der Datenbank zu reduzieren
Damit man sich das Ganze bildlich vorstellen kann, hier eine kleine Architekturskizze unter Verwendung der Enterprise Integration Patterns:
Teil 1 – Auslesen der Aufträge
Zuerst geht es darum, die Aufträge zu verarbeiten. Aus dem Gesamtbild herausgelöst, ist das der Folge Ausschnitt:
Dazu wird eine Hibernate Session Factory verwendet, die die notwendigen DAO (Data Access Object) Klassen kennt und im verwendeten Applikationsserver registriert wird. Hier können bereits, je nach Notwendigkeit, verschiedene Optimierungen vorgenommen werden hinsichtlich paralleler Threads, 2nd-Level-Caches, et cetera. Da das Objekt von nichts als einer vorhandenen Datenbank abhängt, kann diese Komponente separat deployed werden.
Timergesteuertes Auslesen
Aufbauend auf der Factory wird nun ein Service erstellt, der unter Verwendung der Timer-Mechanismen der Applikationsserver regelmäßig aufgerufen wird. Soll diese Komponente unter Verwendung von EJB3 erstellt werden, so kann dieses unter Verwendung von Annotation sehr einfach durchgeführt werden. Grundsätzlich spricht nichts gegen den Einsatz von EJB3, allerdings ist dies nicht notwendig. Von der JEE Idee her, sollten nur Komponenten mit Business-Funktionalitäten als EJB modelliert werden. Alles, was in einem System nur einmal vorhanden ist (sein soll), kann als Service modelliert werden oder aber einfach als Java-Klasse, die den Timer-Service verwendet.
Cache & Publish
Die Funktion der Komponente ist es, über die Hibernate DAO’s neue Aufträge abzufragen. Diese werden anschließend im JBoss Cache hinterlegt und die eindeutige ID auf JMS gepublished. Das Ziel dieser Architektur ist es, die Datenbank nur minimal zu belasten. Hierbei wird, wenn überhaupt schon zugegriffen werden soll, einmalig der gesamte Auftrag ausgelesen und zwischengecached. JBoss Cache ist eine Cache-Implementierung, die es erlaubt, clusterweit Inhalte bereit zu stellen. Sofern die Hibernate-Objekte serialisierbar sind, können diese direkt in den Cache gelegt werden und können dann von anderen Komponenten ausgelesen und verändert werden. Hierdurch sind für weitere Zugriffe auf den Auftrag lediglich Abfragen auf den Cache notwendig, nicht jedoch mehr auf die Datenbank (vorausgesetzt es handelt sich um lesenden Zugriff).
Konfiguration durch “Dependency Injection”
Durch die JEE Mechanismen zur “Dependency Injection” (entweder traditionell über XML Beschreibungen oder EJB3 Annotationen) können sowohl die Hibernate Factory als auch die JMS Informationen der Komponente direkt mitgegeben oder, ohne das weitere Einstellungen notwendig werden.
JMS Queue
Die Aufträge werden zur weiteren Verarbeitung auf eine (JMS-)Queue gestellt. Diese kann entweder “in Memory” oder aber (für clusterweite Funktionalität) als persistente Queue realisiert werden. Der Charme dieser Lösung ist, dass das Messaging System sich selber darum kümmert, das Nachrichten immer nur an einen Abnehmer ausgeliefert erden, ohne das dies die Aufgabe des Applikationsentwicklers ist, der sich primär um die Geschäftsfunktionalität kümmern soll.
Teil 2 – Abarbeiten der Aufträge
Der zweite Teil kümmert sich um die Verarbeitung gelesener Aufträge. Aus dem Gesamtbild heraus stellt das den folgenden Teil dar:
Für die tatsächliche Verarbeitung der Nachrichten benötigt es eine Komponente, die Nachrichten von einer JMS Queue abholen kann, die notwendigen Aktionen durchführt und anschließend den Auftrag als “bearbeitet” markiert. Dies kann durch eine “Message-driven Bean” (MDB) realisiert werden. Hierbei handelt es sich um ein JEE Konstrukt, dass einer JMS Queue zugeordnet ist und sich dort registriert. Sobald neue Nachrichten auf der Queue auftauchen, werden diese der MDB zugeliefert.
JMS Abholung – Message Driven Beans
Hierbei macht eine klassische MDB Implementierung Sinn, da dies die einfachste Möglichkeit ist, Nachrichten von einer JMS Queue abzuholen. Die Nachricht wird anschließend in einen Auftrag konvertiert und der Auftrag aus dem Cache gelesen. Hierbei ist es nicht notwendig, weiter auf die Datenbank zuzugreifen, da die Nachricht bereits vollständig im Cache (und damit im Speicher) vorgehalten wird.
Statusupdate – JMS
Nachdem der Auftrag bearbeitet wurde, muss der Status aktualisiert werden. Dies können prinzipiell zwei Status sein:
- Der Auftrag konnte erfolgreich bearbeitet werden
- Der Auftrag konnte nicht erfolgreich bearbeitet werden
Hierbei ist es eine architektonische Entscheidung, wo der Auftragsstatus aktualisiert werden soll. In diesem Beispiel gehe ich davon aus, dass eine sauber getrennte Architektur verwendet wird. Das bedeutet hierbei, dass das Status-Update nicht in der MDB erfolgt, sondern ausgelagert wird, um eine deutliche “Separation of Concern” herbei zu führen.
Eine Queue oder mehrere – das ist hier die Frage!
Daher wird auf eine weitere Queue die ID des Auftrags gestellt. Auf diese Queue werden ausschließlich die erfolgreich bearbeiteten IDs gestellt, fehlerhafte Bearbeitungen werden in einer separaten Queue erfasst. Der Vorteil der Lösung ist, dass man eine sofortige Übersicht darüber hat, wie viele Aufträge erfolgreich und fehlerhaft bearbeitet wurden, indem man sich die Statistiken der Queues anschaut. Ein alternativer Ansatz wäre es, die Auftrags-ID mitsamt dem Status (erfolgreich bzw. fehlerhaft) auf eine gemeinsame Queue zu publishen. Welchen Ansatz man konkret verfolgt, sei dabei dem persönlichen Gusto überlassen.
Teil 3 – Die Statusaktualisierung
Der letzte Schritt in der Verarbeitung stellt die Statusaktualisierung dar. Aus dem Gesamtbild ist das der folgende Ausschnitt:
Die Statusaktualisierung kümmert sich darum, die Nachrichten die bearbeitet wurden, entweder aus der Auftragstabelle zu entfernen oder, aus Gründen der Historisierung, den Status entsprechend zu setzen. Auch hierbei bietet sich eine klassische MDB Implementierung an. Diese verwendet, wie auch schon in Teil 1 die Hibernate Session Factory und das DAO Objekt, um den Status zu setzen bzw. den Datensatz zu entfernen. Auch hier kommt der Ansatz der “Dependency Injection” zum Tragen, um die MDB mit den notwendigen Informationen über die Außenwelt zu versorgen.
Fazit
Wie gezeigt, lässt sich über relativ einfache JEE Mechanismen eine gut funktionierende Lösung für das clusterweite Worker-Pattern implementieren. Beachtet werden sollte bei der Umsetzung allerdings die Multiplizität, sprich mit wie vielen Instanzen tatsächlich gearbeitet werden soll. Wenngleich die Statusaktualisierung mit mehreren Instanzen arbeiten kann, darf die auslesende Komponente nur einmal im Cluster vorkommen, damit nicht die gleichen Aufträge mehrfach verarbeitet werden. Dies kann, im Falle des JBoss, durch eine Abhängigkeit zum HA-Deployer gelöst werden. Dieser ist nur auf dem ersten gestarteten JBoss-Knoten aktiv, auf den restlichen nicht. Kommt es zu einem Clusterschwenk, wird dieser auf dem neuen Master-Node gestartet und zieht die abhängigen Dienste mit an.
Zudem sollte auf das Timerintervall geachtet werden. Dies sollte in Abhängigkeit von der Auftragslast eingestellt werden, um
- Leerlauf zu vermeiden, wenn nur selten Aufträge eintreffen, aber
- überhöhte Wartezeiten bei vielen Aufträgen ebenfalls zu unterbinden
Insgesamt ist die vorgestellte Lösung eine relativ einfache, wenngleich gut skalierende Variante.
Anmerkungen und Kritik sind natürlich, wie immer, gern gesehen!