Leider gibt es keine einheitliche Definition von Microservices. Darum wird eine solche Architektur von verschiedenen Menschen auch unterschiedlich interpretiert. Viele dieser Interpretationen enthalten jedoch Gemeinsamkeiten.
Eine dieser Gemeinsamkeiten ist die Idee, ein großes System in mehrere kleinere Teile zu schneiden. Genau diese Eigenschaft macht den Kern einer jeden Microservice-Architektur aus. Es wurde erkannt, dass ein System irgendwann so groß wird, dass es nicht mehr zufriedenstellend weiterentwickelt werden kann.
Doch warum wird geglaubt, dass sich ein solches System, das aus vielen kleinen Teilen besteht, langfristig besser weiterentwickeln lässt? Das Schlüsselwort heißt hier „Unabhängigkeit“. Nur wenn es geschafft wird, dass diese kleinen Teile unabhängig voneinander sind, können Vorteile aus einer Microservice-Architektur gezogen werden. Gelingt dies nicht, besteht die Gefahr, dass ein „verteilter Monolith“ gebaut wird. Dieser nutzt keinen der Vorteile einer solchen Architektur aus, bringt aber alle Nachteile eines verteilten Systems mit sich.
Unabhängigkeit
Unabhängigkeit kann dabei auf den drei Ebenen Organisation, Entwicklung und Betrieb erreicht werden.
Organisatorisch sollten Teams möglichst unabhängig voneinander agieren können. Hieraus ergibt sich, dass jedes Team fachliche Anforderungen vollständig umsetzen können sollte. Dies reduziert die Zeit, die für Abstimmungen und Meetings mit anderen Teams gebraucht wird.
Während der Entwicklung bedeutet Unabhängigkeit, dass jedes Team eigene Entscheidungen für oder gegen spezifische Technologien oder Konzepte treffen kann. Jedes Team kann somit die für seine Probleme idealen Tools wählen, ohne bei jeder Entscheidung Rücksicht auf alle anderen zu nehmen. Übergreifende Dinge, wie der Betrieb oder Schnittstellen zwischen den Teams, greifen natürlich in diese Unabhängigkeit ein, sollten jedoch nicht der Normalfall sein. Wichtig ist, dass man die Teams weitestgehend frei entscheiden lässt und sie so selten wie möglich zu etwas zwingt.
Auch der Betrieb der einzelnen Teile soll unabhängig sein. Natürlich müssen die Teile miteinander kommunizieren und sind somit voneinander zu einem gewissen Grad abhängig. Ziel ist es somit vor allem, dass Fehler in einzelnen Teilen so wenig Einfluss wie möglich auf die restlichen Teile haben.
ISA Principles
Die Verfasser der ISA Principles sind in ihrem beruflichen Alltag immer wieder auf Systeme, die auf einer Microservice-Architektur basieren, getroffen. Dabei wurden an vielen Stellen Entscheidungen getroffen, welche Unabhängigkeit geschmälert oder in wenigen Fällen sogar verhindert haben.
Mit der Zeit haben sich dabei Prinzipien herausgestellt, die eine Unabhängigkeit innerhalb eines verteilten Systems fördern. Diese Prinzipien sind anschließend unter einer Open-Source-Lizenz bei GitHub veröffentlicht worden. Die primäre Idee dabei ist es, anderen Entwicklern Hinweise und Hilfestellungen zu geben, ein verteiltes System zu bauen, dessen primäre Eigenschaft Unabhängigkeit ist.
Im Folgenden werden diese Prinzipien vorgestellt und erläutert.
Modularisierung
Das erste Prinzip ist ein sehr allgemeines und bekanntes in der Informatik. Jedes Teil des Systems wird als eigenständiges Modul abgebildet.
Für ein solches Modul gelten dementsprechend auch alle bekannten Best Practices. Hierzu zählen vor allem das Single-Responsiblity-Prinzip, Information-Hiding sowie hohe Kohäsion und geringe Kopplung.
Makro- und Mikro-Architektur
Obwohl die Unabhängigkeit im Mittelpunkt steht, müssen alle Module am Schluss zusammen ein großes Ganzes ergeben. Es wird somit immer Dinge geben, bei denen man diese Unabhängigkeit beschneiden möchte oder muss.
Aus diesem Grund gibt es mehrere Ebenen von Architekturentscheidungen. In der Makro-Architektur werden Entscheidungen getroffen, an die sich alle Module halten müssen. Die Mikro-Architektur hingegen wird für jedes Modul individuell festgelegt. Ziel ist es, möglichst wenige Entscheidungen auf Ebene der Makro-Architektur festzulegen. Man ermöglicht den Modulen somit eine möglichst hohe Freiheit. Durch diese Freiheit können viele der Entscheidungen an der Stelle getroffen werden, an der das Problem auch wirklich besteht.
Alle Dinge, die man in der Makro-Architektur festlegt, verursachen bei einer Änderung hohen Aufwand. Diese Änderung muss schließlich von allen Modulen anschließend umgesetzt werden. Deshalb sollte man darauf achten, dass Entscheidungen in der Makro-Architektur möglichst stabil und langfristig sind.
Laufzeitumgebung
Im dritten Prinzip wird gefordert, dass die Module entweder eigenständige Docker-Container, virtuelle Maschinen oder Prozesse sind.
Diese drei Laufzeitumgebungen ermöglichen weiterhin eine freie Wahl der Programmiersprache oder des eingesetzten Frameworks. Würde man hier etwas Spezifischeres, beispielsweise eine JAR-Datei, fordern, schränkt man die Auswahl deutlich ein. Außerdem erhöht man durch diese Trennung die Widerstandsfähigkeit, da der Ausfall eines einzelnen Moduls die anderen Module nicht direkt stört.
Integration und Kommunikation
Damit die einzelnen Module gemeinsam ein System bilden, sollte die Integration und Kommunikation zwischen den Modulen standardisiert werden.
Die Integration kann dabei über das User-Interface (UI) oder über APIs erledigt werden. Integriert man Module über das UI, sollte man für ein konsistentes Look & Feel sorgen, zum Beispiel über eine Pattern-Library. Zudem sollte entschieden werden, ob man Transklusion nutzt und ob diese client- oder serverseitig erledigt wird.
Weiterhin sollte eine Möglichkeit sowohl für synchrone als auch asynchrone Kommunikation zur Verfügung stehen. Idealerweise ist diese sprachenneutral, um die Module so nicht weiter einzuschränken.
Authentifizierung und Autorisierung
Die Nutzer des Systems sollten von der internen Modularisierung idealerweise nichts mitbekommen. Aus diesem Grund sollten diese sich natürlich nur einmal am System anmelden müssen und nicht an jedem einzelnen Modul. Somit sollte die Authentifizierung zentral in der Makro-Architektur festgelegt werden.
Da Autorisierung meistens mit der Fachlichkeit zusammenhängt, muss diese von jedem Modul individuell umgesetzt werden und kann nicht zentral erfolgen.
Continuous Delivery
Damit Module wirklich unabhängig voneinander sind, müssen diese unabhängig deploybar sein. Um dies sicherzustellen, sollte jedes Modul eine eigene unabhängige Continuous-Delivery-Pipeline besitzen.
Bestandteil einer solchen Pipeline sind natürlich auch alle Tests für das Modul. Neben Unit- gilt dies auch für Integrations- und End-to-End-Tests. Sind diese nicht unabhängig, muss ein Modul in eine geteilte Umgebung deployt und dort gemeinsam mit allen anderen Modulen getestet werden. Erfahrungsgemäß führt dies zu einem Flaschenhals und sollte deswegen gemieden werden.
Um Tests zu entkoppeln, kann man Consumer-Driven Contracts oder dedizierte Integrationsumgebungen für jedes Modul nutzen.
Betrieb
Durch die Aufteilung in mehrere Module ergibt sich für den Betrieb, dass er nun deutlich mehr Prozesse/virtuelle Maschinen/Container betreiben muss.
Um mit dieser erhöhten Anzahl klarzukommen, sollte über Standardisierung und Automatisierung nachgedacht werden. Hierzu gehören neben der Laufzeitumgebung auch Konfiguration, Deployment, Logging, Metriken, Tracing und Alerting.
Natürlich lässt sich hierbei nicht alles standardisieren. Aus diesem Grund sollte hier immer die Möglichkeit offen gelassen werden, auf spezifische Anforderungen der Module einzugehen. Letztlich kann ein System nur erfolgreich betrieben werden, wenn Betrieb und Entwicklung miteinander und nicht gegeneinander arbeiten.
Vorgaben und Standards
Vorgaben in der Makro-Architektur sollten, wenn immer möglich, Standards nutzen und auf Ebene der Schnittstelle festgelegt werden. Nur so bleibt eine unabhängige Wahl von Sprache und genutzter Bibliothek für die einzelnen Teams möglich.
Es könnte beispielsweise vorgegeben werden, dass jede Programmierschnittstelle JSON über HTTP ausliefern muss. Die Wahl der konkreten HTTP- oder JSON-Bibliothek bleibt jedem Team so aber erhalten. Trotzdem kann es sich ergeben, dass alle Teams eine ähnliche oder gleiche Technologieauswahl tätigen. Dies ist nicht schlimm, sofern sie nicht dazu gezwungen wurden.
Widerstandsfähigkeit
Das letzte Prinzip fordert Widerstandsfähigkeit von jedem Modul. Zum einen bedeutet dies, dass jedes Modul damit klarkommen sollte, wenn ein anderes Modul gerade nicht erreichbar ist. Klarkommen meint an dieser Stelle natürlich nicht, dass der Ausfall komplett kompensiert werden muss, sondern nur dass dies nicht zum Ausfall des nächsten Moduls führen sollte.
Zum anderen muss jedes Modul jederzeit damit rechnen, gestoppt und gegebenenfalls an anderer Stelle wieder gestartet zu werden. Dies bedeutet vor allem, serverseitigen State zu vermeiden oder diesen zumindest nicht im Speicher zu halten.
Wird diese Anforderung nicht umgesetzt, leidet die Gesamtverfügbarkeit, da es bei einem verteilten System wahrscheinlicher ist, dass gerade ein Modul nicht verfügbar ist oder Fehler produziert.