Software Engineering15. Juli 20268 min read

Vom Monolithen zu Microservices: Wann sich der Sprung lohnt

Microservices sind kein Reifezeugnis. So erkennen Sie, wann sich eine Aufteilung wirklich lohnt, was sie tatsächlich kostet und wie Sie migrieren, ohne alles zu zerbrechen.

Von Innovation T Team


Irgendwann auf dem Weg wurde »wir stellen auf Microservices um« zu einer Art Signal dafür, dass ein Team erwachsen geworden war. Selten geht das so sauber auf. Zahlreiche Teams haben eine völlig gesunde Anwendung in ein Dutzend Dienste zersplittert, ihre betriebliche Last verdreifacht und festgestellt, dass das ursprüngliche Problem nie die Architektur war. Die ehrliche Frage lautet nicht »Monolith oder Microservices«. Sie lautet »Was wollen wir eigentlich lösen, und ist ein verteiltes System der günstigste Weg dorthin«.

Dieser Leitfaden plädiert für eine langweilige Standardwahl (beginnen Sie mit einem gut strukturierten Monolithen) und legt dann die Signale dar, die eine Aufteilung lohnenswert machen, die Kosten, vor denen Sie niemand warnt, und ein Migrationsmuster, mit dem Sie sich ohne riskantes Neuschreiben bewegen können.

Beginnen Sie mit einem modularen Monolithen

Für die meisten Produkte ist die richtige erste Architektur eine einzelne bereitstellbare Anwendung mit sauberen internen Grenzen. Das ist der modulare Monolith, und er ist kein Kompromiss. Er ist eine legitime langfristige Wahl.

In seinem Inneren ist Ihr Code nach wie vor um Geschäftsdomänen organisiert: Abrechnung, Katalog, Konten, Benachrichtigungen. Jedes Modul besitzt seine eigenen Daten und stellt den anderen eine klare Schnittstelle bereit, doch die Module kommunizieren über Funktionsaufrufe im selben Prozess, nicht über Netzwerkanfragen. Sie erhalten den Großteil der Entwurfsdisziplin von Microservices, ohne fast eine der betrieblichen Abgaben zu zahlen.

Die Vorteile sind real. Eine einzige Codebasis bedeutet einen Build, ein Deployment, eine einzige Stelle, an der man nachsieht, wenn etwas kaputtgeht. Ein Funktionsaufruf funktioniert entweder oder wirft eine Ausnahme, sodass Sie nie einen halb abgeschlossenen Arbeitsablauf debuggen, der über drei Dienste verstreut ist. Refactoring über Grenzen hinweg ist eine vom Compiler unterstützte Umbenennung, und Transaktionen sind einfach, weil alles eine Datenbank teilt.

Entscheidend ist: Ein sauberer modularer Monolith hält Ihnen die Optionen offen. Wenn Sie früh scharfe Grenzen zwischen den Modulen ziehen, lässt sich jedes von ihnen später mit weitaus weniger Schmerz in einen eigenen Dienst herauslösen. Die Disziplin, die Sie in Modulgrenzen investieren, ist genau die Disziplin, die Microservices verlangen, also geht nichts davon verloren. Sie wählen den Monolithen nicht für immer. Sie wählen, die Verteilung aufzuschieben, bis Sie einen konkreten Grund dafür haben.

Die Signale, die eine Aufteilung wirklich rechtfertigen

Eine Aufteilung wird durch organisatorischen und betrieblichen Druck gerechtfertigt, nicht durch Ästhetik. Sehen Sie besser mehr als eines dieser Signale, bevor Sie sich festlegen.

Teamskalierung und Koordinationskosten. Der beständigste Grund für eine Aufteilung sind Menschen. Wenn mehrere Teams alle in dieselbe Codebasis einchecken und um dieselbe Deployment-Pipeline streiten, wird jede Auslieferung zu einer Verhandlung, und die unfertige Funktion eines Teams blockiert die dringende Korrektur eines anderen. Dienste erlauben es jedem Team, sein Stück nach eigenem Zeitplan zu besitzen, bereitzustellen und zu skalieren. Wenn Sie ein oder zwei Teams haben, existiert dieser Druck noch nicht, und eine Aufteilung wird Ihnen nicht helfen.

Ein Bedarf an unabhängigem, häufigem Deployment. Wenn ein Teil des Systems mehrmals täglich ausgeliefert wird, während ein anderer sich einmal im Quartal ändert, erzeugt es Reibung, sie in ein einziges Deployment zu bündeln. Ein Zahlungsmodul, das niemals Regressionen aufweisen darf, möchte nicht jedes Mal neu bereitgestellt werden, wenn sich der Marketingtext ändert. Das ist ein echter Vorteil, aber nur, wenn Ihre Test- und Rollback-Strategie viele kleine Deployments statt weniger großer bewältigen kann.

Wirklich unterschiedlicher Skalierungsbedarf. Manchmal ist eine Komponente CPU-hungrig und schnellt während eines Verkaufs auf ein Vielfaches der normalen Last hoch, während der Rest der Anwendung untätig bleibt. Wenn Sie diesen heißen Pfad nicht skalieren können, ohne dafür zu zahlen, alles rundherum mitzuskalieren, kann es sich lohnen, ihn als Dienst zu isolieren. Seien Sie hier ehrlich: Moderne Anwendungsserver und eine gute Caching-Schicht lösen viele Skalierungsprobleme ganz ohne Aufteilung. Eine Aufteilung, um bei der Rechenleistung zu sparen, lohnt sich nur, wenn die Divergenz groß und dauerhaft ist.

Unabhängige Fehlerisolierung. Wenn eine langsame Drittanbieter-Integration oder ein außer Kontrolle geratener Hintergrundjob Ihre gesamte Anwendung lahmlegen kann, kann das Herausziehen dieser riskanten Arbeit in einen eigenen Dienst den Explosionsradius eingrenzen, besonders wenn Teile des Systems sehr unterschiedliche Zuverlässigkeitsanforderungen haben.

Beachten Sie, was nicht auf dieser Liste steht: »die Codebasis fühlt sich groß an«, »wir wollen für ein Feature eine andere Sprache« oder »ein Konferenzvortrag sagte, Monolithen skalieren nicht«. Das sind keine Gründe. Das sind Stimmungen.

Die Kosten, die niemand in die Präsentation schreibt

Jeder dieser Vorteile wird mit echten, wiederkehrenden Kosten erkauft. Bevor Sie aufteilen, kalkulieren Sie diese ein.

Das Netzwerk liegt nun in Ihrem kritischen Pfad. In einem Monolithen dauert ein Aufruf zwischen Modulen Nanosekunden und kann nicht von sich aus fehlschlagen. Zwischen Diensten reist derselbe Aufruf über ein Netzwerk, das langsam, unzuverlässig und gelegentlich ausgefallen ist. Sie müssen nun Timeouts, Wiederholungsversuche, Teilausfälle und kaskadierende Ausfälle behandeln. Eine Anfrage, die sechs Dienste berührt, sammelt sechs Gelegenheiten zu scheitern.

Datenkonsistenz wird schwierig. Eine einzige Datenbank schenkt Ihnen Transaktionen gratis. Sobald jeder Dienst seinen eigenen Datenspeicher besitzt, verlieren Sie die Fähigkeit, mehrere Dinge atomar zu aktualisieren, und geraten in die Welt der eventuellen Konsistenz, der Sagas und der kompensierenden Aktionen. Eine Bestellung, einen Lagerbestand und eine Zahlung über drei Dienste hinweg ohne gemeinsame Transaktion in Einklang zu bringen, ist eines der wirklich schwierigen Probleme in verteilten Systemen, und man macht dabei leicht einen subtilen Fehler.

Beobachtbarkeit wird zu einem Projekt, nicht zu einer Logdatei. In einem Monolithen erzählt ein Stack-Trace die ganze Geschichte. In einem verteilten System hinterlässt eine einzige Benutzeraktion Spuren über viele Dienste und Maschinen hinweg. Ohne verteiltes Tracing, zentralisiertes Logging und Korrelations-IDs wird das Debuggen zur Archäologie. Sie müssen diese Schicht aufbauen, bevor Sie sie brauchen, nicht nach dem ersten Vorfall um 2 Uhr nachts.

Der betriebliche Aufwand vervielfacht sich. Jeder Dienst braucht seine eigene Pipeline, sein eigenes Deployment, Monitoring, Alerting, Secrets und Bereitschaftsdienst. Lokale Entwicklung bedeutet nun, eine Flotte von Diensten zu betreiben oder sie zu simulieren, und die Cloud-Rechnung steigt. Wenn Sie in diese Richtung gehen, planen Sie Ihr Budget bewusst: Unser Leitfaden zur Optimierung von Cloud-Kosten erklärt, wie Sie verhindern, dass ein Fußabdruck aus vielen Diensten leise Ihr Guthaben aufzehrt.

Verträge zwischen Diensten verkalken. Innerhalb eines Monolithen können Sie eine gemeinsame Schnittstelle ändern und den Compiler jeden Aufrufer finden lassen. Zwischen Diensten, die verschiedenen Teams gehören, wird dieselbe Änderung zu einer versionierten API-Verhandlung mit zu wahrender Abwärtskompatibilität. Diese Schnittstellen gut zu entwerfen, ist enorm wichtig, deshalb lohnt es sich, unseren Leitfaden dazu zu lesen, wie man APIs entwirft, die Entwickler lieben, bevor Sie auch nur eine einzige Grenze festschreiben.

Migrieren Sie mit der Würgefeige, nicht mit einem Neuschreiben

Wenn Sie ehrliche Signale haben und die Kosten einkalkuliert haben, halten Sie nicht die Welt für ein Neuschreiben an. Das Neuschreiben eines funktionierenden Systems ist die Art, wie Unternehmen ein Jahr verlieren und nichts ausliefern. Verwenden Sie stattdessen das Würgefeigen-Muster.

Der Name stammt von einer Kletterpflanze, die um einen Wirtsbaum wächst und nach und nach seine Struktur übernimmt, bis das Original entfernt werden kann. Auf Software angewandt bedeutet es, dass Sie die neue Architektur um die alte herum wachsen lassen, Stück für Stück, während das System weiterläuft.

In der Praxis platzieren Sie eine Routing-Schicht, in der Regel ein API-Gateway oder eine Fassade, vor dem Monolithen, sodass Aufrufer nicht wissen und es sie nicht kümmert, was dahintersteckt. Sie wählen eine gut abgegrenzte Fähigkeit, extrahieren sie in einen neuen Dienst und leiten ihren Verkehr leise über das Gateway um. Der Monolith schrumpft um eine Verantwortlichkeit. Sie verifizieren, Sie stabilisieren, und erst dann wählen Sie das nächste Stück. Über viele kleine, umkehrbare Schritte wird der Monolith bis auf nichts stranguliert, oder bis auf den Kern, den zu behalten sinnvoll ist.

Die Stärke dieses Ansatzes ist, dass jeder Schritt in Produktion geht und für sich allein zurückgerollt werden kann. Sie sind nie nur einen großen Knall von der Katastrophe entfernt, und Sie können an jedem Punkt aufhören, wenn sich der verbleibende Monolith als völlig in Ordnung erweist.

Ziehen Sie Grenzen um Domänen, nicht um Schichten

Die wichtigste Entscheidung ist, wo die Nähte verlaufen, und der häufigste Fehler ist, entlang technischer Schichten zu schneiden: ein »Datenbankdienst«, ein »Geschäftslogikdienst«, ein »UI-Dienst«. Das erzeugt Dienste, die allein nichts Nützliches tun können und ständig plappern müssen, um irgendeine echte Aufgabe zu erledigen. Sie bekommen die gesamten Kosten der Verteilung und nichts von der Unabhängigkeit.

Schneiden Sie stattdessen entlang der Geschäftsdomänen. Ein Dienst sollte eine vollständige Fähigkeit von Anfang bis Ende besitzen, einschließlich seiner eigenen Daten. »Bestellung«, »Abrechnung« und »Inventar« sind gute Grenzen, weil jede etwas darstellt, das das Unternehmen tatsächlich tut. Ein gut gezogener Dienst kann sein Inneres, sein Schema und seine Logik ändern, ohne um Erlaubnis zu bitten, weil nichts außerhalb von ihm davon abhängt.

Eine gute Grenze ist eine, an der die Interaktionen grobkörnig statt geschwätzig sind (ein paar bedeutungsvolle Aufrufe, nicht Hunderte winziger), an der sich die Daten natürlich auf einer Seite der Linie bündeln und an der ein einzelnes Team das Ganze besitzen kann. Wenn zwei »Dienste« ständig im selben Atemzug die Daten des anderen aktualisieren, sind sie in Wahrheit ein einziger Dienst, der an der falschen Stelle aufgeteilt wurde.

Bereitschafts-Checkliste

Bevor Sie Ihren ersten Dienst extrahieren, sollten Sie die meisten dieser Punkte mit Ja beantworten können.

  1. Sie haben ein konkretes Signal (Teamskalierung, unabhängige Deployments oder unterschiedliche Skalierung), nicht nur das Gefühl, dass der Monolith groß ist.
  2. Ihr Monolith ist bereits modular, mit klaren Grenzen, entlang derer Sie schneiden können.
  3. Sie verfügen über ausgereifte Continuous Integration und Delivery, sodass das Ausliefern vieler kleiner Deployments Routine ist und kein Risiko.
  4. Sie haben zentralisiertes Logging, Metriken und verteiltes Tracing bereits im Einsatz oder einen festen Plan, sie zuerst hinzuzufügen.
  5. Sie haben einen Infrastrukturansatz (Container, Orchestrierung, automatisierte Bereitstellung), der das Aufsetzen eines neuen Dienstes günstig macht.
  6. Sie haben einen Plan für dienstübergreifende Datenkonsistenz und wissen, welche Arbeitsabläufe eventuelle Konsistenz benötigen werden.
  7. Die Teams sind so strukturiert, dass sie Dienste von Anfang bis Ende besitzen, einschließlich ihrer Daten und ihres Bereitschaftsdienstes.
  8. Sie haben die laufenden Betriebs- und Cloud-Kosten kalkuliert, und das Unternehmen akzeptiert sie.

Wenn die meisten davon ein Nein sind, liegt die wirkungsvollste Arbeit nicht in der Aufteilung. Sie liegt darin, zuerst den modularen Monolithen aufzuräumen und die betrieblichen Muskeln aufzubauen.

Zu vermeidende Anti-Muster

Der verteilte Monolith. Dienste, die alle zusammen im Gleichschritt bereitgestellt werden müssen, sich eine Datenbank teilen und als eine Einheit ausfallen. Sie haben den vollen Preis der Verteilung bezahlt und jeden Nachteil des Monolithen behalten. Das ist das Schlechteste aus beiden Welten und mit Abstand das häufigste Scheitern.

Aufteilen, um die Codequalität zu verbessern. Schlechter Code wird nicht besser, indem man ihn über ein Netzwerk verteilt. Verhedderte Logik wird zu verhedderter Logik mit aufgesetzter Latenz und Wiederholungsversuchen. Räumen Sie zuerst den Code innerhalb des Monolithen auf.

Nanodienste. Grenzen, die so fein sind, dass eine einzige Benutzeraktion sich über ein Dutzend Dienste auffächert. Der Koordinationsaufwand überschwemmt jeden Nutzen, und niemand kann das System im Kopf behalten.

Eine geteilte Datenbank hinter vielen Diensten. Wenn mehrere Dienste dieselben Tabellen lesen und schreiben, sind sie eng aneinander gekoppelt. Eine einzige Schemaänderung bricht bei allen, und Sie haben Unabhängigkeit nur dem Namen nach.

Neuschreiben statt Strangulieren. Die Feature-Arbeit einfrieren, um alles von Grund auf neu zu bauen. Das alte System entwickelt sich weiter, während Sie neu bauen, das Ziel bewegt sich, und das Projekt läuft aus dem Ruder. Lassen Sie stattdessen das Neue um das Alte herum wachsen.

Das pragmatische Fazit

Microservices sind ein Werkzeug für eine bestimmte Reihe von Problemen, meist organisatorische Skalierung und unabhängiges Deployment, und sie lösen diese Probleme, indem sie Einfachheit gegen Autonomie tauschen. Dieser Tausch lohnt sich, wenn Sie die Menschen, den Druck und die betriebliche Reife haben, um die Kosten zu tragen. Es ist ein schlechter Tausch, wenn Sie einem Trend hinterherjagen oder unordentlichen Code übertünchen. Beginnen Sie modular, bleiben Sie ehrlich in Bezug auf Ihre Signale, migrieren Sie mit der Würgefeige, und ziehen Sie Ihre Grenzen um Domänen statt um Schichten.

Wenn Sie diese Entscheidung abwägen und eine klarsichtige zweite Meinung möchten, ist das genau die Art von Problem, bei der wir Teams helfen, sich durchzuarbeiten. Entdecken Sie unsere Dienstleistungen für Software- und Cloud-Engineering, oder nehmen Sie Kontakt auf, um Ihre Architektur mit dem Team von Innovation T zu besprechen.

#Microservices#Architektur#Monolith#Skalierbarkeit

Bereit, mit Innovation T zu bauen?

Ob Sicherheit, Wachstum oder Engineering, unser Team hilft Ihnen, es gut umzusetzen.