Geteiltes Leid ist doppelte Freud – Erfahrungen zum Zerkleinern eines Software-Systems

Print Friendly, PDF & Email

Der Hype und die Berichterstattung um Microservices scheinen langsam abzuflauen. Umso mehr ist es nun an der Zeit, möglichst objektiv darüber zu berichten, welchen Nutzen und welche Nachteile der Trend zu kleineren Systemen gebracht hat.

Microservices werden nicht nur dafür beworben, mit ihnen neue Systeme zu entwickeln, sondern auch dafür, bestehende System in kleinere Teile zu zerlegen. Letzteres ist der schwierigere Fall, gilt es doch, lange gewachsene Strukturen zu entflechten.

Oft wird empfohlen, klein anzufangen und einzelne, gut abgrenzbare Teile aus einem System herauszuschneiden und als Microservice neu zu entwickeln. Jedoch hilft dieser Ansatz oft nur wenig: Neben dem alten, kaum kleiner gewordenen Monolithen entsteht eine zweite Infrastruktur kleiner Dienste. Nicht selten erhöht sich dadurch die Gesamtkomplexität, denn man erhält nun die Nachteile beider Welten: des Monolithen sowie der Microservices.

Was man anfängt, muss man auch zu Ende bringen. Daher ist es wichtig, von Anfang an den vollständigen Weg bis zum Ziel – einem Verbund kleinerer Systeme – zu planen, zu budgetieren und durchzuhalten!

Das Ziel von Microservices

Der Begriff „Microservice“ verstellt die Sicht auf die eigentliche Intention: Es geht nicht darum, möglichst viele kleine Services zu erstellen. Im schlechtesten Fall entsteht nur ein Distributed Big Ball of Mud. Vielmehr geht es darum, ein großes, schlecht beherrschbares System in kleinere, besser beherrschbare Systeme zu zerlegen.

Wie „micro“ diese kleineren Systeme dann sein müssen, ist Ansichtssache. Das immer weitere Zerlegen eines Systems in kleinste Bestandteile darf jedenfalls kein Selbstzweck sein, sondern muss konkreten Nutzen mit sich bringen und durch eine geeignete (Entwicklungs-, Test- und Deployment-) Infrastruktur unterstützt werden.

Das größte Problem bei Microservices ist auch kein technisches. Technische Probleme gibt es viele, aber die lassen sich lösen und beherrschen. Der bestmögliche fachliche Schnitt zwischen den Services ist schwieriger: Jede Fehlentscheidung, welcher Service die Hoheit über welche fachliche Funktionalität hat, kann die Komplexität einzelner Anwendungsfälle massiv erhöhen – und das dauerhaft, sofern nicht Services restrukturiert werden. Und das ist noch einmal deutlich anspruchsvoller, als Code in einem Monolithen zu restrukturieren.

Klein beginnen, aber anders

Man kann auch anders anfangen, als aus dem großen Monolithen kleine Services abzuspalten. Stattdessen kann man versuchen, einen Schnitt zu finden, um den großen Monolithen in zwei kleinere Monolithen zu spalten. Nach vollzogener Trennung kann man beide kleinere Monolithen aufräumen und danach erneut entscheiden, ob weitere Spaltungen zusätzlichen Nutzen bringen.

Auch hier ist das schwierigste Problem, den passenden Schnitt zu finden. Aber dieses Problem stellt sich ganz zu Beginn und nicht erst dann, wenn einfache, kleinste Services abgetrennt sind und anschließend unklar ist, wie es weitergeht.

Realistischerweise fällt es in vielen Systemen schwer, einen solchen Schnitt zu finden. Aber es gibt Systeme, die zwei oder (wenige mehr) fachlich isolierbare Anwendungsbereiche haben.

Um solche Bereiche zu finden, hilft ein Blick auf die Datenbank. Ein Grundsatz von Microservices ist, dass jeder Service die Hoheit über seine Daten hat. Zu jedem Dienst gibt es also Daten, die nur von diesem Dienst geschrieben werden. Andere Dienste können (durch Replikation oder Schnittstellenaufrufe) nur lesend darauf zugreifen. Entsprechend hilft es zu betrachten, welche Teile des Systems auf welche Daten schreibend zugreifen.

Erfahrungen

Im Folgenden möchte ich meine Erfahrungen beschreiben, die von einem Projekt stammen, bei dem ein größerer Monolith erfolgreich in zwei kleinere Monolithen aufgespalten wurde.

In mehrjähriger Arbeit eines Teams von 5 bis 10 Entwicklern war ein System entstanden, das zwei Zielgruppen bediente: Auftraggeber konnten Ausschreibungen anlegen und nach Auftragnehmern suchen, und Auftragnehmer könnten nach Ausschreibungen suchen und sich darauf bewerben.

Eine Datenbank enthielt alle Nutzerkonten sowie die Profile der Auftragnehmer und die Ausschreibungen der Auftraggeber. Die Hoheit über die Daten war überwiegend klar geregelt: Meistens hatte nur eine der beiden Zielgruppen schreibenden Zugriff auf bestimmte Daten.

Eine Aufspaltung des bestehenden Systems war keine einfache Angelegenheit. Trotz günstiger fachlicher Voraussetzungen musste ein solcher Schritt also wohlbegründet sein. Denn das Team hatte das System eigentlich gut im Griff und konnte neue Anforderungen in steter Geschwindigkeit umsetzen.

Aus Sicht des Teams gab es manche Gründe für die Aufspaltung:

  • Das System war teilweise technisch veraltet und sollte modernisiert werden.
  • Der Code enthielt übriggebliebene Strukturen alter, aber wieder verworfener Anforderungen, die nur schwer aufzuräumen waren.
  • Gemeinsam verwendete Komponenten waren durch die notwendige Parametrisierung komplex und fehleranfällig.
  • Änderungen für die eine Zielgruppe führten manchmal zu Fehlern in der Funktionalität für die andere Zielgruppe.
  • Das Backlog enthielt die Wünsche für Änderungen in allen Bereichen. In jeden Sprint kam von allem etwas hinein, sodass sich größere Änderungen in einem Bereich oft in die Länge zogen.

Entscheidend aber waren die Zukunftspläne des Unternehmens für den Auftraggeberteil des Systems. Hier waren umfangreiche Erweiterungen im Gespräch, die das bestehende System ohne massive Umbaumaßnahmen nicht leisten konnte (z.B. mehrere Logins für ein Kundenkonto, durchgehende Internationalisierung, u.ä.).

Somit gab das Management das Budget frei und den Startschuss dafür, den Auftraggeberteil des Systems abzuspalten:

  • Das bestehende Team teilte sich auf in zwei Teams.
  • Ein neues Backlog wurde von genau einem Produktverantwortlichem geführt.
  • Das neue System entstand technisch auf der grünen Wiese.
  • Aber an den Anforderungen für die bestehenden fachlichen Prozesse änderte sich nur wenig.

Nach einem Dreivierteljahr war das neue System einsatzbereit. Um das Risiko beim Übergang zu minimieren, gab es anstelle einer großen Umstellung einen schrittweisen Prozess:

  • Erst wurden nur Neukunden auf das neue System gelassen. Für Bestandskunden lief das alte System parallel weiter.
  • Nach etwas Nachjustieren wurden einzelne Bestandskunden persönlich angesprochen und gezielt migriert.
  • Schließlich wurden stapelweise die übrigen Bestandskunden migriert.

Nachdem die Migration abgeschlossen war, konnte der Auftraggeberteil im alten System erst stillgelegt und dann nach und nach entfernt werden.

Die gelernten Lektionen

Datenmigration

Alle Daten, über die das neue System die Hoheit hatte, mussten migriert werden. Die neue Datenstruktur war der alten ähnlich, jedoch wurden einige Ungereimtheiten korrigiert. Im Zuge der Migration stellte sich heraus, dass der alte Datenbestand einige Inkonsistenzen behielt, z.B. durch schon lange korrigierte Fehler in der Datenvalidierung.

Anders, als bei der vollständigen Ablösung eines Systems, konnte/musste diese Datenbereinigung nur für die Daten durchgeführt werden, deren Hoheit (d.h. deren Schreibzugriff) in das neue System wechselte.

Der Aufwand für die Datenmigration lag bei rund 15-20 % des gesamten Aufwands. Der Code zum Migrieren der Daten wurde schrittweise und immer parallel mit der Migration der entsprechenden Funktionalität geschrieben.

Gute Praktiken zum Vorgehen von Datenmigrationen, denen wir gefolgt sind, dokumentiert eine Patternsammlung zum Thema Datenmigration.

Datenreplikation

Auch wenn die Hoheit zum Schreiben von Daten klar geregelt war, müssen doch beide Systeme viele Daten des jeweils anderen Systems lesen. Daher mussten wir eine Datenreplikation zwischen den beiden Systemen aufsetzen.

Migrierte Daten wurden pro Kunde genau einmal aus der alten Datenbank in die neue Datenbank übernommen. Die zu replizierenden Daten jedoch werden bei jeder Änderungen zwischen den Systemen abgeglichen.

Die Entwicklung dieser Datenreplikation machte vermutlich weitere 5-10 % des gesamten Aufwands aus.

Code-Migration

Da sich die fachliche Logik nur wenig ändern sollte, stand ursprünglich die Annahme im Raum, dass sich viel Code einfach kopieren und anpassen ließe. Dem standen jedoch die technischen Änderungen entgegen.

Um das neue System moderner zu gestalten, wurden teilweise andere Vorgehen und Bibliotheken verwendet als zuvor. Somit war es nur noch teilweise möglich, Code direkt zu übernehmen. Des weiteren war ein wichtiges Ziel des Teams, übrig gebliebene Strukturen zu entfernen.

In den meisten Fällen war es daher einfacher, den Code sauber neu zu entwickeln, als den alten Code anzupassen. Nur in Einzelfällen (z.B. einzelne Validatoren) konnte der alte Code direkt übernommen werden.

Nichtsdestotrotz war der alte Code enorm wichtig, um keine fachlichen Details der alten Implementierung zu übersehen.

Schnittstellen

Einige Prozesse (z.B. die Bewerbung eines Auftragnehmers auf eine Ausschreibung) können nicht innerhalb nur eines Systems implementiert werden. Anstelle von Methodenaufrufen innerhalb eines Systems mussten beide Systeme neue Schnittstellen implementieren, die das jeweils andere System aufruft.

Manche Schnittstellen wurden synchron entwickelt, andere Event-basiert asynchron, je nach Anwendungsfall. Die Ausfallsicherheit dieser Schnittstellen war sehr wichtig: Wenn nur eines der beiden Systeme ausfällt, dürfen keine Prozesse abgebrochen werden, und das zweite System darf von einem Ausfall des ersten Systems nicht beeinträchtigt werden.

Der Aufwand für die Entwicklung dieser Schnittstellen dürfte bei weiteren 10 % des gesamten Aufwands gelegen haben.

Fazit

Nachdem das neue System im Betrieb genommen worden war, entbrannte ein regelrechter Wettlauf darum, wer im alten System alten Code löschen darf. Endlich konnte viel Code entfernt werden, der immer wieder für Probleme gesorgt hatte. Viele Fallunterscheidungen fielen weg, die gesamte Codebasis wurde schlanker und übersichtlicher. Dieser Prozess des Aufräumens alleine zog sich über Wochen hin. Immer wieder wurde alter Code gefunden, der entfernt werden konnte.

Damit war zwar das alte System nicht modernisiert. Aber der Status quo war erheblich einfacher zu verwalten. Nach und nach wurden einige Ansätze, die im neuen System erfolgreich eingeführt waren, ins alte System migriert, wodurch sich die Code-Qualität weiter verbesserte.

Die Komplexität der Kommunikation zwischen den beiden System ist nicht zu unterschätzen. Anstelle einer gemeinsamen Datenbank, in der alle Daten verfügbar sind, müssen nun immer wieder Schnittstellen und Replikationen angepasst werden, um neue Funktionen (und deren Daten) aus dem einen System ins andere übertragen zu können. Etwa ein Drittel des gesamten Aufwands der Neuentwicklung entfiel auf Funktionalität, die zuvor im alten Monolithen schlicht nicht benötigt war.

Das Team hatte sich bewusst dagegen entschieden, weitere Komponenten als gemeinsame Dienste zu extrahieren. Insbesondere die Nutzerverwaltung wäre ein Kandidat gewesen. Aber bei genauerem Blick unterschied sich die Nutzerverwaltung doch deutlich. Der zusätzliche Aufwand für ein weiteres System und weitere Schnittstellen stand nicht im Verhältnis zum Nutzen.

Und das waren die wichtigsten Erkenntnisse:

  • Jedes Trennen von zuvor gemeinsamen Code muss einen Nutzen haben. Der Aufwand, Systeme zu spalten ist immens.
  • Die technischen Herausforderungen bei einer solchen Abspaltung sind groß. Die fachliche Seite muss daher klar sein. Fachliche Anforderungen gleichzeitig zu ändern, ist sehr heikel.
  • Unabhängig von einer Abspaltung oder kompletten Neuentwicklung: Eine schrittweise Migration mit Parallelbetrieb von altem und neuem System reduziert das Risiko enorm.
  • Nach einer erfolgreichen Abspaltung braucht es Zeit, um den verbleibenden Code aufzuräumen und neu zu sortieren. Erst dann sollte man weitere Abspaltungen in Erwägung ziehen.

Schreibe einen Kommentar

Fügen Sie die notwendige Nummer ins freie Feld ein *