Serviceorientierte Architektur – das Buzzword ist in alle Munde. Auch ich darf mich regelmäßig im Rahmen meiner Arbeit mit diesem Architektur-Typ auseinander setzen.
Die Idee an und für sich ist ja schon interessant: Funktionen werden über wohldefinierte Schnittstellen (z.B. REST) angeboten, wie die Antwort zu einem Request entsteht ist dann völlig unerheblich – das reicht von einer anderen Programmiersprache bis hin zu einem vollständig anderen physikalischen System. Modularisierung auf die Spitze getrieben. Eigentlich der Traum eines jeden Entwicklers sollte man meinen. Minimale Kopplung, wenn ein Service ersetzt werden soll, muss nur noch die Schnittstelle erhalten bleiben, und man kann es sogar ganz einfach umstellen.
Klingt ja erst mal alles vernünftig, aber jede Medaille (und jede Technik) hat bekanntlich zwei Seiten. Wo ist die Schattenseite der Service-Orientierung?
Ein kurzer Ausflug in die Historie hilft uns das Konzept besser zu verstehen. Schon seit geraumer Zeit haben wir uns an eine Art Gewaltenteilung gewöhnt: Daten „leben“ in der Regel in einem eigenen Umfeld, das unterschiedlich ausgestaltet sein kann – sei es eine relationale Datenbank (die es wiederum in verschiedensten Geschmacksrichtungen gibt: MySQL, MariaDB, Postgreß, DB2, etc), in Dateien oder auch in den modernen NoSQL-Datenbanken. Wichtig dabei ist nur: Man sollte die Datenhaltung so wählen, dass sie zum Anwendungszweck passt. Der Blick über den Tellerrand hat auch hier noch nie geschadet: Nur weil man bisher alles in ein relationales Schema gepresst hat, muss das für eine (neue) Anwendung noch lange nicht eine gute Wahl sein.
Ohne es sich groß bewusst zu machen, haben wir hier die erste Stufe einer Service-Orientierung eingeführt – angesprochen wird der Service über eine definierte Schnittstelle. In ganz vielen Fällen ist die Schnittstelle in der Sprache der eigentlichen Applikation bereits etwas abstrahiert um sie leichter nutzen zu können (auch bekannt als „Connector“). Ein ähnliches Konstrukt mit einem anderen Focus hat bereits Dan Bernstein für qmail verwendet. Triebfeder dort war die Sicherheit – auch das bietet eine definierte, gekapselte Schnittstelle.
Ist die Sache mit den Services also nur „neuer Wein in alten Schläuchen“? Ganz so einfach ist es nicht, es hat sich doch einiges geändert – die Schnittstellen haben sich verändert – weg von festen Techniken wie etwa einem MySQL-Adapter – hin zu gerneralisierten Schnittstellen wie SOAP oder JSON als Transport-Kappsel. Das hat natürlich Vorteile – man kann die Services etwas weiter auseinander ziehen, z.B. auf spezielle Server mit angepasster Hardware oder etwa einen zentralen Authentifizierungs-Server für Single-Sign-On.
Ein weiterer oftmals genannter Vorteil ist die Skalierbarkeit: Wenn mehrere Rechner den gleichen Service anbieten, dann kann man die Last darauf gleichmäßig verteilen. Besonders sinnvoll ist es wenn man die Server-Funktion dann noch fachlich genauer eingrenzen kann. So kann man sich vorstellen, dass man mehrere Server aufsetzt und dann alphabetisch anhand der Nutzernamen aufteilt – natürlich so, dass jeder Server im Zweifel die Aufgaben eines anderen nutzen kann – aber durch die Spezialisierung wirken Cache-Mechanismen um ein Vielfaches besser.
Soweit die vielen positiven Aspekte (es gibt noch einige mehr). Aber jede Medaille hat ja zwei Seiten. Was sind Nachteile einer derartigen Struktur?
Ausfallsicherheit gibt es nicht ohne Preis – es müssen ggf. mehrere Server gestartet werden, auch wenn das heute durch Container und Virtualisierungslösungen einfacher geworden ist, man muss sich im administrativen um eine ganze Menge mehr Maschinen kümmern.
Die nächste Stelle an der man mehr Aufwand hat, ist der Aufruf und die Kodierung der Daten. Hier handelt man sich bei Netwerkaufrufen gleich einmal den vollständigen TCP/IP-Stack ein, mit allen seinen sieben Schichten. Das mag nicht übermäßig viel sein, aber es kommt definitiv etwas Latenz hinzu – wenn der entfernte Server dann noch weiter weg steht und womöglich etwas knapp angebunden ist, merkt man das sehr schnell. Besonders heftig ist es, wenn man dann noch (ohne es zu wissen oder zu wollen) eine ganze Kette solcher Services lostritt, weil der abgefragte Server wiederum andere Services anstößt…
Ein weiteres oftmals vernachlässigtes Problem ist die Datenkonsistenz und Datenaggregation: Liegen die Daten alle in einer Datenbank gehört es zum guten Ton Fremdschlüssel zu verwenden und so etwaige Inkonsistenzen zu vermeiden. So kann man beispielsweise sicherstellen, dass mit jeder Veränderung eines Datensatzes der aktuell angemeldete Benutzer mitgeschrieben wird, und dass dieser gültig ist (also zum Beispiel seine ID in der Benutzertabelle existiert). Ändert sich nun der Benutzername, ist das alles kein Drama, die Datenbank bleibt in sich konsistent. Das Löschen einen Benutzers mit noch vorhandenen Datensätzen lässt sich ebenfalls unterbinden. Will man etwas in dieser Form mit getrennten Services realisieren hat man sehr schnell eine Synchronisationshölle geschaffen. Im besten Fall kann man noch eine ID des Benutzers oder dessen Login-Namen als String speichern. Wird dieser dann auf dem Authentifikationsserver gelöscht, bekommt die eigene Anwendung davon gar nichts mit. Der Benutzer kann sich zwar nicht mehr anmelden, aber die Tatsache, dass dieser Datensatz gelöscht wurde (was ggf. für die Applikation sehr interessant sein könnte) wird nicht weiter propagiert. Besonders heftig trifft es wenn Veränderungen am Login-Namen vorgenommen werden. Das passiert häufiger als man denkt: Ein Benutzer heiratet und schwupps ändert sich in vielen Firmen der Login, da er an den Nachnamen gekoppelt ist. Für das aufrufende System ist es ein neuer Benutzer. Der Benutzer kann seine Datensätze vom Vortag nicht mehr editieren, weil er ist ja nicht der Autor. Die Szenarien in dieser Hinsicht sind vielfältigst. Will man diesen Problemen Herr werden benötigt man wieder zusätzliche Services die einen Rückschluss auf die Veränderung zulassen (also z.B. die Frage: Wurde Benutzer x irgerndwann einmal umbenannt?)
Der Weg zu einer „hippen“ Microservice-Architektur will also gut überlegt sein. Es kommt sehr auf den Anwendungsfall an und ob es mir im gegebenen Kontext überhaupt möglich ist einzelne Services so heraus zu lösen, dass sie vollständig eigenständig sind. Praktischerweise ist die Architekturwelt an dieser Stelle nicht vollständig digital bzw. schwarz/weiß, es gibt beliebig viele Abstufungen die man wählen kann und die alle legitim sind. Weder ist ein monolithische Architektur per se falsch und böse, noch sind Microservices das allein seelig machende Allheilmittel. Es hilft, sich über die Rahmenbedingungen im Klaren zu sein:
- Habe ich tatsächlich unabhängige fachliche Teilbereiche?
- Wie groß ist die Entwickler-Gruppe – lohnt es sich mehrere Teams zu bilden?
- Gibt es eine hohe Abhängigkeit von Daten zwischen Anwendungen / Prozessen?
Man kann noch eine Reihe weiterer Fragen stellen um ein genaueres Bild zu erhalten.
Zum Abschluss noch ein kurzer Blick zurück in die Geschichte bzw. ältere Software-Architekturen. Modularisierung von Software gab es ja bereits schon früher. Dort hat sich der Ansatz „high cohesion, loose coupeling“ bewährt. Wenn man es genau betrachtet, sind Mikroservices eine logische Weiterentwicklung dieses Ansatzes. Innerhalb eines Sources-Code-Repositories kann man diese Vorgabe nur als Regel für die Entwickler vorgeben: Es ist zu vermeiden, dass man von jeder beliebigen Stelle im Code andere Funktionen aufruft. Der Ansatz der Objektorientierung hat hier bereits wichtige moderierende Funktion. Ebenso kann man durch den Einsatz von Modulen das Bewusstsein schärfen, wenn ein (fachlicher) Bereich verlassen wird. Microservices schließlich erzwingen das Konzept durch weitere Hürden, wie das Verpacken in universelle Pakete (JSON, SOAP siehe auch oben). Ob derartige Zwänge immer zu besserer Software führen ist jedoch fraglich. Zudem gibt es in jedem Projekt doch immer wieder Funktionen die gleichartig sind – mit Mikroservices nimmt man bewusst in Kauf, dass ggf. Funktionalität zweimal ausprogrammiert wird. Ob das dann gute Architektur ist, darf sich jeder selbst überlegen.