In diesem Artikel werde ich darĂŒber sprechen, wie sich das Projekt, in dem ich arbeite, von einem groĂen Monolithen in eine Reihe von Mikrodiensten verwandelt hat.
Das Projekt begann seine Geschichte vor langer Zeit, Anfang 2000. Die ersten Versionen wurden in Visual Basic 6 geschrieben. Im Laufe der Zeit wurde klar, dass die zukĂŒnftige Entwicklung dieser Sprache schwer zu unterstĂŒtzen sein wĂŒrde, da die IDE und die Sprache selbst schlecht entwickelt sind. In den spĂ€ten 2000er Jahren wurde beschlossen, auf ein vielversprechenderes C # umzusteigen. Die neue Version wurde parallel zur Verfeinerung der alten Version geschrieben, nach und nach befand sich immer mehr Code in .NET. Das Backend in C # konzentrierte sich zunĂ€chst auf die Dienstarchitektur. WĂ€hrend der Entwicklung wurden jedoch gemeinsam genutzte Bibliotheken mit Logik verwendet und Dienste in einem einzigen Prozess gestartet. Es stellte sich heraus, dass die Anwendung, die wir "Service-Monolith" nannten.
Einer der wenigen Vorteile dieses Bundles war die FĂ€higkeit der Dienste, sich gegenseitig ĂŒber eine externe API aufzurufen. Es gab offensichtliche Voraussetzungen fĂŒr den Ăbergang zu einem korrekteren Service und in Zukunft zu einer Microservice-Architektur.
Wir haben unsere Zersetzungsarbeiten um 2015 begonnen. Wir haben noch keinen idealen Zustand erreicht - es gibt Teile eines groĂen Projekts, die schwer als Monolithen zu bezeichnen sind, aber sie sehen auch nicht wie Mikrodienste aus. Die Fortschritte sind jedoch erheblich.
Ich werde im Artikel ĂŒber ihn sprechen.

Inhalt
Architektur und Probleme der bestehenden Lösung
AnfĂ€nglich sah die Architektur wie folgt aus: Die BenutzeroberflĂ€che ist eine separate Anwendung, der monolithische Teil ist in Visual Basic 6 geschrieben, die Anwendung in .NET war eine Reihe verwandter Dienste, die mit einer ziemlich groĂen Datenbank arbeiten.
Nachteile der vorherigen LösungSingle Point of FailureWir hatten einen einzigen Fehlerpunkt: Die .NET-Anwendung wurde in einem Prozess ausgefĂŒhrt. Wenn eines der Module abstĂŒrzte, schlug die gesamte Anwendung fehl und Sie mussten sie neu starten. Da wir eine groĂe Anzahl von Prozessen fĂŒr verschiedene Benutzer automatisieren, konnten einige aufgrund eines Fehlers in einem von ihnen fĂŒr einige Zeit nicht funktionieren. Und mit einem Softwarefehler hat auch die Redundanz nicht geholfen.
Die Aufstellung der VerbesserungenDieser Fehler ist eher organisatorisch. Unsere Anwendung hat viele Kunden, und alle möchten sie so schnell wie möglich fertigstellen. Bisher war dies nicht parallel möglich, und alle Kunden standen in einer Schlange. Dieser Prozess wirkte sich negativ auf das GeschĂ€ft aus, da sie nachweisen mussten, dass ihre Aufgabe wertvoll war. Und das Entwicklungsteam hat Zeit damit verbracht, diese Aufstellung zu organisieren. Dies nahm viel Zeit und MĂŒhe in Anspruch, und das Produkt konnte sich daher nicht so schnell Ă€ndern, wie es von ihm gewesen wĂ€re.
Unangemessener Einsatz von RessourcenWenn Sie Services in einem einzigen Prozess platzieren, haben wir die Konfiguration immer vollstÀndig von Server zu Server kopiert. Wir wollten die am meisten ausgelasteten Dienste separat platzieren, um keine Ressourcen zu verschwenden und eine flexiblere Verwaltung unseres Bereitstellungsschemas zu erhalten.
Es ist schwer, moderne Technologie einzufĂŒhrenEin Problem, das allen Entwicklern bekannt ist: 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, um dem Teamleiter zu beweisen, dass es mehr Boni als verbrauchte Nerven bringt.
Schwierigkeiten beim Ausgeben von ĂnderungenDies war das schwerwiegendste Problem - wir haben alle zwei Monate Veröffentlichungen veröffentlicht.
Jede Veröffentlichung wurde trotz Tests und der BemĂŒhungen der Entwickler zu einer echten Katastrophe fĂŒr die Bank. Das GeschĂ€ft verstand, dass zu Beginn der Woche einige der Funktionen fĂŒr ihn nicht funktionieren wĂŒrden. Und die Entwickler haben verstanden, dass sie auf eine Woche mit schwerwiegenden ZwischenfĂ€llen warten.
Jeder hatte den Wunsch, die Situation zu Àndern.
Microservice-Erwartungen
Lieferung der Komponenten nach VerfĂŒgbarkeit. Lieferung von Komponenten, sobald diese verfĂŒgbar sind, aufgrund der Zersetzung der Lösung und der Trennung verschiedener Prozesse.
Kleine Food-Teams. Dies ist wichtig, da ein groĂes Team, das an einem alten Monolithen arbeitet, schwierig zu verwalten war. Ein solches Team war gezwungen, nach einem strengen Prozess zu arbeiten, aber ich wollte mehr KreativitĂ€t und UnabhĂ€ngigkeit. Nur kleine Teams konnten es sich leisten.
Isolierung von Diensten in getrennten Prozessen. Im Idealfall wollte ich in Containern isolieren, aber eine groĂe Anzahl von in .NET Framework geschriebenen Diensten wird nur unter Windows ausgefĂŒhrt. Jetzt gibt es Dienste in .NET Core, aber bisher gibt es nur wenige.
FlexibilitÀt bei der Bereitstellung. Ich möchte Dienste nach Bedarf kombinieren und nicht nach den Anforderungen des Codes.
Einsatz neuer Technologien. Dies ist fĂŒr jeden Programmierer interessant.
Ăbergangsprobleme
Wenn es einfach wĂ€re, einen Monolithen in Microservices zu zerlegen, mĂŒssten Sie natĂŒrlich nicht auf Konferenzen darĂŒber sprechen und Artikel schreiben. In diesem Prozess gibt es viele Fallstricke, ich werde die wichtigsten beschreiben, die uns gestört haben.
Das erste Problem ist typisch fĂŒr die meisten Monolithen: die KohĂ€renz der GeschĂ€ftslogik. Wenn wir einen Monolithen schreiben, möchten wir unsere Klassen wiederverwenden, um keinen zusĂ€tzlichen Code zu schreiben. Beim Wechsel zu Microservices wird dies zu einem Problem: Der gesamte Code ist eng miteinander verbunden, und es ist schwierig, Dienste zu trennen.
Zu Beginn der Arbeiten verfĂŒgte das Repository ĂŒber mehr als 500 Projekte und mehr als 700.000 Codezeilen. Dies ist eine ziemlich groĂe Lösung und das
zweite Problem . Es war nicht möglich, es einfach in Microservices zu unterteilen.
Das dritte Problem ist der Mangel an notwendiger Infrastruktur. TatsÀchlich waren wir daran beteiligt, den Quellcode manuell auf die Server zu kopieren.
So wechseln Sie von Monolith zu Microservices
Microservice-ZuordnungZunĂ€chst stellten wir sofort fest, dass die Trennung von Mikrodiensten ein iterativer Prozess ist. Wir waren immer verpflichtet, die Entwicklung von GeschĂ€ftsaufgaben parallel durchzufĂŒhren. Wie wir dies technisch durchfĂŒhren werden, ist bereits unser Problem. Deshalb haben wir uns auf den iterativen Prozess vorbereitet. Es funktioniert nicht anders, wenn Sie eine groĂe Anwendung haben, und es ist nicht bereit, von Anfang an neu geschrieben zu werden.
Mit welchen Methoden isolieren wir Microservices?
Die erste Möglichkeit besteht darin, vorhandene Module als Dienste zu portieren. In dieser Hinsicht hatten wir GlĂŒck: Es gab bereits formalisierte Dienste, die am WCF-Protokoll arbeiteten. Sie wurden in separaten Baugruppen veröffentlicht. Wir haben sie separat verschoben und jeder Baugruppe einen kleinen Launcher hinzugefĂŒgt. Es wurde mit der wunderbaren Topshelf-Bibliothek geschrieben, mit der Sie die Anwendung sowohl als Dienst als auch als Konsole ausfĂŒhren können. Dies ist praktisch fĂŒr das Debuggen, da in der Lösung keine zusĂ€tzlichen Projekte erforderlich sind.
Services wurden gemÀà der GeschÀftslogik verbunden, da sie gemeinsame Assemblys verwendeten und mit einer gemeinsamen Datenbank arbeiteten. Es war schwierig, sie als Microservices in ihrer reinen Form zu bezeichnen. Trotzdem könnten wir diese Dienstleistungen separat in verschiedenen Prozessen ausgeben. Dies ermöglichte es bereits, ihren gegenseitigen Einfluss zu verringern und das Problem der parallelen Entwicklung und eines einzelnen Fehlerpunkts zu verringern.
Das Erstellen mit einem Host ist nur eine Codezeile in der Program-Klasse. Wir haben Topshelf in einer Helferklasse versteckt.
namespace RBA.Services.Accounts.Host { internal class Program { private static void Main(string[] args) { HostRunner<Accounts>.Run("RBA.Services.Accounts.Host"); } } }
Der zweite Weg, um Microservices zu isolieren: Erstellen Sie sie, um neue Probleme zu lösen. Wenn der Monolith nicht gleichzeitig wĂ€chst, ist dies bereits hervorragend, was bedeutet, dass wir uns in die richtige Richtung bewegen. Um neue Probleme zu lösen, haben wir versucht, separate Dienste bereitzustellen. Wenn es eine solche Gelegenheit gab, haben wir mehr âkanonischeâ Dienste erstellt, die ihr Datenmodell vollstĂ€ndig steuern, eine separate Datenbank.
Wir haben wie viele andere mit Authentifizierungs- und Autorisierungsdiensten begonnen. Sie sind perfekt dafĂŒr. Sie sind unabhĂ€ngig, haben in der Regel ein separates Datenmodell. Sie selbst interagieren nicht mit dem Monolithen, nur er wendet sich an sie, um einige Probleme zu lösen. Bei diesen Diensten können Sie mit dem Ăbergang zu einer neuen Architektur beginnen, die Infrastruktur auf ihnen debuggen, einige AnsĂ€tze im Zusammenhang mit Netzwerkbibliotheken ausprobieren usw. In unserer Organisation gibt es keine Teams, die keinen Authentifizierungsdienst durchfĂŒhren könnten.
Der dritte Weg, die von uns verwendeten
Mikrodienste zu isolieren , ist fĂŒr uns ein wenig spezifisch. Dadurch wird die GeschĂ€ftslogik aus der UI-Ebene herausgezogen. Wir haben die Hauptanwendung fĂŒr die Desktop-BenutzeroberflĂ€che, die wie das Backend in C # geschrieben ist. Entwickler machten regelmĂ€Ăig Fehler und fĂŒhrten die UI-Teile der Logik aus, 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 sehen, dass der gröĂte Teil dieser Lösung echte GeschĂ€ftslogik enthĂ€lt, die in anderen Prozessen nĂŒtzlich ist, nicht nur zum Erstellen eines UI-Formulars.

Die eigentliche UI-Logik besteht nur aus den letzten paar Zeilen. Wir haben es auf den Server ĂŒbertragen, damit wir es wiederverwenden können, wodurch die BenutzeroberflĂ€che reduziert und die richtige Architektur erreicht wird.
Die vierte, wichtigste Methode zur Isolierung von Mikrodiensten , mit der Sie den Monolithen reduzieren können, ist das Entfernen vorhandener Dienste bei der Verarbeitung. Wenn wir vorhandene Module so wie sie sind herausnehmen, ist das Ergebnis fĂŒr Entwickler nicht immer angenehm, und der GeschĂ€ftsprozess ab dem Zeitpunkt der Erstellung der FunktionalitĂ€t kann veraltet sein. Dank 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 gibt viele Vorteile.
Die Abteilung Verarbeitungsdienstleistungen ist untrennbar mit dem Konzept eines begrenzten Kontexts verbunden. Dies ist ein Konzept aus dem themenorientierten Design. Dies bedeutet einen DomÀnenmodellabschnitt, in dem alle Begriffe einer einzelnen Sprache eindeutig definiert sind. Betrachten Sie als Beispiel den Kontext von Versicherungen und Rechnungen. Wir haben eine monolithische Anwendung, und es ist notwendig, mit dem Konto in der Versicherung zu arbeiten. Wir erwarten, dass der Entwickler die vorhandene "Account" -Klasse in einer anderen Assembly findet, einen Link von der "Insurance" -Klasse herstellt und einen Arbeitscode erhÀlt. Das DRY-Prinzip wird eingehalten, die Aufgabe durch die Verwendung von vorhandenem Code wird schneller erledigt.
Infolgedessen stellt sich heraus, dass die Kontexte von Konten und Versicherungen miteinander verbunden sind. Wenn neue Anforderungen auftreten, wird diese Verbindung die Entwicklung beeintrĂ€chtigen und die KomplexitĂ€t einer bereits komplexen GeschĂ€ftslogik erhöhen. Um dieses Problem zu lösen, mĂŒssen Sie die Grenzen zwischen den Kontexten im Code finden und deren VerstöĂe entfernen. Im Zusammenhang mit Versicherungen ist es beispielsweise durchaus möglich, dass die 20-stellige Kontonummer der Zentralbank und das Datum der Kontoeröffnung ausreichen.
Um diese begrenzten Kontexte voneinander zu trennen und mit dem Extrahieren von Mikrodiensten aus einer monolithischen Lösung zu beginnen, haben wir einen Ansatz verwendet, z. B. das Erstellen externer APIs innerhalb der Anwendung. Wenn wir wussten, dass ein Modul zu einem Microservice werden sollte, der sich im Rahmen des Prozesses irgendwie Ă€ndert, haben wir sofort die Logik, die zu einem anderen begrenzten Kontext gehört, durch externe Aufrufe aufgerufen. Zum Beispiel ĂŒber REST oder WCF.
Wir haben uns entschieden, Code, der verteilte Transaktionen erfordert, nicht zu vermeiden. In unserem Fall stellte sich heraus, dass es recht einfach war, diese Regel einzuhalten. Wir haben solche Situationen immer noch nicht erlebt, in denen hart verteilte Transaktionen wirklich benötigt werden - die endgĂŒltige Konsistenz zwischen den Modulen ist völlig ausreichend.
Betrachten Sie ein bestimmtes Beispiel. Wir haben das Konzept eines Orchesterförderers, der die Essenz der "Anwendung" verarbeitet. Er erstellt abwechselnd einen Kunden, ein Konto und eine Bankkarte. Wenn der Client und das Konto erfolgreich erstellt wurden und die Erstellung der Karte fehlgeschlagen ist, wechselt die Anwendung nicht in den Status "erfolgreich" und bleibt im Status "Karte nicht erstellt". In Zukunft wird die HintergrundaktivitÀt es aufnehmen und beenden. Das System befindet sich seit einiger Zeit in einem Zustand der Inkonsistenz, aber dies passt insgesamt zu uns.
Wenn dennoch eine Situation auftritt, in der ein Teil der Daten konsistent gespeichert werden muss, werden wir höchstwahrscheinlich den Service erweitern, um dies in einem Prozess zu verarbeiten.
Betrachten wir ein Beispiel fĂŒr die Zuweisung von Mikroservices. Wie kann es relativ sicher in die Produktion gebracht werden? In diesem Beispiel haben wir einen separaten Teil des Systems - das Gehaltsservice-Modul, einen der Abschnitte des Codes, aus denen wir einen Microservice erstellen möchten.

ZunÀchst erstellen wir einen Microservice, indem wir den Code neu schreiben. Wir verbessern einige Punkte, die nicht zu uns passen. Wir realisieren neue GeschÀftsanforderungen vom Kunden. Wir erweitern das Bundle zwischen der BenutzeroberflÀche und dem Gateway-API-Backend, das die Anrufweiterleitung ermöglicht.

Als nĂ€chstes geben wir diese Konfiguration in Betrieb, jedoch im Status des Piloten. Die meisten unserer Benutzer arbeiten immer noch mit alten GeschĂ€ftsprozessen. FĂŒr neue Benutzer entwickeln wir eine neue Version einer monolithischen Anwendung, die dieser Prozess nicht mehr enthĂ€lt. TatsĂ€chlich haben wir eine Reihe von Monolithen und Mikroservices, die in Form eines Piloten arbeiten.

Mit einem erfolgreichen Piloten verstehen wir, dass die neue Konfiguration wirklich funktionsfÀhig ist. Wir können den alten Monolithen aus der Gleichung entfernen und die neue Konfiguration anstelle der alten Lösung belassen.

Insgesamt verwenden wir fast alle vorhandenen Methoden zum Aufteilen des Quellcodes eines Monolithen. All dies ermöglicht es uns, die GröĂe von Teilen der Anwendung zu reduzieren und sie in neue Bibliotheken zu ĂŒbertragen, wodurch ein besserer Quellcode entsteht.
Arbeiten Sie mit einer DB
Die Datenbank kann schlechter unterteilt werden als der Quellcode, da sie nicht nur das aktuelle Schema, sondern auch die gesammelten historischen Daten enthÀlt.
Unsere Datenbank hatte wie viele andere einen weiteren wichtigen Nachteil - ihre enorme GröĂe. Diese Datenbank wurde in Ăbereinstimmung mit der komplizierten GeschĂ€ftslogik des Monolithen entworfen, und es haben sich VerknĂŒpfungen zwischen Tabellen verschiedener begrenzter Kontexte angesammelt.
In unserem Fall trat bei vielen groĂen Projekten ein Problem auf, um alle Probleme zu lösen (eine groĂe Datenbank, viele Beziehungen, manchmal unverstĂ€ndliche Grenzen zwischen Tabellen): die Verwendung der gemeinsam genutzten Datenbankvorlage. Daten wurden aus Tabellen durch Ansicht, durch Replikation entnommen und an andere Systeme gesendet, auf denen diese Replikation benötigt wird. Infolgedessen konnten wir die Tabellen nicht in einem separaten Schema herausnehmen, da sie aktiv verwendet wurden.
Die Trennung hilft uns, in begrenzte Kontexte im Code aufzubrechen. Es gibt uns normalerweise eine ziemlich gute Vorstellung davon, wie wir Daten auf Datenbankebene aufteilen. Wir verstehen, welche Tabellen sich auf einen begrenzten Kontext beziehen und welche sich auf einen anderen beziehen.
Wir haben zwei globale Methoden zum Partitionieren der Datenbank angewendet: Partitionieren vorhandener Tabellen und Partitionieren mit Verarbeitung.
Die Trennung vorhandener Tabellen ist eine Methode, die sich gut eignet, wenn die Datenstruktur von hoher QualitĂ€t ist, die GeschĂ€ftsanforderungen erfĂŒllt und fĂŒr alle geeignet ist. In diesem Fall können wir vorhandene Tabellen in einem separaten Schema auswĂ€hlen.
Eine Verarbeitungsabteilung ist erforderlich, wenn sich das GeschÀftsmodell stark geÀndert hat und die Tabellen uns nicht mehr vollstÀndig zufrieden stellen.
Separate vorhandene Tabellen. Wir mĂŒssen bestimmen, was wir trennen werden. Ohne dieses Wissen wird nichts daraus, und hier hilft uns die Trennung begrenzter Kontexte im Code. Wenn es möglich ist, die Grenzen von Kontexten im Quellcode zu verstehen, wird in der Regel klar, welche Tabellen zur Trennung in die Liste aufgenommen werden sollen.
Stellen Sie sich vor, wir haben eine Lösung, bei der zwei Monolithmodule mit einer Datenbank interagieren. Wir mĂŒssen sicherstellen, dass nur ein Modul mit dem Teil der getrennten Tabellen interagiert und das andere ĂŒber die API mit ihm interagiert. FĂŒr den Anfang reicht es aus, dass nur ein Eintrag ĂŒber die API erfolgt. Dies ist eine notwendige Bedingung, damit wir ĂŒber die UnabhĂ€ngigkeit von Mikrodiensten sprechen können. Das Lesen von Links kann so lange bestehen bleiben, bis ein groĂes Problem vorliegt.

Im nĂ€chsten Schritt können wir bereits einen Codeabschnitt auswĂ€hlen, der mit abnehmbaren Tabellen mit oder ohne Verarbeitung in einem separaten Microservice funktioniert, und ihn in einem separaten Prozesscontainer ausfĂŒhren. Dies ist ein separater Dienst mit Kommunikation mit der Monolith-Datenbank und den Tabellen, die nicht direkt damit zusammenhĂ€ngen. Der Monolith interagiert immer noch mit dem abnehmbaren Teil zum Lesen.

SpĂ€ter werden wir diese Verbindung entfernen, dh das Lesen der Daten der monolithischen Anwendung aus den getrennten Tabellen wird ebenfalls an die API ĂŒbertragen.

Als nÀchstes wÀhlen wir aus der allgemeinen Datenbank die Tabellen aus, mit denen nur der neue Microservice arbeitet. Wir können Tabellen in einem separaten Schema oder sogar in einer separaten physischen Datenbank platzieren. Es gab eine Verbindung zum Lesen zwischen dem Microservice und der Monolith-Datenbank, aber es gibt keinen Grund zur Sorge, in dieser Konfiguration kann sie lange leben.

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. Wir treffen uns regelmĂ€Ăig.
Verarbeitungsabteilung. Diese Methode ist der ersten sehr Ă€hnlich und geht nur in umgekehrter Reihenfolge vor. Wir haben sofort eine neue Datenbank und einen neuen Mikroservice, der ĂŒber die API mit dem Monolithen interagiert. Gleichzeitig bleibt jedoch eine Reihe von Datenbanktabellen ĂŒbrig, die wir in Zukunft löschen möchten. Wir werden es nicht mehr brauchen, in dem neuen Modell haben wir es ersetzt.

Damit dieses Schema funktioniert, benötigen wir höchstwahrscheinlich eine Ăbergangszeit.
Es gibt zwei mögliche AnsÀtze.
Erstens : Wir duplizieren alle Daten in den neuen und alten Datenbanken. In diesem Fall haben wir Datenredundanz, es kann Probleme mit der Synchronisation geben. Aber dann können wir zwei verschiedene Kunden nehmen. Einer wird mit der neuen Version arbeiten, der andere mit der alten.
Zweitens : Wir teilen Daten nach bestimmten GeschÀftsmerkmalen. In unserem System waren beispielsweise 5 Produkte in der alten Datenbank gespeichert. Als sechste haben wir im Rahmen einer neuen GeschÀftsaufgabe eine neue Datenbank erstellt. Wir benötigen jedoch die Gateway-API, die diese Daten synchronisiert und dem Client zeigt, wohin und was er nehmen soll.
Beide AnsÀtze funktionieren, je nach Situation wÀhlen.
Nachdem wir sichergestellt haben, dass alles funktioniert, kann der Teil des Monolithen, der mit den alten Datenbankstrukturen funktioniert, deaktiviert werden.

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

Zusammenfassend können wir sagen, dass wir Probleme mit der Datenbank haben: Es ist schwierig, damit zu arbeiten, verglichen mit dem Quellcode, es ist schwieriger zu trennen, aber dies kann und sollte getan werden. Wir haben einige Möglichkeiten gefunden, die dies ziemlich sicher ermöglichen, aber es ist einfacher, einen Fehler mit den Daten als mit dem Quellcode zu machen.
Arbeiten mit Quellcode
So sah das Quellcodediagramm aus, als wir mit der Analyse eines monolithischen Projekts begannen.

Es kann bedingt in drei Schichten unterteilt werden. Dies ist eine Schicht aus gestarteten Modulen, Plugins, Diensten und einzelnen AktivitĂ€ten. TatsĂ€chlich waren dies die Eintrittspunkte innerhalb der monolithischen Lösung. Alle von ihnen waren fest mit einer gemeinsamen Schicht verbunden. Es hatte GeschĂ€ftslogik, die zwischen Diensten geteilt wurde, und viele Verbindungen. Jeder Dienst und jedes Plugin verwendete je nach GröĂe und Gewissen der Entwickler bis zu 10 oder mehr gĂ€ngige Assemblys.
Wir hatten GlĂŒck, wir hatten Infrastrukturbibliotheken, die separat genutzt werden konnten.
Manchmal trat eine Situation auf, in der einige der allgemeinen Objekte nicht zu dieser Schicht gehörten, sondern Infrastrukturbibliotheken waren. Dies wurde durch Umbenennen entschieden.
Am meisten besorgt ĂŒber begrenzte Kontexte. FrĂŒher mischten sich 3-4 Kontexte in einer gemeinsamen Assembly und verwendeten sich gegenseitig innerhalb derselben GeschĂ€ftsfunktionen. Es war notwendig zu verstehen, wo und an welchen Grenzen dies unterteilt werden kann und was als nĂ€chstes mit der Zuordnung dieser Trennung in Quellcode-Assemblys zu tun ist.
Wir haben mehrere Regeln fĂŒr den Codetrennungsprozess formuliert.
Erstens : Wir wollten keine GeschĂ€ftslogik mehr zwischen Diensten, AktivitĂ€ten und Plugins teilen. Sie wollten die GeschĂ€ftslogik im Rahmen von Microservices unabhĂ€ngig machen. Andererseits werden Microservices im Idealfall als Dienste wahrgenommen, die völlig unabhĂ€ngig existieren. Ich glaube, dass dieser Ansatz etwas verschwenderisch ist und schwer zu erreichen ist, da beispielsweise Dienste in C # auf jeden Fall durch eine Standardbibliothek verbunden werden. Unser System ist in C # geschrieben, andere Technologien wurden noch nicht verwendet. Aus diesem Grund haben wir beschlossen, dass wir es uns leisten können, gemeinsame technische Baugruppen zu verwenden. Die Hauptsache ist, dass sie keine Fragmente der GeschĂ€ftslogik haben. Wenn Sie einen praktischen Wrapper ĂŒber dem von Ihnen verwendeten ORM haben, ist das Kopieren von Service zu Service sehr teuer.
Unser Team ist ein Fan von themenorientiertem Design, daher ist die âZwiebelarchitekturâ perfekt fĂŒr uns. Die Basis unserer Services war keine Datenzugriffsschicht, sondern eine Assembly mit DomĂ€nenlogik, die nur GeschĂ€ftslogik enthĂ€lt und keine Infrastrukturverbindungen aufweist. Gleichzeitig können wir die DomĂ€nenassembly unabhĂ€ngig Ă€ndern, um die mit den Frameworks verbundenen Probleme zu lösen.
Zu diesem Zeitpunkt stieĂen wir auf das erste ernsthafte Problem. Der Dienst sollte sich auf eine DomĂ€nenassembly beziehen, wir wollten die Logik unabhĂ€ngig machen, und hier hat uns das DRY-Prinzip stark gestört. Um Doppelarbeit zu vermeiden, wollten die Entwickler Klassen aus benachbarten Assemblys wiederverwenden. Infolgedessen begannen die DomĂ€nen wieder miteinander zu kommunizieren. Wir haben die Ergebnisse analysiert und festgestellt, dass das Problem möglicherweise auch im Bereich des Quellcode-SpeichergerĂ€ts liegt. Wir hatten ein groĂes Repository, in dem alle Quellcodes lagen. Die Lösung fĂŒr das gesamte Projekt war auf einer lokalen Maschine sehr schwierig zu montieren. Aus diesem Grund wurden separate kleine Lösungen fĂŒr die Teile des Projekts erstellt, und niemand verbot, ihnen eine gemeinsame oder DomĂ€nen-Assembly hinzuzufĂŒgen und sie wiederzuverwenden. Das einzige Tool, das uns dies nicht erlaubte, war der ĂberprĂŒfungscode. Aber manchmal stĂŒrzte er auch ab.
Dann begannen wir, zu einem Modell mit separaten Repositorys zu wechseln. Die GeschĂ€ftslogik flieĂt nicht mehr von Service zu Service, DomĂ€nen sind wirklich unabhĂ€ngig geworden. Begrenzte Kontexte werden klarer unterstĂŒtzt. Wie verwenden wir Infrastrukturbibliotheken wieder? Wir haben sie einem separaten Repository zugewiesen und sie dann in die Nuget-Pakete gestellt, die wir in Artifactory abgelegt haben. Bei jeder Ănderung erfolgt die Zusammenstellung und Veröffentlichung automatisch.

Unsere Dienstleistungen bezogen sich auf interne Infrastrukturpakete genauso wie auf externe. Wir laden externe Bibliotheken von Nuget herunter. Um mit Artifactory zu arbeiten, wo wir diese Pakete abgelegt haben, haben wir zwei Paketmanager verwendet. In kleinen Repositories haben wir auch Nuget verwendet. In Repositorys mit mehreren Diensten haben wir Paket verwendet, das mehr Versionskonsistenz zwischen Modulen bietet.

Durch die Arbeit am Quellcode, die geringfĂŒgige Ănderung der Architektur und die gemeinsame Nutzung von Repositorys 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, Sie benötigen neue Bibliotheken fĂŒr die Infrastruktur.
Manuelle Installation in UmgebungenZunĂ€chst haben wir die Lösung manuell in der Umgebung installiert. Um diesen Prozess zu automatisieren, haben wir eine CI / CD-Pipeline erstellt. Wir haben uns fĂŒr den kontinuierlichen Lieferprozess entschieden, da eine kontinuierliche Bereitstellung fĂŒr uns aus Sicht der GeschĂ€ftsprozesse noch nicht akzeptabel ist. Daher erfolgt das Senden an den Betrieb ĂŒber die SchaltflĂ€che und zum Testen - automatisch.

Wir verwenden Atlassian, Bitbucket zum Speichern des Quellcodes und Bamboo zum Zusammenstellen. Wir schreiben gerne Assembly-Skripte in Cake, weil es das gleiche C # ist. Vorgefertigte Pakete kommen zu Artifactory, und Ansible gelangt automatisch zu den Testservern, wonach sie sofort getestet werden können.

Separate Protokollierung
Zu einer Zeit war eine der Ideen des Monolithen die Bereitstellung einer gemeinsamen Protokollierung. Wir mussten auch verstehen, was mit den einzelnen Protokollen auf den DatentrĂ€gern zu tun ist. Protokolle werden in Textdateien an uns geschrieben. Wir haben uns fĂŒr den Standard-ELK-Stack entschieden. Wir haben nicht ĂŒber Anbieter direkt an die ELK geschrieben, sondern beschlossen, die Textprotokolle fertigzustellen und die darin enthaltene Ablaufverfolgungs-ID als Kennung aufzuschreiben und den Dienstnamen hinzuzufĂŒgen, damit diese Protokolle dann analysiert werden können.

Mit Filebeat haben wir die Möglichkeit, unsere Protokolle von Servern zu sammeln und sie dann zu konvertieren. Mit Kibana können Sie Anforderungen in der BenutzeroberflÀche erstellen und beobachten, wie der Anruf zwischen den Diensten verlaufen ist. Die Trace-ID hilft dabei sehr.
Testen und Debuggen von verwandten Diensten
Anfangs haben wir nicht vollstĂ€ndig verstanden, wie entwickelte Dienste zu debuggen sind. Mit dem Monolithen war alles einfach, wir haben ihn auf dem lokalen Computer ausgefĂŒhrt. Zuerst haben sie versucht, dasselbe mit Microservices zu tun, aber manchmal mĂŒssen Sie mehrere andere ausfĂŒhren, um einen Microservice vollstĂ€ndig zu starten, was unpraktisch ist. , , , . , prod. , , . , , .
, production- . , .
Specflow. NUnit Ansible. , . - . , , Jira.
, . JMeter, â InfluxDB, â Grafana.
?
-, «». , production-, -. 1,5 , , .
. , , . .
. , .
, . , . Scrum-. , .
Zusammenfassung
- . , , , . .
- . , , . , , , Scrum.
- â . . . legacy, , .
: . . , , , , , , , â , . . , , .
PS ( ) â .
.