Web

Wann Microservices (nicht) sinnvoll sind

Slice down the Monolith: Wann sind Microservices sinnvoll?
Keine Kommentare

Microservices haben mittlerweile für viele Unternehmen eine sehr hohe Bedeutung gewonnen. Durch die Verwendung dieser neuen Technologie erhofft man sich größere Flexibilität, um auf künftige Änderungen reagieren zu können. Allerdings ist es nicht damit getan, das bestehende Projekt in kleine Fragmente aufzuteilen und diese als Services zu bezeichnen. Neben den technischen Herausforderungen sind auch viele strukturelle Rahmenbedingungen zwingend erforderlich, um langfristig erfolgreich zu sein. Dieser Artikel zeichnet ein weites Bild um die vielen Details und hilft dabei, Entscheidungen zu verstehen.

Fragt man nach den Beweggründen, weshalb in einem Unternehmen Microservices eingeführt werden sollen, sind die Antworten meist identisch. Durch einen modularen Aufbau der gesamten Anwendung haben viele Projektverantwortliche das Bild eines LEGO-Baukastens vor Augen. Man glaubt, einzelne Fragmente beliebig kombinieren zu können, um so einen hohen Grad an Wiederverwendung und natürlich implizit auch erhebliche Kosteneinsparungen in der Entwicklung zu erzielen. Die Realität schaut aber ein wenig anders aus. Ein gebetsmühlenartiges Proklamieren von wiederverwendbaren Komponenten ist für den Erfolg nicht ausreichend. Diese Evangelien wurden bereits aus dem objektorientierten Design, der vorangegangenen Religion, abgeschrieben. Auf die Erfüllung warten viele Jünger teilweise noch heute.

Standhaftigkeit

Wenn wir von Stabilität sprechen, bewegen wir uns im Kontext der nicht funktionalen Anforderungen. Aus Erfahrung wissen wir um die Schwierigkeiten, eine solche Anforderung zu artikulieren. Die meisten Beteiligten haben oft eine eigene oder nur eine vage Vorstellung über die für das Projekt relevante Bedeutung des Begriffs Stabilität. Um hier ein gemeinsames Verständnis zu schaffen, ist es unumgänglich abzugrenzen, wie im Konkreten Stabilität verstanden werden soll. Betrachten wir hierzu eine Arbeit von Boehm [1] aus dem Jahr 1976, deren Thematik Softwarequalität ist. Die von Boehm aufgezeigten verschieden Betrachtungsweisen, was der Qualitätsbegriff im Kontext von Software bedeuten kann, soll auch uns als Ausgangspunkt dienen. So können wir für den Begriff Stabilität die Aspekte Fehlertoleranz von Benutzerinteraktionen oder Netzwerklatenzen außen vor lassen. Fokussieren wir uns daher auf den Bereich Maintenance (Wartung). Denn bei Enterprise-Projekten können wir uns stets gewiss sein: Die Laufzeit der entstandenen Anwendung wird viele Jahre, wenn nicht sogar Jahrzehnte umfassen. Das impliziert kontinuierliche Anpassungen und Erweiterungen. So existieren hinreichend viele Beispiele von Anwendungen, die komplett neu entwickelt werden mussten, da sich die Aufwendungen für notwendige neue Funktionen nicht tragen konnten. So sehen einige Unternehmen in der Umstellung zu einer Service-orientierten Architektur durchaus reale Chancen, Altlasten loszuwerden. Um diese neuen Pfade erfolgreich zu beschreiten, ist es notwendig, sich mit gewonnen Erkenntnissen und Standards auseinanderzusetzen, die wir im Folgenden genauer betrachten werden.

Bonusrunden

Rekapitulieren wir an dieser Stelle besser einmal, welche Anforderungen mit einer monolithischen Architektur schwierig umzusetzen sind. Ein sehr wichtiger Aspekt ist die Risikominimierung bei der Zusammenarbeit mit externen Dienstleistern. So können beispielsweise eigenständig lauffähige Module als separates Projekt mit eigenem SCM Repository verwaltet werden, ohne dass den Offshore-Kontraktoren Zugang zu sämtlichen Interna bereitgestellt werden muss. Dieses Bild lässt sich auch für verschiedenste kollaborative Szenarien weiterentwickeln. So verhindert man bei Teams, die noch keine gemeinsame Kultur entwickelt haben, dass ein Entwickler mal schnell nach eigenem Ermessen Änderungen an Codebereichen vornimmt, die er nicht zu verantworten hat.

Wenn bei der Entwicklung auch einige Standards und Disziplin eingehalten werden, können die so entstandenen Artefakte durchaus in weiteren Applikationen ihre Verwendung finden. Neben den üblichen Fallstricken im Design der Wartbarkeit gehört zu einer erfolgreichen Wiederverwendung auch eine ausführliche Dokumentation, die mehr als nur triviale Beispiele aufführt. Wenn die Forderung nach wiederverwendbaren Artefakten essenziell ist, müssen sich die Beteiligten sehr wohl im Klaren darüber sein, dass dies zusätzliche Kosten verursachen wird. Zur Risikominimierung ist es unumgänglich, den Funktionsumfang von wiederverwendbaren Komponenten erheblich einzuschränken. Es sollen für das entsprechende Einsatzszenario hochspezialisierte Komponenten entstehen und keine „eierlegende Wollmilchsau.“ Diese Forderung findet sich auch in der Konzeption von Microservices wieder. Ein iteratives Vorgehen, das dafür Sorge trägt, dass keine Funktionalität im Voraus entwickelt wird und nur aktuell notwendige Anforderungen umgesetzt werden, senkt die Entwicklungskosten erheblich. Ein Grund dafür liegt in reduzierten Testaufwendungen, die für ungenutzte Codefragmente nicht umgesetzt werden müssen. Auch lässt sich eine tatsächliche Anforderung zusammen mit der anfragenden Fachabteilung sauberer spezifizieren und anschließend implementieren. Auch eine mehrfache Umsetzung von Anforderungen, etwa weil diese nicht im Team kommuniziert wurden, lässt sich so vermeiden. Als Ergebnis fehlender Disziplin sind korrodierte Architekturen, überdurchschnittlich viel Sourcecode in Relation zum Funktionsumfang und hohe Wartungsaufwände zu nennen.

Das führt uns nun auch gleich zu einem weiteren Punkt, den es zu beachten gilt. Es gilt stets zu vermeiden, dass innerhalb einer Applikation unterschiedliche Versionen des gleichen Service eingebunden werden. Auch wenn sich mit einigen technischen Raffinessen eine solche Torheit beherrschen lässt, ist die Gefahr von unerwarteten Seiteneffekten und anderen Fehlerquellen sehr hoch. Gerade in Projektphasen, die für alle Beteiligten eine hohe Arbeitslast erfordern, wirken sich solche Seiteneffekte dann kritisch aus. Wegen des erhöhten Stresslevels können Unachtsamkeiten zu gravierenden Fehlern führen. Die vermeintlichen wirtschaftlichen Einsparungen sind mit zusätzlichen Tests und verschiedenen Managementaktivitäten erheblich teurer gegenzufinanzieren.

Auch wenn man bei Monolithen typischerweise von komplexen und schwer überschaubaren Gebilden spricht, ist die entstehende Komplexität in einer modularen, Service-getriebenen Architektur nicht zu unterschätzen. Sehr deutlich zeigt sich in Projekten, die bereits eine Entwicklungszeit von mehreren Jahren vorweisen können, dass kontinuierliche Maßnahmen zur Erhaltung der Wartbarkeit notwendig sind. Diese Anforderungen gelten auch für vergleichsweise kleine Artefakte wie Microservices. Diese sind ebenso nicht vor Architekturerodierung gefeit.

Bindungsstatus

Anhand der aufgeführten Überlegungen gibt es zwei maßgebliche Beweggründe, sich für eine modulare Architektur zu entscheiden. Das populärste Argument ist wie bereits erwähnt die Wiederverwendung von Komponenten. Ein anderes Argument ist die Zerlegung komplexer Probleme in gut verständliche Teilprobleme. Diese Vorgehensweise wurde bereits von dem französischen Philosophen und Mathematiker René Descartes empfohlen. Eine bewährte Methode, um geeignete Bruchstellen zu identifizieren, ist das von Eric Evans beschriebene Domain-driven Design (DDD) [2].

Die Qualität der entstandenen Teilmengen lässt sich anhand der Kopplungsstärke zwischen den Modulen bewerten. In Monolithen ist sehr oft eine hohe Kopplung zwischen den verschiedenen Fachlichkeiten zu finden. Beabsichtigt man, diese Monolithen in Microservices zu überführen, ist es zwingend erforderlich, die Bindung der Module untereinander erheblich zu vereinfachen. Wird diese Maßnahme nicht durchgeführt, entstehen zusätzliche, äußerst komplexe und störanfällige Build-Strukturen. Bei der Bewertung der Abhängigkeiten gilt eine einfache Faustregel. Direkte 1:1-Relationen sind weniger kritisch. Problematisch sind Verkettungen zwischen drei oder mehr Komponenten, von denen Komponente A eine Abhängigkeit in Komponente C ist und diese wiederum eine Abhängigkeit in Komponente B. Hoch kritisch sind transitive Abhängigkeiten, die umgehend aufzulösen sind, sobald sie bekannt werden. Auch wenn Projektverantwortliche es immer wieder beteuern und davon auch selbst überzeugt sind, dass in ihrem Projekt keine transitiven Abhängigkeiten vorkommen, sollte man das erst glauben, wenn es auch bewiesen wurde. Die Erfahrung zeigt, dass solche Konstrukte in umfangreichen Projekten sehr leicht entstehen können. Wenn nicht permanent explizite Maßnahmen durchgeführt werden, um die Bindungsstärke aller beteiligten Komponenten zu bewerten, ist die Wahrscheinlichkeit, auf unliebsame Überraschungen zu treffen, überdurchschnittlich hoch. Abbildung 1 verdeutlicht die Zusammenhänge grafisch.

Abb. 1: Transitive Abhängigkeiten

Abb. 1: Transitive Abhängigkeiten

Frühjahrsputz

Wer einmal die Gelegenheit hat, in verschiedenen Projekten die Verzeichnisstrukturen für die Quelldateien zu erkunden, wird aus dem Fragen kaum wieder herauskommen. Ein sehr weit verbreiteter Effekt ist das Überschreiten der Dateinamen inklusive der Pfadangaben von 255 Zeichen. Viele Entwicklerarbeitsplätze sind auf Microsoft-Windows-Systemen eingerichtet, sodass dies immer wieder zu einigen Frustrationen führt. Aber auch ohne Überschreiten der von Windows vorgesehen 255 Zeichen für Dateipfade zeigen viele Benennungen den Missstand auf, dass es keine einheitlichen Strukturen innerhalb der Projekte gibt. Auch das Benennen von einigen Verzeichnissen nach Model, View und Controller ist keine tatsächliche Hilfe. Die folgende Auflistung vermittelt einen Eindruck, wie eine vermeintliche Ordnung sich in einigen Jahren entwickeln kann. Ein exemplarisches Beispiel sind die Datei und Verzeichnisstruktur aus dem Projekt mchange-commons-java, das auf GiHub zu finden ist (Abb. 2).

Abb. 2: Undurchsichtige Verzeichnisstruktur des Projekts mchange-commons-java

Abb. 2: Undurchsichtige Verzeichnisstruktur des Projekts mchange-commons-java

Hier wurden die Packages nach Funktionalität benannt und die entsprechenden Klassen darauf verteilt. Zusätzlich findet sich noch eine Unterteilung in v1 bis v3, was auf verschiedene Versionen hindeutet. In einer solchen Ordnung ist es sehr leicht zu sehen, wie schwierig die Orientierung darin ist. Es ist keine Seltenheit, dass bei einer tiefergehenden Analyse einer solchen beschriebenen Ordnung die Lines of Code im Umfang den tatsächlichen Bedarf der Implementierung weit überschreiten. Ursachen sind unter anderem ungenutzte Codefragmente, ungünstige Implementierungen oder Sachunkenntnis des Entwicklers. Mit Hilfe der Schichtenarchitektur können projektübergreifende Konventionen etabliert werden, die es den Entwicklern ermöglichen, sich besser in den entsprechenden Projekten zurechtzufinden. Dies hilft auch dem Architekten, bei seiner Qualitätsbewertung Schwachstellen schneller identifizieren zu können. Das verkürzt zum einen die Einarbeitungsphase neuer Teammitglieder, zum anderen können mögliche Problemstellen früher erkannt und entsprechende Korrekturmaßnahmen rechtzeitig eingeleitet werden. Softwarearchitekturen und die damit verbundenen Strukturen dienen keinem Selbstzweck. Die Notwendigkeit lässt sich auf drei wesentliche Punkte herunterbrechen:

  • Verständlichkeit: Nur einer einfachen, für jedermann leicht verständlichen Architektur kann man auch folgen.
  • Benutzbarkeit: Eine Architektur sollte den Entwicklern helfen, sich besser zurechtzufinden, um ihre Arbeit effizient zu erledigen.
  • Fehlertolerant: Eine Architektur unterstützt den Entwickler bei der Vermeidung von Fehlern in der Implementierung.

Diese Punkte gelten stets gemeinsam und dienen als Qualitätsmerkmal der gewählten Architektur. Wird einer oder werden mehrere Punkte verletzt, ist dies ein Hinweis darauf, dass demnächst ein Refactoring anstehen könnte. Weiterführende Punkte wie Wartbarkeit ergeben sich implizit bei der Einhaltung der genannten drei Grundforderungen.

Werkzeugkasten

Bevor es daran geht, sich für Frameworks und Technologien zu entscheiden, muss die Basisinfrastruktur optimal etabliert sein. Zu den Grundlagen gehört der sichere Umgang mit Werkzeugen wie Maven, Jenkins, Git und Test-Frameworks. Solange dies nicht beherrscht wird, ist es weniger sinnvoll, sich Gedanken zur Einführung eines Tools wie Docker zu machen.

Die Aussage von Simon Brown, „If you can‘t build a monolith, what makes your think microservices are the answer?“, mag vielleicht ein wenig hart formuliert sein, doch sie trifft den Kern vieler Probleme in Softwareentwicklungsprojekten. Projekte, die bisher keine einzige Zeile an Testcode vorweisen können, haben wenig Chancen auf eine erfolgreiche Migration zu Microservices. Hier gilt es vornehmlich, den Entwicklungsteams zu zeigen, wie ein testgetriebenes Vorgehen den Arbeitsalltag vereinfacht und die Qualität der Implementierungen deutlich verbessert. Dazu sei allerdings anzumerken, dass man keinem Paradigma blind folgen sollte. Es ist durchaus richtig, hin und wieder einige Adaptionen vorzunehmen. Für den TDD-Ansatz ist es sehr praktikabel, nicht mit einer Überspezifikation zu beginnen, die den Anspruch hat, sämtliche Testfälle vor der Implementierung bereitstellen zu wollen. Hier ist ein agiles Vorgehen meist praktikabler. Die Testfälle entstehen parallel während der Implementierung. Dabei prüft der Entwickler regelmäßig selbstständig die Testabdeckung seiner Funktionalitäten, um Probleme bereits zur Entwicklungszeit sehen und beheben zu können. Das setzt natürlich voraus, dass in dem Projekt eine hohe Automatisierung des Builds und Reportings vorhanden ist, die möglichst ohne großen Aufwand auf jedem Entwicklungsarbeitsplatz ausgeführt werden kann. Ein gutes Gespann für eine solche Aufgabe sind beispielsweise Maven und das Test-Coverage-Tool Cobertura.

Auch den Releaseprozess darf man bei allen Bemühungen nicht außen vorlassen. Ein klassisches Missverständnis bei der Verwendung von Continuous Delivery ist, dass Feature Toggles und stetiges Deployment den Releaseprozess ersetzen. In modular aufgebauten Applikationen ist es umso essenzieller, einem leichtgewichtigen Verfahren zu folgen, das möglichst kurzfristige Releases gestattet. Semantic Versioning ist ein sehr empfehlenswertes Vorgehen, um den administrativen Aufwand zu reduzieren. Es sollte sehr viel Sorgfalt darauf verwendet werden, dass in diesem Zusammenhang keine unfertigen Artefakte in Produktion gelangen.

Fazit

Wie wir sehen konnten, sind Microservices für viele Problemstellungen eine hervorragende Lösung. Und Monolithen gehören per se nicht aufs Altenteil. Alles hat seine Berechtigung. Um erfolgreiche Projekte abzuwickeln, müssen wir lernen, uns auf die wesentlichen Dinge zu konzentrieren und diese dann im optimalen Fall mit agilen Methoden umsetzen. Die zu Beginn investierte Zeit, eine wohlüberlegte Struktur zu entwickeln und dieser dann konsequent zu folgen, amortisiert sich sehr rasch bei den Aufwendungen für Implementierung und Tests. Das soll aber nicht bedeuten, dass bereits in der Planungsphase das gesamte API mit sämtlichen Methodensignaturen bereits unverrückbar festgelegt wird. Stabile Interfaces eines API entwickeln sich parallel bei einem testgetriebenen Entwicklungsansatz. Hier wird nicht, wie oft praktiziert, gegen eine Benutzeroberfläche implementiert, sondern gegen Testfälle, die gemeinsam mit der Funktionalität iterativ entwickelt werden.

Literaturverzeichnis

  • [1] Boehm; B. W.: „Quantitative Evaluation of Software Quality“, ICSE 76
  • [2] Evans, Eric: „Domain Driven Design“, Addison Wesley

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -