Angenommen, es gibt einen Worker-Dienst, der Nachrichten von einer Warteschlange empfängt, das Produkt mit der angegebenen ID aus einer Dokumentendatenbank liest, eine auf der Nachricht basierende Manipulationslogik anwendet und das aktualisierte Produkt schließlich in die Datenbank schreibt (a) .
Diese Arbeit kann bei verschiedenen Produkten sicher parallel ausgeführt werden, sodass wir horizontal skalieren können (b). Wenn jedoch mehr als eine Dienstinstanz für dasselbe Produkt verwendet wird, kommt es möglicherweise zu Parallelitätsproblemen oder Nebenläufigkeitsausnahmen von der Datenbank. In diesem Fall sollten wir eine Wiederholungslogik anwenden (und die erneute Ausführung schlägt möglicherweise erneut fehl usw.). .
Frage : Wie vermeiden wir das? Kann ich sicherstellen, dass zwei Instanzen nicht am selben Produkt arbeiten?
Beispiel / Anwendungsfall : Ein Online-Shop hat einen großen Verkauf auf Produkt A, Produkt B und Produkt C, die in einer Stunde endet und Hunderte von Kunden kaufen. Für jeden Einkauf wird eine Nachricht in die Warteschlange eingereiht (productId, numberOfItems, price). Ziel : Wie können wir drei Instanzen unseres worker-Service ausführen und sicherstellen, dass alle Nachrichten für productA in instanceA, productB in instanceB und productC in instanceC enden (was zu keinen Problemen im Zusammenhang mit Parallelität führt)?
Hinweise : Mein Dienst ist in C # geschrieben, auf Azure als Worker Role gehostet, ich verwende Azure Queues für Messaging und denke darüber nach, Mongo als Speicher zu verwenden. Außerdem sind die Entitäts-IDs GUID
.
Es geht mehr um die Technik / den Entwurf. Wenn Sie also verschiedene Werkzeuge verwenden, um das Problem zu lösen, bin ich immer noch interessiert.
Für diese Art von Dingen verwende ich Blob-Leases. Grundsätzlich erstelle ich ein Blob mit der ID einer Entität in einem bekannten Speicherkonto. Wenn Arbeitskraft 1 die Entität aufnimmt, versucht sie, eine Lease für den Blob zu erhalten (und den Blob selbst zu erstellen, falls er nicht existiert). Wenn beide erfolgreich sind, erlaube ich die Verarbeitung der Nachricht. Geben Sie den Mietvertrag immer danach frei. Wenn ich nicht erfolgreich bin, dump ich die Nachricht zurück in die Warteschlange
Ich folge dem hier ursprünglich von Steve Marx beschriebenen Ansatz Ссылка obwohl optimiert, um neue Speicherbibliotheken zu verwenden
Nach Kommentaren bearbeiten: Wenn Sie eine potenziell hohe Rate von Nachrichten haben, die alle mit der gleichen Entität sprechen (wie Ihre Empfehlung besagt), würde ich Ihren Ansatz irgendwo anders umgestalten. Entweder Entity-Struktur oder Messaging-Struktur.
Beispiel: Betrachten Sie das CQRS-Entwurfsmuster und speichern Sie die Änderungen unabhängig von der Verarbeitung jeder Nachricht. Wobei die Produkteinheit nun ein Aggregat aller Änderungen ist, die von verschiedenen Mitarbeitern an der Entität vorgenommen wurden, die sequentiell erneut angewendet und in ein einzelnes Objekt rehydriert wurden
Jede Lösung, die versucht, die Last auf verschiedene Artikel in derselben Sammlung (wie Bestellungen) aufzuteilen, ist zum Scheitern verurteilt. Der Grund dafür ist, dass Sie, wenn Sie eine hohe Rate an Transaktionen haben, eines der folgenden Dinge tun müssen:
hey guys, are anyone working with this?
) Was ist mit diesen Ansätzen falsch?
Der erste Ansatz besteht darin, Transaktionen in einer Datenbank zu replizieren. Wenn Sie nicht viel Zeit darauf verwenden, die Strategie zu optimieren, ist es besser, sich auf Transaktionen zu verlassen.
Die zweiten beiden Optionen verringern die Leistung, da Sie Nachrichten dynamisch nach IDs weiterleiten und die Strategie zur Laufzeit ändern müssen, um auch neu eingefügte Nachrichten einzuschließen. Es wird irgendwann scheitern.
Hier sind zwei Lösungen, die Sie auch kombinieren können.
Stattdessen haben Sie irgendwo einen Einstiegspunkt, der aus der Nachrichtenwarteschlange liest.
Darin hast du so etwas:
%Vor%Was Sie stattdessen tun könnten, um eine sehr einfache Fehlertoleranz zu erhalten, besteht darin, nach einem Fehler erneut zu versuchen:
%Vor%Sie könnten natürlich nur DB-Exceptions (oder eher Transaktionsfehler) abfangen, um diese Nachrichten einfach wiederzugeben.
Ich weiß, Micro Service ist ein Schlagwort. Aber in diesem Fall ist es eine großartige Lösung. Anstatt einen monolithischen Kern zu haben, der alle Nachrichten verarbeitet, teilen Sie die Anwendung in kleinere Teile. Oder deaktivieren Sie in Ihrem Fall einfach die Verarbeitung bestimmter Nachrichtentypen.
Wenn Sie fünf Knoten haben, auf denen Ihre Anwendung läuft, können Sie sicherstellen, dass Knoten A Nachrichten in Bezug auf Aufträge empfängt, Knoten B Nachrichten in Bezug auf Versand usw. empfängt.
Dadurch können Sie Ihre Anwendung horizontal skalieren, Sie erhalten keine Konflikte und es ist wenig Aufwand erforderlich (ein paar weitere Nachrichtenwarteschlangen und Neukonfiguration jedes Knotens).
Wenn Sie die Datenbank immer auf dem neuesten Stand halten möchten und immer mit den bereits verarbeiteten Einheiten konsistent sind, dann haben Sie mehrere Aktualisierungen für die gleiche veränderbare Entität.
Um dies zu erreichen, müssen Sie die Aktualisierungen für dieselbe Entität serialisieren. Entweder indem Sie Ihre Daten bei Produzenten partitionieren, oder Sie sammeln die Ereignisse für die Entität in derselben Warteschlange an, entweder Sie sperren die Entität im Worker mithilfe einer verteilten Sperre oder einer Sperre auf Datenbankebene.
Sie können ein Akteurmodell (in der Welt von java / scala mit akka) verwenden, das für jede Entität oder Gruppe von Entitäten, die sie seriell verarbeiten, eine Nachrichtenwarteschlange erstellt.
AKTUALISIERT Sie können versuchen, einen akka-Port zu .net und hier . Hier finden Sie ein schönes Tutorial mit Beispielen zur Verwendung von akka in scala . Aber für allgemeine Prinzipien sollten Sie mehr über [Akteurmodell] suchen. Es hat trotzdem Nachteile.
Letztendlich gehört dazu, Ihre Daten zu partitionieren und einen einzigartigen spezialisierten Mitarbeiter (der im Falle eines Fehlers wiederverwendet und / oder neu gestartet werden könnte) für eine bestimmte Entität zu erstellen.
Ich nehme an, Sie haben die Möglichkeit, über alle Worker-Services sicher auf die Produktwarteschlange zuzugreifen. Ein einfacher Weg zur Vermeidung eines Konflikts besteht darin, globale Warteschlangen pro Produkt neben der Hauptwarteschlange zu verwenden.
%Vor%Der Zugriff auf Warteschlangen muss atomar sein
1) Jede hoch skalierte Datenlösung, die ich mir vorstellen kann, hat etwas eingebaut, um genau diese Art von Konflikt zu bewältigen. Die Details hängen von Ihrer endgültigen Wahl für die Datenspeicherung ab. Im Falle einer traditionellen relationalen Datenbank kommt dies ohne zusätzliche Arbeit von Ihrer Seite. Detaillierte Informationen finden Sie in der Dokumentation Ihrer gewählten Technologie.
2) Verstehen Sie Ihr Datenmodell und Ihre Nutzungsmuster. Entwerfen Sie Ihren Datenspeicher entsprechend. Entwerfen Sie keinen Maßstab, den Sie nicht haben. Optimieren Sie Ihre häufigsten Nutzungsmuster.
3) Fordern Sie Ihre Annahmen heraus. Haben Sie , um dieselbe Entität sehr häufig aus mehreren Rollen zu mutieren? Manchmal lautet die Antwort ja, aber oft können Sie einfach eine neue Entität erstellen, die dem Update ähnelt. IE, nehmen Sie eine Journaling / Logging Ansatz anstelle eines Single-Entity-Ansatzes. Letztendlich werden große Mengen von Updates in einer einzelnen Entität niemals skaliert.
Tags und Links azure sharding scalability microservices horizontal-scaling