Seeding von Microservices-Datenbanken

10

Bei gegebenem Service A (CMS), der ein Modell steuert (Produkt, nehmen wir an, dass die einzigen Felder ID, Titel, Preis sind) und Services B (Versand) und C (E-Mails), die das gegebene Modell anzeigen müssen, wie der Ansatz aussehen soll bestimmte Modellinformationen über diese Dienste hinweg im Event-Sourcing-Ansatz zu synchronisieren? Nehmen wir an, dass sich der Produktkatalog selten ändert (aber ändert) und dass es Administratoren gibt, die sehr oft auf Daten von Sendungen und E-Mails zugreifen können (Beispielfunktionen sind: B: display titles of products the order containedund C :) display content of email about shipping that is going to be sent. Jeder der Dienste verfügt über eine eigene Datenbank.

Lösung 1

Senden Sie alle erforderlichen Informationen zum Produkt innerhalb der Veranstaltung - dies bedeutet folgende Struktur für order_placed:

{
    order_id: [guid],
    product: {
        id: [guid],
        title: 'Foo',
        price: 1000
    }
}

Bei Service B und C werden Produktinformationen im productJSON-Attribut in der ordersTabelle gespeichert

Daher werden zur Anzeige der erforderlichen Informationen nur die vom Ereignis abgerufenen Daten verwendet

Probleme : Abhängig davon, welche anderen Informationen in B und C dargestellt werden müssen, kann die Datenmenge im Ereignisfall zunehmen. B und C erfordern möglicherweise nicht dieselben Informationen zum Produkt, aber das Ereignis muss beide enthalten (es sei denn, wir trennen die Ereignisse in zwei Teile). Wenn gegebenen Daten nicht vorhanden innerhalb der gegebenen Fall ist, Code kann es nicht verwenden - wenn wir eine hinzufügen werden Farbe Option gegeben Produkt, für bestehende Aufträge in B und C, wird gegeben Produkt farblos sein , wenn wir die Ereignisse aktualisieren und sie dann erneut ausführen .

Lösung 2

Senden Sie nur eine Anleitung des Produkts innerhalb der Veranstaltung - dies bedeutet folgende Struktur für order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

Bei den Diensten B und C werden die Produktinformationen im product_idAttribut in der ordersTabelle gespeichert

Produktinformationen werden von den Diensten B und C bei Bedarf durch Ausführen eines API-Aufrufs an den A/product/[guid]Endpunkt abgerufen

Probleme : Dies macht B und C (jederzeit) von A abhängig. Wenn sich das Schema des Produkts in A ändert, müssen Änderungen an allen von ihnen abhängigen Diensten (plötzlich) vorgenommen werden.

Lösung 3

Senden Sie nur eine Produktrichtlinie innerhalb des Ereignisses - dies bedeutet die folgende Struktur für order_placed:

{
    order_id: [guid],
    product_id: [guid]
}

Bei den Diensten B und C werden die Produktinformationen in einer productsTabelle gespeichert . es gibt noch product_idauf ordersTabelle, doch die Replikation von productsDaten zwischen A, B und C; B und C enthalten möglicherweise andere Informationen zum Produkt als A.

Produktinformationen werden beim Erstellen der Services B und C festgelegt und aktualisiert, wenn sich Informationen zu Produkten ändern, indem der A/productEndpunkt aufgerufen wird (der die erforderlichen Informationen aller Produkte anzeigt) oder indem ein direkter DB-Zugriff auf A durchgeführt und die erforderlichen Produktinformationen kopiert werden Bedienung.

Probleme : Dies macht B und C von A abhängig (beim Säen). Wenn sich das Schema des Produkts in A ändert, müssen Änderungen an allen von ihnen abhängigen Diensten vorgenommen werden (beim Seeding).


Nach meinem Verständnis wäre der richtige Ansatz, Lösung 1 zu verwenden und entweder den Ereignisverlauf gemäß einer bestimmten Logik zu aktualisieren (wenn sich der Produktkatalog nicht geändert hat und wir die anzuzeigende Farbe hinzufügen möchten, können wir den Verlauf sicher aktualisieren, um den aktuellen Status zu erhalten von Produkten und füllen Sie fehlende Daten innerhalb der Ereignisse) oder sorgen Sie für das Nichtvorhandensein bestimmter Daten (wenn sich der Produktkatalog geändert hat und wir die anzuzeigende Farbe hinzufügen möchten, können wir nicht sicher sein, ob zu diesem Zeitpunkt in der Vergangenheit ein bestimmtes Produkt angegeben wurde hatte eine Farbe oder nicht - wir können davon ausgehen, dass alle Produkte im vorherigen Katalog schwarz waren und durch Aktualisierung von Ereignissen oder Code berücksichtigt werden)

eithed
quelle
In Bezug auf updating event history- Bei der Beschaffung von Ereignissen ist die Ereignishistorie Ihre Quelle der Wahrheit und sollte niemals geändert werden, sondern nur vorwärts gehen. Wenn sich Ereignisse ändern, können Sie die Ereignisversionierung oder ähnliche Lösungen verwenden. Wenn Sie Ihre Ereignisse jedoch bis zu einem bestimmten Zeitpunkt wiedergeben, sollte der Status der Daten so sein, wie er zu diesem Zeitpunkt war.
Nein,
In Bezug auf das Speichern der Daten (Schemas usw.) für die Abfrage und das Hinzufügen / Entfernen von Feldern usw. haben wir uns bei cosmosDB verwendet, um die Daten in JSON zu speichern, wie es zu diesem Zeitpunkt ist. Das einzige, was dann versioniert werden muss, sind Ereignisse und / oder Befehle. Sie müssen auch Endpunktverträge aktualisieren und Objekte bewerten, die die Daten enthalten, die auf Anfragen von einem Client (Web, Mobile usw.) antworten. Ältere Daten, die kein Feld haben, haben einen Standardwert oder ein Leerzeichen, was je nach Unternehmen passt. Der Ereignisverlauf bleibt jedoch erhalten und wird nur fortgesetzt.
Nein,
@Nope by updating event historyIch meine: Gehen Sie alle Ereignisse durch und kopieren Sie sie von einem Stream (v1) in einen anderen Stream (v2), um ein konsistentes Ereignisschema beizubehalten.
Am
Abgesehen davon möchten Sie im Bereich Handel / E-Commerce möglicherweise den angegebenen Preis erfassen, da sich die Preise häufig ändern. Ein dem Benutzer angezeigter Preis kann zum Zeitpunkt der Erfassung der tatsächlich bestellten Bestellung unterschiedlich sein. Es gibt eine Reihe von Möglichkeiten, um das Problem zu lösen, aber es sollte in Betracht gezogen werden.
CPerson
@CPerson yup - Preis kann eines der Attribute sein, die innerhalb des Ereignisses selbst übergeben werden. Andererseits kann die URL für das Bild innerhalb des Ereignisses (das die Absicht von darstellt display image at the point when purchase was made) oder nicht (das die Absicht von darstellt display current image as it within catalog) vorhanden sein
entweder

Antworten:

3

Lösung 3 kommt der richtigen Idee sehr nahe.

Eine Art und Weise zu denken , über diese: B und C jeweils Caching „lokale“ Kopien der Daten , die sie benötigen. Bei B (und ebenfalls bei C) verarbeitete Nachrichten verwenden die lokal zwischengespeicherten Informationen. Ebenso werden Berichte unter Verwendung der lokal zwischengespeicherten Informationen erstellt.

Die Daten werden über eine stabile API von der Quelle in die Caches repliziert. B und C müssen nicht einmal dieselbe API verwenden - sie verwenden das für ihre Anforderungen geeignete Abrufprotokoll. Tatsächlich definieren wir einen Vertrag - Protokoll und Nachrichtenschema -, der den Anbieter und den Verbraucher einschränkt. Dann kann jeder Verbraucher für diesen Vertrag mit jedem Lieferanten verbunden werden. Rückwärts inkompatible Änderungen erfordern einen neuen Vertrag.

Die Dienste wählen die für ihre Anforderungen geeignete Strategie zur Ungültigmachung des Caches aus. Dies kann bedeuten, dass Änderungen regelmäßig aus der Quelle abgerufen werden oder als Reaktion auf eine Benachrichtigung, dass sich die Dinge möglicherweise geändert haben, oder sogar "on demand" - als Durchlesecache fungieren und auf die gespeicherte Kopie der Daten zurückgreifen, wenn Die Quelle ist nicht verfügbar.

Dies gibt Ihnen "Autonomie" in dem Sinne, dass B und C weiterhin Geschäftswert liefern können, wenn A vorübergehend nicht verfügbar ist.

Empfohlene Lektüre: Daten von außen, Daten von innen , Pat Helland 2005.

VoiceOfUnreason
quelle
Ja, ich stimme voll und ganz dem zu, was Sie hier geschrieben haben, und Lösung 3 ist die von mir angewendete goto-Lösung. Es handelt sich jedoch nicht um den Event-Sourcing-Ansatz, da wir dies nicht unbedingt tun möchten, wenn wir die Ereignisse erneut abspielen Verwenden Sie den aktuellen Status des Produkts. Wir wollen den Zustand so verwenden, wie er zum Zeitpunkt des Ereignisses war. Dies kann natürlich in Ordnung sein (abhängig von den Geschäftsanforderungen). Wenn wir jedoch Änderungen am Katalog
nachverfolgen
1
Ich denke, Sie haben es mit Lösung Nr. 3. Wenn Sie eine Wiederholungskonsistenz mit dem Katalog benötigen, geben Sie auch die Ereignisquelle an. Sie müssen nur erneut spielen, wenn Sie erneut starten, was wahrscheinlich beim Start der Fall ist. Sobald Sie aktiv sind, müssen Sie sich nur noch neue Ereignisse ansehen, sodass die Datenmenge wahrscheinlich kein wirkliches Problem darstellt. Selbst dann haben Sie (falls erforderlich) die Möglichkeit, Checkpoints zu verwenden, dh "Hier ist der Status ab Ereignis 1.000". Nehmen Sie dies und müssen Sie jetzt nur noch das Ereignis 1.001 bis zum aktuellen Verlauf anstelle des gesamten Verlaufs wiedergeben .
Mike B.
2

In der Informatik gibt es zwei schwierige Dinge , und eine davon ist die Ungültigmachung des Caches.

Lösung 2 ist absolut meine Standardposition, und Sie sollten generell nur in Betracht ziehen, das Caching zu implementieren, wenn Sie auf eines der folgenden Szenarien stoßen:

  1. Der API-Aufruf von Service A verursacht Leistungsprobleme.
  2. Die Kosten für Service A, der nicht verfügbar ist und die Daten nicht abrufen kann, sind für das Unternehmen von Bedeutung.

Leistungsprobleme sind wirklich der Haupttreiber. Es gibt viele Möglichkeiten zur Lösung von Problem Nr. 2, bei denen kein Caching erforderlich ist, z. B. die Sicherstellung, dass Service A hoch verfügbar ist.

Das Zwischenspeichern erhöht die Komplexität eines Systems erheblich und kann zu schwer zu begründenden Randfällen und zu schwer zu replizierenden Fehlern führen. Sie müssen auch das Risiko verringern, veraltete Daten bereitzustellen, wenn neuere Daten vorhanden sind. Dies kann aus geschäftlicher Sicht viel schlimmer sein, als (zum Beispiel) die Meldung anzuzeigen, dass "Service A nicht verfügbar ist - versuchen Sie es später erneut."

Aus diesem ausgezeichneten Artikel von Udi Dahan:

Diese Abhängigkeiten schleichen sich langsam an Sie heran, binden Ihre Schnürsenkel zusammen, verlangsamen allmählich das Entwicklungstempo und untergraben die Stabilität Ihrer Codebasis, wenn Änderungen an einem Teil des Systems andere Teile beschädigen. Es ist ein langsamer Tod durch tausend Schnitte, und als Ergebnis ist niemand genau sicher, welche große Entscheidung wir getroffen haben, die dazu geführt hat, dass alles so schlecht gelaufen ist.

Wenn Sie zu einem bestimmten Zeitpunkt Produktdaten abfragen müssen, sollte dies so gehandhabt werden, wie die Daten in der Produktdatenbank gespeichert sind (z. B. Start- / Enddaten), und in der API deutlich verfügbar gemacht werden (Datum des Inkrafttretens muss erforderlich sein) eine Eingabe für den API-Aufruf sein, um die Daten abzufragen).

Phil Sandler
quelle
1
@SavvasKleanthous "Das Netzwerk ist zuverlässig" ist einer der Irrtümer des verteilten Rechnens. Die Antwort auf diesen Irrtum sollte jedoch nicht darin bestehen, "jedes Datenbit von jedem Dienst in jedem anderen Dienst zwischenzuspeichern" (mir ist klar, dass dies ein bisschen hyperbolisch ist). Erwarten Sie, dass ein Dienst möglicherweise nicht verfügbar ist, und behandeln Sie dies als Fehlerbedingung. Wenn Sie eine seltene Situation haben, in der der Ausfall von Service A erhebliche geschäftliche Auswirkungen hat, sollten Sie (sorgfältig!) Andere Optionen in Betracht ziehen.
Phil Sandler
1
@SavvasKleanthous berücksichtigt auch (wie ich in meiner Antwort erwähnt habe), dass die Rückgabe veralteter Daten in vielen Fällen viel schlimmer sein kann, als einen Fehler auszulösen .
Phil Sandler
1
@eithed Ich bezog mich auf diesen Kommentar: "Wenn wir jedoch Änderungen am Katalog verfolgen möchten, müssen diese auch mit Event-Sourcing versehen werden." In jedem Fall haben Sie die richtige Idee - der Produktservice sollte für die Verfolgung von Änderungen im Laufe der Zeit verantwortlich sein, nicht die nachgelagerten Services.
Phil Sandler
1
Das Speichern von Daten, die Sie beobachten, weist zwar einige Ähnlichkeiten mit dem Caching auf, weist jedoch nicht dieselben Probleme auf. Insbesondere ist eine Ungültigmachung nicht erforderlich. Sie erhalten die neue Version der Daten, wenn es passiert. Was Sie erleben, ist verzögerte Konsistenz. Selbst bei Verwendung von Webanfragen besteht jedoch ein Fenster der Inkonsistenz (wenn auch ein winziges).
Savvas Kleanthous
1
@SavvasKleanthous Auf jeden Fall geht es mir nicht darum, Probleme zu lösen, die noch nicht existieren, insbesondere nicht um Lösungen, die ihre eigenen Probleme und Risiken mit sich bringen. Option 2 ist die einfachste Lösung und sollte die Standardauswahl sein, bis sie nicht den Geschäftsanforderungen entspricht . Wenn Sie der Meinung sind, dass die Auswahl der einfachsten Lösung, die funktionieren kann, (wie Sie sagen) "wirklich schlecht" ist, dann sind wir uns einfach nicht einig.
Phil Sandler
2

Es ist sehr schwer zu sagen, dass eine Lösung besser ist als die andere. Die Auswahl einer der Lösungen Nr. 2 und Nr. 3 hängt von anderen Faktoren ab (Cache-Dauer, Konsistenztoleranz, ...).

Meine 2 Cent:

Die Ungültigmachung des Caches mag schwierig sein, aber in der Problemstellung wird erwähnt, dass sich der Produktkatalog selten ändert. Diese Tatsache macht Produktdaten zu einem guten Kandidaten für das Caching

Lösung Nr. 1 (NOK)

  • Daten werden systemübergreifend dupliziert

Lösung 2 (OK)

  • Bietet starke Konsistenz
  • Funktioniert nur, wenn der Produktservice hoch verfügbar ist und eine gute Leistung bietet
  • Wenn der E-Mail-Dienst eine Zusammenfassung erstellt (mit vielen Produkten), kann die Gesamtantwortzeit länger sein

Lösung Nr. 3 (komplex, aber bevorzugt)

  • Bevorzugen Sie den API-Ansatz anstelle des direkten DB-Zugriffs, um Produktinformationen abzurufen
  • Ausfallsichere Dienste werden nicht beeinträchtigt, wenn der Produktservice ausfällt
  • Konsumentenanwendungen (Versand- und E-Mail-Dienste) rufen Produktdetails sofort nach Veröffentlichung eines Ereignisses ab. Die Möglichkeit, dass der Produktservice innerhalb dieser wenigen Millisekunden ausfällt, ist sehr gering.
Sudhir
quelle
1

Im Allgemeinen würde ich Option 2 wegen der zeitlichen Kopplung zwischen diesen beiden Diensten dringend empfehlen (es sei denn, die Kommunikation zwischen diesen Diensten ist sehr stabil und nicht sehr häufig). Zeitliche Kopplung ist das, was Sie beschreiben this makes B and C dependant upon A (at all times), und bedeutet, dass B und C ihre Funktion nicht erfüllen können, wenn A von B oder C aus nicht erreichbar ist.

Ich persönlich glaube, dass beide Optionen 1 und 3 Situationen haben, in denen sie gültige Optionen sind.

Wenn die Kommunikation zwischen A und B & C so hoch ist oder die Datenmenge, die für das Ereignis benötigt wird, groß genug ist, um es zu einem Problem zu machen, ist Option 3 die beste Option, da die Belastung des Netzwerks viel geringer ist Die Latenz der Vorgänge nimmt mit abnehmender Nachrichtengröße ab. Weitere hier zu berücksichtigende Bedenken sind:

  1. Vertragsstabilität: Wenn sich der Vertrag zum Verlassen von Nachricht A häufig ändert, führt das Einfügen vieler Eigenschaften in die Nachricht zu vielen Änderungen bei den Verbrauchern. In diesem Fall glaube ich jedoch, dass dies kein großes Problem ist, weil:
    1. Sie haben erwähnt, dass System A ein CMS ist. Dies bedeutet, dass Sie an einer stabilen Domain arbeiten und ich glaube nicht, dass Sie häufige Änderungen sehen werden
    2. Da es sich bei B und C um Versand und E-Mail handelt und Sie Daten von A erhalten, werden Sie wahrscheinlich additive Änderungen erfahren, anstatt diese zu beschädigen. Diese können sicher hinzugefügt werden, wenn Sie sie ohne Nacharbeit entdecken.
  2. Kopplung: Hier gibt es sehr wenig bis gar keine Kopplung. Erstens, da die Kommunikation über Nachrichten erfolgt, gibt es keine andere Kopplung zwischen den Diensten als eine kurze zeitliche während des Seedings der Daten und dem Vertrag dieser Operation (was keine Kopplung ist, die Sie vermeiden können oder sollten).

Option 1 würde ich allerdings nicht ablehnen. Es gibt das gleiche Maß an Kopplung, aber in Bezug auf die Entwicklung sollte es einfach sein (keine besonderen Maßnahmen erforderlich), und die Stabilität der Domäne sollte bedeuten, dass sich diese nicht oft ändern (wie ich bereits erwähnt habe).

Eine andere Option, die ich vorschlagen würde, ist eine geringfügige Abweichung von 3, bei der der Prozess nicht während des Startvorgangs ausgeführt wird, sondern stattdessen ein Ereignis "ProductAdded" und "ProductDetailsChanged" für B und C beobachtet wird, wenn sich der Produktkatalog ändert Dies würde Ihre Bereitstellungen beschleunigen (und es einfacher machen, ein Problem / einen Fehler zu beheben, wenn Sie einen finden).


Bearbeiten 2020-03-03

Ich habe eine bestimmte Reihenfolge von Prioritäten bei der Festlegung des Integrationsansatzes:

  1. Was kostet die Konsistenz? Können wir einige Millisekunden Inkonsistenz zwischen Dingen akzeptieren, die sich in A geändert haben und die sich in B & C widerspiegeln?
  2. Benötigen Sie zeitpunktbezogene Abfragen (auch zeitliche Abfragen genannt)?
  3. Gibt es eine Wahrheitsquelle für die Daten? Ein Dienst, dem sie gehören und der als vorgelagert gilt?
  4. Wenn es einen Eigentümer / eine einzige Quelle der Wahrheit gibt, ist das stabil? Oder erwarten wir häufige Veränderungen?

Wenn die Kosten für Inkonsistenzen hoch sind (im Grunde müssen die Produktdaten in A so schnell wie möglich mit den in B und C zwischengespeicherten Produkten konsistent sein), können Sie es nicht vermeiden, die Unverfügbarkeit zu akzeptieren und eine synchrone Anfrage zu stellen (wie ein Web) / Rest Request) von B & C nach A, um die Daten abzurufen. Sei vorsichtig! Dies bedeutet immer noch nicht transaktionskonsistent, sondern minimiert nur die Fenster für Inkonsistenzen. Wenn Sie unbedingt sofort konsistent sein müssen, müssen Sie Ihre Servicegrenzen erweitern. Allerdings habe ich sehr fest davon überzeugt , dies kein Problem sein sollte. Aus Erfahrung ist es tatsächlich äußerst selten, dass das Unternehmen einige Sekunden Inkonsistenz nicht akzeptieren kann, sodass Sie nicht einmal synchrone Anforderungen stellen müssen.

Wenn Sie zu bestimmten Zeitpunkten Abfragen benötigen (die ich in Ihrer Frage nicht bemerkt und daher oben möglicherweise nicht berücksichtigt habe, möglicherweise zu Unrecht), sind die Kosten für die Wartung dieser nachgelagerten Dienste so hoch (Sie müssten sie duplizieren interne Ereignisprojektionslogik in allen nachgelagerten Diensten), die die Entscheidung klar macht: Sie sollten das Eigentum A überlassen und A ad-hoc über eine Webanforderung (oder ähnliches) abfragen, und A sollte die Ereignisbeschaffung verwenden, um alle Ereignisse abzurufen, von denen Sie wussten zu der Zeit, um an den Staat zu projizieren und es zurückzugeben. Ich denke, dies ist Option 2 (wenn ich es richtig verstanden habe?), Aber die Kosten sind so hoch, dass die zeitliche Kopplung zwar besser ist als die Wartungskosten für duplizierte Ereignisse und die Projektionslogik.

Wenn Sie keinen Zeitpunkt benötigen und es keinen eindeutigen Eigentümer der Daten gibt (was ich in meiner ersten Antwort aufgrund Ihrer Frage angenommen habe), wäre es ein sehr vernünftiges Muster, Darstellungen zu halten des Produkts in jedem Service separat. Wenn Sie die Daten für Produkte aktualisieren, aktualisieren Sie A, B und C parallel, indem Sie parallele Webanforderungen an jede einzelne senden, oder Sie haben eine Befehls-API, die mehrere Befehle an A, B und C sendet. B & C verwendet deren lokale Version der Daten, um ihre Arbeit zu erledigen, die veraltet sein kann oder nicht. Dies ist keine der oben genannten Optionen (obwohl es nahe an Option 3 liegen könnte), da die Daten in A, B und C unterschiedlich sein können und das "Ganze" des Produkts eine Zusammensetzung aller drei Daten sein kann Quellen.

Zu wissen, ob die Quelle der Wahrheit ein stabiler Vertrag ist, ist nützlich, da Sie damit die Domänen- / internen Ereignisse (oder Ereignisse, die Sie in Ihrer Ereignisbeschaffung als Speichermuster in A speichern) für die Integration zwischen A und den Diensten B und C verwenden können. Wenn der Vertrag stabil ist, können Sie über die Domain-Ereignisse integrieren. Dann haben Sie jedoch ein zusätzliches Problem, wenn häufig Änderungen vorgenommen werden oder der Nachrichtenvertrag groß genug ist, um den Transport zu einem Problem zu machen.

Wenn Sie einen eindeutigen Eigentümer mit einem Vertrag haben, von dem erwartet wird, dass er stabil ist, sind Option 1 die besten Optionen. Eine Bestellung würde alle notwendigen Informationen enthalten und dann würden B und C ihre Funktion unter Verwendung der Daten im Ereignis ausführen.

Wenn sich der Vertrag nach Ihrer Option 3 ändern oder häufig brechen kann, ist es eine bessere Option, auf Webanfragen zum Abrufen von Produktdaten zurückzugreifen, da es viel einfacher ist, mehrere Versionen zu verwalten. Also würde B eine Anfrage für Version 3 des Produkts stellen.

Savvas Kleanthous
quelle
Ja, ich stimme zu. Während ProductAddedoder ProductDetailsChangedum die Komplexität der Verfolgung von Produktkatalogänderungen zu erhöhen, müssen wir diese Daten in irgendeiner Weise zwischen Datenbanken synchronisieren, falls Ereignisse wiedergegeben werden und wir auf Katalogdaten aus der Vergangenheit zugreifen müssen.
Am
@eithed Ich habe die Antwort aktualisiert, um einige meiner Annahmen zu erweitern.
Savvas Kleanthous