Freigeben über


Cloudbasierte Datenmuster

Tipp

Diese Inhalte sind ein Auszug aus dem E-Book „Architecting Cloud Native .NET Applications for Azure“, verfügbar in der .NET-Dokumentation oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.

Cloud Native .NET apps for Azure eBook cover thumbnail.

Wie wir in diesem Buch gesehen haben, verändert ein cloudnativer Ansatz die Art und Weise, wie Sie Anwendungen entwerfen, bereitstellen und verwalten. Es verändert auch die Art und Weise, wie Sie Daten verwalten und speichern.

In Abbildung 5-1 sind die Unterschiede dargestellt.

Data storage in cloud-native applications

Abbildung 5–1. Datenverwaltung in cloudnativen Anwendungen

Erfahrene Entwickler werden die Architektur auf der linken Seite von Abbildung 5-1 leicht erkennen. In dieser monolithischen Anwendung sind die Komponenten der Geschäftsdienste in einer freigegebenen Dienstebene zusammengefasst und nutzen Daten aus einer einzelnen relationalen Datenbank.

In vielerlei Hinsicht sorgt eine einzelne Datenbank für eine einfache Datenverwaltung. Die Abfrage von Daten über mehrere Tabellen hinweg ist ganz einfach. Änderungen an Daten werden zusammen aktualisiert oder sie werden alle zurückgesetzt. ACID-Transaktionen garantieren eine starke und sofortige Konsistenz.

Bei der Entwicklung von cloudnativen Lösungen verfolgen wir einen anderen Ansatz. Beachten Sie auf der rechten Seite von Abbildung 5-1, wie die Geschäftsfunktionalität in kleine, unabhängige Microservices unterteilt wird. Jeder Microservice kapselt eine bestimmte Geschäftsfunktion und seine eigenen Daten. Die monolithische Datenbank wird in ein verteiltes Datenmodell mit vielen kleineren Datenbanken zerlegt, die jeweils auf einen Microservice ausgerichtet sind. Wenn sich der Rauch lichtet, kommen wir mit einem Entwurf zum Vorschein, der eine Datenbank pro Microservice bereitstellt.

Warum Datenbank pro Microservice?

Diese Datenbank pro Microservice bietet viele Vorteile, vor allem für Systeme, die sich schnell weiterentwickeln und eine massive Skalierung unterstützen müssen. Dieses Modell ermöglicht Folgendes:

  • Domänendaten werden im Dienst gekapselt.
  • Das Datenschema kann weiterentwickelt werden, ohne dass sich dies direkt auf andere Dienste auswirkt.
  • Jeder Datenspeicher kann unabhängig skaliert werden.
  • Ein Datenspeicherfehler in einem Dienst wirkt sich nicht direkt auf andere Dienste aus.

Die Trennung der Daten ermöglicht es jedem Microservice, den Datenspeichertyp zu implementieren, der für seine Workload, seinen Speicherbedarf und seine Lese-/Schreibmuster am besten geeignet ist. Zur Auswahl stehen relationale, Dokument-, Schlüssel-Wert- und sogar graphbasierte Datenspeicher.

Abbildung 5-2 zeigt das Prinzip der mehrsprachigen Persistenz in einem cloudnativen System.

Polyglot data persistence

Abbildung 5-2. Mehrsprachige Datenpersistenz

Beachten Sie in der vorherigen Abbildung, wie jeder Microservice einen anderen Datenspeichertyp unterstützt.

  • Der Produktkatalog-Microservice nutzt eine relationale Datenbank, um die reichhaltige relationale Struktur der zugrunde liegenden Daten aufzunehmen.
  • Der Warenkorb-Microservice nutzt einen verteilten Cache, der seinen einfachen Schlüssel-Wert-Datenspeicher unterstützt.
  • Der Bestellung-Microservice verwendet sowohl eine NoSQL-Dokumentendatenbank für Schreibvorgänge als auch einen stark denormalisierten Schlüssel-Wert-Speicher, um hohe Mengen an Lesevorgängen zu bewältigen.

Während relationale Datenbanken für Microservices mit komplexen Daten weiterhin relevant sind, haben NoSQL-Datenbanken erheblich an Popularität gewonnen. Sie bieten massive Skalierung und Hochverfügbarkeit. Ihre schemalose Natur erlaubt es Entwicklern, sich von einer Architektur aus typisierten Datenklassen und ORMs zu verabschieden, die Änderungen teuer und zeitaufwendig machen. Wir behandeln NoSQL-Datenbanken später in diesem Kapitel.

Die Kapselung von Daten in separaten Microservices kann zwar die Flexibilität, Leistung und Skalierbarkeit erhöhen, birgt aber auch viele Herausforderungen. Im nächsten Abschnitt erörtern wir diese Herausforderungen zusammen mit Mustern und Methoden, die helfen, sie zu überwinden.

Dienstübergreifende Abfragen

Während Microservices unabhängig sind und sich auf bestimmte Funktionen wie Bestand, Versand oder Bestellung konzentrieren, erfordern sie häufig eine Integration in andere Microservices. Oft geht es bei der Integration darum, dass ein Microservice einen anderen nach Daten abfragt. Abbildung 5-3 zeigt das Szenario.

Querying across microservices

Abbildung 5-3. Abfragen über Microservices

In der vorangegangenen Abbildung sehen wir einen Warenkorb-Microservice, der einen Artikel zum Warenkorb eines Benutzers hinzufügt. Der Datenspeicher für diesen Microservice enthält zwar Warenkorb- und Positionsdaten, aber keine Produkt- oder Preisdaten. Stattdessen gehören diese Datenelemente zu den Microservices für Katalog und Preisgestaltung. Dieser Aspekt stellt ein Problem dar. Wie kann der Warenkorb-Microservice ein Produkt zum Warenkorb des Benutzers hinzufügen, wenn er weder über Produkt- noch Preisdaten in seiner Datenbank verfügt?

Eine Option, die in Kapitel 4 besprochen wird, ist ein direkter HTTP-Aufruf vom Warenkorb zu den Microservices für Katalog und Preisgestaltung. In Kapitel 4 haben wir jedoch gesagt, dass synchrone HTTP-Aufrufe Microservices aneinander koppeln, was ihre Autonomie einschränkt und ihre architektonischen Vorteile schmälert.

Sie könnten auch ein Anforderung-Antwort-Muster mit separaten Eingangs- und Ausgangswarteschlangen für jeden Dienst implementieren. Dieses Muster ist jedoch kompliziert und erfordert die Korrelation von Anforderungs- und Antwortnachrichten. Zwar werden die Back-End-Microservice-Aufrufe entkoppelt, aber der aufrufende Dienst muss immer noch synchron auf den Abschluss des Aufrufs warten. Eine Überlastung des Netzwerks, vorübergehende Fehler oder ein überlasteter Microservice können zu zeitintensiven und sogar fehlerhaften Vorgängen führen.

Ein weithin akzeptiertes Muster zum Entfernen von dienstübergreifenden Abhängigkeiten ist das in Abbildung 5-4 dargestellte Muster für materialisierte Sichten.

Materialized view pattern

Abbildung 5-4. Muster für materialisierte Sichten

Mit diesem Muster platzieren Sie eine lokale Datentabelle (ein so genanntes Lesemodell) in den Warenkorb-Dienst. Diese Tabelle enthält eine denormalisierte Kopie der Daten, die von den Microservices für Produkte und Preisgestaltung benötigt werden. Durch das Kopieren der Daten direkt in den Warenkorb-Microservice entfallen die teuren dienstübergreifenden Aufrufe. Mit den lokal für den Dienst vorgehaltenen Daten verbessern Sie die Antwortzeit und Zuverlässigkeit des Diensts. Außerdem macht eine eigene Kopie der Daten den Warenkorb-Dienst resilienter. Sollte der Katalog-Dienst nicht mehr verfügbar sein, hätte dies keine direkten Auswirkungen auf den Warenkorb-Dienst. Der Warenkorb kann mit den Daten aus seinem eigenen Speicher weiterarbeiten.

Der Haken bei dieser Vorgehensweise ist, dass Sie jetzt doppelte Daten in Ihrem System besitzen. Die strategische Duplizierung von Daten in cloudnativen Systemen ist jedoch eine gängige Praxis und wird nicht als Anti-Muster oder schlechte Vorgehensweise angesehen. Denken Sie daran, dass nur ein einziger Dienst ein Dataset besitzen kann und die Autorität darüber hat. Sie müssen die Lesemodelle synchronisieren, wenn das Datensatzsystem aktualisiert wird. Die Synchronisierung erfolgt in der Regel über asynchrones Messaging mit einem Veröffentlichen/Abonnieren-Muster, wie in Abbildung 5.4 dargestellt.

Verteilte Transaktionen

Die Abfrage von Daten über Microservices hinweg ist schon schwierig, aber die Implementierung einer Transaktion über mehrere Microservices hinweg ist noch komplexer. Die Herausforderung, die Datenkonsistenz über unabhängige Datenquellen in verschiedenen Microservices hinweg aufrechtzuerhalten, ist nicht zu unterschätzen. Das Fehlen von verteilten Transaktionen in cloudnativen Anwendungen bedeutet, dass Sie verteilte Transaktionen programmgesteuert verwalten müssen. Sie bewegen sich von einem System der unmittelbaren Konsistenz zu einem System der letztendlichen Konsistenz.

Abbildung 5-5 zeigt das Problem.

Transaction in saga pattern

Abbildung 5-5. Implementieren einer Transaktion über Microservices hinweg

In der vorherigen Abbildung nehmen fünf unabhängige Microservices an einer verteilten Transaktion teil, die eine Bestellung erstellt. Jeder Microservice verwaltet seinen eigenen Datenspeicher und implementiert eine lokale Transaktion für seinen Speicher. Um die Bestellung zu erstellen, muss die lokale Transaktion für jeden einzelnen Microservice erfolgreich sein, oder alle müssen den Vorgang abbrechen und einen Rollback ausführen. Während integrierte Transaktionsunterstützung in jedem der Microservices verfügbar ist, gibt es keine Unterstützung für eine verteilte Transaktion, die sich über alle fünf Dienste erstreckt, um die Daten konsistent zu halten.

Stattdessen müssen Sie diese verteilte Transaktion programmgesteuert erstellen.

Ein beliebtes Muster zum Hinzufügen von verteilter Transaktionsunterstützung ist das Saga-Muster. Es wird implementiert, indem lokale Transaktionen programmgesteuert gruppiert und nacheinander aufgerufen werden. Wenn eine der lokalen Transaktionen scheitert, bricht das Saga-Muster den Vorgang ab und ruft eine Reihe von Ausgleichstransaktionen auf. Die Ausgleichstransaktionen machen die von den vorangegangenen lokalen Transaktionen vorgenommenen Änderungen rückgängig und stellen die Datenkonsistenz wieder her. Abbildung 5-6 zeigt eine fehlerhafte Transaktion mit dem Saga-Muster.

Roll back in saga pattern

Abbildung 5–6. Ausführen eines Rollbacks für eine Transaktion

In der vorherigen Abbildung war der Vorgang Bestand aktualisieren im Bestand-Microservice nicht erfolgreich. Das Saga-Muster ruft eine Reihe von Ausgleichstransaktionen (in rot) auf, um die Bestandszahlen anzupassen, die Zahlung und die Bestellung zu stornieren und die Daten für jeden Microservice wieder in einen konsistenten Zustand zu bringen.

Saga-Muster werden in der Regel als eine Reihe zusammenhängender Ereignisse koordiniert oder als eine Reihe zusammenhängender Befehle orchestriert. In Kapitel 4 haben wir das Dienstaggregatormuster erörtert, das die Grundlage für eine orchestrierte Saga-Implementierung bilden würde. Darüber hinaus haben wir auch die Ereignisverwaltung und die Themen Azure Service Bus und Azure Event Grid besprochen, die die Grundlage für eine koordinierte Saga-Implementierung bilden würden.

Daten mit hohem Volumen

Große cloudnative Anwendungen unterstützen oft große Datenmengen. In diesen Szenarien können herkömmliche Datenspeichertechniken zu Engpässen führen. Bei komplexen Systemen, die im großen Stil bereitgestellt werden, können sowohl Command and Query Responsibility Segregation (CQRS) als auch Ereignissourcing die Leistung der Anwendung verbessern.

CQRS-Architektur

CQRS ist ein Architekturmuster, das zur Maximierung von Leistung, Skalierbarkeit und Sicherheit beitragen kann. Das Muster trennt Vorgänge, die Daten lesen, von solchen, die Daten schreiben.

Für normale Szenarien werden dasselbe Entitätsmodell und dasselbe Datenrepositoryobjekt sowohl für Lese- als auch für Schreibvorgänge verwendet.

Ein Szenario mit hohem Datenvolumen kann jedoch von getrennten Modellen und Datentabellen für Lese- und Schreibvorgänge profitieren. Um die Leistung zu verbessern, könnte der Lesevorgang eine stark denormalisierte Darstellung der Daten abfragen, um teure, sich wiederholende Tabellenjoins und Tabellensperren zu vermeiden. Der Schreibvorgang, der als Befehl bezeichnet wird, würde anhand einer vollständig normalisierten Darstellung der Daten aktualisiert, die Konsistenz garantiert. Anschließend müssen Sie einen Mechanismus implementieren, um beide Darstellungen synchron zu halten. Wenn die Schreibtabelle geändert wird, veröffentlicht sie in der Regel ein Ereignis, das die Änderung in der Lesetabelle repliziert.

Abbildung 5-7 zeigt eine Implementierung des CQRS-Musters.

Command and Query Responsibility Segregation

Abbildung 5-7. CQRS-Implementierung

In der vorherigen Abbildung sind separate Befehls- und Abfragemodelle implementiert. Jeder Schreibvorgang wird im Schreibspeicher gespeichert und dann an den Lesespeicher weitergegeben. Achten Sie genau darauf, wie der Datenweitergabeprozess nach dem Prinzip der letztendlichen Konsistenz funktioniert. Das Lesemodell wird schließlich mit dem Schreibmodell synchronisiert, aber es kann zu einer gewissen Verzögerung kommen. Im nächsten Abschnitt wird die letztendliche Konsistenz erörtert.

Durch diese Trennung können Lese- und Schreibvorgänge unabhängig voneinander skaliert werden. Lesevorgänge verwenden ein für Abfragen optimiertes Schema, während die Schreibvorgänge ein für Aktualisierungen optimiertes Schema verwenden. Leseabfragen beziehen sich auf denormalisierte Daten, während komplexe Geschäftslogik auf das Schreibmodell angewendet werden kann. Außerdem können Sie Schreibvorgänge stärker absichern als Lesevorgänge.

Die Implementierung von CQRS kann die Leistung von Anwendungen für cloudnative Dienste verbessern. Allerdings führt dies zu einem komplexeren Entwurf. Wenden Sie dieses Prinzip sorgfältig und strategisch auf die Teile Ihrer cloudnativen Anwendung an, die davon profitieren werden. Weitere Informationen zu CQRS finden Sie in dem Microsoft-Buch .NET-Microservices: Architektur für .NET-Containeranwendungen.

Ereignissourcing

Ein weiterer Ansatz zur Optimierung von Szenarien mit hohem Datenaufkommen ist das Ereignissourcing.

Ein System speichert normalerweise den aktuellen Zustand einer Datenentität. Wenn ein Benutzer z. B. seine Telefonnummer ändert, wird der Datensatz des Kunden mit der neuen Nummer aktualisiert. Wir kennen immer den aktuellen Zustand einer Datenentität, aber jede Aktualisierung überschreibt den vorherigen Zustand.

In den meisten Fällen funktioniert dieses Modell einwandfrei. In Systemen mit hohem Datenvolumen kann jedoch der Mehraufwand durch Transaktionssperren und häufige Aktualisierungsvorgänge die Leistung und Reaktionsfähigkeit der Datenbank beeinträchtigen und die Skalierbarkeit einschränken.

Das Ereignissourcing verwendet einen anderen Ansatz zum Erfassen von Daten. Jeder Vorgang, der sich auf Daten auswirkt, wird in einem Ereignisspeicher beibehalten. Anstatt den Zustand eines Datensatzes zu aktualisieren, fügen wir jede Änderung an eine sequenzielle Liste früherer Ereignisse an – ähnlich wie das Hauptbuch eines Buchhalters. Der Ereignisspeicher wird zum Datensatzsystem für die Daten. Er wird verwendet, um verschiedene materialisierte Sichten innerhalb des begrenzten Kontexts eines Microservices zu verbreiten. Abbildung 5.8 zeigt das Muster.

Event Sourcing

Abbildung 5-8. Ereignissourcing

In der vorherigen Abbildung sehen Sie, wie jeder Eintrag (in blau) für den Warenkorb eines Benutzers an einen zugrunde liegenden Ereignisspeicher angefügt wird. In der nebenstehenden materialisierten Sicht projiziert das System den aktuellen Zustand, indem es alle Ereignisse wiedergibt, die jedem Warenkorb zugeordnet sind. Diese Sicht bzw. dieses Lesemodell wird dann wieder auf der Benutzeroberfläche angezeigt. Ereignisse können auch mit externen Systemen und Anwendungen integriert oder abgefragt werden, um den aktuellen Zustand einer Entität zu ermitteln. Mit diesem Ansatz behalten Sie den Verlauf bei. Sie kennen nicht nur den aktuellen Zustand einer Entität, sondern auch, wie Sie diesen Zustand erreicht haben.

Mechanisch gesehen vereinfacht das Ereignissourcing das Schreibmodell. Es gibt keine Aktualisierungen oder Löschvorgänge. Das Anfügen jedes Dateneintrags als unveränderliches Ereignis minimiert Konflikte, Sperrungen und Nebenläufigkeitskonflikte, die bei relationalen Datenbanken auftreten. Das Erstellen von Lesemodellen mit dem Muster der materialisierten Sicht ermöglicht es Ihnen, die Sicht vom Schreibmodell zu entkoppeln und den besten Datenspeicher zu wählen, um die Anforderungen der Benutzeroberfläche Ihrer Anwendung zu optimieren.

Betrachten Sie für dieses Muster einen Datenspeicher, der das Ereignissourcing direkt unterstützt. Azure Cosmos DB, MongoDB, Cassandra, CouchDB und RavenDB sind gute Kandidaten.

Wie bei allen Mustern und Technologien sollten Sie sie strategisch und nach Bedarf implementieren. Das Ereignissourcing kann zwar eine höhere Leistung und Skalierbarkeit bieten, geht aber auf Kosten der Komplexität und einer Lernkurve.