Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

In diesem Artikel werde ich darüber sprechen, wie sich das Projekt, an dem ich arbeite, von einem großen Monolithen in eine Reihe von Mikrodiensten verwandelt hat.

Die Geschichte des Projekts begann schon vor langer Zeit, Anfang 2000. Die ersten Versionen wurden in Visual Basic 6 geschrieben. Mit der Zeit wurde klar, dass die Entwicklung in dieser Sprache in Zukunft aufgrund der IDE nur schwer zu unterstützen sein würde und die Sprache selbst sind schlecht entwickelt. Ende der 2000er Jahre wurde beschlossen, auf das vielversprechendere C# umzusteigen. Die neue Version wurde parallel zur Überarbeitung der alten Version geschrieben, nach und nach wurde immer mehr Code in .NET geschrieben. Das Backend in C# konzentrierte sich zunächst auf eine Service-Architektur, doch während der Entwicklung wurden gemeinsame Bibliotheken mit Logik verwendet und Services wurden in einem einzigen Prozess gestartet. Das Ergebnis war eine Anwendung, die wir „Service-Monolith“ nannten.

Einer der wenigen Vorteile dieser Kombination war die Möglichkeit der Dienste, sich gegenseitig über eine externe API aufzurufen. Es gab klare Voraussetzungen für den Übergang zu einem korrekteren Dienst und in Zukunft zur Microservice-Architektur.

Wir begannen unsere Arbeit zur Zersetzung etwa im Jahr 2015. Wir haben noch keinen Idealzustand erreicht – es gibt immer noch Teile eines großen Projekts, die kaum als Monolithen bezeichnet werden können, aber auch nicht wie Microservices aussehen. Dennoch sind die Fortschritte erheblich.
Ich werde im Artikel darüber sprechen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Inhalt

Architektur und Probleme der bestehenden Lösung


Ursprünglich sah die Architektur so aus: Die Benutzeroberfläche ist eine separate Anwendung, der monolithische Teil ist in Visual Basic 6 geschrieben, die .NET-Anwendung ist eine Reihe verwandter Dienste, die mit einer ziemlich großen Datenbank arbeiten.

Nachteile der bisherigen Lösung

Der Punkt des Versagens
Wir hatten einen Single Point of Failure: Die .NET-Anwendung lief in einem einzigen Prozess. Wenn ein Modul ausfiel, scheiterte die gesamte Anwendung und musste neu gestartet werden. Da wir eine große Anzahl von Prozessen für verschiedene Benutzer automatisieren, konnte aufgrund eines Fehlers bei einem von ihnen jeder für einige Zeit nicht arbeiten. Und im Falle eines Softwarefehlers half auch ein Backup nicht.

Warteschlange der Verbesserungen
Dieser Nachteil ist eher organisatorischer Natur. Unsere Anwendung hat viele Kunden und alle möchten sie so schnell wie möglich verbessern. Bisher war dies nicht parallel möglich und alle Kunden standen Schlange. Für Unternehmen war dieser Prozess negativ, da sie nachweisen mussten, dass ihre Aufgabe wertvoll war. Und das Entwicklungsteam hat Zeit damit verbracht, diese Warteschlange zu organisieren. Dies kostete viel Zeit und Mühe und das Produkt konnte letztendlich nicht so schnell geändert werden, wie sie es sich gewünscht hätten.

Suboptimale Ressourcennutzung
Beim Hosten von Diensten in einem einzigen Prozess haben wir die Konfiguration immer vollständig von Server zu Server kopiert. Wir wollten die am stärksten ausgelasteten Dienste separat platzieren, um keine Ressourcen zu verschwenden und eine flexiblere Kontrolle über unser Bereitstellungsschema zu erhalten.

Schwierig, moderne Technologien zu implementieren
Ein allen Entwicklern bekanntes Problem: Es besteht der Wunsch, moderne Technologien in das Projekt einzuführen, aber es gibt keine Möglichkeit. Bei einer großen monolithischen Lösung wird jede Aktualisierung der aktuellen Bibliothek, ganz zu schweigen vom Übergang zu einer neuen, zu einer eher nicht trivialen Aufgabe. Es dauert lange, dem Teamleiter zu beweisen, dass dies mehr Boni als verschwendete Nerven bringt.

Schwierigkeiten bei der Ausgabe von Änderungen
Das war das größte Problem – wir veröffentlichten alle zwei Monate Veröffentlichungen.
Trotz der Tests und Bemühungen der Entwickler wurde jede Veröffentlichung zu einer echten Katastrophe für die Bank. Dem Unternehmen war klar, dass zu Beginn der Woche einige seiner Funktionen nicht funktionieren würden. Und den Entwicklern war klar, dass ihnen eine Woche voller schwerwiegender Vorfälle bevorstand.
Jeder hatte den Wunsch, die Situation zu ändern.

Erwartungen an Microservices


Ausgabe der Komponenten, wenn sie fertig sind. Lieferung fertiger Komponenten durch Zerlegung der Lösung und Trennung verschiedener Prozesse.

Kleine Produktteams. Dies ist wichtig, da es schwierig war, ein großes Team an dem alten Monolithen zu verwalten. Ein solches Team war gezwungen, nach einem strengen Prozess zu arbeiten, wollte aber mehr Kreativität und Unabhängigkeit. Das konnten sich nur kleine Teams leisten.

Isolierung von Diensten in separaten Prozessen. Idealerweise würde ich es gerne in Containern isolieren, aber eine große Anzahl von Diensten, die im .NET Framework geschrieben sind, laufen nur unter WindowsDienste, die auf .NET Core basieren, tauchen jetzt auf, aber es gibt noch wenige davon.

Flexibilität bei der Bereitstellung. Wir möchten Dienste so kombinieren, wie wir sie brauchen, und nicht so, wie der Code es erzwingt.

Einsatz neuer Technologien. Das ist für jeden Programmierer interessant.

Übergangsprobleme


Wenn es einfach wäre, einen Monolithen in Microservices aufzuteilen, wäre es natürlich nicht nötig, auf Konferenzen darüber zu sprechen und Artikel zu schreiben. Es gibt viele Fallstricke in diesem Prozess; ich werde die wichtigsten beschreiben, die uns behindert haben.

Erstes Problem typisch für die meisten Monolithen: Kohärenz der Geschäftslogik. Wenn wir einen Monolithen schreiben, möchten wir unsere Klassen wiederverwenden, um keinen unnötigen Code zu schreiben. Und bei der Umstellung auf Microservices wird dies zum Problem: Der gesamte Code ist ziemlich eng miteinander verbunden und es ist schwierig, die Services zu trennen.

Zum Zeitpunkt des Arbeitsbeginns umfasste das Repository mehr als 500 Projekte und mehr als 700 Codezeilen. Das ist eine ziemlich große Entscheidung und zweites Problem. Es war nicht möglich, es einfach in Microservices aufzuteilen.

Drittes Problem — Mangel an notwendiger Infrastruktur. Tatsächlich haben wir den Quellcode manuell auf die Server kopiert.

Wie man vom Monolithen zu Microservices wechselt


Bereitstellung von Microservices

Zunächst haben wir für uns sofort festgestellt, dass die Trennung von Microservices ein iterativer Prozess ist. Wir mussten immer parallel geschäftliche Probleme entwickeln. Wie wir das technisch umsetzen, ist schon jetzt unser Problem. Deshalb haben wir uns auf einen iterativen Prozess vorbereitet. Anders funktioniert es nicht, wenn Sie eine große Anwendung haben und diese zunächst nicht zum Umschreiben bereit ist.

Mit welchen Methoden isolieren wir Microservices?

Das erste Verfahren — Vorhandene Module als Dienste verschieben. In dieser Hinsicht hatten wir Glück: Es gab bereits registrierte Dienste, die mit dem WCF-Protokoll arbeiteten. Sie wurden in separate Versammlungen aufgeteilt. Wir haben sie separat portiert und jedem Build einen kleinen Launcher hinzugefügt. Es wurde mit der wunderbaren Topshelf-Bibliothek geschrieben, die es Ihnen ermöglicht, die Anwendung sowohl als Dienst als auch als Konsole auszuführen. Dies ist praktisch für das Debuggen, da in der Lösung keine zusätzlichen Projekte erforderlich sind.

Die Dienste wurden nach Geschäftslogik verbunden, da sie gemeinsame Assemblys verwendeten und mit einer gemeinsamen Datenbank arbeiteten. Man kann sie in ihrer reinen Form kaum als Microservices bezeichnen. Wir könnten diese Dienste jedoch auch separat und in unterschiedlichen Prozessen bereitstellen. Dies allein ermöglichte es, ihren gegenseitigen Einfluss zu verringern und das Problem der parallelen Entwicklung und eines Single Point of Failure zu reduzieren.

Die Assemblierung mit dem Host ist nur eine Codezeile in der Program-Klasse. Wir haben die Arbeit mit Topshelf in einer Hilfsklasse versteckt.

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner<Accounts>.Run("RBA.Services.Accounts.Host");

       }
    }
}

Die zweite Möglichkeit, Microservices zuzuweisen, ist: Erstellen Sie sie, um neue Probleme zu lösen. Wenn gleichzeitig der Monolith nicht wächst, ist das bereits hervorragend, wir sind also auf dem richtigen Weg. Um neue Probleme zu lösen, haben wir versucht, separate Dienste zu schaffen. Wenn es eine solche Möglichkeit gäbe, hätten wir „kanonischere“ Dienste geschaffen, die ihr eigenes Datenmodell, eine separate Datenbank, vollständig verwalten.

Wir haben, wie viele andere auch, mit Authentifizierungs- und Autorisierungsdiensten begonnen. Dafür sind sie perfekt geeignet. Sie sind unabhängig, in der Regel verfügen sie über ein eigenes Datenmodell. Sie selbst interagieren nicht mit dem Monolithen, er wendet sich lediglich an sie, um einige Probleme zu lösen. Mit diesen Diensten können Sie den Übergang zu einer neuen Architektur beginnen, die Infrastruktur darauf debuggen, einige Ansätze im Zusammenhang mit Netzwerkbibliotheken ausprobieren usw. Wir haben keine Teams in unserer Organisation, die keinen Authentifizierungsdienst erstellen könnten.

Die dritte Möglichkeit, Microservices zuzuweisenDie von uns verwendete ist etwas spezifisch für uns. Dies ist die Entfernung der Geschäftslogik aus der UI-Ebene. Unsere Haupt-UI-Anwendung ist der Desktop; sie ist, wie das Backend, in C# geschrieben. Die Entwickler machten regelmäßig Fehler und übertrugen Teile der Logik auf die Benutzeroberfläche, die im Backend hätten vorhanden sein und wiederverwendet werden sollen.

Wenn Sie sich ein reales Beispiel aus dem Code des UI-Teils ansehen, können Sie erkennen, dass der Großteil dieser Lösung echte Geschäftslogik enthält, die in anderen Prozessen nützlich ist, nicht nur zum Erstellen des UI-Formulars.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Die eigentliche UI-Logik ist erst in den letzten paar Zeilen vorhanden. Wir haben es auf den Server übertragen, damit es wiederverwendet werden kann, wodurch die Benutzeroberfläche reduziert und die richtige Architektur erreicht wurde.

Die vierte und wichtigste Möglichkeit, Microservices zu isolieren, was es ermöglicht, den Monolithen zu reduzieren, ist die Entfernung bestehender Dienste mit der Verarbeitung. Wenn wir vorhandene Module so entfernen, wie sie sind, entspricht das Ergebnis nicht immer dem Geschmack der Entwickler, und der Geschäftsprozess ist seit der Erstellung der Funktionalität möglicherweise veraltet. Mit Refactoring können wir einen neuen Geschäftsprozess unterstützen, da sich die Geschäftsanforderungen ständig ändern. Wir können den Quellcode verbessern, bekannte Fehler beseitigen und ein besseres Datenmodell erstellen. Es ergeben sich viele Vorteile.

Die Trennung von Diensten und Verarbeitung ist untrennbar mit dem Konzept des begrenzten Kontexts verbunden. Dies ist ein Konzept von Domain Driven Design. Es bezeichnet einen Abschnitt des Domänenmodells, in dem alle Begriffe einer einzelnen Sprache eindeutig definiert sind. Schauen wir uns als Beispiel den Kontext von Versicherungen und Rechnungen an. Wir haben eine monolithische Anwendung und müssen mit dem Versicherungskonto arbeiten. Wir erwarten, dass der Entwickler eine vorhandene Account-Klasse in einer anderen Assembly findet, in der Insurance-Klasse darauf verweist und dann über funktionierenden Code verfügt. Das DRY-Prinzip wird respektiert, die Aufgabe wird durch die Verwendung vorhandener Codes schneller erledigt.

Im Ergebnis zeigt sich, dass die Kontexte von Konten und Versicherungen miteinander verbunden sind. Wenn neue Anforderungen entstehen, beeinträchtigt diese Kopplung die Entwicklung und erhöht die Komplexität der bereits komplexen Geschäftslogik. Um dieses Problem zu lösen, müssen Sie die Grenzen zwischen Kontexten im Code finden und deren Verstöße beseitigen. Beispielsweise ist es im Versicherungsbereich durchaus möglich, dass eine 20-stellige Zentralbankkontonummer und das Datum der Kontoeröffnung ausreichen.

Um diese begrenzten Kontexte voneinander zu trennen und mit der Trennung von Microservices von einer monolithischen Lösung zu beginnen, haben wir einen Ansatz wie die Erstellung externer APIs innerhalb der Anwendung verwendet. Wenn wir wussten, dass ein Modul zu einem Microservice werden sollte, der innerhalb des Prozesses irgendwie geändert wurde, dann riefen wir sofort über externe Aufrufe die Logik auf, die zu einem anderen begrenzten Kontext gehört. Zum Beispiel über REST oder WCF.

Wir waren fest davon überzeugt, dass wir Code, der verteilte Transaktionen erfordern würde, nicht vermeiden würden. In unserem Fall erwies es sich als recht einfach, diese Regel zu befolgen. Wir sind noch nicht auf Situationen gestoßen, in denen streng verteilte Transaktionen wirklich erforderlich sind – die endgültige Konsistenz zwischen den Modulen ist völlig ausreichend.

Schauen wir uns ein konkretes Beispiel an. Wir haben das Konzept eines Orchestrators – einer Pipeline, die die Entität der „Anwendung“ verarbeitet. Er erstellt nacheinander einen Kunden, ein Konto und eine Bankkarte. Wenn der Kunde und das Konto erfolgreich erstellt wurden, die Kartenerstellung jedoch fehlschlägt, wechselt der Antrag nicht in den Status „erfolgreich“ und verbleibt im Status „Karte nicht erstellt“. In Zukunft wird es durch Hintergrundaktivitäten aufgenommen und fertiggestellt. Das System befindet sich seit einiger Zeit in einem Zustand der Inkonsistenz, mit dem wir aber im Großen und Ganzen zufrieden sind.

Wenn eine Situation entsteht, in der es notwendig ist, einen Teil der Daten konsistent zu speichern, werden wir höchstwahrscheinlich eine Konsolidierung des Dienstes anstreben, um sie in einem Prozess zu verarbeiten.

Schauen wir uns ein Beispiel für die Zuweisung eines Microservices an. Wie kann man es relativ sicher zur Produktion bringen? In diesem Beispiel haben wir einen separaten Teil des Systems – ein Gehaltsabrechnungsdienstmodul, einen der Codeabschnitte, aus denen wir einen Mikroservice erstellen möchten.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Zunächst erstellen wir einen Microservice, indem wir den Code neu schreiben. Wir verbessern einige Aspekte, mit denen wir nicht zufrieden waren. Wir setzen neue Geschäftsanforderungen des Kunden um. Wir fügen der Verbindung zwischen der Benutzeroberfläche und dem Backend ein API-Gateway hinzu, das die Anrufweiterleitung ermöglicht.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Als nächstes geben wir diese Konfiguration in Betrieb, allerdings im Pilotzustand. Die meisten unserer Anwender arbeiten noch mit alten Geschäftsprozessen. Für neue Benutzer entwickeln wir eine neue Version der monolithischen Anwendung, die diesen Prozess nicht mehr enthält. Im Wesentlichen haben wir eine Kombination aus einem Monolithen und einem Microservice, der als Pilot fungiert.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Nach einem erfolgreichen Pilotprojekt verstehen wir, dass die neue Konfiguration tatsächlich praktikabel ist. Wir können den alten Monolithen aus der Gleichung entfernen und die neue Konfiguration anstelle der alten Lösung belassen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Insgesamt nutzen wir nahezu alle existierenden Methoden zur Aufteilung des Quellcodes eines Monolithen. Sie alle ermöglichen es uns, die Größe von Teilen der Anwendung zu reduzieren und sie in neue Bibliotheken zu übersetzen, um so einen besseren Quellcode zu erstellen.

Arbeiten mit der Datenbank


Die Datenbank lässt sich schlechter aufteilen als der Quellcode, da sie nicht nur das aktuelle Schema, sondern auch akkumulierte historische Daten enthält.

Unsere Datenbank hatte, wie viele andere auch, einen weiteren wichtigen Nachteil – ihre enorme Größe. Diese Datenbank wurde gemäß der komplexen Geschäftslogik eines Monolithen entworfen und sammelte Beziehungen zwischen den Tabellen verschiedener begrenzter Kontexte.

In unserem Fall kam zu allen Problemen (große Datenbank, viele Verbindungen, manchmal unklare Grenzen zwischen Tabellen) ein Problem hinzu, das in vielen großen Projekten auftritt: die Verwendung der gemeinsamen Datenbankvorlage. Daten wurden aus Tabellen über die Ansicht und Replikation entnommen und an andere Systeme gesendet, wo diese Replikation erforderlich war. Daher konnten wir die Tabellen nicht in ein separates Schema verschieben, da sie aktiv genutzt wurden.

Die gleiche Unterteilung in begrenzte Kontexte im Code hilft uns bei der Trennung. Normalerweise erhalten wir dadurch eine ziemlich gute Vorstellung davon, wie wir die Daten auf Datenbankebene aufschlüsseln. Wir verstehen, welche Tabellen zu einem begrenzten Kontext gehören und welche zu einem anderen.

Wir haben zwei globale Methoden der Datenbankpartitionierung verwendet: Partitionierung vorhandener Tabellen und Partitionierung mit Verarbeitung.

Die Abspaltung bestehender Tabellen ist eine gute Methode, wenn die Datenstruktur gut ist, den Geschäftsanforderungen entspricht und alle damit zufrieden sind. In diesem Fall können wir vorhandene Tabellen in ein separates Schema aufteilen.

Eine Abteilung mit Bearbeitung wird dann benötigt, wenn sich das Geschäftsmodell stark verändert hat und die Tische uns überhaupt nicht mehr zufriedenstellen.

Vorhandene Tabellen aufteilen. Wir müssen festlegen, was wir trennen werden. Ohne dieses Wissen geht nichts, und hier hilft uns die Trennung der begrenzten Kontexte im Code. Wenn man die Grenzen von Kontexten im Quellcode versteht, wird in der Regel klar, welche Tabellen in die Liste für die Abteilung aufgenommen werden sollten.

Stellen wir uns vor, wir hätten eine Lösung, bei der zwei Monolithmodule mit einer Datenbank interagieren. Wir müssen sicherstellen, dass nur ein Modul mit dem Abschnitt der getrennten Tabellen interagiert und das andere beginnt, über die API damit zu interagieren. Zunächst reicht es aus, dass nur die Aufzeichnung über die API erfolgt. Dies ist eine notwendige Voraussetzung dafür, dass wir über die Unabhängigkeit von Microservices sprechen können. Leseverbindungen können bestehen bleiben, solange keine größeren Probleme vorliegen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Der nächste Schritt besteht darin, dass wir den Codeabschnitt, der mit getrennten Tabellen arbeitet, mit oder ohne Verarbeitung, in einen separaten Microservice aufteilen und ihn in einem separaten Prozess, einem Container, ausführen können. Dabei handelt es sich um einen separaten Dienst mit einer Verbindung zur Monolith-Datenbank und den Tabellen, die nicht direkt damit in Zusammenhang stehen. Der Monolith interagiert weiterhin zum Lesen mit dem abnehmbaren Teil.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Später werden wir diese Verbindung entfernen, d. h. das Lesen von Daten aus einer monolithischen Anwendung aus getrennten Tabellen wird ebenfalls an die API übertragen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Als nächstes wählen wir aus der allgemeinen Datenbank die Tabellen aus, mit denen nur der neue Microservice funktioniert. Wir können die Tabellen in ein separates Schema oder sogar in eine separate physische Datenbank verschieben. Es besteht immer noch eine Leseverbindung zwischen dem Microservice und der Monolith-Datenbank, aber es besteht kein Grund zur Sorge, in dieser Konfiguration kann es ziemlich lange leben.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Der letzte Schritt besteht darin, alle Verbindungen vollständig zu entfernen. In diesem Fall müssen wir möglicherweise Daten aus der Hauptdatenbank migrieren. Manchmal möchten wir einige Daten oder Verzeichnisse, die von externen Systemen repliziert wurden, in mehreren Datenbanken wiederverwenden. Das passiert uns regelmäßig.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Bearbeitungsabteilung. Diese Methode ist der ersten sehr ähnlich, nur in umgekehrter Reihenfolge. Wir weisen sofort eine neue Datenbank und einen neuen Microservice zu, der über eine API mit dem Monolithen interagiert. Gleichzeitig verbleibt jedoch eine Reihe von Datenbanktabellen, die wir in Zukunft löschen möchten. Wir brauchen es nicht mehr; wir haben es im neuen Modell ersetzt.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Damit dieses Schema funktioniert, werden wir wahrscheinlich eine Übergangszeit benötigen.

Dann gibt es zwei mögliche Vorgehensweisen.

Erste: Wir duplizieren alle Daten in der neuen und alten Datenbank. In diesem Fall liegen Datenredundanz und Synchronisierungsprobleme vor. Aber wir können zwei verschiedene Kunden annehmen. Der eine funktioniert mit der neuen Version, der andere mit der alten.

Zweite: Wir teilen die Daten nach einigen Geschäftskriterien auf. Wir hatten zum Beispiel 5 Produkte im System, die in der alten Datenbank gespeichert waren. Den sechsten platzieren wir innerhalb der neuen Geschäftsaufgabe in einer neuen Datenbank. Aber wir brauchen ein API-Gateway, das diese Daten synchronisiert und dem Client zeigt, wo und was er bekommen kann.

Beide Ansätze funktionieren, wählen Sie je nach Situation.

Nachdem wir sicher sind, dass alles funktioniert, kann der Teil des Monolithen, der mit alten Datenbankstrukturen funktioniert, deaktiviert werden.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Der letzte Schritt besteht darin, die alten Datenstrukturen zu entfernen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Zusammenfassend können wir sagen, dass wir Probleme mit der Datenbank haben: Es ist im Vergleich zum Quellcode schwieriger, damit zu arbeiten, es ist schwieriger, sie zu teilen, aber es kann und sollte getan werden. Wir haben einige Möglichkeiten gefunden, dies recht sicher zu machen, aber es ist immer noch einfacher, bei Daten Fehler zu machen als beim Quellcode.

Arbeiten mit Quellcode


So sah das Quellcodediagramm aus, als wir mit der Analyse des monolithischen Projekts begannen.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Es kann grob in drei Schichten unterteilt werden. Dies ist eine Schicht gestarteter Module, Plugins, Dienste und einzelner Aktivitäten. Tatsächlich handelte es sich hierbei um Einstiegspunkte innerhalb einer monolithischen Lösung. Alle waren mit einer Common-Schicht dicht verschlossen. Es verfügte über eine Geschäftslogik, die von den Diensten gemeinsam genutzt wurde, und über viele Verbindungen. Jeder Dienst und jedes Plugin nutzte bis zu 10 oder mehr gemeinsame Assemblys, abhängig von ihrer Größe und dem Gewissen der Entwickler.

Wir hatten das Glück, über Infrastrukturbibliotheken zu verfügen, die separat genutzt werden konnten.

Manchmal kam es vor, dass einige gemeinsame Objekte eigentlich nicht zu dieser Schicht gehörten, sondern Infrastrukturbibliotheken waren. Dies wurde durch Umbenennung behoben.

Die größte Sorge galten begrenzten Kontexten. Es kam vor, dass 3-4 Kontexte in einer Common Assembly gemischt waren und sich innerhalb derselben Geschäftsfunktionen gegenseitig nutzten. Es war notwendig zu verstehen, wo und entlang welcher Grenzen dies unterteilt werden konnte und was als nächstes mit der Abbildung dieser Unterteilung in Quellcode-Assemblys zu tun ist.

Wir haben mehrere Regeln für den Code-Splitting-Prozess formuliert.

Die erste: Wir wollten keine Geschäftslogik mehr zwischen Diensten, Aktivitäten und Plugins teilen. Wir wollten die Geschäftslogik innerhalb von Microservices unabhängig machen. Unter Microservices hingegen versteht man idealerweise Dienste, die völlig unabhängig existieren. Meiner Meinung nach ist dieser Ansatz etwas verschwenderisch und schwierig umzusetzen, da beispielsweise Dienste in C# ohnehin durch eine Standardbibliothek verbunden sind. Unser System ist in C# geschrieben; wir haben bisher keine anderen Technologien verwendet. Deshalb haben wir beschlossen, dass wir es uns leisten können, gängige technische Baugruppen zu verwenden. Die Hauptsache ist, dass sie keine Fragmente der Geschäftslogik enthalten. Wenn Sie einen Convenience-Wrapper über das von Ihnen verwendete ORM haben, ist das Kopieren von Dienst zu Dienst sehr kostspielig.

Unser Team ist ein Fan von domänengesteuertem Design, daher passte die Zwiebelarchitektur hervorragend zu uns. Die Basis unserer Dienste ist nicht die Datenzugriffsschicht, sondern eine Assembly mit Domänenlogik, die ausschließlich Geschäftslogik enthält und keine Verbindungen zur Infrastruktur hat. Gleichzeitig können wir die Domänenassembly unabhängig ändern, um Probleme im Zusammenhang mit Frameworks zu lösen.

Zu diesem Zeitpunkt stießen wir auf unser erstes ernstes Problem. Der Dienst musste sich auf eine Domänenassembly beziehen, wir wollten die Logik unabhängig machen und das DRY-Prinzip hat uns hier stark behindert. Die Entwickler wollten Klassen benachbarter Assemblys wiederverwenden, um Duplikate zu vermeiden, und als Folge davon begannen Domänen wieder miteinander verknüpft zu werden. Wir haben die Ergebnisse analysiert und sind zu dem Schluss gekommen, dass das Problem möglicherweise auch im Bereich des Quellcode-Speichergeräts liegt. Wir hatten ein großes Repository, das den gesamten Quellcode enthielt. Es war sehr schwierig, die Lösung für das gesamte Projekt auf einem lokalen Computer zusammenzustellen. Daher wurden für Teile des Projekts separate kleine Lösungen erstellt, und niemand verbot, ihnen allgemeine oder Domänenassemblys hinzuzufügen und sie wiederzuverwenden. Das einzige Tool, das uns dies nicht ermöglichte, war die Codeüberprüfung. Aber manchmal scheiterte es auch.

Dann begannen wir mit der Umstellung auf ein Modell mit separaten Repositorys. Die Geschäftslogik fließt nicht mehr von Dienst zu Dienst, Domänen sind wirklich unabhängig geworden. Begrenzte Kontexte werden klarer unterstützt. Wie können wir Infrastrukturbibliotheken wiederverwenden? Wir haben sie in ein separates Repository aufgeteilt und sie dann in Nuget-Pakete gepackt, die wir in Artifactory abgelegt haben. Bei jeder Änderung erfolgt die Zusammenstellung und Veröffentlichung automatisch.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Unsere Dienste begannen, interne Infrastrukturpakete genauso zu referenzieren wie externe. Wir laden externe Bibliotheken von Nuget herunter. Um mit Artifactory zu arbeiten, wo wir diese Pakete platziert haben, haben wir zwei Paketmanager verwendet. In kleinen Repositories haben wir auch Nuget verwendet. In Repositorys mit mehreren Diensten haben wir Paket verwendet, das eine größere Versionskonsistenz zwischen Modulen bietet.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Indem wir am Quellcode arbeiten, die Architektur leicht ändern und die Repositories trennen, machen wir unsere Dienste unabhängiger.

Infrastrukturprobleme


Die meisten Nachteile der Umstellung auf Microservices hängen mit der Infrastruktur zusammen. Sie benötigen eine automatisierte Bereitstellung und neue Bibliotheken, um die Infrastruktur auszuführen.

Manuelle Installation in Umgebungen

Zunächst haben wir die Lösung für Umgebungen manuell installiert. Um diesen Prozess zu automatisieren, haben wir eine CI/CD-Pipeline erstellt. Wir haben uns für den Continuous-Delivery-Prozess entschieden, da Continuous Deployment für uns aus Sicht der Geschäftsprozesse noch nicht akzeptabel ist. Daher erfolgt der Versand zum Betrieb per Knopfdruck und zum Testen automatisch.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Wir verwenden Atlassian, Bitbucket zur Speicherung des Quellcodes und Bamboo zum Erstellen. Wir schreiben gerne Build-Skripte in Cake, weil es mit C# identisch ist. Fertige Pakete kommen zu Artifactory und Ansible gelangt automatisch zu den Testservern, wo sie sofort getestet werden können.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Separate Protokollierung


Eine der Ideen des Monolithen bestand einst darin, eine gemeinsame Protokollierung bereitzustellen. Wir mussten auch verstehen, was mit den einzelnen Protokollen auf den Festplatten geschehen soll. Unsere Protokolle werden in Textdateien geschrieben. Wir haben uns für die Verwendung eines Standard-ELK-Stacks entschieden. Wir haben nicht direkt über die Anbieter an ELK geschrieben, sondern beschlossen, die Textprotokolle zu ändern und die Trace-ID als Kennung in sie zu schreiben und den Dienstnamen hinzuzufügen, damit diese Protokolle später analysiert werden können.

Der Übergang vom Monolithen zu Microservices: Geschichte und Praxis

Mit Filebeat können wir unsere Protokolle sammeln von ServerAnschließend können Sie die Daten transformieren, mit Kibana Abfragen in der Benutzeroberfläche erstellen und nachvollziehen, wie der Aufruf zwischen den Diensten weitergeleitet wurde. Trace-IDs sind dabei sehr hilfreich.

Testen und Debuggen verwandter Dienstleistungen


Anfangs verstanden wir nicht ganz, wie man die zu entwickelnden Dienste debuggt. Mit dem Monolithen war alles einfach; wir haben ihn auf einem lokalen Rechner ausgeführt. Zuerst versuchten sie, dasselbe mit Mikrodiensten zu tun, aber manchmal muss man mehrere andere starten, um einen Mikrodienst vollständig zu starten, und das ist unpraktisch. Uns wurde klar, dass wir zu einem Modell übergehen müssen, bei dem wir auf dem lokalen Computer nur den oder die Dienste belassen, die wir debuggen möchten. Die restlichen Dienste werden von Servern verwendet, die der Konfiguration mit prod entsprechen. Nach dem Debuggen werden beim Testen für jede Aufgabe nur die geänderten Dienste an den Testserver ausgegeben. Somit wird die Lösung in der Form getestet, in der sie in Zukunft in der Produktion erscheinen wird.

Es gibt Server, auf denen nur Produktionsversionen von Diensten ausgeführt werden. Diese Server werden im Falle von Zwischenfällen, zur Überprüfung der Zustellung vor dem Einsatz und für interne Schulungen benötigt.

Wir haben einen automatisierten Testprozess mithilfe der beliebten Specflow-Bibliothek hinzugefügt. Tests werden automatisch mit NUnit ausgeführt, unmittelbar nach der Bereitstellung aus Ansible. Wenn die Aufgabenabdeckung vollautomatisch erfolgt, sind keine manuellen Tests erforderlich. Allerdings sind manchmal noch zusätzliche manuelle Tests erforderlich. Wir verwenden Tags in Jira, um zu bestimmen, welche Tests für ein bestimmtes Problem ausgeführt werden sollen.

Darüber hinaus ist der Bedarf an Belastungstests gestiegen; bisher wurden diese nur in seltenen Fällen durchgeführt. Wir verwenden JMeter zum Ausführen von Tests, InfluxDB zum Speichern und Grafana zum Erstellen von Prozessdiagrammen.

Was haben wir erreicht?


Erstens haben wir das Konzept der „Veröffentlichung“ abgeschafft. Vorbei sind die zweimonatigen monströsen Veröffentlichungen, bei denen dieser Koloss in einer Produktionsumgebung eingesetzt wurde und Geschäftsprozesse vorübergehend störte. Jetzt stellen wir Dienste im Durchschnitt alle 1,5 Tage bereit und gruppieren sie, weil sie nach der Genehmigung in Betrieb gehen.

Es gibt keine schwerwiegenden Ausfälle in unserem System. Wenn wir einen Microservice mit einem Fehler veröffentlichen, wird die damit verbundene Funktionalität beeinträchtigt und alle anderen Funktionen werden nicht beeinträchtigt. Dies verbessert das Benutzererlebnis erheblich.

Wir können das Bereitstellungsmuster steuern. Bei Bedarf können Sie Gruppen von Diensten getrennt vom Rest der Lösung auswählen.

Darüber hinaus haben wir das Problem durch eine große Warteschlange an Verbesserungen deutlich reduziert. Wir haben jetzt separate Produktteams, die mit einigen der Dienste unabhängig voneinander arbeiten. Der Scrum-Prozess passt hier bereits gut. Ein bestimmtes Team kann einen separaten Product Owner haben, der ihm Aufgaben zuweist.

Zusammenfassung

  • Microservices eignen sich gut zur Zerlegung komplexer Systeme. Dabei beginnen wir zu verstehen, was sich in unserem System befindet, welche begrenzten Kontexte es gibt und wo ihre Grenzen liegen. Dadurch können Sie Verbesserungen korrekt auf die Module verteilen und Codeverwirrung vermeiden.
  • Microservices bieten organisatorische Vorteile. Sie werden oft nur als Architektur bezeichnet, aber jede Architektur wird zur Lösung von Geschäftsanforderungen benötigt und nicht für sich allein. Daher können wir sagen, dass Microservices gut geeignet sind, um Probleme in kleinen Teams zu lösen, da Scrum mittlerweile sehr beliebt ist.
  • Trennung ist ein iterativer Prozess. Sie können eine Anwendung nicht einfach in Microservices aufteilen. Es ist unwahrscheinlich, dass das resultierende Produkt funktionsfähig ist. Bei der Bereitstellung von Microservices ist es von Vorteil, das bestehende Erbe neu zu schreiben, das heißt, es in Code umzuwandeln, der uns gefällt und der die Geschäftsanforderungen in Bezug auf Funktionalität und Geschwindigkeit besser erfüllt.

    Eine kleine Einschränkung: Die Kosten für die Umstellung auf Microservices sind recht hoch. Es hat lange gedauert, das Infrastrukturproblem allein zu lösen. Wenn Sie also eine kleine Anwendung haben, die keine spezielle Skalierung erfordert, sind Microservices heute möglicherweise nicht das, was Sie brauchen, es sei denn, Sie haben eine große Anzahl von Kunden, die um die Aufmerksamkeit und Zeit Ihres Teams konkurrieren. Es ist ziemlich teuer. Wenn man den Prozess mit Microservices startet, dann sind die Kosten zunächst höher, als wenn man das gleiche Projekt mit der Entwicklung eines Monolithen startet.

    PS Eine emotionalere Geschichte (und wie für Sie persönlich) - laut Link.
    Hier finden Sie die vollständige Version des Berichts.

Source: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz und VPS-VDS-Servern 🔥 Kaufen Sie zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server | ProHoster