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:
UserService
- für CRUDding-Benutzer im System; sollte letztendlich den[users]
Tisch verwalten; undProductService
- für CRUDding-Produkte im System; sollte letztendlich den[products]
Tisch verwalten; undOrderService
- 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)?
Antworten:
Dies ist subjektiv, aber die folgende Lösung hat für mich, mein Team und unser DB-Team funktioniert.
Contact
Dienst CRUD-Kontakte (Metadaten zu Kontakten: Namen, Telefonnummern, Kontaktinformationen usw.)User
Dienst CRUD-Benutzer mit Anmeldeinformationen, Autorisierungsrollen usw. CRUD.Payment
Dienst Zahlungen CRUD und unter der Haube mit einem PCI-kompatiblen Dienst eines Drittanbieters wie Stripe usw. arbeiten.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:
Modellieren Sie es so:
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
Payment
Entität eineUser
Referenz geben würde, könnte ich den Benutzer mit einem einzigen Anruf für eine bestimmte Zahlung gewinnen:Wenn Sie jedoch das verwenden, was ich hier vorschlage, benötigen Sie zwei Anrufe:
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.
quelle
Orders
Tabelle kann in einem eigenen Schema leben und eine indizierteuser_id
Spalte haben, die nicht "true" FK ist, sondern nur die ID des vomUsers
Microservice erhaltenen Benutzers , währendusers
Tabelle 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.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. 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
findProductById
Endpunkt verfügbar. Order Microservice kann diesen Endpunkt anstelle von Join verwenden.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
Hochleistungslesen
Es ist möglich, eine hohe Leseleistung zu erzielen, indem Lösungen wie redis / memcached über der
read only view
Lö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.
quelle
Mir ist klar, dass dies möglicherweise keine gute Antwort ist, aber was solls. Ihre Frage war:
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).
quelle
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?
quelle