Wie man Brei aus Microservices kocht

Einer der GrĂŒnde fĂŒr die PopularitĂ€t von Microservices ist die Möglichkeit einer autonomen und unabhĂ€ngigen Entwicklung. Im Wesentlichen ist die Microservice-Architektur der Austausch der Möglichkeit einer autonomen Entwicklung fĂŒr eine komplexere (im Vergleich zu einem Monolithen) Bereitstellung, Test, Debugging und Überwachung. Beachten Sie jedoch, dass Microservices die Aufgabentrennung nicht verzeihen. Wenn die Aufgabentrennung falsch ist, treten bei verschiedenen Diensten hĂ€ufig abhĂ€ngige Änderungen auf. Und dies ist viel schmerzhafter und komplizierter als koordinierte Änderungen im Rahmen verschiedener Module oder Pakete innerhalb des Monolithen. Konsistente Änderungen an Microservices werden durch das konsistente Layout, die Bereitstellung, das Testen usw. erschwert.

Und ich möchte ĂŒber die verschiedenen Muster und Antimuster der Aufteilung der ZustĂ€ndigkeiten in Mikrodienste sprechen.

Service Entity als Antipattern


"Service Entity" ist eines der möglichen (Anti) Muster des Designs von Microservice-Architekturen, das zu stark abhĂ€ngigem Code in verschiedenen Diensten fĂŒhrt und innerhalb von Diensten lose gekoppelt ist.

FĂŒr die meisten Entwickler scheint es, dass er bei der Auswahl von Diensten nach dem Wesen des Themenbereichs: „GeschĂ€ft“, „Person“, „Kunde“, „Auftrag“, „Bild“ den GrundsĂ€tzen der alleinigen Verantwortung folgt, und darĂŒber hinaus erscheint dies hĂ€ufig logisch. Der Service-Entity-Ansatz kann sich jedoch in ein Antimuster verwandeln. Dies liegt daran, dass die meisten Funktionen oder Änderungen mehrere EntitĂ€ten betreffen und nicht eine. Infolgedessen kombiniert jeder dieser Services die Logik verschiedener GeschĂ€ftsprozesse.

Nehmen Sie zum Beispiel einen Online-Shop. Wir haben uns entschlossen, die Dienstleistungen „Produkt“, „Bestellung“, „Kunde“ hervorzuheben.

Welche Änderungen und Dienstleistungen sollte ich vornehmen, um die Lieferung nach Hause hinzuzufĂŒgen?
Zum Beispiel können Sie dies tun:

  • FĂŒgen Sie im Service "Bestellung" die Lieferadresse, die gewĂŒnschte Zeit und den Lieferboten hinzu
  • FĂŒgen Sie im Client-Service eine Liste ausgewĂ€hlter Lieferadressen fĂŒr den Client hinzu
  • FĂŒgen Sie im Service „Produkt“ eine EntitĂ€tsliste der Waren hinzu

FĂŒr die Schnittstelle des Lieferanten muss im Bestelldienst eine separate API-Methode erstellt werden, die eine Liste der diesem bestimmten Anbieter zugewiesenen Bestellungen enthĂ€lt. DarĂŒber hinaus sind Methoden erforderlich, um Waren aus der Bestellung zu entfernen, die nicht passten oder die der Kunde zum Zeitpunkt der Lieferung abgelehnt hat.

Oder welche Änderungen und bei welchen Dienstleistungen muss ich vornehmen, um Rabatte auf den Aktionscode hinzuzufĂŒgen?
Zumindest benötigen Sie:

  • FĂŒgen Sie dem Bestellservice einen Aktionscode hinzu
  • FĂŒgen Sie im Service "Produkt" hinzu, ob auf den Aktionscode fĂŒr dieses Produkt Rabatte gelten
  • FĂŒgen Sie im Kundenservice eine Liste der Aktionscodes hinzu, die dem Kunden ausgestellt wurden

In der ManageroberflĂ€che ist das HinzufĂŒgen eines personalisierten Aktionscodes zum Kunden eine separate Methode im Kundenservice, die nur fĂŒr Filialleiter verfĂŒgbar ist, dem Kunden jedoch nicht. Erstellen Sie im Service "Produkt" eine Methode, die eine Liste der Produkte enthĂ€lt, die vom Aktionscode betroffen sind, damit der Kunde die Auswahl in seiner BenutzeroberflĂ€che erleichtern kann.

Die Ursachen fĂŒr Änderungen im Service können verschiedene GeschĂ€ftsprozesse sein - Auswahl und Design, Zahlung und Abrechnung, Lieferung. Jeder der Problembereiche hat seine eigenen EinschrĂ€nkungen, Invarianten und Anforderungen fĂŒr die Bestellung. Infolgedessen stellt sich heraus, dass wir im „Produkt“ -Dienst Informationen ĂŒber das Produkt, ĂŒber Rabatte und Produktbilanzen in Lagern speichern. Und in der "Bestellung" ist die Logik des Zustellers gespeichert.

Mit anderen Worten, eine Änderung der GeschĂ€ftslogik, die auf mehrere Dienste verteilt ist, fĂŒhrt zu abhĂ€ngigen Änderungen in mehreren Diensten. Gleichzeitig ist in einem Dienst ein Code enthalten, der nicht miteinander verbunden ist.

Speicherdienste


Es scheint, dass dieses Problem gelöst werden kann, wenn ein separater "Layer" -Dienst ĂŒber EntitĂ€tsdiensten erstellt wird, der die gesamte Logik kapselt. Aber normalerweise endet das auch schlecht. Weil dann EntitĂ€tsdienste zu Speicherdiensten werden, d.h. Die gesamte GeschĂ€ftslogik wird mit Ausnahme der Speicherung aus ihnen herausgewaschen.

Wenn die Daten in verschiedenen Datenbanken auf verschiedenen Maschinen gespeichert sind, dann wir

  • Wir verlieren an Leistung, weil wir Daten nicht direkt aus der Datenbank, sondern ĂŒber die Serviceschicht bereitstellen
  • Wir verlieren an FlexibilitĂ€t, da die Service-API normalerweise viel weniger flexibel ist als SQL oder eine andere Abfragesprache
  • Wir verlieren an FlexibilitĂ€t, weil es schwierig ist, Daten von verschiedenen Diensten zusammenzufĂŒhren

Wenn verschiedene EntitĂ€tsdienste Zugriff auf andere Datenbanken haben, erfolgt die Kommunikation zwischen Diensten implizit. Über eine gemeinsame Datenbank ist es nur möglich, Änderungen vorzunehmen, die sich auf eine Änderung des Datenschemas auswirken, nachdem ĂŒberprĂŒft wurde, dass durch diese Änderung nicht alle anderen Dienste beschĂ€digt werden, die diese Datenbank oder dieses Tablet verwenden .

Neben der komplexen Entwicklung werden solche Services zu kritisch und stark ausgelastet. Bei fast jeder Anforderung eines Top-Level-Service mĂŒssen Sie mehrere Anforderungen an verschiedene Service-EntitĂ€ten stellen. Dies bedeutet, dass die Bearbeitung noch schwieriger wird, um den gestiegenen Anforderungen an ZuverlĂ€ssigkeit und Leistung gerecht zu werden.

Aufgrund solcher Schwierigkeiten bei der Entwicklung und UnterstĂŒtzung von Entity-Services in ihrer reinen Form sehen Sie selten ein Muster. In der Regel werden Entity-Services zu einem oder zwei zentralen „Microservice-Monolithen“, die sich hĂ€ufig Ă€ndern und die HauptgeschĂ€ftslogik und Placer kleiner Microservices enthalten, bei denen es sich normalerweise um Infrastruktur handelt und kleine, die sich selten Ă€ndern.

Trennung nach Problembereichen


VerĂ€nderungen an sich werden nicht geboren, sie kommen aus einem Problembereich. Ein Problembereich ist ein Aufgabenbereich, in dem Probleme, die Änderungen im Code erfordern, in einer Sprache unter Verwendung eines Satzes von Konzepten formuliert oder durch GeschĂ€ftslogik miteinander verbunden werden. Dementsprechend gibt es im Rahmen eines Problembereichs höchstwahrscheinlich eine Reihe von EinschrĂ€nkungen, Invarianten, auf die Sie sich beim Schreiben von Code verlassen können.

Die Trennung der Verantwortung von Diensten nach Problembereichen und nicht nach EntitĂ€ten fĂŒhrt normalerweise zu einer besser unterstĂŒtzten und verstĂ€ndlicheren Architektur. Problembereiche entsprechen meist GeschĂ€ftsprozessen. FĂŒr den Online-Shop sind die wahrscheinlichsten Problembereiche "Zahlung und Abrechnung", "Lieferung", "Bestellvorgang".

Änderungen, die mehrere Problembereiche gleichzeitig betreffen wĂŒrden, sind geringer als Änderungen, die mehrere EntitĂ€ten betreffen wĂŒrden.

DarĂŒber hinaus können nach GeschĂ€ftsprozessen aufgeschlĂŒsselte Services in Zukunft wiederverwendet werden. Wenn wir beispielsweise neben dem Online-Shop einen weiteren Verkauf von Flugtickets tĂ€tigen möchten, können wir den allgemeinen Service „Abrechnung und Zahlung“ wiederverwenden. Und machen Sie keine anderen Ă€hnlich, sondern spezifisch fĂŒr den Verkauf von Tickets.

Zum Beispiel können wir so in Dienstleistungen unterteilen:

  • Eine Dienstleistung oder eine Gruppe von Dienstleistungen „Lieferung“, in der die Arbeitslogik mit der Lieferung eines bestimmten Auftrags, der Organisation der Arbeit der Lieferanten, der Bewertung der QualitĂ€t ihrer Arbeit, der mobilen Anwendung des Lieferanten usw. gespeichert wird.
  • Ein Dienst oder eine Gruppe von Diensten „Abrechnung und Zahlung“, in denen die Logik der Arbeit mit Zahlungen, Zahlungskonten fĂŒr juristische Personen, der Erstellung von VertrĂ€gen und Abschlussdokumenten gespeichert wird.
  • Service oder Gruppe von Services „Bestellprozess“, in dem die Logik der Produktauswahl, Katalogisierung, Marken, Warenkorblogik usw. des Kunden gespeichert ist.
  • Service "Autorisierung und Authentifizierung".
  • Es kann sogar sinnvoll sein, den Rabattdienst zu trennen.

Um miteinander zu interagieren, können Dienste das Ereignismodell verwenden oder einfache Objekte miteinander austauschen (erholsame API, GRPC usw.). Es ist zwar erwÀhnenswert, dass es nicht einfach ist, die Interaktion zwischen solchen Diensten korrekt zu organisieren. Zumindest hat die Datendezentralisierung irgendwann Probleme mit der Konsistenz (eventuelle Konsistenz) und der TransaktionsfÀhigkeit (falls dies wichtig ist).

Die Dezentralisierung von Daten, der Austausch einfacher Objekte hat Vor- und Nachteile. Einerseits ermöglicht die Dezentralisierung die unabhĂ€ngige Entwicklung und den Betrieb mehrerer Dienste. Andererseits die Kosten fĂŒr das Speichern von zwei oder drei Kopien von Daten und die Aufrechterhaltung der Konsistenz in verschiedenen Systemen.

Im wirklichen Leben passiert oft etwas dazwischen. Service-EntitĂ€t mit einem Mindestsatz von Attributen, die von allen Services von Verbrauchern verwendet wird. Und eine minimale Logikschicht - zum Beispiel ein Statusmodell und Ereignisse in der Warteschlange mit der Benachrichtigung ĂŒber alle Änderungen in der EntitĂ€t. Gleichzeitig behalten Verbraucherdienste immer noch hĂ€ufig einen „Cache“ von Daten. Es wird alles Mögliche getan, damit sich an einem solchen Dienst so wenig wie möglich Ă€ndert, und dies ist im Prinzip schwierig, da es viele Verbraucher gibt.

Gleichzeitig ist es wichtig zu verstehen, dass jede Partition - sowohl nach EntitĂ€t als auch nach Problembereich - kein Patentrezept ist. Es wird immer Funktionen geben, die abhĂ€ngige Änderungen in mehreren Diensten erfordern. Es ist nur so, dass es bei einer Panne viel mehr solche Änderungen geben wird als bei einer anderen. Die Aufgabe der Entwicklung besteht darin, die Anzahl der abhĂ€ngigen Änderungen zu minimieren.

Eine ideale Aufteilung ist nur möglich, wenn Sie zwei völlig unabhÀngige Produkte haben. In jedem GeschÀft ist alles mit allem verbunden. Die einzige Frage ist, wie viel damit verbunden ist.

Und die Frage ist die Trennung der Verantwortlichkeiten und die Höhe der Hindernisse fĂŒr Abstraktionen.

Design Service API


Das Entwerfen von Schnittstellen innerhalb des Dienstes wiederholt den Verlauf mit der Aufteilung in Dienste nur in kleinerem Maßstab. Das Ändern der Schnittstelle (nicht nur einer Erweiterung) ist komplex und zeitaufwĂ€ndig. In komplexen Anwendungen sollte die Schnittstelle universell genug sein, um keine stĂ€ndigen Änderungen zu verursachen, und sie sollte spezifisch und spezifisch genug sein, um keine Ausbreitung von Verantwortung und Semantik zu verursachen.

Daher mĂŒssen Dienstschnittstellen so gestaltet sein, dass ihre Semantik Änderungen widersteht. Dies ist möglich, wenn sich die Semantik oder der Verantwortungsbereich der Schnittstelle auf die EinschrĂ€nkungen des Problembereichs stĂŒtzt.

CRUD-Schnittstellen fĂŒr Services mit komplexer GeschĂ€ftslogik


Eine zu breite und unspezifische Schnittstelle trĂ€gt entweder zur Erosion der Verantwortung oder zu ĂŒbermĂ€ĂŸiger KomplexitĂ€t bei.

Beispiel: CRUD-API fĂŒr Dienste mit komplexer GeschĂ€ftslogik. Solche Schnittstellen kapseln kein Verhalten. Sie ermöglichen nicht nur, dass GeschĂ€ftslogik in andere Dienste eindringt und die Verantwortung des Dienstes untergrĂ€bt, sondern provozieren auch die Verbreitung von GeschĂ€ftslogik - EinschrĂ€nkungen, Invarianten und Methoden fĂŒr die Arbeit mit Daten sind jetzt in anderen Diensten enthalten. Interface User Services (APIs) mĂŒssen die Logik selbst implementieren.

Wenn wir versuchen, ohne die Schnittstelle wesentlich zu Ă€ndern, die GeschĂ€ftslogik auf den Service zu ĂŒbertragen, erhalten wir eine zu universelle und zu komplizierte Methode.

Zum Beispiel gibt es einen Ticketservice. Es gibt verschiedene Arten von Tickets. Jeder Typ hat einen anderen Satz von Feldern und eine etwas andere Validierung. Das Ticket hat auch ein Statusmodell - eine Zustandsmaschine fĂŒr den Übergang von einem Status zu einem anderen.

Lassen Sie die API folgendermaßen aussehen: POST / PATCH / GET-Methoden, URL /api/v1/tickets/{ticket_idasket.json

So können Sie das Ticket aktualisieren

PATCH /api/v1/tickets/{ticket_id}.json { "type": "bug", "status": "closed", "description": "   " } 

Wenn das Statusmodell vom Ticket abhĂ€ngt, sind Konflikte der GeschĂ€ftslogik möglich. Ändern Sie zuerst den Status gemĂ€ĂŸ dem alten Statusmodell und dann den Ticket-Typ. Oder umgekehrt?

Es stellt sich heraus, dass es innerhalb der API-Methode Code gibt, der nicht miteinander verbunden ist - sich Ă€ndernde EntitĂ€tsfelder, eine Liste verfĂŒgbarer Felder, abhĂ€ngig vom Ticket-Typ, und ein Statusmodell. Sie Ă€ndern sich aus verschiedenen GrĂŒnden und es ist sinnvoll, sie nach verschiedenen API-Methoden und -Schnittstellen zu verteilen.

Wenn das Ändern eines Felds im Rahmen von API-CRUD-Methoden nicht nur eine DatenĂ€nderung ist, sondern eine Operation, die sich auf eine koordinierte Änderung des Status einer EntitĂ€t bezieht, sollte diese Operation in eine separate Methode ĂŒbernommen und nicht direkt geĂ€ndert werden. Wenn das Ändern einer API ohne AbwĂ€rtskompatibilitĂ€t (fĂŒr öffentliche APIs) sehr schlecht ist, sollten Sie beim Entwerfen der API sofort darĂŒber nachdenken.

Um solche Probleme zu vermeiden, ist es daher besser, die Schnittstellen klein, spezifisch und so problemorientiert wie möglich zu gestalten, als universelle datenzentrierte.

Dieses (Anti) Muster ist hĂ€ufiger charakteristisch fĂŒr RESTful-Schnittstellen, da standardmĂ€ĂŸig nur wenige datenzentrierte „Verben“ von Aktionen zum Erstellen, Löschen, Aktualisieren und Lesen vorhanden sind. Keine geschĂ€ftsspezifischen EntitĂ€tsoperationen

Was kann getan werden, um RESTful problemorientierter zu machen?
ZunĂ€chst können Sie EntitĂ€ten Methoden hinzufĂŒgen. Die Schnittstelle wird weniger erholsam. Aber es gibt eine solche Gelegenheit. Wir kĂ€mpfen immer noch nicht fĂŒr die Reinheit des Rennens, sondern lösen praktische Probleme

/api/v1/tickets.json Sie anstelle der universellen Ressource /api/v1/tickets.json weitere Ressourcen hinzu:

/api/v1/tickets/{ticket_id}/migrate.json - Migrieren Sie von einem Typ zu einem anderen
/api/v1/tickets/{ticket_id}/status.json - wenn es ein /api/v1/tickets/{ticket_id}/status.json gibt

Zweitens können Sie sich jede Operation als Ressource im Rahmen von REST vorstellen. Gibt es eine Ticketmigrationsoperation von einem Typ zu einem anderen (oder von einem Projekt zu einem anderen?). Ok, es wird also eine Ressource geben
/api/v1/tickets/migration.json

Gibt es einen GeschÀftsbetrieb zum Erstellen eines Testabonnements?
/api/v1/subscriptions/trial.json

Gibt es eine Geldtransferoperation?
/api/v1/money_transfers.json

Usw.

Das Antipattern mit der datenzentrierten API bezieht sich tatsĂ€chlich auch auf die RPC-Interaktion. Zum Beispiel das Vorhandensein zu allgemeiner Methoden wie editAccount () oder editTicket (). "Objekt Ă€ndern" trĂ€gt nicht die semantische Last, die dem Problembereich zugeordnet ist. Dies bedeutet, dass diese Methode aus verschiedenen GrĂŒnden aufgerufen wird, aus verschiedenen GrĂŒnden, um sich zu Ă€ndern.

Es ist zu beachten, dass datenzentrierte Schnittstellen in Ordnung sind, wenn der Problembereich nur das Speichern, Empfangen und Ändern von Daten umfasst.

Ereignismodell


Eine Möglichkeit, Codeteile zu lösen, besteht darin, die Interaktion zwischen Diensten ĂŒber eine Nachrichtenwarteschlange zu organisieren.

Wenn wir beispielsweise im Dienst bei der Registrierung eines Benutzers einen BegrĂŒĂŸungsbrief senden, eine Anfrage fĂŒr einen Kundenmanager in CRM erstellen usw. mĂŒssen, ist es logisch, keinen externen Serviceabruf zu tĂ€tigen, sondern die Meldung "Benutzer 123 ist registriert" in den Registrierungsdienst aufzunehmen ”, Und alle notwendigen Dienste werden diese Nachricht lesen und die notwendigen Maßnahmen ergreifen. Gleichzeitig erfordert das Ändern der GeschĂ€ftslogik keine Änderung des Registrierungsdienstes.

Meistens werden nicht nur Nachrichten in die Warteschlange geworfen, sondern auch Ereignisse. Da die Warteschlange nur ein Transportprotokoll ist, gelten fĂŒr die Datenschnittstelle dieselben EinschrĂ€nkungen wie fĂŒr die regulĂ€re synchrone Schnittstelle. Um Probleme beim Ändern der BenutzeroberflĂ€che und bei nachfolgenden Änderungen in anderen Diensten zu vermeiden, ist es daher am besten, Ereignisse so problemorientiert wie möglich zu gestalten. Dennoch werden solche Ereignisse hĂ€ufig als DomĂ€nenereignisse bezeichnet. Gleichzeitig hat die Verwendung des Ereignismodells in der Regel keinen großen Einfluss auf die Grenzen, an denen (Mikro-) Dienste kĂ€mpfen.

Da DomĂ€nenereignisse praktisch 1: 1 in synchrone API-Methoden ĂŒbersetzt werden, schlagen sie manchmal sogar vor, einen Ereignisstrom anstelle eines Ereignisstroms anstelle eines API-Aufrufs zu verwenden (Event Sourcing). Durch den Ablauf von Ereignissen können Sie jederzeit den Status von Objekten wiederherstellen, haben aber auch einen freien Verlauf. TatsĂ€chlich ist dieser Ansatz normalerweise nicht sehr flexibel - Sie mĂŒssen alle Ereignisse unterstĂŒtzen, und es ist oft einfacher, eine Story neben der ĂŒblichen API zu halten.

Microservices und Leistung. Cqrs


Im Prinzip impliziert der Problembereich Änderungen im Code, die nicht nur mit funktionalen GeschĂ€ftsanforderungen verbunden sind, sondern auch mit nicht funktionalen - zum Beispiel der Leistung. Wenn es zwei Codeteile mit unterschiedlichen Leistungsanforderungen gibt, bedeutet dies, dass die Trennung dieser beiden Codeteile möglicherweise sinnvoll ist. Und sie sind normalerweise in separate Dienste unterteilt, um verschiedene Sprachen und Technologien verwenden zu können, die fĂŒr die Aufgabe besser geeignet sind.

Beispielsweise gibt es in einem in PHP geschriebenen Dienst eine CPU-gebundene Rechnermethode, die komplexe Berechnungen durchfĂŒhrt. Mit zunehmender Last und Datenmenge hörte er auf zu bewĂ€ltigen. Und natĂŒrlich ist es als eine der Optionen sinnvoll, Berechnungen nicht in PHP-Code, sondern in einem separaten Hochleistungssystem-Daemon durchzufĂŒhren.

Als eines der Beispiele fĂŒr die Aufteilung von Diensten nach dem ProduktivitĂ€tsprinzip - die Trennung von Diensten in Lesen und Ändern (CQRS). Diese Trennung wird hĂ€ufig angeboten, da die Leistungsanforderungen von Lesediensten und Schreiben unterschiedlich sind. Die Leselast ist oft eine GrĂ¶ĂŸenordnung höher als die Schreiblast. Und die Anforderungen an die Antwortgeschwindigkeit von Leseanforderungen sind viel höher als fĂŒr das Schreiben.

Der Kunde verbringt 99% der Zeit mit der Suche nach Waren und nur 1% der Zeit mit dem Bestellvorgang. FĂŒr einen Kunden in einem Suchstatus ist die Anzeigegeschwindigkeit wichtig und Funktionen in Bezug auf Filter, verschiedene Optionen zum Anzeigen von Waren usw. Daher ist es sinnvoll, einen separaten Dienst hervorzuheben, der fĂŒr die Suche, Filterung und Anzeige von Waren zustĂ€ndig ist. Ein solcher Dienst funktioniert höchstwahrscheinlich mit einer Art ELK, einer dokumentenorientierten Datenbank mit denormalisierten Daten.

Offensichtlich ist eine naive Unterteilung in Lese- und Änderungsdienste möglicherweise nicht immer gut.

Ein Beispiel. FĂŒr einen Manager, der mit dem FĂŒllen der Produktpalette arbeitet, besteht das Hauptmerkmal in der Möglichkeit, Waren bequem hinzuzufĂŒgen, zu löschen, zu Ă€ndern und anzuzeigen. Es gibt nicht viel Last, wenn wir das Lesen trennen und in separate Dienste umwandeln, werden wir nichts von einer solchen Trennung erhalten, außer bei Problemen, wenn es notwendig ist, koordinierte Änderungen an den Diensten vorzunehmen.

Source: https://habr.com/ru/post/de469001/


All Articles