Vom Monolithen zu Microservices
Haben Sie Erfahrung beim Zerschlagen oder besser beim langsamen Rückbau und Entkernen von Monolithen? Gerne möchte ich Ihnen einen Weg beschreiben, den wir über 5 Jahre gegangen sind. In diesen 5 Jahren haben wir einen vorhandenen Monolithen peu à peu zurückgebaut, und das Ziel verfolgt, die vorhandene Architektur langsam zu einer Microservice-Technologie zu entwickeln. Während dieser Zeit haben wir nicht nur technische Schwierigkeiten überwunden, sondern mussten auch viele unserer Vorgehensweisen den neuen Herausforderungen anpassen. Ich berichte hier hauptsächlich von technischen Umbauten – aber natürlich lag die Priorität in der Weiterentwicklung der Geschäftslogik. Technik war und ist nur Mittel zum Zweck.
Ausgangspunkt: Der Business-Prozess
Als erstes möchte ich ein wenig den Business-Prozess vorstellen, den wir mit unserer Software unterstützten. Die im Folgenden beschriebenen Maßnahmen würden in einem weniger komplexen System anders aussehen. Daher ist es immer wichtig, den zu Grunde liegenden Business-Prozess im Blick zu haben. Bei dem betrachteten System handelt es sich um ein System, das es Firmen erlaubt, Ausschreibungen zu erstellen und Lieferanten erlaubt, an diesen Ausschreibungen teilzunehmen.
Neben den Hauptgeschäftsprozessen Lieferantenauswahl, Ausschreibung, Bemusterung, Lieferung, Rechnung und Beschwerdeprozess gab es die unterstützenden Prozesse Zugriffskontrolle, Zusammenarbeit und Dokumenten-Management.
Dieser Business-Prozess wurde durch mehrere Applikationen unterstützt. Die Applikationen waren historisch gewachsen und bildeten den Geschäftsprozess eher wie ein Flickenteppich als eine Folge von technischen Prozessen ab.
Wie Sie in der zweiten Abbildung sehen können, decken die einzelnen Applikationen die Geschäftsprozesse nur unzureichend ab – teilweise überlappend, teilweise mit Lücken. Dies war mir am Anfang nicht klar. Dieses klare Bild ergab sich erst über die Jahre. Jetzt weiß ich, dass diese nicht dem Business-Prozess entsprechender Serviceschnitt ein Grund für die Probleme war, die wir hatten. Aber ich greife vor.
Systeminstabilitäten
Die genannten Anwendungen waren auf zwei große Server verteilt.
Die Applikationen Benutzer/Login, Stammdaten und Lieferantenmanagement waren in einer Deploymenteinheit innerhalb einer Java Virtual Machine (JVM) auf dem ersten Server-Paar installiert. Auf dem zweiten Cluster befanden sich die wesentlichen Geschäftsanwendungen Prozesse, Gelbe Seiten, Ausschreibung und Beschwerden. Aber auch die unterstützenden Prozesse Dokumente und Nachrichten waren hier in der gleichen JVM installiert. Ein allererster winziger Schritt war zu diesem Zeitpunkt aber schon getan – die Nachrichten hatten schon ihre eigene Datenbank.
Die Applikationen Lieferung, Rechnung und Projekte waren zu diesem Zeitpunkt schon auf eigenen Clustern mit eigenen Datenbanken installiert. Über diese wird im Weiteren nicht berichtet.
Wie weit die einzelnen Applikationen voneinander abhängig waren, zeigt auch die folgende Darstellung:
Hier sehen Sie, dass insbesondere die Prozesse Lieferantenauswahl und Ausschreibung sich nicht nur über mehrere Applikationen, sondern auch über beide Cluster erstreckten. Diese Nichteinhaltung des architektonischen Prinzips der “Separation of Concerns” führte unter anderem zu nicht mehr tragbaren Instabilitäten – das heißt zu Ausfällen. Von einer geplanten Verfügbarkeit von 95% konnten wir nur knapp 90% erreichen. Die entsprechenden Diskussionen in Betrieb und Entwicklung kann sich jeder – so glaube ich – lebhaft vorstellen.
Erste Hilfsmaßnahmen
Nach diesen Erfahrungen wussten wir schon, dass wir was Prinzipielles ändern mussten. Wir wussten aber zu diesem Zeitpunkt weder genau was noch wie! Für uns hieß das, wir mussten den Patienten erst einmal soweit stabilisieren, dass wir genügend Zeit hatten, um etwas Wirkliches zu ändern. Da die Hauptlast auf dem Cluster der Geschäftslogik lag, haben wir die Anzahl der Server dort verdoppelt.
Allerdings waren die Applikationen nicht auf ein solches horizontales Skalieren vorbereitet. Wir mussten an vielen Stellen kleinere Änderungen vornehmen, um überhaupt diese kleine Stabilisierungsmaßnahme zu ermöglichen (natürlich nicht automatisch, sondern einfach manuell von 2 auf 4). Allerdings war bei 4 auch Schluss, da ansonsten die interne Synchronisation der einzelnen Knoten zu viel Performanz gekostet hätte. Es war eine Erste-Hilfe-Maßnahmen mit der wir zu mindestens die folgenden Monate überleben konnten.
Allererste kleine Schritte
Natürlich bleibt die Welt – nur weil wir technische Probleme lösen mussten – nicht stehen. Es wurden neue Funktionen und Applikationen gebraucht. Die Applikation-Prozesse musste durch eine Neuimplementierung ersetzt werden und wir mussten softwaretechnisch den Prozess Audit unterstützen. Wir wussten, dass wir diese Anwendungen nicht einfach in die vorhandenen Anwendungen hinein implementieren konnten. Dies hätte das Chaos komplett gemacht. Weitere Zugriffe über neue Anwendungen hätten die JVMs in den jeweiligen Clustern gefährdet (und ein einfacher Restart kostete schon mindestens eine halbe Stunde). Zu diesem Zeitpunkt war ich schon davon überzeugt, dass nur ein konsequenter Microservice-Ansatz hier helfen konnte. Allerdings war es schwierig einen so radikalen Ansatz beim Management durchzusetzen. Aber die Anwendungen wurden wenigstens als eigene Deployment-Units gebaut, auch wenn sie (noch) nicht ihre eigene Datenbank verwendeten.
Weitere Systeminstabilitäten
Erfreulicherweise hatten wir Erfolg mit unseren neuen Anwendungen. Aber für das Gesamtsystem verhieß es nichts Gutes. Für die neuen Anwendungen wurden mehr und aktuellere Stammdaten benötigt. Diese Stammdaten wurden in der Stammdaten-Applikation auf dem Cluster „Login und Stammdaten“ erfasst und anschließend via Oracle Streams zu den Stammdaten-verwendenden Applikationen auf den Clustern Geschäftslogik, Audit und Prozesse verteilt. Es wurden in kurzer Zeit mehr Stammdaten erfasst und verteilt als zuvor. Die Streams waren nicht mehr in der Lage diese zu verteilen und beide Datenbanken waren down. Auch spezifische Anforderungen an die Geschwindigkeit der Synchronisation konnten wir mit diesem Ansatz nicht erfüllen. Wiederum griffen wir auf Ansätze der Microservice-Welt zurück.
Wir implementierten einen asynchronen Stammdaten-Verteiler:
Ein Benutzer oder ein externer Service kann Stammdaten – z.B. neue Lieferantenfirmen – im Stammdatenmanagement anlegen. Services, die diesen Service gebucht haben (subscribed), werden über die entsprechende Änderung mit einer eindeutigen Identifikation (ID) informiert. Der entsprechende Service kann nun – wenn es passt 😊 – die Daten mit der ID beim Stammdatenservice abholen und bei sich lokal speichern.
Durch die Entkopplung von Geschäftslogik und Stammdatenservice konnten wir das Gesamtsystem wesentlich stabilisieren. Weiterhin konnte die Performanz wesentlich erhöht werden, da kritische Änderungen wie bspw. das Löschen eines Lieferanten priorisiert verarbeitet werden konnten. Die Gesamtperformanz des Systems wurde auch dadurch erhöht, dass die abhängigen Services nicht mehr gezwungen waren, die Stammdaten in der Logik des Stammdatenservice zu speichern. Vielmehr konnten sie die Daten so speichern, wie sie sie benötigten, was unnötiges Mappen, Umformatieren etc. zur Laufzeit vermied.
Stammdatenservice und Geschäftslogik waren nun entkoppelt. Aber die Komplexität hatte sich nun in die Kommunikation verlagert.
Online Deployment
Aber wie das Leben so spielt, es gibt immer (wieder) etwas zu tun. Wir hatten eine Stabilität erreicht, die es unseren Kunden schmerzlich bewusst machte, dass unseren Downtime-Zeiten viel zu lang waren. Solange Du gegen ungeplante Ausfallzeiten kämpfst, interessieren Dich die geplanten nur wenig. Aber die ungeplanten Ausfallzeiten waren jetzt nicht mehr da – also waren wir durch die Erwartungshaltung unserer Kunden gezwungen, auch eine Lösung für die geplanten Ausfallzeiten zu finden. Diese Lösung hieß für mich zu diesem Zeitpunkt ein Online Deployment im Blue-Green-Ansatz mit einer Datenbank.
- Die Datenbank wird auf die neue Version gehoben. Dies muss online geschehen und die alte Version muss mit der neuen Version auch arbeiten können.
- Beide Zweige werden nach außen freigegeben. Das heißt, sowohl Benutzer mit der ursprünglichen Version 1.0 als auch Benutzer mit der neuen Version 1.1 greifen auf das System zu. Neue Benutzer werden immer auf die neue Version geroutet. Benutzer, die im System sind, bleiben auf der alten Version bis sie sich abmelden.
- Der blaue Zweig wird auf die neue Version gehoben und das System steht für das nächste Upload bereit.
Ein solcher Ansatz erlaubt es, dass Benutzer immer auf das System zugreifen können. Benutzer verlieren keine Eingaben, weil sie erst nach einer Neuanmeldung auf das neue System zugreifen. Bei Fehlern in der neuen Version kann schnell und einfach auf die Vorgängerversion zurückgeschaltet werden.
Voraussetzung für einen solchen Ansatz sind einfache und unkomplexe Deployments. Das heißt, Änderungen müssen klein sein – große Änderungen führen in der Regel zu großen, komplexen Deployments. Daher muss man geradezu bei Online-Deployments öfter releasen, damit die Deployments nicht zu komplex werden.
Die höheren Infrastrukturkosten, die sich aus der Verdopplung der Serverknoten ergeben, werden in der Regel durch die Einsparung an Wochenendarbeiten und Sondereinsätzen bei ungeplanten Downtimes eingespart.
Entkopplung der Geschäftslogik
Endlich waren die massiven Instabilitäten und die ewig langen Produktivstellungen am Wochenende Geschichte. Aber nun mussten wir aktiv werden. Höhere Lasten auf dem System waren schon abzusehen und damit kamen die Instabilitäten garantiert wieder. Daher blieb nur der Weg, die Microservice-Ideen weiter zu entwickeln und die Geschäftslogik weiter zu entkoppeln.
Wir wussten, dass die höchste Last auf der Ausschreibungskomponente lag. Daher lag es nah, die Ausschreibungskomponente aus der Deployment-Einheit der Geschäftslogik zu schälen. Dem stand eine sehr enge Kopplung der Ausschreibungskomponente und der Dokumenten-Komponente entgegen. Daher haben wir beide zusammen herausgelöst. Dies war aber nur sinnvoll, wenn wir auch die Datenbanken trennten. Die sich daraus ergebende Architektur zeigt folgendes Bild:
Natürlich mussten wir diese enge Kopplung auflösen. Dies war keine technische Forderung – wir hätten mit dieser bequemen „Developer-Freundlichen“ Lösung durchaus weiterleben können. Es waren vor allem wirtschaftliche Überlegungen, die uns zu einer Änderung zwangen. Regression-Tests des Gesamtsystems bei jedem Release sind schlichtweg zu teuer. Und diese zentralen Komponenten waren definitiv nicht pflegearm – wer findet schon eine Funktionalität, die vor einem Jahr implementiert wurde und die keiner mehr nutzt.
Unsere Serveranzahl war in der Zwischenzeit von 4 auf 28 gestiegen – auch ich war manchmal überfragt, wo denn was jetzt lief. Dies war manuell einfach nicht mehr zu stemmen. Daher lag eine Automation der Deployment-Aktivitäten nah. Wir bauten eine automatische Deployment-Pipeline mit weiterer Testautomatisierung. Die Deployment-Automatisierung war ein weiterer Treiber, die Kopplung durch die zentralen Komponenten aufzulösen.
Die zentralen Komponenten waren nun einfache Bibliotheken, die beim automatischen Bauen mit strenger Versionskontrolle in das Bauen der Anwendungen einbezogen wurden.
So haben wir automatisches Deployment bis zur Produktion erreicht, wobei die Produktivstellung manuell durch Verantwortliche im Vieraugenprinzip freigegeben werden mussten.
Petit Fours
Wer mich kennt, weiß, dass ich eine kleine Naschkatze bin. Ich mag Petit Fours. Vielleicht ist dies auch für Software-Architekturen richtig; gut schmecken Kuchen und Süßigkeiten nur, wenn sie klein sind.
Zu dieser Zeit hatten wir das ständige Feuerlöschen und Troubleshooting hinter uns. Endlich, endlich konnten wir pro-aktiv die Architektur gestalten. Ich hatte zu diesem Zeitpunkt folgende Überlegungen: Was muss man einfach (und möglichst automatisch) skalieren können, da es am sensibelsten auf Last reagiert? Dies war die Login-Komponente. Ziel war es letztlich die Login-Komponenten automatisch – abhängig von der Last – horizontal skalieren zu können.
Also haben wir die Login-Komponente herausgelöst. Benutzer- und Stammdatenverwaltung verbleiben im Cluster Stammdaten. Auch bekam Login-Komponente eine eigene Datenbank. Sie war damit die erste Komponente, die Namen “Microservice” verdient hatte. Mein erstes – hart erarbeitetes – “Petit Four”.
Alles viel zu teuer
Wenn man sich diese Entwicklung ansieht sind wir von anfänglichen 4 Servern und 3 Datenbanken zu 36 Servern und 6 Datenbanken gekommen. Wer dies sieht, wird die Hände über dem Kopf zusammenschlagen und mich für total verrückt erklären. Um ehrlich zu sein, war mein damaliger Chef kurz davor 😉. Aber wir hatten bewiesen, dass wir stabiler sind. Wir hatten bewiesen, dass wir schneller auf Veränderungen reagieren können. Und wir hatten bewiesen, dass wir Testaufwände sparen können, da wir gezielter die separierten Services testen können.
Natürlich kosten mehr Server mehr Geld. Allerdings konnten die zusätzlichen Server viel kleiner sein. Auch die schon vorhandenen Server konnten verkleinert werden. Allerdings nicht so viel, dass sie die Mehrkosten für die zusätzlichen Kosten auffangen konnten. Die wirklichen Einsparungen wurden durch die Kostentreiber „Wochenendarbeit“ und „Troubleshooting“ erreicht. Denn diese waren 0. Dadurch konnten wir eine sehr schnelle Rentabilität erreichen. Letztlich konnten die anfänglichen Infrastrukturkosten auf 50% gesenkt werden.
Und jetzt?
Wir hatten wirklich große Anwendungen, die den Namen Monolith wirklich verdienten. Selbst kleinere Änderungen waren teuer und risikoreich. Wir hatten in den Jahren viel erreicht. Aber wir sahen, dass der Weg nur bedingt so weiter ging. Eine Neuimplementierung der wesentlichen Anwendungen „Ausschreibung“ und „Gelbe Seiten“ war notwendig.
Diese Neuimplementierung kann nicht mehr mit klassischen Mitteln durchgeführt werden – die Auswirkungen der Sackgassen hatten wir schmerzlich gefühlt. Vielmehr führte der Weg in die Cloud mit einem echten Microservice-Ansatz. Dabei kann man sich auf die Funktionalitäten konzentrieren und die Infrastruktur den Experten überlassen. Eine gute Business-Analyse und damit ein sinnvoller Schnitt der Services sind allerdings Grundvoraussetzung um Geschäftsanwendungen erfolgreich in der Cloud zu entwickeln.
Zusammenfassung
Muss man einen Monolithen immer zerschlagen oder wie im vorliegenden Beispiel langsam auseinander zu nehmen? Definitiv nein. Wie der Bericht zeigt, haben uns gute Gründe dahin gebracht, den Monolithen auseinander zu nehmen. Stabilität, Performanz, Automatisierung, Testbarkeit, Pflegbarkeit sind gute Gründe Microservices einzusetzen. Die Verbesserung dieser nicht-funktionaler Anforderung müssen gegen die wachsende Komplexität der Kommunikation abgewogen werden.
Microservices sind ein empfehlenswerter Weg um nicht-funktionale Anforderungen zu erfüllen. Um die wachsende Komplexität zu beherrschen, muss in einem hohen Maß automatisiert werden. Am Ende muss man zwischen Stabilität, Aufwänden für die Automatisierung und der Beherrschung einer erhöhten Komplexität im Betrieb abwägen.
Hinweise:
Interessieren Sie sich für weitere Tipps aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.
Dr. Annegret Junker hat im t2informatik Blog weitere Beiträge veröffentlicht:
Dr. Annegret Junker
Dr. Annegret Junker arbeitet als Lead Architect bei Allianz Deutschland. Seit über 30 Jahren ist sie in der Software-Industrie in verschiedensten Rollen und unterschiedlichen Domänen wie Automotive, Versicherungen und Finanzdienstleistungen tätig. Besonders interessiert sie sich für DDD, Microservices und alles, was damit zusammenhängt. Derzeit arbeitet sie in einem großen Versicherungs-Projekt als übergreifende Architektin.