Wie man Microservices am besten trennt

Dekomposition von Microservices: Wie schneide ich richtig?

Thomas Franz

(c) Shutterstock/ezheviks

Auch wenn sich Programmiersprachen und -paradigmen immer wieder neu erfinden, haben manch alte Prinzipien heute noch ihre Gültigkeit. Bei der Frage, wie man Microservices am besten trennt, hilft das Prinzip des Information Hiding aus den 70er-Jahren. So können Microservices ihre vollen Stärken ausspielen.

Microservices ermöglichen die parallele Entwicklung und Inbetriebnahme von Software in kleinen Teams. Sie unterstützen daher sowohl agile Vorgehensweisen als auch geschäftliche Ziele, wie eine schnellere Time to Market. Microservices setzen die vierzig Jahre alten Ziele der Modularisierung um, wie sie schon 1972 von David Parnas beschrieben wurden:

1. Managerial: Development time should be shortened, because separate groups would work on each module with little need for communication.
2. Product flexibility: It should be possible to make drastic changes to one module without a need to change others.
3. Comprehensibility: It should be possible to study the system one module at a time. The whole system can therefore be better designed because it is better understood.

Die meisten Programmiersprachen stellen heute Modularisierungskonzepte bereit, zum Beispiel Pakete in Java und Module in Python. Allerdings ist es oft ohne großen Aufwand möglich, Klassen anderer Module oder Pakete zu verwenden. Dadurch entsteht die Gefahr einer engen Kopplung, der Verstrickung von Code, die den Zielen der Modularisierung widerspricht.

Microservices forcieren per Definition eine lose Kopplung: Ein Microservice ist ein eigenständiger Prozess, der ausschließlich über Netzwerkschnittstellen mit anderen Microservices kommuniziert. Diese harte Trennung erschwert enge Kopplung und hilft dadurch Modularisierung einzuhalten.

Problemdekomposition ist kritischer denn je

Entsprechend obiger Definition von Microservices muss sich der Entwickler sehr sicher sein, was durch welchen Microservice implementiert wird und was nicht. Denn falls die Modularisierung, d. h. die Dekomposition, nachträglich geändert werden muss, kann das teuer werden – der harten Trennung wegen. Wenn beispielsweise zwei Microservices in unterschiedlichen Programmiersprachen geschrieben sind, ist eine aufwändige Portierung von Code notwendig. Innerhalb einer monolithischen Architektur hingegen lassen sich viele Veränderungen durch Refactoring-Maßnahmen, die zusätzlich noch von vielen Entwicklungswerkzeugen unterstützt werden, vergleichsweise günstig erledigen.

Aktuell wird vor dieser Herausforderung sogar diskutiert, ob ein Microservices-Projekt monolithisch oder wirklich direkt mit Microservices beginnen sollte [1]. Für die Antwort auf diese Frage ist es natürlich auch relevant, die methodische, technologische und prozessuale Reife zu betrachten. Dieser Artikel konzentriert sich auf den architekturbezogenen Kern dieser Fragestellung: Wie sicher ist die Dekomposition und damit die Stabilität der Dekomposition?

Könnte man diese Frage mit absoluter Sicherheit beantworten, könnte die Entwicklung – sofern die methodische und technische Reife vorhanden sind – bedenkenlos sofort mit echten Microservices beginnen, d. h. mit der Implementierung autarker Dienste, die ausschließlich über das Netzwerk kommunizieren. Im Folgenden wird an einem Beispiel erläutert, wie Entwickler Authentifizierung und Autorisierung in einer Microservices-Architektur umsetzen können. Das grundsätzliche Vorgehen lässt sich aber auch auf weitere domänenspezifische und fachlich orientierte Modularisierungsentscheidungen übertragen.

Authentifizierung und Autorisierung: gebündelt oder einzeln?

Jede Software implementiert die Authentifizierung und Autorisierung von Personen, um sicherzustellen, dass ausschließlich Personen mit entsprechender Berechtigung den Status der Software oder der mit ihr verwalteten Informationen ändern können. Zu Beginn der Softwareentwicklung stellte sich die Frage, wie sich diese grundsätzlichen Aufgaben in einer Microservices-Architektur beantworten lassen. Dazu betrachten wir für jede Anforderung zwei Möglichkeiten der Umsetzung:

1. Als Teil jedes Microservice, der diese Anforderung umsetzen muss.
2. Als dedizierter Microservice, der von anderen Microservices genutzt werden kann.

Entscheidet man sich für Variante 1, wird vielfach die Entwicklung einer Bibliothek empfohlen, die allgemeine Anforderungen implementiert und von jedem Microservice eingebunden werden kann. Dieser Ansatz wird jedoch hier nicht weiter diskutiert, da er nicht zu der Beantwortung der konzeptionellen Problemstellung beiträgt: Sollte eine Anforderung pro Microservice oder als eigener Microservice umgesetzt werden? Fällt die Entscheidung für die Variante pro Microservice, so kann im nächsten Schritt über die Auslagerung allgemeingültiger Logik als Bibliothek nachgedacht werden. Für das Beispiel Authentifizierung und Autorisierung gibt es also die in Tabelle 1 gezeigten Lösungsalternativen.

Alternative 1 Alternative 2
Authentifizierung In jedem Service (pro Microservice) Als eigener Microservice
Autorisierung In jedem Service (pro Microservice) Als eigener Microservice

Tabelle 1: Lösungsalternativen für Authentifizierung und Autorisierung

Sowohl Authentifizierung als auch Autorisierung sind eine Funktionalität, die in jeder Komponente benötigt wird. Also könnte jede Komponente auch diese Funktionalität umsetzen (Abb. 1).

Abb. 1: Alternative 1: Jeder Microservice setzt Authentifizierung und Autorisierung um

Abb. 1: Alternative 1: Jeder Microservice setzt Authentifizierung und Autorisierung um

 

Auf der einen Seite würde bei dieser Alternative eine erhebliche Menge Logik mehrfach umgesetzt werden. Auf der anderen Seite könnte jedes Microservices-Team selbst und den spezifischen fachlichen Anforderungen entsprechend arbeiten. Auf der gegenüberliegenden Seite möglicher Varianten liegt die gesonderte Umsetzung von Authentifizierung und Autorisierung jeweils in einem dedizierten Microservice.

Abb. 2: Alternative 2: Dezidierte Microservices für Authentifizierung und Autorisierung

Abb. 2: Alternative 2: Dezidierte Microservices für Authentifizierung und Autorisierung

 

Jeder Microservice könnte Nutzer, die nicht authentifiziert sind, an den dafür erstellten Service weiterleiten. Dieser würde einmalig und einheitlich Authentifizierung gewährleisten. Ebenso könnte die Autorisierung durch einen dedizierten Dienst umgesetzt werden, sodass auch die Autorisierung einmalig und einheitlich für alle fachlichen Fälle umgesetzt wäre.

Lösungsansatz mit Information Hiding

Tatsächlich wurde für das Projekt keine dieser Varianten gewählt, sondern eine andere (Tabelle 2).

Variante 1 Variante 2
Authentifizierung In jedem Service Als eigener Service
Autorisierung In jedem Service Als eigener Service

Tabelle 2: Die Lösungsvariante nutzen, die bei einer Änderung eines Service am wenigsten Änderungen bei Schnittstellen oder anderen Services erfordert

Abb. 3: Lösung: Authentifizierung als Microservice, Autorisierung in jedem Service

Abb. 3: Lösung: Authentifizierung als Microservice, Autorisierung in jedem Service

Die Begründung für diesen Schnitt beruht auf den Erkenntnissen von Parnas aus dem Jahre 1972 [2]. Er empfiehlt, Designentscheidungen auf der Basis wahrscheinlicher Änderungen und Auswirkungen auf genutzte und nutzende Komponenten zu erarbeiten: „We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change.“ Daraus ergeben sich in diesem Beispiel mindestens die folgenden zwei Fragestellungen:

1. Wenn sich Anforderungen an die Authentifizierung ändern, wie viele Teams müssen involviert werden?
2. Wenn sich die Anforderungen an die Autorisierung ändern, wie viele Teams müssen involviert werden?

Konfrontieren wir Alternative 1 mit Frage 1, stellt sich heraus, dass eine Veränderung von Authentifizierungsmechanismen die Bearbeitung sämtlicher Microservices zur Folge hätte. Bei Alternative 2 hingegen können sich Authentifizierungsmechanismen ändern, diese ändern jedoch in vielen Fällen nicht einmal die Schnittstelle des dedizierten Authentifizierungs-Microservice. Es könnten also Änderungen in einem Team unabhängig von anderen Teams erfolgen.

Anders verhält es sich bei der Autorisierung. Wenn die Autorisierung als dedizierter Microservice wie in Alternative 2 umgesetzt ist, ist es wahrscheinlich, dass jedes Team von der Umsetzung des Autorisierungs-Microservice abhängig ist. Denn Autorisierung ist stark abhängig von fachlichen Anforderungen, die wiederum in entsprechenden fachlich fokussierten Microservices implementiert sind, beispielsweise wenn sich das Datenmodell ändert. Setzt hingegen jeder Microservice die für ihn relevanten Autorisierungsaspekte um, so lässt es sich vermeiden, dass mehrere Teams die Veränderung eines Microservice fordern, wenn sich die fachlichen Anforderungen ändern, die der Microservice umsetzt. Da Autorisierung stark mit fachlichen, funktionalen Anforderungen verknüpft ist, wurde es daher in diesem Projekt nicht als Kandidat für einen Microservice identifiziert.

Fokus auf Information Hiding und Modularisierung

Die Prinzipien des Information Hiding und der Modularisierung sind keineswegs neu und werden – teilweise auch unbewusst – in jedem Softwareentwicklungsprojekt eingesetzt. Microservices-Architekturen geben diesen Prinzipien ein neues Gewicht, da sie erhöhte Kosten für nachträgliche Veränderungen von Designentscheidungen mitbringen. Für die notwendige Kreation der relevanten Fragestellungen ist die Kenntnis der Domäne ausschlaggebend.

Aufmacherbild: Scissors icon von Shutterstock / Urheberrecht: ezheviks

Geschrieben von
Thomas Franz
Thomas Franz
Dr. Thomas Franz ist Entrepreneur, Wissenschaftler und Technologieexperte für die adesso AG. Er interessiert sich für Methoden der agilen Geschäftsentwicklung wie Lean Startup und insbesondere dem Zusammenspiel mit neuen technologischen Treibern.
Kommentare
  1. Joachim Arrasz2016-01-26 10:31:03

    "Microservices forcieren per Definition eine lose Kopplung: Ein Microservice ist ein eigenständiger Prozess, der ausschließlich über Netzwerkschnittstellen mit anderen Microservices kommuniziert. Diese harte Trennung erschwert enge Kopplung und hilft dadurch Modularisierung einzuhalten."

    Spannend! Wo ist das definiert? Hierzu hätte ich gerne die Quelle!

  2. Frank Beyer2016-01-26 17:56:28

    Der Artikel ist unglücklich, weil er eine Architektur für ein technisches Problem - nämlich Sicherheit diskutiert. Entscheidend ist jedoch gerade bei Microservices die fachlich Architektur. Außerdem für Variante 1 eine gemeinsame Bibliothek zu empfehlen, ist eine wirklich schlechte Idee. Das bedeutet in einer Microservices-Architektur eine Code-Abhängigkeit, die es dort zu vermeiden gilt - das ist breiter Konsens. Ich have das schon in der Praxis gesehen und kann nur davor warnen. Am Ende kommt der Artikel immerhin zu der richtigen Empfehlung.

    PS: Leider ist das Zitat falsch. Der Artikel stammt *nicht* von Martin Fowler, sondern von Stefan Tilkov. Und er heißt nicht "Don't start monolith" sondern "Don't start with a monolith"

  3. Sven2016-01-27 08:32:57

    "Außerdem für Variante 1 eine gemeinsame Bibliothek zu empfehlen, ist eine wirklich schlechte Idee. Das bedeutet in einer Microservices-Architektur eine Code-Abhängigkeit, die es dort zu vermeiden gilt..."

    Naja, wie wäre es z.B. wenn es eine Library ist, die als Open-Source-Projekt auf GitHub veröffentlicht ist? Es dürfte ja nichts dagegensprechen Libraries/Frameworks einzusetzen. Allerdings funktioniert das nicht immer, wenn Microservices in verschiedenen Programmiersprachen geschrieben werden. Ob das sinnvoll ist, finde ich aber auch fraglich. Verschiedene Programmiersprachen einzusetzen bringt meiner Meinung nach eher mehr Probleme und Kosten als Vorteile/Nutzen mitsich.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.