Mehr Container an die (Modell-)Bahn!
Eine etwas ungewöhnliche Anwendung ist vor einigen Jahren bei einem unserer Code Camps entstanden: v5t11 ist der Projektname und es handelt sich dabei um eine Steuerung für eine Modellbahn. Nun erwarten Sie heute zu Recht keinen Artikel über eine Anwendung aus dem Jahre 2012. Es geht im Folgenden daher um die Version 2 von v5t11, die im Gegensatz zu der monolithischen Struktur von damals als eine Gruppe von Microservices aufgebaut ist, und damit um ein Thema, das uns heute auch in "normalen" Anwendungen begleitet, nämlich die Aufteilung eines Monolithen in separat deploy- und lauffähige Services. Und da wir gerade beim Aufnehmen aktueller Trends sind: Zielumgebung soll Docker und Kubernetes sein.
Zunächst sollte wohl der Projektname v5t11 erklärt werden. Er ist ein Anagramm der Bezeichnung eines legendären Zugs der Baureihe VT 11.5, der in der Mitte des letzten Jahrhunderts als TEE unterwegs war [1]. Stellt man die Zeichen in v5t11 um, erhält man eine etwas nerdige Abkürzung für Visual Traincontrol, was auf die Aufgaben der Anwendung hindeutet:
- Visualisierung des Gleisplans.
- Anzeige von Gleisbelegungen.
- Reservieren und Freigeben von Fahrstraßen inklusive der dazu nötigen Weichen- und Signalstellungen.
- Aber: keine (vollständige) Automatisierung des Fahrbetriebs!
Ausgangspunkt Monolith
Die Anwendung wurde damals als monolithische Anwendung auf Basis von Java EE 6 entwickelt. Die komplette Logik aus EJBs, CDI Beans und JPA Entities befand sich in einem WAR-Deployment, das zur Anbindung der Mehrzugsteuerung einen Resource Adapter in einem RAR-Deployment nutzte. Beides wurde auf einem JBoss 7.1 deployt. Als GUI lief eine Swing-Anwendung mit Anbindung über EJB-Remoting.
Bei der nun erfolgten Zerteilung des Monolithen in Microservices sollte möglichst viel Code wiederverwendet werden, um den Aufwand in Grenzen zu halten – bei diesem "Spiel"-Projekt nicht anders als in Real-Life-Projekten.
Mehrzugsteuerung Selectrix
Um die Funktionsweise der Anwendung verstehen zu können, muss man wissen, wie die technische Anbindung an die Modellbahn aussieht. Hier kommt eine sog. Mehrzugsteuerung zum Einsatz, über die die Fahrzeuge und Geräte gesteuert werden. Anders als bei den Spieleisenbahnen unserer Kindertage werden nicht mehrere Stromkreise zur unabhängigen Steuerung von Lokomotiven genutzt, sondern eine auf der gesamten Anlage vorhandene Fahrspannung mit einem aufgeprägten Digitalsignal, das von Decodern in den Fahrzeugen in Fahrgeschwindigkeit, Richtung, Lichtzustand etc. umgesetzt wird. Auf meiner Anlage kommt das System namens Selectrix der Firma Trix (mittlerweile Märklin) zum Einsatz, da es schon in den 1980ern, als ich mit dem Hobby begann, sehr kleine Decoder mit guten Fahreigenschaften anbot.
Neben den Lokdecodern stehen noch weitere Busgeräte zur Verfügung wie bspw. Funktionsdecoder zur Ansteuerung von Weichen und Signalen sowie Besetztmelder, die Gleisabschnitte auf Stromverbrauch überwachen und melden, ob dort ein Zug steht.
Schließlich gibt es einen Interface-Baustein mit einer seriellen Schnittstelle, über die Kommandos und Statusinformationen zwischen Selectrix und v5t11 ausgetauscht werden können. Die ausgetauschten Informationen sind sehr klein: Jeder Lokdecoder, Funktionsdecoder oder Besetztmelder hat eine Adresse und einen Wert, jeweils ein Byte groß. Im Wert eines Lokdecoders sind Fahrstufe (5 Bits), Fahrrichtung, Licht (an/aus) und Horn (an/aus) codiert.
Der Wert eines Funktionsdecoders stellt den Zustand von 8 Weichen oder Signalen dar. Analog meldet ein Besetztmelder in seinem Wert die Belegung von 8 Gleisabschnitten.
Serienschnittstelle
Das Selectrix-Interface verfügt über eine Serienschnittstelle (RS 232). Für deren Ansteuerung aus einer Java-Anwendung heraus benötigt man eine Zusatzbibliothek, da die Standardbibliothek keine seriellen, parallelen oder USB-Ports kennt. Bewährt hat sich für diesen Zweck eine Open-Source-Bibliothek namens RXTX[2], die allerdings einen nativen Teil in Form einer Shared Library oder DLL benötigt. In dem Fork namens NRJavaSerial[3] sind die nativen Anteile im JAR-File integriert, was die Nutzung der Bibliothek sehr vereinfacht.
Aufteilung des Monolithen
Microservices haben vordergründig drei Vorteile:
- Der kleinere Umfang der einzelnen Services ermöglicht übersichtlicheren Code.
- Die Services können weitgehend unabhängig voneinander in Betrieb genommen werden. Die Start- bzw. Deploymentzeiten sind angenehm klein.
- Die technologische Basis der Services kann unterschiedlich sein.
Damit sich die Vorteile wirklich einstellen ist es wichtig, dass jeder Service eine – und nur eine – gut abgegrenzte Aufgabe erledigt. Man spricht hier von möglichst hoher Kohäsion innerhalb der Services und möglichst geringer Kopplung untereinander. Für v5t11 war also eine Aufteilung zu finden, die diese Bedingungen erfüllt, allerdings mit der Nebenbedingung, dass es einen Service geben muss, der die Kommunikation mit der Mehrzugsteuerung aufrecht erhält. Folgende Services wurden realisiert bzw. geplant:
- v5t11-status: Dieser Service kommuniziert mit der Mehrzugsteuerung und hält ein Objektmodell im Speicher, das den Zustand der Anlage wiedergibt: Gleisbelegungen, Weichen- und Signalstellungen, Lokzustände. Über ein REST-API können Teile des Modells abgefragt und manipuliert werden.
- v5t11-fahrstrassen: Eine Fahrstraße dient dazu, einen Zug von A nach B fahren lassen zu können. Sie umschließt die zu befahrenden Gleisabschnitte sowie die dazu nötigen Weichen- und Signalstellungen. Der Service hat die Aufgabe, die möglichen Fahrstraßen zwischen A und B aufzustellen und darin die (kollisions-)freien zu ermitteln. Ein REST-API gibt anderen Services Zugriff zu diesen Informationen und ermöglicht die Reservierung und Freigabe von einzelnen Fahrstraßen. Zudem gibt v5t11-fahrstrassen eine durchfahrene Fahrstraße automatisch wieder frei.
- v5t11-position: Gleisbesetztmelder liefern i. A. nur die Information, ob ein Gleisabschnitt besetzt ist, aber nicht, welches Fahrzeug dort steht. Aus einem gegebenen Ausgangszustand soll dieser Service anhand der gestellten Fahrwege und Änderungen der Gleisbelegungen die Position aller Züge berechnen. Das ist derzeit aber noch Zukunftsmusik: dieser Service befindet sich erst in Planung.
- v5t11-vorsignal: Die Stellung von Hauptsignalen muss i. d. R. weit vorher angekündigt werden, da Züge in der Realität recht träge sind und nicht mal eben so anhalten können. Die Stellung eines Vorsignals ist dabei natürlich vom Fahrweg abhängig. Dieser ebenfalls noch in Planung befindliche Service wird sich darum kümmern.
Für eine Anwendung auf Basis von Microservices ist immer wieder zu entscheiden, wie das User Interface realisiert werden soll: Hat jeder Service sein eigenes UI oder gibt es eine übergreifende Oberfläche? Für beides gibt es gute Argumente: Einzelne UIs entsprechen eher dem Ansatz der Aufgabenteilung und dem unabhängigen Deployment. Ein übergreifendes UI kann dagegen leichter "wie aus einem Guss" erscheinen. Für v5t11 war die Entscheidung, für die Visualisierung einen separaten Service namens v5t11-leitstand bereitzustellen, der als einziger ein Nutzerinterface betreibt. Hintergrund der Entscheidung war die Tatsache, dass im UI die Informationen nahezu aller anderen Services eng integriert zusammenfließen müssen. Ein Gleisabschnitt bspw. wird als solcher visualisiert und zeigt gleichzeitig an, ob er Teil einer Fahrstraße ist und welcher Zug gerade auf ihm steht.
Aus dem Gesagten wird klar, dass einige Informationen in mehreren Services – teilweise in allen – zur Verfügung stehen müssen: v5t11-fahrstrassen benötigt bspw. aktuelle Gleisbelegungen, um kollidierende Fahrstraßen auszuschließen und reservierte Fahrstraßen nach der Durchfahrt wieder freizugeben. Hier zeigt sich ein Preis der Aufteilung in einzelne Services: Daten, die nicht nur lokal in einem einzelnen Service benötigt werden, müssen verteilt werden. Dabei hat jede Information einen primären "Heimat"-Service. Andere Services halten je nach Bedarf Kopien (Replikate), die in ihren Details unterschiedlich ausgeprägt sein können. So verwaltet v5t11-status als primärer Service die Besetztzustände von Gleisabschnitten. Für die Kommunikation mit der Mehrzugsteuerung ist hier auch konfiguriert, welcher Besetztmelder dafür zuständig ist. v5t11-fahrstrassen und v5t11-leitstand begnügen sich dagegen mit der einfachen Statusinformation.
Der primäre Service bietet für "seine" Informationen APIs an, über die sich andere Services informieren können und mit denen – soweit erlaubt – auch Änderungen ausgelöst werden können. Diese synchronen Aufrufwege zwischen den Services sollten zyklenfrei sein, um die Entkopplung der Services nicht vollends aufzuheben.
Um die Daten systemweit aktuell zu halten, nutzt v5t11 zusätzlich asynchrone Replikation der Daten mittels Messaging. Ändert sich eine Information im primären Service, wird eine Meldung verschickt, aus der andere Services die veränderten Daten entnehmen können. Auch diese asynchronen Meldwege sind zyklenfrei.
Insgesamt ergibt sich damit die in Abb. 5 gezeigte Kommunikationsarchitektur.
Der zentrale Message Broker ("MB") wird in v5t11 der Einfachheit halber durch einen JMS-Dienst in v5t11-status realisiert, da der Statusdienst wegen der Anbindung der Mehrzugsteuerung und des dafür bereits vorhandenen Resource-Adapters ohnehin auf einem klassischen JEE-Server deployt wird und dieser JMS ohne weiteren Aufwand anbietet.
Technik
Wie angekündigt laufen die v5t11-Services in einem Kubernetes-Cluster. Kubernetes im Detail darzustellen würde den Rahmen des Artikels sprengen. Eine grobe und eingeschränkte Übersicht zeigt Abb. 6.
Um als Container in einem Kubernetes-Cluster laufen zu können, werden die Einzelservices in Docker-Images verpackt. Als Basis dient dabei jeweils ein Image, das die nötigen Basisfeatures mitbringt, z. B. ein installiertes Java oder ein WildFly-Applikationsserver. Die Anwendung in Form eines WAR- oder JAR-Files wird dann mit dem Basisimage verknüpft als neues Image abgelegt, das später als Container in einem Kubernetes-Pod gestartet wird.
v5t11-status ist als klassische JEE-Anwendung aufgebaut und nutzt einen WildFly 14 als Server. Das entsprechende Image wird zweistufig erstellt: Zunächst wird das offizielle WildFly-14-Basisimage um das Deployment des Resource-Adapters ergänzt. Das so erstellte Image wird im zweiten Schritt um das WAR-File des Services ergänzt. Im Projekt finden Sie die entsprechenden Dockerfiles in den Teilprojekten v5t11-selectrix-adapter und v5t11-status. Die sog. Kubernetes-Manifeste für den Betrieb in Kubernetes liegen im Projektverzeichnis v5t11-status/src/main/k8s. Sie umfassen das eigentliche Deployment sowie einen Service zum Zugriff darauf. Für die Benutzung von außerhalb des Clusters ist zudem noch ein Ingress definiert.
v5t11-fahrstrassen läuft als kleiner Service auf dem Microprofile-Framework Apache Meecrowave [4]. Hier wird als Basisimage eines genutzt, dass ein OpenJDK enthält. Hierauf wird die Anwendung in Form eines JAR-Files inkl. der von Meecrowave benötigten Dependencies gepackt. Für Kubernetes wird in v5t11-fahrstrassen/src/main/k8s analog zu oben ein Deployment, ein Service sowie ein Ingress definiert.
v5t11-leitstand befindet sich derzeit noch in einem Zwischenzustand. Die UI des Monolithen war als Swing-basierte Client-Anwendung aufgebaut. Die Migration des UI zu einer Webanwendung, die bspw. Angular im Browser nutzt, ist zwar geplant, bislang aber dem Druck anderer Projekte zum Opfer gefallen. Um dennoch über ein UI zu verfügen – und sonst wäre das v in v5t11 ja schließlich gelogen – haben wir uns eines Tricks bedient, der sicher für die eine oder andere Migration eines Real-Life-Projektes auch interessant sein könnte: Als Basisimage wird eines genutzt, das die Remote-Desktop-Software VNC sowie den dazu passenden Browser-Client noVNC enthält. Damit kann der Service, der ebenfalls mit Apache Meecrowave gebaut wurde, das alte funktionsfähige Swing-UI weiter betreiben. Über noVNC und VNC kann dann die Anwendung im Browser bedient werden. Das funktioniert sehr gut, hat aber die Einschränkung, dass ein zweites Browserfenster nicht ein zweites UI anzeigen würde, sondern nur den gleichen virtuellen Desktop, der bereits im ersten Fenster erreicht wird.
Das Ganze sieht nun zur Laufzeit so aus wie in Abb. 7.
Eines fehlt noch: Sie erinnern sich daran, dass die Kommunikation mit der Mehrzugsteuerung über eine Serienschnittstelle erfolgt? Würde v5t11-status diese Schnittstelle tatsächlich selbst betreiben, müsste der Service auf einen bestimmten Knoten fixiert werden – eben den mit dem seriellen oder USB-Port. Um das zu vermeiden, wurde die Mehrzugsystemschnittstelle mit einem Wrapper versehen, der den seriellen Port über einen LAN-Socket erreichbar macht. Es ist geplant, dies mit einer kleinen Microcontroller-Hardware direkt am Port-Stecker zu realisieren. Bis es soweit ist, läuft der Wrapper als kleine Anwendung außerhalb des Clusters (Teilprojekt v5t11-com-server).
Ausfallsicherheit
Die Aufteilung in Microservices hat einen weiteren Vorteil: Fällt einer der Services aus oder wird er bewusst außer Betrieb genommen, laufen die anderen weiter. Wirklich? Na ja, nur wenn ein solcher Teilausfall in den anderen Services berücksichtigt wurde. Konkret heißt das für v5t11, dass API-Aufrufe von ausgefallenen Services mit einer sinnvollen Reaktion hinterlegt sein müssen und eine unterbrochene asynchrone Replikation automatisch wieder aufgenommen werden muss. Wird bspw. v5t11-status für eine Änderung der Konfigurationsdaten kurz außer Betrieb genommen, schwingt sich das System danach schnell wieder ein.
Fazit
Die Modellbahnsteuerung v5t11 ist ein "Spielprojekt" und hat in einigen Belangen nicht die gleichen Ansprüche wie Real-Life-Projekte. Dennoch kann man die Vorteile – aber auch den Aufwand – der Zerlegung von monolithischen Anwendungen in Microservices erkennen:
- Der Programmcode der einzelnen Services ist kleiner und übersichtlicher geworden.
- Es können unterschiedliche Technologien verwendet werden. v5t11 nutzt zwar "nur" klassische JEE-Server und Microprofile-Frameworks, aber es wäre ebenso möglich gewesen, einen Teil als NodeJS- oder C#-Anwendung zu realisieren.
- Die Services sind unabhängig voneinander deploybar, brauchen dazu allerdings eine eingebaute Ausfallsicherheit.
- Die Aufteilung verlangt teilweise nach der Replikation von Daten, wozu entsprechende synchrone und asynchrone Schnittstellen bzw. Verfahren zu implementieren sind.
Das Projekt finden Sie auf Github [5]. Nutzen Sie bitte den Branch develop für die derzeit in der Entwicklung befindliche Microservice-Version. Im Branch monolith liegt die alte monolithische Version zum Vergleich. Und bei Fragen wenden Sie sich gerne an mich!
- Wikipedia: DB-Baureihe VT 11.5
- RXTX
- Github: NRJavaSerial
- Apache Meecrowave
- Projekt auf Github: v5t11