“Nur wer weiß, wohin er fährt, weiß auch, welcher Wind gut ist.”
Friedrich Wilhelm Nietzsche
Nach einigen Beiträgen zu Microservices soll in diesem Beitrag ein wenig über die Schattenseiten der Technologie berichtet werden. Insbesondere über Bad-Smells und Anti-Pattern, den falschen Lösungsansätzen, bei der Entwicklung eigener Microservices.
Hier nun erst einmal eine Definition von Microservices, die freundlicherweise vom Jetpack AI-Tool meiner WordPress Installation bereitgestellt wurde.
“Ein Microservice ist ein Architekturkonzept für die Entwicklung von Software-Anwendungen. Dabei wird die Anwendung aus kleinen, unabhängigen Diensten zusammengesetzt, die über Netzwerk-Protokolle kommunizieren. Jeder Dienst erfüllt eine spezifische Aufgabe in Bezug auf eine isolierte Funktion innerhalb eines größeren Gesamtsystems. Im Gegensatz zu einem monolithischen Ansatz können die einzelnen Dienste unabhängig voneinander skaliert, aktualisiert und entwickelt werden.“
Bei der Entwicklung von Microservice Anwendungen kann viel falsch gemacht werden. Häufig wir der Kommunikationsaufwand zwischen den Microservices nicht beachtet oder die Unabhängigkeit der Dienste nicht in all ihren Konsequenzen durchdacht.
Insbesondere wird ein großer Vorteil eines Monolithen häufig vergessen. Es kann nur ein einziges System ausfallen. In dem Fall kann zwar keinerlei Aufgabe durch den Dienst erfüllt werden, aber es kann in der Regel zügig auf den Systemausfall reagiert werden.
Bei einer Microservice Anwendung können architektonisch bedingt viele Dienste ausfallen. Hat man alles richtig gemacht, dann läuft die Anwendung mit Abstrichen an der Funktionalität weiter. Finden sich jedoch gewisse Bad-Smells in der Anwendung, dann fällt auch bei einer Microservices Architektur die gesamte Anwendung aus.
Anti-Pattern, die einen größeren Systemausfall wahrscheinlicher machen, sind Call in Series, Service Fuse, Data Fuse, Service Fan Out und Data Fan Out. Allen gemein ist der synchrone Zugriff auf weitere Dienste oder Datenquellen. Fallen diese aus, dann fällt auch der zugreifende Dienst aus. Call in Series ist eine sehr fragile Angelegenheit, weil mehrere Dienste synchron hintereinandergeschaltet sind. Die Ausfallwahrscheinlichkeit multipliziert sich aus den Ausfallwahrscheinlichkeiten aller beteiligten Dienste.
Das Anti-Pattern Data Fuse findet sich nicht nur in der technischen Ebene, sondern auch in der Entwicklung. Als Integration Database Anti-Pattern macht es Entwicklern schon lange das Leben schwer. Wenn sich mehreren Diensten Daten auf einer Datenbank teilen, dann wird die unabhängige Entwicklung der Dienste zwangsläufig leiden. Das Datenmodell aller beteiligten Dienste muss gemeinsam angepasst werden.
Viele Anti-Pattern entstehen durch fehlende Anpassungen in der Organisation an kleine unabhängige Dienste. Da diese ihre Aufgabe in einem Bounded Context innerhalb der Gesamtdomäne erfüllen, sollte die Organisation – Conways Law folgend – daran angepasst werden. Manchmal sind die Teams technisch organisiert (Frontend, Backend, Datenbanken), dann werden die Microservices plötzlich von mehreren Teams entwickelt oder erfüllen nur technische Dienste.
Unter den technischen Diensten finden sich Entity-Services und Anemic-Services. Ersterer sind CRUD Services auf Basis von Entitäten der Gesamtdomäne. Sie erfüllen keine domänenspezifischen Aufgaben, sondern liefern Entitäten, die für alle anderen Dienste gedacht sind. Aus diesem Grund sind die Entitäten für alle Dienste zu groß und geschwätzig und können ungünstiger Weise von vielen Diensten falsch geändert werden. Bei einem Anemic-Service handelt es sich um Microservice, der technische Aufgaben erfüllt und kein wirkliches Modell aufweist. Beispiele dafür sind ein Logging-Service oder ein Email-Service. Wie der Entity-Service produzieren sie eine Menge Netzwerkverkehr für Aufgaben, die lokal erfüllt werden können.
Da die Dienste unabhängig voneinander entwickelt werden sollen, muss auch jede Art von enger Kopplung der Dienste und der Teams vermieden werden. Ein oft gesehener Bad-Smell ist dann die fehlende Versionierung der Schnittstellen zwischen den Diensten. Ändert solch ein Dienst seine Schnittstelle, dann kann er erst deployed werden, wenn alle abhängigen Dienste die entsprechenden Anpassungen erfahren haben. Und auch dann müssen alle diese Dienste orchestriert deployed werden. Bei einer versionierten API kann der Dienst mit der zusätzlichen neue Version deployt werden und andere Dienste können später auf die neue Version migirieren.
Weitere und überraschende Bad-Smells sind Code-Reuse und Microplatform. In anderen Bereichen der Software-Entwicklung sind sie bewährte Praktiken, aber im Umfeld von Microservices verkehren sich sich ins Gegenteil und fördern die Kopplung zwischen verschiedenen Teams und Diensten.
Es ist offensichtlich, dass ein Team behindert wird, wenn es für die Weiterentwicklung eines Dienstes auf die Bibliothek eines anderen Teams warten muss. Oder noch schlimmer, mehrere Teams an der Weiterentwicklung einer Bibliothek beteiligt sind.
Nicht so offensichtlich ist es oft für Software-Entwickler, dass eine Entity nicht zwangsläufig zwischen verschiedenen Diensten geteilt werden sollte. Denn eine Entity im Bounded-Context eines Dienstes kann sich trotz großer Ähnlichkeit von der Entity eines anderen Dienstes unterscheiden. Die Faustregel “Dein Customer ist nicht mein Customer” bedeutet, Anforderung, Nutzen und Ausprägung einer Entity in einer Subdomäne Billing kann sich fundamental von der Ausprägung in der Subdomäne Advertising unterscheiden.
Manches Problem entsteht durch den Wechsel von einer monolithischen Architektur auf eine Microservice Architektur. Das bekannteste Anti-Pattern im Bezug auf solche einen Wechsel ist der Distributed Monolith.
Der Distributed Monolith entsteht bei der Zerlegung eines Monolithen nach technischen und nicht nach domänenspezifischen Aspekten. Die generierten Microservices haben viele Abhängigkeiten untereinander, können nicht unabhängig entwickelt und deployed werden und erzeugen viel Netzwerkverkehr. Letztendlich wurde nur die schnelle interne Kommunikation zwischen einzelnen Modulen durch eine langsame Netzkommunikation ersetzt.
Noch gänzlich ohne Namen ist das organisatorische Problem, dass die Migration auf Microservices innerhalb verschiedener Abteilungen erfolgt. Dann begünstigt Conways Law, dass manche Microservices mehrfach entwickelt werden. Da die Bereichsgrenzen die Kommunikation unterbinden, entsteht eine Microservice Architektur mit ungeplant redundanten Diensten.
Innerhalb eines Monolithen wird fast immer synchron kommuniziert, in einer Microservices Architektur ist aber die asynchrone Kommunikation das bessere Mittel, denn sie erhöht die Ausfallsicherheit und bietet eine gute Entkopplung der Dienste. Bei einer Migration wird dies dann aber vergessen und es entsteht ein fragiles Netzt von Diensten, die nur synchron miteinander reden können.
Damit ist die lange Liste der Bad-Smells und Anti-Pattern noch lange nicht am Ende angekommen. Dieser Beitrag aber schon.