Microservices und ihre Alternativen

Vorteile des Monolithen

Wenn wir den Monolithen noch einmal genauer betrachten, fällt auf, dass er eben nicht nur Nachteile, sondern auch eine ganze Reihe von Vorteilen mit sich bringt. Zu diesen Vorteilen gehört neben der Möglichkeit, das Gesamtsystem einfacher zu erfassen, auch eine bessere Nutzung der vorhandenen Ressourcen, als dies bei Microservices der Fall ist. Damit einher gehen durchschnittlich geringere Antwortzeiten, weil ein Grossteil des Datenaustauschs bei einem Monolithen innerhalb des gleichen Prozesses stattfindet. Es ist eben doch ein Unterschied, ob Daten über ein Netzwerk übertragen werden müssen oder ob sie einfach nur von derselben CPU verarbeitet und im selben Arbeitsspeicher abgelegt werden. Im Rahmen von Webapplikationen ist das selbstverständlich eher selten ein massiver Vorteil. Webapplikationen leben davon, dass sie entsprechend verteilt sind, und stellen besondere Anforderungen an Ausfallsicherheit und Skalierung.
Gerade im Rahmen von Microsoft-Technologie hat man es aber nicht selten auch mit grösseren Desktopsystemen zu tun. Zu diesen Desktopsystemen gehören beispielsweise auch Hardwareansteuerung und Ähnliches. In diesem Bereich ist die Nutzung vieler unterschiedlicher Dienste möglicherweise behindernd, weil Daten, die von der Hardware kommen, nicht schnell genug verarbeitet werden können. Es empfiehlt sich daher, in diesen Bereichen auf einen entsprechenden Monolithen zu setzen, da die Skalierbarkeit keinen Mehrwert bietet, eine effektive Nutzung der vorhandenen Ressourcen aber durchaus einen entsprechenden Vorteil.
Doch wenn nun der Monolith seinerseits zu einer schlechten Architektur führt und auf längere Sicht schwer zu überblicken ist? Was kann man tun, um dieser Komplexität Herr zu werden? Man geht den gleichen Weg wie bei Microservices: Man teilt die fachlichen Bestandteile auf und separiert sie voneinander.

Modulithen

Hinter dem Begriff des Modulithen verbirgt sich nichts anderes als ein modulorientierter Monolith. Diese werden in aller Regel als Deployment-Monolithen betrieben, in der Entwicklungszeit aber in stark separierte Softwaremodule zerlegt. Diese Module bilden geschlossene Fachkontexte ab und besitzen ihr eigenes Daten-Modell, möglicherweise sogar eigene Datenbanken und Ähnliches. Sie kommunizieren nur über wohldefinierte Schnittstellen mit anderen Modulen und werden zur Laufzeit über einen Microkernel beziehungsweise ein entsprechendes Applikationsframework zur eigentlichen Applikation zusammengefügt. Typische Applikationsframeworks können dabei ASP.NET, aber auch das Prism Framework sein, das im Zusammenhang mit WPF sehr gern verwendet wird.
Interessant ist an dieser Stelle, dass es sich bei Modulithen nicht wirklich um etwas Neues handelt. Schon mit dem Composite Application Block hat Microsofts Expertengruppe Patterns & Practices 2007 Unterstützung bei der Entkopplung der inneren Struktur von WinForms-Monolithen geboten. Die Erkenntnisse daraus hatten Einfluss auf das spätere WPF, welches mit dem Prism Framework ein eigenes Applikationsframework erhielt.
Bild 6 zeigt die Unterschiede zwischen Microservices und Modulithen noch einmal deutlicher. Die Fachkontexte sind als Module umgesetzt, die einen unterschiedlich grossen Umfang haben können. Alle Softwarebestandteile werden gemeinsam deployt. Dies bedeutet aber nicht, dass sie zur Laufzeit auch alle geladen werden. Vielmehr kann der Modulith anhand von Umgebungsparametern beim Start entscheiden, welche Module tatsächlich zu laden sind.
Vergleich von Modulith und Microservices (Bild 6)
Quelle: Oliver Drotbohm
Die Kommunikation aller Module geschieht rein prozess­intern, wobei sie selbst natürlich auch auf externe Ressourcen zugreifen können. Bei Microservices stellt jeder Dienst für sich ein fachliches Modul dar, das eigenständig deployt wird. Die Interaktion zwischen den Diensten geschieht als externe Kommunikation, da es sich bei den Diensten um getrennte Prozesse handelt. 

Aufbau und Interaktion von Modulen

Es stellt sich noch die Frage, wie die Module aufgebaut sein sollten. Dies stellt Bild 7 dar, bei dem die Module Einkauf und Verkauf über eine Rahmenapplikation aggregiert und somit zur eigentlichen Applikation zusammengefügt werden. Das Vorgehen orientiert sich hierbei an umfangreichen Rich ­Clients und kann für Webapplikationen variieren. Dort gibt es aber dank ASP.NET sehr ähnliche Konzepte. Die Rahmenapplika­tion muss somit nicht nur über die beiden Bestandteile Startup und Interfaces verfügen, es sind auch die einzigen, die für die Module von Bedeutung sind. Denn beim Startup wird die Rahmenapplikation nach den Root-Elementen jedes Moduls suchen und diese initialisieren (Nummer 1). Die Wurzelelemente kennen alle Bestandteile des Moduls und können diese während des Starts bei den zentralen Diensten der Rahmenapplikation registrieren. Ein typischer Zentraldienst ist hierbei der IoC- beziehungsweise DI-Container. Dafür nutzen sie die Schnittstellen, die von der Rahmenapplikation bereitgestellt werden (Nummer 2). Meist bietet die Rahmenapplikation hier schon für das Wurzelelement eine Schnittstelle, mit der sich die Initialisierung steuern lässt und nach deren Implementierung während des Starts gesucht werden kann.
Interner Aufbau eines Modulithen (Bild 7)
Quelle: Autor
Intern sind die Module weiter zerlegt. Dabei kann die Zerlegung im einfachsten Fall über eigenständige Namensräume realisiert werden. Es kann sich bei komplexeren Modulen auch um völlig eigenständige Assemblies handeln. Die Zerlegung muss dabei nicht zwangsläufig wie in Bild 7 geschehen. Hierbei wurden die Geschäftslogik (Logic), die Datenzugriffsschicht (Data) und die Benutzerschnittstellen (UI) jeweils separiert, um eine technische Schichtung zu erzwingen, denn all diese Bestandteile haben keine direkten Abhängigkeiten zueinander, sondern nutzen nur Interfaces und fordern deren Implementierung über Dependency Injection an.
Sollte ein Modul Informationen oder Funktionalität eines anderen Moduls benötigen (Nummer 3), wird es ebenfalls nicht direkt auf dessen Logik zugreifen. Vielmehr nutzt es auch hier die öffentlich verfügbaren Schnittstellen und fragt deren Implementierung über die zentralen Dienste ab. Dabei entsteht auch eine gewisse Herausforderung, kann es doch gerade bei der Initialisierung zu Deadlocks kommen, falls Module Kreisabhängigkeiten zueinander aufweisen. Solche sind in jedem Fall zu vermeiden und sollten über statische Codeanalyse verhindert werden. Hierfür können, je nach Werkzeug, die Zugriffe aus bestimmten Namensräumen auf andere als schädlich markiert werden, wodurch dann beispielsweise der Build fehlschlägt. Als Werkzeuge können dabei beispielsweise NDepend, SonarQube oder Arch­UnitNet dienen. 

Hendrik Lösch
Autor(in) Hendrik Lösch



Das könnte Sie auch interessieren