Wie funktioniert die Denormalisierung von Daten mit dem Microservice-Muster?

77

Ich habe gerade einen Artikel über Microservices und PaaS-Architektur gelesen . In diesem Artikel, ungefähr ein Drittel des Weges nach unten, erklärt der Autor (unter Denormalize wie Crazy ):

Refaktorieren Sie Datenbankschemata und de-normalisieren Sie alles, um eine vollständige Trennung und Partitionierung von Daten zu ermöglichen. Verwenden Sie keine zugrunde liegenden Tabellen, die mehrere Microservices bedienen. Es sollte keine gemeinsame Nutzung von zugrunde liegenden Tabellen geben, die sich über mehrere Mikrodienste erstrecken, und keine gemeinsame Nutzung von Daten. Wenn mehrere Dienste Zugriff auf dieselben Daten benötigen, sollten diese stattdessen über eine Dienst-API (z. B. eine veröffentlichte REST- oder eine Nachrichtendienstschnittstelle) gemeinsam genutzt werden.

Während dies theoretisch großartig klingt , muss es in der Praxis einige ernsthafte Hürden überwinden. Die größte davon ist , dass, oft werden Datenbanken eng gekoppelt und jeder Tisch hat eine Fremdschlüsselbeziehung mit zumindest einer anderen Tabelle. Aus diesem Grund kann es unmöglich sein, eine Datenbank in n Unterdatenbanken zu partitionieren, die von n Mikrodiensten gesteuert werden .

Ich frage also: Wie kann man bei einer Datenbank, die ausschließlich aus verwandten Tabellen besteht, diese in kleinere Fragmente (Gruppen von Tabellen) denormalisieren, damit die Fragmente von separaten Mikrodiensten gesteuert werden können?

Zum Beispiel angesichts der folgenden (eher kleinen, aber beispielhaften) Datenbank:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime
user_id

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
product_id
order_id
quantity_ordered

Verbringen Sie nicht zu viel Zeit damit, mein Design zu kritisieren, ich habe dies spontan getan. Der Punkt ist, dass es für mich logisch sinnvoll ist, diese Datenbank in drei Microservices aufzuteilen:

  1. UserService- für CRUDding-Benutzer im System; sollte letztendlich den [users]Tisch verwalten; und
  2. ProductService- für CRUDding-Produkte im System; sollte letztendlich den [products]Tisch verwalten; und
  3. OrderService- für CRUDding-Aufträge im System; sollte letztendlich die [orders]und [products_x_orders]Tabellen verwalten

Alle diese Tabellen haben jedoch Fremdschlüsselbeziehungen miteinander. Wenn wir sie denormalisieren und als Monolithen behandeln, verlieren sie ihre gesamte semantische Bedeutung:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
quantity_ordered

Jetzt kann man nicht mehr wissen, wer was, in welcher Menge oder wann bestellt hat.

Ist dieser Artikel also ein typisches akademisches Hullabaloo oder gibt es eine praktische Anwendbarkeit für diesen Denormalisierungsansatz, und wenn ja, wie sieht er aus (Bonuspunkte für die Verwendung meines Beispiels in der Antwort)?

smeeb
quelle
WRT "denormalisieren wie verrückt". . . Warum? Ich habe in dem Artikel keine konkreten Gründe gesehen.
Mike Sherrill 'Cat Recall'
21
Haben Sie dieses Problem bereits gelöst? Scheint eines der am meisten vermiedenen Probleme zu sein, wenn jemand Microservices betreibt.
Code
Hallo @ ccit-spence - bitte sehen Sie meine Antwort und lassen Sie mich wissen, was Sie denken. Ich musste diese Lösung selbst entwickeln und sie funktioniert seit einigen Monaten einwandfrei, war aber interessiert, was andere Entwickler darüber denken.
Smeeb
1
Vielleicht ist es erwähnenswert, dass sich der Artikel auf eine Datenbank bezieht, die nicht einmal Fremdschlüsseleinschränkungen unterstützt (was für mich ein Indikator dafür ist, dass der Autor Fremdschlüsseleinschränkungen nicht wertschätzt - vielleicht weiß er nicht einmal, was verloren gegangen ist? ).
Rob Bygrave

Antworten:

35

Dies ist subjektiv, aber die folgende Lösung hat für mich, mein Team und unser DB-Team funktioniert.

  • Auf der Anwendungsebene werden Microservices in semantische Funktionen zerlegt.
    • z. B. kann ein ContactDienst CRUD-Kontakte (Metadaten zu Kontakten: Namen, Telefonnummern, Kontaktinformationen usw.)
    • Beispielsweise kann ein UserDienst CRUD-Benutzer mit Anmeldeinformationen, Autorisierungsrollen usw. CRUD.
    • Beispielsweise kann ein PaymentDienst Zahlungen CRUD und unter der Haube mit einem PCI-kompatiblen Dienst eines Drittanbieters wie Stripe usw. arbeiten.
  • Auf der DB-Ebene können die Tabellen organisiert werden, jedoch möchten die Entwickler / DBs / Devops die Tabellen organisieren

Das Problem liegt in der Kaskadierung und den Servicegrenzen: Bei Zahlungen muss ein Benutzer möglicherweise wissen, wer eine Zahlung vornimmt. Anstatt Ihre Dienste wie folgt zu modellieren:

interface PaymentService {
    PaymentInfo makePayment(User user, Payment payment);
}

Modellieren Sie es so:

interface PaymentService {
    PaymentInfo makePayment(Long userId, Payment payment);
}

Auf diese Weise Unternehmen , die nur an andere Microservices gehören , werden referenzierte innerhalb eines bestimmten Dienstes von ID, nicht durch Objektreferenz. Auf diese Weise können DB-Tabellen überall Fremdschlüssel haben, aber auf der App-Ebene sind "fremde" Entitäten (dh Entitäten, die in anderen Diensten leben) über die ID verfügbar. Dadurch wird verhindert, dass die Kaskadierung von Objekten außer Kontrolle gerät, und die Dienstgrenzen werden klar abgegrenzt.

Das Problem besteht darin, dass mehr Netzwerkanrufe erforderlich sind. Wenn ich beispielsweise jeder PaymentEntität eine UserReferenz geben würde, könnte ich den Benutzer mit einem einzigen Anruf für eine bestimmte Zahlung gewinnen:

User user = paymentService.getUserForPayment(payment);

Wenn Sie jedoch das verwenden, was ich hier vorschlage, benötigen Sie zwei Anrufe:

Long userId = paymentService.getPayment(payment).getUserId();
User user = userService.getUserById(userId);

Dies kann ein Deal Breaker sein. Wenn Sie jedoch intelligent sind und Caching implementieren und ausgereifte Microservices implementieren, die bei jedem Anruf in 50 bis 100 ms reagieren, besteht kein Zweifel daran, dass diese zusätzlichen Netzwerkanrufe so gestaltet werden können, dass keine Latenz für die Anwendung entsteht.

smeeb
quelle
1
Sind alle Dienste an dieselbe Datenbank gebunden? In unserem Fall ist jeder Dienst ein eigenständiger Dienst auf einer eigenen Serverinstanz. Jeder Dienst verfügt über eine dedizierte Datenbank für diesen Dienst.
Code
7
Fremdschlüssel erhöhen die Leistung nicht. Die Indizes erhöhen die Leistung. Indizes für FK-ähnliche Spalten können jedoch in jedem Schema erstellt werden, nicht unbedingt in demselben. Beispiel: OrdersTabelle kann in einem eigenen Schema leben und eine indizierte user_idSpalte haben, die nicht "true" FK ist, sondern nur die ID des vom UsersMicroservice erhaltenen Benutzers , während usersTabelle in einem eigenen Schema lebt. Es gibt fast keinen Leistungsverlust, aber ich kann immer noch nicht verstehen, wie eine gewisse Filterung / Stapelverarbeitung erreicht werden kann. Zum Beispiel: Finden Sie alle Benutzer, die eine Bestellung haben, deren Produkt einen Preis> 100 hat.
Ruslan Stelmachenko
1
Was ich aber wirklich sagen möchte, ist: Wenn Sie bereits solche Microservices verwenden, müssen sich die Tabellen nicht in einer einzelnen Datenbank mit "echten" FKs befinden. Sie können jeweils in ihrer eigenen DB leben. Sie sollten nur Indizes für "gefälschte" FK-Spalten haben. Sie können JOINs aufgrund von Microservices bereits nicht verwenden, sodass Sie nichts verlieren, wenn Sie die DB in kleinere DBs aufteilen.
Ruslan Stelmachenko
1
Was aber, wenn ich eine Entität mit einer nicht vorhandenen FK erstelle, z. B. eine Bestellung mit einem Verweis auf einen nicht vorhandenen Kunden? Wenn ich eine gewisse Konsistenz wünscht, muss ich einige Überprüfungen durchführen, indem ich auf andere Microservices verweise. Nein?
Cecemel
2
"Sie können JOINs wegen Microservices bereits nicht verwenden ..." ... Ich denke, dies ähnelt der Aussage, dass wir uns vom Datenbankabfrageplaner (kostenbasierter Optimierer) entfernen. Das heißt, wenn wir in viele kleine DBs einbrechen, verlieren wir die Vorteile des kostenbasierten Optimierers und implementieren jetzt "JOINS" über rest / rpc usw.
Rob Bygrave,
19

Es ist in der Tat eines der Hauptprobleme bei Mikrodiensten, das in den meisten Artikeln ganz praktisch weggelassen wird. Glücklicherweise gibt es dafür Lösungen. Als Diskussionsgrundlage haben wir Tabellen, die Sie in der Frage angegeben haben. Geben Sie hier die Bildbeschreibung ein Das Bild oben zeigt, wie Tabellen in Monolithen aussehen. Nur wenige Tabellen mit Joins.


Um dies auf Microservices umzustellen, können wir einige Strategien anwenden:

Api Join

Bei dieser Strategie werden Fremdschlüssel zwischen Microservices unterbrochen und Microservice macht einen Endpunkt verfügbar, der diesen Schlüssel nachahmt. Beispiel: Der Produkt-Microservice macht den findProductByIdEndpunkt verfügbar. Order Microservice kann diesen Endpunkt anstelle von Join verwenden.

Geben Sie hier die Bildbeschreibung ein Es hat einen offensichtlichen Nachteil. Es ist langsamer.

Nur-Lese-Ansichten

In der zweiten Lösung können Sie eine Kopie der Tabelle in der zweiten Datenbank erstellen. Kopie ist schreibgeschützt. Jeder Mikrodienst kann veränderbare Operationen für seine Lese- / Schreibtabellen verwenden. Wenn es um schreibgeschützte Tabellen geht, die aus anderen Datenbanken kopiert wurden, können sie (offensichtlich) nur Lesevorgänge verwenden Geben Sie hier die Bildbeschreibung ein

Hochleistungslesen

Es ist möglich, eine hohe Leseleistung zu erzielen, indem Lösungen wie redis / memcached über der read only viewLösung eingeführt werden. Beide Seiten der Verbindung sollten in eine flache Struktur kopiert werden, die zum Lesen optimiert ist. Sie können einen völlig neuen zustandslosen Mikroservice einführen, der zum Lesen aus diesem Speicher verwendet werden kann. Obwohl es sehr mühsam zu sein scheint, ist anzumerken, dass es zusätzlich zur relationalen Datenbank eine höhere Leistung als eine monolithische Lösung bietet.


Es gibt nur wenige mögliche Lösungen. Diejenigen, die am einfachsten zu implementieren sind, haben die geringste Leistung. Die Implementierung von Hochleistungslösungen wird einige Wochen dauern.

Marcin Szymczak
quelle
Koppelt das die Leser nicht an das Schema der Ansichten, die sie lesen? Jeder einzelne Artikel über Microservices sagt, sie sollten ihren eigenen Datenspeicher haben, ihre Daten privat halten ...
Steve Chamaillard
Ja, das verbindet Leser zu einem gewissen Grad mit Produzenten, auf der positiven Seite können Leser nur einen Teil des Ereignisses lesen und kümmern sich nicht um ganze Informationen. In der Praxis benötigen Sie in so ziemlich jeder großen Anwendung einen gemeinsamen Status zwischen Microservices. Genau wie im Beispiel. Bestellung hat Produkt und Benutzer. Es ist schwer, diesen Fall ohne gemeinsame Informationen neu zu gestalten
Marcin Szymczak
5

Mir ist klar, dass dies möglicherweise keine gute Antwort ist, aber was solls. Ihre Frage war:

Wie denormalisiert man eine Datenbank, die vollständig aus verwandten Tabellen besteht, in kleinere Fragmente (Gruppen von Tabellen)?

WRT das Datenbankdesign Ich würde sagen "Sie können nicht ohne Entfernen von Fremdschlüsseln" .

Das heißt, Leute, die Microservices mit der strengen No-Shared-DB-Regel pushen, fordern Datenbankdesigner auf, Fremdschlüssel aufzugeben (und das tun sie implizit oder explizit). Wenn sie den Verlust von FKs nicht explizit angeben, fragen Sie sich, ob sie den Wert von Fremdschlüsseln tatsächlich kennen und erkennen (weil er häufig überhaupt nicht erwähnt wird).

Ich habe große Systeme gesehen, die in Gruppen von Tabellen unterteilt waren. In diesen Fällen kann es entweder A) keine FKs zwischen den Gruppen geben oder B) eine spezielle Gruppe, die "Kerntabellen" enthält, auf die FKs auf Tabellen in anderen Gruppen verweisen können.

... aber in diesen Systemen sind "Gruppen von Tabellen" oft mehr als 50 Tabellen, also nicht klein genug für die strikte Einhaltung von Microservices.

Für mich ist das andere verwandte Problem, das beim Microservice-Ansatz zur Aufteilung der Datenbank zu berücksichtigen ist, die Auswirkung dieser Berichterstellung, die Frage, wie alle Daten für die Berichterstellung und / oder das Laden in ein Data Warehouse zusammengeführt werden.

Etwas verwandt ist auch die Tendenz, integrierte DB-Replikationsfunktionen zugunsten von Messaging zu ignorieren (und wie sich die DB-basierte Replikation der Kerntabellen / des gemeinsam genutzten DDD-Kernels auf das Design auswirkt).

BEARBEITEN: (die Kosten für JOIN über REST-Aufrufe)

Wenn wir die Datenbank wie von Microservices vorgeschlagen aufteilen und FKs entfernen, verlieren wir nicht nur die erzwungene deklarative Geschäftsregel (der FK), sondern auch die Fähigkeit der Datenbank, die Verknüpfungen über diese Grenzen hinweg auszuführen.

In OLTP sind FK-Werte im Allgemeinen nicht "UX-freundlich" und wir möchten uns ihnen häufig anschließen.

Wenn wir im Beispiel die letzten 100 Bestellungen abrufen, möchten wir die Kunden-ID-Werte wahrscheinlich nicht in der UX anzeigen. Stattdessen müssen wir den Kunden ein zweites Mal anrufen, um seinen Namen zu erhalten. Wenn wir jedoch auch die Bestellpositionen wünschen, müssen wir den Produktservice erneut anrufen, um den Produktnamen, die Artikelnummer usw. anstelle der Produkt-ID anzuzeigen.

Im Allgemeinen können wir feststellen, dass wir, wenn wir das DB-Design auf diese Weise aufteilen, viele "JOIN via REST" -Aufrufe ausführen müssen. Wie hoch sind die relativen Kosten dafür?

Aktuelle Geschichte: Beispielkosten für 'JOIN via REST' im Vergleich zu DB Joins

Es gibt 4 Microservices, die viel "JOIN via REST" beinhalten. Eine Benchmark-Last für diese 4 Dienste beträgt ~ 15 Minuten . Diese 4 Mikrodienste, die in einen Dienst mit 4 Modulen für eine gemeinsam genutzte Datenbank konvertiert wurden (die Verknüpfungen zulässt), führen dieselbe Last in ~ 20 Sekunden aus .

Dies ist leider kein direkter Vergleich von Äpfeln zu Äpfeln für DB-Joins mit "JOIN via REST", da wir in diesem Fall auch von einer NoSQL-DB zu Postgres gewechselt haben.

Ist es eine Überraschung, dass "JOIN via REST" im Vergleich zu einer Datenbank mit einem kostenbasierten Optimierer usw. relativ schlecht abschneidet?

Bis zu einem gewissen Grad, wenn wir die Datenbank so aufteilen, entfernen wir uns auch vom 'kostenbasierten Optimierer' und all dem, was mit der Planung der Abfrageausführung für uns zu tun hat, zugunsten des Schreibens unserer eigenen Verknüpfungslogik (wir schreiben etwas relativ unsere eigene nicht anspruchsvoller Abfrageausführungsplan).

Rob Bygrave
quelle
0

Ich würde jeden Microservice als Objekt betrachten, und wie bei jedem ORM verwenden Sie diese Objekte, um die Daten abzurufen und dann Verknüpfungen in Ihren Code- und Abfragesammlungen zu erstellen. Microservices sollten auf ähnliche Weise behandelt werden. Der Unterschied besteht nur darin, dass jeder Microservice jeweils ein Objekt darstellt als ein vollständiger Objektbaum. Eine API-Schicht sollte diese Dienste nutzen und die Daten so modellieren, dass sie präsentiert oder gespeichert werden müssen.

Das Zurückrufen mehrerer Dienste für jede Transaktion hat keine Auswirkungen, da jeder Dienst in einem separaten Container ausgeführt wird und alle diese Aufrufe parallel ausgeführt werden können.

@ ccit-spence, ich mochte den Ansatz von Kreuzungsdiensten, aber wie kann er von anderen Diensten entworfen und genutzt werden? Ich glaube, es wird eine Art Abhängigkeit für andere Dienste schaffen.

Irgendwelche Kommentare bitte?

user1294878
quelle
1
@ user1294787 Sie haben Recht, die Möglichkeit der Kopplung besteht. Ein vollständig entkoppeltes System wird am Ende nichts bewirken. Die Dienste, die aggregiert werden, haben tatsächlich keine Kenntnis von dem Dienst, der sie aggregiert. Tatsächlich könnten Sie viele Dienste haben, die Aggregation für verschiedene Zwecke anbieten. Wenn der zu aggregierende Dienst nicht mehr benötigt wird, werden auch die Aggregationsdienste selbst nicht mehr benötigt.
Code