Monolithic Architecture
Die gesamte Fachlichkeit in einer Anwendung
Die Idee und ihr Nutzen
Ein kleines Team baut für eine neue Fahrradversicherung eine Anwendung, die Tarif, Vertrag, Schaden und Zahlung umfasst. Statt diese Fachgebiete auf mehrere eigenständige Programme zu verteilen, baut das Team ein einziges Programm, das es als Ganzes ausliefert und betreibt. So kann das Team eine Änderung, die mehrere Fachgebiete betrifft, in einem Schritt ausliefern, etwa einen neuen Rabatt, der Tarif und Vertrag ändert. Außerdem arbeiten die Teile des Programms ohne Umweg über das Netz zusammen. Ein solches Programm heißt Monolith, die Bauweise Monolithic Architecture.
Wie funktioniert es?
Ein Kunde schließt beim Vertragsteil einen Vertrag für ein Zweitfahrrad ab. Vertragsteil, Tarifberechnung, Schadenteil und Zahlungsteil gehören zu einem Programm, das gemeinsam ausgeliefert wird. Ausliefern heißt, eine neue Version des Programms in Betrieb zu nehmen. Alle vier Teile speichern ihre Daten in der gemeinsamen Datenbank. Kurz zuvor hat das kleine Team einen neuen Rabatt für Zweitfahrräder eingeführt: Es hat Tarifberechnung und Vertragsteil geändert und beides gemeinsam ausgeliefert, ohne sich mit einem anderen Team abzustimmen. Der Vertragsteil lässt die Tarifberechnung den Beitrag mit diesem Rabatt berechnen. Dieser Aufruf ist ein gewöhnlicher Programmaufruf innerhalb desselben laufenden Programms und braucht kein Netz. Die Tarifberechnung gibt den Beitrag mit Rabatt zurück. Der Vertragsteil speichert Vertrag und Beitragsplan in der gemeinsamen Datenbank in einer einzigen Datenbanktransaktion, also so, dass die Datenbank entweder beides oder nichts speichert. Danach gibt der Vertragsteil den Beitrag an den Zahlungsteil weiter, der ihn einzieht. Meldet der Kunde später einen Schaden, fragt der Schadenteil den Vertragsteil, ob der Vertrag gültig ist, wieder mit einem Aufruf im selben Programm. Drei Jahre später hat das Produkt Erfolg, und vier Teams arbeiten an der Anwendung. Jede Auslieferung muss nun mit allen vier Teams abgestimmt werden, und das Bauen und Testen des ganzen Programms dauert eine Stunde, weil bei jeder Änderung die Tests aller vier Fachgebiete laufen. Die Teams teilen das Programm deshalb zuerst in getrennte Teile je Fachgebiet, die einander nur über festgelegte Aufrufe nutzen, und prüfen erst danach, ob sich ein einzelnes Fachgebiet als eigener Dienst lohnt, also als eigenständiges Programm, das die anderen Teile über das Netz aufrufen.
Typisch für einen Monolithen sind diese Eigenschaften, von denen nicht jede zwingend ist:
- Das Programm wird als zusammengehörige Einheit gebaut und ausgeliefert. Innen kann es trotzdem aus getrennten Teilen wie Vertragsteil und Schadenteil bestehen.
- Meist nutzen alle Fachgebiete eine gemeinsame Datenbank. Getrennte Datenbanken sind möglich.
- Die Teile rufen einander innerhalb desselben laufenden Programms auf. Solche Aufrufe gehen nicht über das Netz. Sie dauern deshalb nur einen Bruchteil eines Netzaufrufs und können nicht an einer Netzstörung scheitern.
Braucht das Programm mehr Rechenleistung, startet das Team mehrere Exemplare davon. Jede Anfrage geht dann zuerst an einen Lastverteiler, ein Programm, das die Anfrage an eines der Exemplare weitergibt. Jedes Exemplar enthält dabei das ganze Programm mit allen Fachgebieten.
Scheitert das Speichern von Vertrag und Beitragsplan, entsteht dank der gemeinsamen Datenbanktransaktion kein Vertrag ohne Beitragsplan. Der Einzug des Beitrags bei der Bank liegt dagegen außerhalb der Datenbanktransaktion. Scheitert der Einzug, bleibt der gespeicherte Vertrag bestehen, und der Zahlungsteil muss den gescheiterten Einzug selbst erkennen und wiederholen. Stürzt das Programm ab, fallen alle Fachgebiete zugleich aus. Verteilt ein Lastverteiler die Anfragen auf mehrere Exemplare, gibt er die Anfragen dann an die übrigen Exemplare weiter.
Monolith: eine Anwendung, ein Aufruf, eine Transaktion
Wann ist es sinnvoll und wann nicht?
Ein Monolith lohnt sich
- Ein neues Produkt entsteht, und vieles ist noch unklar: welche Funktionen die Kunden brauchen, wo die Grenzen zwischen den Fachgebieten liegen, wie viele Nutzer kommen. Verteilt ein Team die Anwendung zu früh auf getrennte Dienste, zieht es die Grenzen oft falsch. Gehört die Prüfung der Vertragsgültigkeit zum Beispiel doch in den Vertragsteil statt in den Schadenteil, müssen die Teams zwei Dienste und die Schnittstelle zwischen ihnen ändern und beide Dienste gemeinsam ausliefern.
- Ein kleines oder mittleres Team arbeitet an der Anwendung, und die Zahl der Nutzer ist anfangs klein. Bei getrennten Diensten rufen sich die Teile über das Netz auf, die Daten liegen in mehreren Datenbanken, und jedes Programm muss einzeln ausgeliefert und überwacht werden. Dieser Aufwand lohnt sich erst, wenn mehrere Teams an der Anwendung arbeiten; ein kleines Team verbringt sonst viel Zeit mit dem Betrieb statt mit den Funktionen, die Kunden sehen. Mit dem Monolithen betreibt und überwacht das kleine Team ein einziges Programm mit einer Datenbank.
- Änderungen betreffen oft mehrere Fachgebiete zugleich und sollen in einer gemeinsamen Datenbanktransaktion gespeichert werden. Vertrag und Beitragsplan speichert der Vertragsteil zum Beispiel nur zusammen. Lägen Vertrag und Beitragsplan in den Datenbanken zweier getrennter Dienste, könnte ein Vertrag ohne Beitragsplan entstehen, wenn eines der beiden Speichern scheitert.
- Nur ein Programm zu betreiben ist wichtiger als die unabhängige Auslieferung einzelner Teile. Das Team will vor allem neue Funktionen ausprobieren und jede Änderung in einem Schritt ausliefern. Mit getrennten Diensten müsste das Team für den Rabatt für Zweitfahrräder zwei Dienste ändern und ihre Auslieferung aufeinander abstimmen.
Ein Monolith ist nicht das Richtige
- Viele Teams wollen gleichzeitig unabhängig voneinander ausliefern. Im Monolithen wartet dann zum Beispiel das Team der Tarifberechnung mit seiner fertigen Änderung, bis auch die Änderungen der Teams für Vertrag, Schaden und Zahlung fertig und getestet sind. Dann teilen die Teams die Anwendung in eigenständige Dienste je Fachgebiet mit eigener Datenbank, und jedes Team liefert seinen Dienst aus, ohne sich mit den anderen Teams abzustimmen. Diese Bauweise heißt Microservices. Dafür kommen Aufrufe über das Netz und der Betrieb vieler Programme hinzu.
- Einzelne Teile werden ganz unterschiedlich stark belastet oder müssen mit anderer Technik betrieben werden. Im Monolithen enthält jedes Exemplar alle Fachgebiete, und alle Teile verwenden dieselbe Technik. Dann löst das Team diesen Teil als eigenen Dienst heraus, den die übrigen Teile über das Netz aufrufen. Vom stark gefragten Dienst, etwa der Tarifberechnung, startet das Team weitere Exemplare, statt das ganze Programm mehrfach zu starten. Ein Dienst mit anderer Technik läuft unabhängig vom übrigen Programm.
- Kein Fachgebiet soll direkt auf die Daten oder den internen Code eines anderen zugreifen, obwohl alle Fachgebiete gemeinsam ausgeliefert und betrieben werden. Ein Monolith ohne Module verhindert nicht, dass etwa der Zahlungsteil direkt Vertragsdaten liest. Dann teilt das Team das Programm in Module mit strengen Grenzen zwischen den Fachgebieten, einen Modular Monolith. Ein automatischer Test weist dann jeden Zugriff auf die internen Teile eines fremden Moduls ab, bevor der Code ausgeliefert wird.
Nachteile und typische Fehler
Jede noch so kleine Änderung erfordert eine Auslieferung des ganzen Programms. Arbeiten später mehrere Teams daran, muss jede Auslieferung mit allen Teams abgestimmt werden, und lange Bau- und Testzeiten halten dann alle Teams auf. Das ganze Programm ist an eine Programmiersprache und eine Technik gebunden; ein Wechsel betrifft alles auf einmal. Mehr Rechenleistung bekommt der Monolith nur, indem das ganze Programm in mehreren Exemplaren läuft, auch wenn nur die Tarifberechnung stark gefragt ist. Ein Monolith verhindert auch nicht von selbst, dass ein Teil direkt auf die Daten eines anderen zugreift, etwa der Zahlungsteil auf die Vertragsdaten. Weil jeder Teil jeden anderen direkt aufrufen kann, müssen die Entwickler die Grenzen zwischen den Fachgebieten selbst festlegen und prüfen.
Typische Fehler und woran man sie erkennt:
- Fehlende Grenzen zwischen den Fachgebieten: Die Teile rufen einander kreuz und quer auf, und jede Änderung kann überall Folgen haben. Der Zahlungsteil liest zum Beispiel direkt Felder aus den Vertragsdaten, die nur der Vertragsteil verwalten sollte. Benennt ein Entwickler im Vertragsteil ein solches Feld um, zieht der Zahlungsteil keine Beiträge mehr ein. Erkennbar ist das daran, dass eine kleine Änderung am Vertragsteil Fehler im Schadenteil und im Zahlungsteil auslöst.
- Ein Fehler legt alles lahm: Ein Fehler in einem Teil legt das ganze Programm lahm. Der Schadenteil lädt zum Beispiel bei jeder Schadenmeldung alle Fotos in den Arbeitsspeicher und gibt sie nie wieder frei. Alle Teile laufen im selben Programm und nutzen denselben Arbeitsspeicher. Ist der Arbeitsspeicher voll, stürzt das ganze Programm ab. Erkennbar ist das daran, dass alle Fachgebiete gleichzeitig ausfallen, obwohl nur eines den Fehler enthält.
- Auslieferungsstau: Bauen und Testen dauern so lange, dass die Teams Änderungen sammeln und selten ausliefern. Bei einer Stunde für Bauen und Testen und vier Teams, die jede Auslieferung abstimmen müssen, liefern die Teams zum Beispiel nur noch einmal im Monat aus. Jede Auslieferung enthält dann viele Änderungen auf einmal, und ein Fehler lässt sich schwer einer davon zuordnen. Erkennbar ist das an wachsenden Abständen zwischen Auslieferungen.
- Zustand im Arbeitsspeicher: Die Anwendung merkt sich angemeldete Kunden nur im Arbeitsspeicher eines Exemplars. Läuft sie in mehreren Exemplaren, auf die ein Lastverteiler die Anfragen verteilt, landet die nächste Anfrage eines Kunden womöglich bei einem anderen Exemplar, das den Kunden nicht kennt, und der Kunde wird abgemeldet. Erkennbar ist das an Kunden, die nach dem Start weiterer Exemplare plötzlich abgemeldet werden.
Weiterführend
Verwandte Ansätze
- Layered Architecture: eine übliche Aufteilung eines Monolithen in übereinanderliegende technische Schichten wie Oberfläche, fachliche Regeln und Datenzugriff.
- Strangler Fig: beschreibt, wie man einen Monolithen Stück für Stück durch neue Teile ersetzt.
Werkzeuge und Umsetzungen
Ein Monolith ist eine Bauform, kein Werkzeug. Viele verbreitete Programmrahmen, also fertige Programmbibliotheken, auf denen eine Anwendung aufbaut, sind für diese Bauform gemacht:
- Ruby on Rails: Programmrahmen, der bewusst auf eine zusammenhängende Anwendung setzt.
- Django: Programmrahmen für Python, mit dem sich vollständige Webanwendungen als ein einziges Programm bauen lassen.
- Spring Boot: Java-Grundlage, die für Monolithen ebenso genutzt wird wie für einzelne Dienste.
- Laravel: Programmrahmen für PHP für vollständige Webanwendungen.
Quellen
- Martin Fowler: Monolith First: begründet, warum neue Produkte oft besser als Monolith beginnen, weil sich die richtigen Grenzen zwischen Fachgebieten erst im Betrieb zeigen.
- Chris Richardson: Pattern: Monolithic Architecture: beschreibt die Anwendung als eine auslieferbare Einheit mit einer Datenbank, ihre Vorteile wie gemeinsame Transaktionen und ihre Nachteile mit wachsender Zahl von Teams.