Über unsMediaKontaktImpressum
Herbert Dowalil 05. März 2019

Modulith First! Der angemessene Weg zu Microservices

Microservices sind ein toller Mechanismus, modulare Abgrenzung in einem System auf Dauer sicherzustellen. Als Architekturstil beantworten sie allerdings nicht die Frage, wo genau die Abgrenzung zwischen den einzelnen Services am besten funktioniert. Fachliche, vertikale Strukturen und Domain-Driven-Design sind in aller Munde, stellen aber ebenfalls kein einfach anzuwendendes Patentrezept für eine effiziente Abtrennung von Modulen und Services dar. In diesem Artikel werfen wir einen Blick auf vergleichsweise objektive, weil durch Metriken unterstützte, Ansätze. Zumindest am Beginn setzen wir dabei auf einen sogenannten "Modulithen", weil ein solcher sich besser vermessen lässt als verteilte Systeme wie beispielsweise Microservices.

Es war Juni 2015 als im Blog von Martin Fowler innerhalb weniger Tage zwei Artikel erschienen. Nämlich zuerst "MonolithFirst" von Martin Fowler selbst [1] und kurz darauf "Don´t start with a monolith when your goal ist a microservices architecture" von Stefan Tilkov [2]. Dabei drängt sich einem die Frage auf, wie es sein kann, dass es bei diesen beiden Koryphäen der Software-Architektur zu solch grundlegend unterschiedlichen Ansichten kommt, was dieses mögliche Vorgehen angeht!? Interessanterweise lässt sich dies auf unterschiedliche Definitionen des Begriffes Monolith zurückführen. Während der erste Artikel wohl von einem Deployment-Monolithen spricht, scheint der zweite wohl eher einen Architektur-Monolithen zu meinen. Der Unterschied ist beträchtlich: Bei einem Deployment-Monolithen handelt es sich um ein Software-System, dessen einzelne Artefakte immer auf einmal deployed werden müssen. Während mit dem Begriff Architektur-Monolith wiederum ein Stück Software gemeint ist, das sich durch Abwesenheit von Modularisierung oder sonstigen Substrukturen auszeichnet.

Im Zuge dieses Artikels werde ich, um weitere Missverständnisse zu vermeiden, ausdrücklich von einem Modulithen sprechen. Dabei handelt es sich um einen Deployment-Monolithen, welcher sich per Definition in effizient modulare Substrukturen zerlegt, die aber eben der Einschränkung unterliegen, immer miteinander deployed werden zu müssen. Im weiteren Lebenszyklus eines Software-Produktes kann dies ein Nachteil sein, da bei Inproduktionsnahme von jedem Modul auch ein Stand vorhanden sein muss, der auch reif für die Produktionsumgebung ist. Diese Randbedingung kann mit parallel laufenden Entwicklungstätigkeiten kollidieren. Während dies bei späteren Inproduktionsnahmen im Zuge einer Continuous Delivery zu einer Herausforderung werden kann, stört uns dies im Zuge der ersten Entwicklungstätigkeiten meist noch gar nicht.

Was ich hier explizit nicht propagieren möchte, ist der Start mit einem Architektur-Monolithen. Prinzipiell unstrukturiert vorzugehen ist eigentlich niemals empfehlenswert. Weder zu Beginn, noch zum Ende eines Projektes. Der Einwand, dass bei einem Deployment-Monolithen die Gefahr besteht, auf Dauer auch zu einem Architekturmonolithen zu werden, ist allerdings nicht von der Hand zu weisen. Die Praktiken, welche hilfreich sind, dass es dazu erst gar nicht kommt, sind auch genau jene, die uns auch zu Beginn helfen, mit einem gut strukturierten Deployment-Monolithen (eben einem Modulith) zu starten. Dies ist gleich eine wunderbare Überleitung zum Thema Metriken.

Metriken

Es war 1972, als David Parnas das Information-Hiding-Prinzip formulierte [3]. Darin forderte er, dass Module ihre Implementierungsdetails so gut es geht vor der Außenwelt verbergen sollten. Heutzutage geht die Argumentation diesbezüglich gerne in die Richtung, dass bei Microservice-Architekturen das Netzwerk, welches einzelne Microservices voneinander abtrennt, als eine Art Architekturfirewall fungiert. Ungewünschte Abhängigkeiten könnten sich so angeblich nicht einschleichen, und die ungewollte Wiederverwendung einer Java-Klasse (und somit eine starke Kopplung an dieses andere Modul) wären dadurch nicht möglich. Die Klasse bleibt hinter der Firewall Netzwerk verborgen und somit ein Geheimnis des jeweiligen Microservice. Bei Java-Packages geht dies angeblich nicht [4]. Dem ist zunächst entgegenzuhalten, dass es selbstverständlich möglich ist, eine Java-Klasse innerhalb eines Packages zu verbergen. Um dies zu forcieren empfiehlt es sich, die Defaults in der IDE für die Neuerstellung einer Klasse zu adaptieren, und zwar von:

public class ClassName

auf:

final class ClassName

Letzteres ist dann von Klassen außerhalb desselben Packages verborgen. Das Schlüsselwort final verhindert außerdem noch ungewollte Child-Klassen und ist somit quasi das Pendant zur Verhütung in Java (composition over inheritance). Als erfahrener Architekt gebe ich aber gerne zu, dass dies für gewöhnlich nicht ausreicht. Es stimmt nämlich sehr wohl, dass die Hürde zur Wiederverwendung einer Klasse, welche einfach nur durch die Bordmittel von Java verborgen wurde, wesentlich geringer ist, als wenn diese nur indirekt via REST API in einem anderen Service angesprochen werden kann. Mit wenigen Handgriffen in der IDE ist die Definition der Klasse adaptiert und kann ab dann auch außerhalb des Packages verwendet werden. Wirklich verhindern lässt sich das kaum, solange die Entwickler gemeinsam an der selben Codebasis arbeiten. Dagegenlenken kann man, indem man diese kleinen Sünden dem Team auf automatische Art und Weise transparent macht. Ich denke nämlich nicht, dass gezielte Sabotage jemals die Ursache für strukturelle Degeneration gewesen ist. Sobald man das ausschließen kann, ist es ausreichend, den Grad der Sichtbarkeit der Klassen regelmäßig zu messen und auf die Eigenmotivation der Mitarbeiter zu setzen. Aber wie?

Visibility-Metriken

Im Zuge einer anderen Veröffentlichung [5] musste ich zu meiner eigenen Überraschung feststellen, dass es aktuell keine Metrik gibt, welche den Grad der Umsetzung des Information-Hiding-Prinzips in einem Softwaresystem gut wiedergab. Aus diesem Grund habe ich im Zuge dessen drei neue Metriken entwickelt [6], welche ich hier kurz anhand eines Beispiels erläutern möchte (s. Abb. 1). Bei den Äußeren Bausteinen könnte es sich hier um Packages handeln, und bei den Inneren Bausteinen um Klassen. Die erste neue Metrik, genannt Relative Visibility (RV), gibt den Prozentsatz der inneren Bausteine an, welche außerhalb der äußeren Bausteine noch sichtbar sind. Für Baustein 1 ergibt das einen Wert von 33,33 Prozent und für Baustein 2 66,66 Prozent. Die Average Relative Visibility (ARV) berechnet aus den einzelnen RV-Werten den Durchschnitt, was im konkreten Fall exakt 50 Prozent ergibt. Die Global Relative Visibility (GRV) berechnet die Relation an sichtbaren Bausteinen für die gesamte Ebene. Im konkreten Fall sind fünf von neun der inneren Bausteine jeweils außerhalb sichtbar, was einen Wert von 55,55 Prozent ergibt.

Relational Cohesion

Eine häufig anzutreffende Schwäche von Microservice-Architekturen ist ein zu hohes Maß an Kommunikation zwischen den Microservices. Diese lässt sich durch hohe Kohäsion der einzelnen Microservices allerdings indirekt beeinflussen und dadurch verringern. Vereinfacht gesagt kommt es durch großen Zusammenhalt der einzelnen Subbausteine (wie Klassen) eines Microservice fast automatisch zu loserer Kopplung zwischen den Microservices. Wenn Sie einen Blick auf Abb. 2 werfen, dann sehen Sie dort eine typische Datenbank-Zugriffsschicht. Die einzelnen DAO-Klassen werden jeweils hauptsächlich von der Schicht darüber benützt, während es zwischen den einzelnen DAOs kaum Abhängigkeiten gibt. Ihr Instinkt sagt Ihnen vermutlich sofort, dass so ein Layer sich nicht wirklich zur Abtrennung als Microservice eignet. Wie lässt sich so eine Problematik aber messen bzw. von einem Tool automatisch aufspüren, wenn die Angelegenheit nicht so offensichtlich ist? Dafür möchte ich die Kennzahl Relational Cohesion vorstellen. Sie berechnet sich wie folgt:

Relational Cohesion = (Anzahl Abhängigkeiten zwischen den Subbausteinen + 1) / Anzahl Subbausteine

Für das Beispiel aus Abb. 2 ergibt das 0,5. Gesunde Werte liegen aber bei mindestens 1,5. Zu hohe Werte größer als vier sind ebenfalls ein Alarmsignal, da es ein Indiz dafür sein könnte, dass es dem Baustein an effizienten inneren Strukturen mangelt. Besonders interessant finde ich diese Kennzahl, weil sie sich immer noch ganz einfach von vielen Tools nach der Abtrennung eines Microservice berechnen lässt. Suchen Sie ruhig mal unter Ihren Microservices nach solchen mit einer Relational Cohesion außerhalb des oben empfohlenen Grenzwertebereiches. Wenn Sie viele davon haben, so kann das ein Indiz dafür sein, dass die Microservices nicht ideal geschnitten wurden.

Für viele weitere Aspekte einer effizienten Modularisierung gibt es durchweg passende und aussagekräftige Metriken. Ein paar weitere Beispiele sind im nächsten Kapitel angeführt, wobei ich für detailliertere Informationen dazu auf [7] verweise. Zur Messung von Metriken empfiehlt sich die Verwendung eines Tools, wie den gratis verfügbaren Sonargraph-Explorer [8], der all die in diesem Artikel vorgestellten Metriken berechnen kann. Vor einer Sache möchte ich Sie aber noch warnen: Davor, einfach Grenzwerte für diese Metriken festzulegen. Dies ist aus meiner Sicht kein besonders umsichtiger Umgang mit dem Thema. Einigen Sie sich am besten mit dem Team, welche Metriken im Zuge der Entwicklungsarbeit interessant sind und verfolgt werden sollen und reflektieren Sie gemeinsam mit dem Team regelmäßig über den aktuellen Status Quo. Beispielsweise im Zuge von Sprint-Retrospektiven. Tatssächlich kann man bei einem unüberlegten Umgang mit dem Thema Metriken auch jede Menge Schaden anrichten. Für nähere Informationen dazu verweise ich auf [9] (Stichwort: Kobra-Effekt).

Metriken-Tabelle

Metrik Beschreibung Tools
Relative Cyclicity Der Anteil an Bausteinen, welche durch ihre Abhängigkeiten Teil von Strukturzyklen sind. So gibt ein Wert von 25% an, dass ein Viertel aller Bausteine zyklisch von anderen Bausteinen abhängig ist. Der Anteil sollte möglichst gering gehalten werden. Sonargraph
Relative Average Component Dependency (RACD) Diese Kennzahl von John Lakos gibt den Prozentsatz an Bausteinen an, von dem jeder einzelne Baustein im Durchschnitt abhängig ist. Dabei werden auch transitive Abhängigkeiten berücksichtigt. Ein Wert von beispielsweise 50% besagt, dass man zum Kompilieren, Verstehen und Testen eines einzelnen Bausteins im Schnitt die Hälfte aller Bausteine des Systems benötigt. Hier ist natürlich ein möglichst niedriger Wert wünschenswert. Sonargraph, STAN
LCOM(4) Hohe LCOM-Werte indizieren Substrukturen, welche nicht in Verbindung zueinander stehen, und somit eher getrennt werden sollten. Während diese Metrik hauptsächlich auf Klassenebene berechnet wird, ist derselbe Mechanismus auch auf höheren Abstraktionsebenen wie Packages, Modulen oder Microservics anwendbar. Sonargraph, STAN, NDepend / CppDepend / JArchitect
Cyclomatic Complexity Die Komplexität von Code entsteht zunächst einmal weniger durch seine schiere Menge, sondern viel eher durch die Anzahl seiner Verzweigungen in Schleifen und if-Bedingungen. Dies wird gemessen durch diese recht bekannte Kennzahl von Thomas McCabe. Sollte ein Modul einen gewissen Grenzwert dafür überschreiten ist dies ein Indiz dafür, dass dieses zu groß und komplex geworden ist und weiter aufgeteilt werden sollte. Axivion Bauhaus Suite, Sonargraph, JDepend, Structure101, STAN, NDepend / CppDepend / JArchitect

Modulith First

Damit nochmal zurück zum Knackpunkt dieses Artikels: Ich wollte damit nicht nur darstellen, dass man einen Deployment-Monolithen ebenfalls dauerhaft modularisieren kann, sondern dass er in gewissen Fällen sogar Vorteile haben kann. Vor allem solange der Nachteil des gemeinsamen Deployments aller Module noch nicht zum Tragen kommt, was vor der ersten Inproduktionsnahme ziemlich sicher der Fall sein wird. Die Vorteile bei der Entwicklung eines Modulithen sehe ich vor allem in den folgenden Punkten:

  • Die Vermessug der strukturellen Qualität eines Verteilten Systems (wie Microservices) durch statische Code-Analyse ist meist gar nicht erst möglich, da die Abhängigkeiten im Code nicht transparent sind. Einen Überblick über die Abhängigkeiten oder die Qualität der Kohäsion (Relational Cohesion) kann man so nicht auf einfache Art und Weise ermitteln. Abhängigkeiten in Microservice-Architekturen sind nämlich eher dynamisch als statisch und dadurch volatiler und schwieriger nachzuvollziehen.
  • Durch die intensive gemeinsame Arbeit des Teams mit und an den Kennzahlen wird eine besonders hohe Sensibilität für das Thema "effiziente Strukturierung" geschaffen, die nur durch den reinen Einsatz des Architekturstils Microservices alleine nicht unbedingt gegeben ist.
  • Strukturelle Sünden, die sich am Beginn der Entwicklung zeigen, sind noch relativ einfach zu beheben. Mit ein paar wenigen Handgriffen in der IDE kann eine Klasse von einem Package in ein anderes verschoben werden. Besonders zu Beginn der Entwicklung eines neuen Produktes, wo oft die Problemdomäne noch nicht so gut verstanden wurde, kommt dies besonders oft vor.

Wie weit Sie dieses Spiel treiben hängt ganz von Ihnen und vom jeweiligen Anwendungsfall ab. Entscheiden Sie je nachdem, wann (und ob) der zunächst gestartete Modulith in Microservices zerlegt wird. Beachten Sie dabei, dass es sich bei Erstellung eines verteilten Systems auch immer um einen Trade-Off handelt. Die Vorteile, die Sie durch die Verteilung der Services über das Netzwerk bekommen, bekommen Sie nämlich nie ohne die entsprechenden Nachteile. So werden aus meiner Sicht die Konsequenzen der danach nicht mehr einfach möglichen ACID-Transaktionen nicht selten unterschätzt.

Sobald man in der Lage ist, aus einem Deployment-Monolithen durch den Einsatz der hier vorgestellten Praktiken dauerhaft auch einen Modulithen zu machen, ergeben sich außerdem noch weitere Möglichkeiten.

  • Right-Sized-Services: Der Micro-Aspekt der Microservices wird dabei nicht so sehr in den Vordergrund gerückt. Es ist in Ordnung, wenn ein Service einmal etwas größer ausfällt, um seine Qualitätsanforderungen durch ACID-Transaktionen zu erfüllen. Dabei zerlegt es sich wunderbar in weiter Code-Substrukturen. Zur Steuerung der Obergrenze eines solchen Services eignet sich die o. a. Komplexitätskennzahl von McCabe [9].
  • Self-Contained-Systems (SCS): Wenn Sie bei der Entwicklung einer Systemlandschaft auf unabhängige (Sub-)Systeme setzen, kann sich die Architektur dieser einzelne Systeme voneinander unterscheiden. Sie können dann Modulithen mit Microservices kombinieren. Wenn die Systeme tatsächlich unabhängig sind, sollten diese nicht von den jeweils anderen Designentscheidungen betroffen sein.
  • Migration von Architektur-Monolithen: Die laufende Messung von Kennzahlen bietet sich außerdem beim Re-Engineering eines Architektur-Monolithen an. Dies ist einer schrittweisen Extraktion von Funktionalität in Microservcies in eine StranglerApplication [10] oft vorzuziehen. Möglich ist es allerdings nur, wenn der Legacy-Code noch ausreichend Qualität aufweist, um diese laufenden Änderungen mit ausreichend geringem Risiko möglich zu machen. Sobald eine Strukturierung erfolgt ist, ist eine spätere Migration in eine Microservice-Architektur immer noch möglich.

Metriken beim Re-Engineering eines Monolithen

Metriken spielen beim letzten der eben angeführten Punkte noch eine weitere Rolle: Sie können potentiell auch dabei helfen, die Startpunkte bei der Extraktion von Microservices aus einem Deployment-Monolithen zu identifizieren. Idealerweise beginnt man nämlich mit Bausteinen, welche entweder nur eingehende oder nur ausgehende Abhängigkeiten haben. Hat ein Baustein nämlich beides, ist er in die Struktur des Monolithen relativ fest verankert, und eine potentielle Extraktion zieht meist zu viele Änderungen am Monolith nach sich. Im Beispiel in Abb. 3 gibt es 3 Bausteine, bei welchen dies nicht der Fall ist:

  • Baustein B hat vier ausgehende Abhängigkeiten, aber keine eingehenden. Er könnte als ein Microservice abgebildet werden, welches den Monolithen benutzt, welches aber der Monolith selbst nicht benutzen müsste.
  • Baustein M hat nur eingehende Abhängigkeiten. Er könnte als Microservice extrahiert werden, welches vom Monolithen benutzt wird, diesen aber selbst wiederum nicht benutzt.
  • Baustein C hat weder ein- noch ausgehende Abhängigkeiten. Höchstwahrscheinlich handelt es sich um toten Code, welcher gar nicht erst migriert werden muss und vermutlich gleich gelöscht werden kann.

Die relevanten Kennzahlen um diese Bausteine zu identifizieren stammen aus den Software Package Metrics von Robert C. Martin:

  • Afferent Coupling: Gesamtzahl der eingehenden Abhängigkeiten eines Bausteins.
  • Efferent Coupling: Gesamtzahl der ausgehenden Abhängigkeiten eines Bausteins.
  • Instability: Verhältnis von ausgehenden Abhängigkeiten (Efferent) zu allen Abhängigkeiten (Afferent + Efferent). Bausteine mit exakt 0.0 oder 1.0 identifizieren mögliche Startpunkte zur Extraktion. In unserem Beispiel hätten die Bausteine B und C einen Wert von 0.0, und Baustein M einen Wert von 1.0.

Fazit

Hinter dem Microservice-Architekturstil steckt u. a. die Idee, mittels forcierter technischer Abgrenzung durch das Netzwerk die Motivation zur strukturellen und fachlichen Abgrenzung der einzelnen Module (dann Services genannt) zu erhöhen. Dies klappt beileibe nicht immer. Die guten alten Grundregeln der Modularisierung werden dadurch keineswegs außer Kraft gesetzt, sondern forciert. Wenn dies nicht klappen sollte, wird man mit den Problemen einerseits früher, aber andererseits auch wesentlich gnadenloser konfrontiert. Ein Start mit einem "vermessenen" Modulithen kann hier Abhilfe schaffen und erhöht von Beginn weg die Sensibilisierung im Team für die Fragen der Modularisierung und des effizienten Softwareentwurfs.

Quellen
  1. M. Fowler: MonolithFirst
  2. S. Tilkov: Don´t start with a monolith if your goal is a microservices architecture
  3. D. Parnas: On the Criteria To Be Used in Decomposing Systems into Modules
  4. E. Wolff, 2018: Microservices - oder doch nicht?, in Java Magazin 10/2018
  5. Architektur-Spicker
  6. H. Dowalil: Visibility Metrics and the Importance of Hiding Things
  7. H. Dowalil: Grundlagen des modularen Softwareentwurfs, Carl Hanser Verlag 05/2018
  8. Sonargraph-Explorer
  9. Über die Vermessung von Software
  10. M. Fowler: StranglerApplication

Autor

Herbert Dowalil

Herbert Dowalil unterstützt Teams bei der Etablierung guter Softwarearchitekturpraktiken in Unternehmen. Ob bei der Erstellung von konkreten Designs oder beim Refactoring zur Verbesserung bestehender Systeme.
>> Weiterlesen
Das könnte Sie auch interessieren
botMessage_toctoc_comments_9210