Laden...

Bewertung SW-Architektur "High-Performance" - Echtzeitaktualisierung

Erstellt von UndercoverDeveloper vor 3 Jahren Letzter Beitrag vor 3 Jahren 617 Views
U
UndercoverDeveloper Themenstarter:in
16 Beiträge seit 2015
vor 3 Jahren
Bewertung SW-Architektur "High-Performance" - Echtzeitaktualisierung

Hallo,

ich möchte gerne mal eine entworfene Software Architektur von euch bewerten lassen. Vielleicht hat jemand von euch ja noch eine bessere Idee oder zumindest Optimierungsvorschläge 🙂

Szenario:

Es ist eine Web-Anwendung mit einem REST-API-Backend. In dieser Webanwendung können in einer Tabelle Daten geändert werden (Inline-Editing). Sobald ein Benutzer Daten in dieser Tabelle ändert sollen auch alle anderen Benutzer die geänderten Daten sofort sehen. Datenänderungen in der Tabelle passieren sehr häufig, sodass pro Minute hunderte Requests an die API gesendet werden könnten. Die Lösung muss also schnell sein und soll den Server nicht unnötig belasten.

Lösungsweg:

Um alle anderen Clients zu benachrichtigen haben wir bereits eine Lösung, wir verwenden dazu Azure SignalR. Das Problem, welches es zu lösen gilt, ist wie die anderen Clients an die geänderten Daten kommen. Zwar könnte jeder Client einen einfachen GET-Request an die API schicken und die API berechnet für jeden GET-Request ein neues Response-Objekt aber wenn das hunderte Clients gleichzeitig machen wird es Probleme geben.

Lösungen welche ich bisher ausgeschlossen habe:

  • Der erste GET-Request einen Clients welcher die geänderten Daten von der API abruft führt dazu das die API das genierte Response-Objekt im Cache speichert. Diese Lösung wäre technisch am einfachsten umzusetzen, wird aber nicht funktionieren, weil sehr viele Clients gleichzeitig die Benachrichtigung über geänderte Daten erhalten. Alle Clients fragen parallel die geänderten Daten an der API an und daher gibt es keinen Request welcher zuvor den Cache generiert.

  • Der POST/PUT-Endpunkt, welcher die Daten in die Datenbank speichert, erzeugt auch gleichzeitig ein Response-Objekt und speichert es im Cache. Alle anderen Clients können dann direkt auf das Response-Objekt im Cache zugreifen. Diese Lösung habe ich auch verworfen, weil es zusätzliche Rechenzeit im POST/PUT - Endpunkt bedeutet und die Antwortzeiten dieser Endpunkte verlängert.

Aktuelle Lösung:

Ich habe eine Skizze meiner erdachten Lösung angehängt, ich hoffe es ist halbwegs verständlich gezeichnet 🙂 Im Prinzip geht es darum das, dass Response-Objekt vorberechnet wird bevor alle anderen Clients über die Datenänderung benachrichtigt werden. Somit können alle anderen Clients das neue Response-Objekt direkt aus dem Cache beziehen. Das wesentliche Merkmal ist aber, dass nicht der POST/PUT - Endpunkt das Response-Objekt selber erstellt, sondern das an einen Hintergrundprozess weiterreicht. Der POST/PUT-Endpunkt kann somit schnell Antworten während in einem separaten Prozess die Cache-Generierung und Benachrichtigung aller anderen Clients läuft.

Weitere Ideen

Hier noch ein paar Ideen, welche in die Skizze noch nicht eingearbeitet sind aber vielleicht Sinn machen.

  • Man könnte zwischen API und Hintergrundprozess noch eine MessageQueue setzten (Azure Storage Queue, Azure Service Bus, RabbitMQ etc.). Das würde die Zuverlässigkeit erhöhen, falls der Hintergrundprozess für ein paar Sekunden nicht erreichbar sein sollte. Ist der Hintergrundprozess erreichbar kann er die Queue abarbeiten. Bei Systemen mit vielen Komponenten ist immer die Gefahr das eine Komponenten ausfällt. Was haltet ihr davon?

Ich danke schonmal jedem der sich die Zeit nimmt sich das mal anzuschauen 🙂

F
10.010 Beiträge seit 2004
vor 3 Jahren

Ich habe das mal vor Jahren direkt mit SignalR gemacht.
Man kann ja schließlich auch eine Payload mitgeben.

Also nicht nur alle benachrichtigen das eine Änderung vorhanden ist, sondern diese auch gleich mit senden.
Macht natürlich nur Sinn bei "kleinen" Payloads.

C
439 Beiträge seit 2008
vor 3 Jahren

Ich mache das auch immer so mit SignalR, entweder nur ID in und dann GET oder wie FZelle bereits gesagt hat einen Payload mitschicken.
Performancetechnisch funktioniert das bei uns auch mit 100en verbundenen Clients super - auch wenns da nicht 100e Einträge sind.

A programmer is just a tool, which converts coffeine into code! 🙂

U
UndercoverDeveloper Themenstarter:in
16 Beiträge seit 2015
vor 3 Jahren

Danke euch erstmal. Ja man könnte das neue Response-Objekt natürlich auch direkt über SignalR senden. Der Payload sollte nur nicht größer als 32KB sein, weil es dann die Performance negativ beeinflusst ( https://github.com/SignalR/SignalR/issues/1205 )

Zudem gibt es noch den Kostenfaktor wenn man wie wir SignalR nicht selber hostet sondern Azure verwendet. Eine Message hat 2KB und Messages die größer sind werden aufgesplittet und als einzelne Nachricht abgerechnet:

Messages and connections in Azure SignalR Service.

Also das ganze Response-Objekt würde ich, zumindest in unserem konkreten Szenario, nicht über SignalR schicken.

Vielleicht mache ich mir auch gerade einen zu großen Kopf um Performance. Muss ich vielleicht mal testen wie weit das skaliert wenn man nur die Id schickt und jeder Client selbst die Daten von der API holt (ohne Cache). Auch wenn das dann viele Zugriffe auf die SQL-Datenbank bedeutet.

16.835 Beiträge seit 2008
vor 3 Jahren

Du sprichst wörtlich von High Performance, wovon ich aber weder an den Technologien noch am Schema etwas erkenne.
Du meinst eigentlich Skalierung und eine akzeptable Antwortzeit, oder?

High Performance, Echtzeit und Skalierung sind drei unterschiedliche Dinge.

U
UndercoverDeveloper Themenstarter:in
16 Beiträge seit 2015
vor 3 Jahren

Ja, da habe ich mich wohl falsch ausgedrückt. Skalierbarkeit ist hier der bessere Begriff, richtig 🙂 Die Lösung soll skalieren, sodass auch bei einer großen Anzahl von Benutzern die Antwortzeiten akzeptabel sind.

16.835 Beiträge seit 2008
vor 3 Jahren

Also wenn Du alles hoch- und frei skalierend umsetzen willst, dann würde ich an Deiner Stelle vollständig auf Event-Sourcing setzen.
Da Du bereits auf Azure bist kannst Du folgende Dinge dafür nutzen:

  • REST API basierend auf einem App Service (zwar nicht Serverless, skaliert aber sehr gut); wenn REST nicht zwingend erforderlich ist, dann verwende gRPC, da Du hier den Text-Serialize Overhead sparst. Aktuell gibts paar Anzeichen, dass ASP.NET Core bald im Function Context laufen könnte (und damit Serverless wäre), wie es bereits in AWS Lambda möglich ist.
  • Azure Functions zum Processing. Als Serverless Modell wenn Dir die Standard-Performance reicht; Isolated wenn Du mehr IO brauchst.
  • Azure SignalR für die Client Benachrichtigung
  • Event Hub / Grid für die Orchestrierung (quasi Deine Eventing Queue); je nachdem wie groß Du denkst brauchst Du dann noch ne Event Hub Namespace / Partition Architektur
  • Azure CosmosDB; weil Azure Functions hier direkt mit einem Trigger arbeiten können

Endspricht im Endeffekt den Kernkomponenten aus https://docs.microsoft.com/de-de/azure/architecture/data-guide/big-data/real-time-processing bzw. https://docs.microsoft.com/de-de/azure/architecture/serverless-quest/reference-architectures

Funktionsweise:
Über REST kippst Du eine Änderung an; diese kann durch das Event Processing laufen, wenn Du auch Write skalieren musst, oder eben direkt gegen die CosmosDB, wenn Dir das ausreicht
CosmosDB erhält einen Trigger, der die Änderung über die Eventing Queue an SignalR weiter gibt, der dann die Clients informiert; entweder mit oder ohne Payload.
Es gab mal einen experimentellen Ansatz für den SQL Server und Azure Functions; aber das Bindung wurde nie GA und mittlerweile existiert es nicht mehr.
Siehe Triggers and bindings in Azure Functions

pro Minute hunderte Requests an die API

Das ist prinzipiell nicht viel.

Das Forum hier basiert auf einem Azure App Service for Linux und kann in unserer kleinsten Variante ca. 18.000 Requests pro Minute beantworten (mit unserer Testdarstellung).
Natürlich sind gewisse Seiten langsamer; aber wäre in Deinem Fall dann auch ein Implementierungsthema, wo man die Performance her holt. Was unsere Seite derzeit langsam macht ist das TTFB an CloudFlare. Azure oder ASP.NET Core ist hier weniger das Problem.

Meine Test App ( ASP.NET Core - Entity Framework Core 5 - App Service for Linux B1 - Azure SQL 50 DTU ), die ich zB. auf Vorträgen zum Thema ASP.NET Performance auf Azure verwende, liefert ca. 860 Requests / Minute, um mal einen groben Anhaltspunkt für die aller kleinste Ausbaustufe für solch eine App auf Azure zu geben.
Aber wie gesagt: kommt immer drauf an, was man macht.

Caching würde ich so lange wie möglich vermeiden.

Wenn Du jedoch Caching in Betracht ziehen willst, dann ist der Weg über

Der POST/PUT-Endpunkt, welcher die Daten in die Datenbank speichert, erzeugt auch gleichzeitig ein Response-Objekt und speichert es im Cache. Alle anderen Clients können dann direkt auf das Response-Objekt im Cache zugreifen. Diese Lösung habe ich auch verworfen, weil es zusätzliche Rechenzeit im POST/PUT - Endpunkt bedeutet und die Antwortzeiten dieser Endpunkte verlängert.

absolut der richtige Weg - mit einer Ausnahme: Du darfst nicht die Standard Caching Implementierung von ASP.NET Core verwenden, da dieser bis heute kein aktives Cache-Invalidation unterstützt.
Da musst Dir einen Mechanismus zB. über Redis ausdenken, dann funktioniert das erstklassig; im Endeffekt funktionieren so auch hoch-frequente APIs wie Aktien-Indizes etc.

Ob Du in diesem Schritt auch das Objekt erstellst: würde halt nicht wirklich sinn machen, wenn es ein Event-Sourcing Konzept ist.
Daher eher 204 Created.

U
UndercoverDeveloper Themenstarter:in
16 Beiträge seit 2015
vor 3 Jahren

Vielen dank für diese ausführliche Antwort, Abt 🙂 Da sind interessante Ansätze dabei. Wir müssen allerdings auch unsere spezifische Systemumgebung noch mit einbeziehen, diese hatte ich hier zugegebenermaßen nicht erwähnt. Wir können z.B nicht auf CosmosDB wechseln, weil es sich um eine Neuentwicklung einer Altanwendung handelt. Diese setzt bereits auf einen SQL Server und da hängt noch einiges mehr dran. Außerdem muss Alt und Neu datentechnisch kompatibel bleiben.

Meine Hauptsorge ist auch mehr der SQL Server und nicht die Skalierung der API an sich. Die API hosten wir in Azure und ist daher an sich schon skalierbar aber der SQL Server muss leider erstmal On-Premise bleiben. Jeder API Request erzeugt demnach Last auf dem SQL Server und das versuchte ich durch das Caching zu vermeiden.

Wir haben hier intern auch nochmal diskutiert und werden die Lösung von FZelle umsetzen. Also die Datenänderungen direkt über SignalR senden. So groß sind die Objekte nun auch nicht, wir haben das nochmal kalkuliert. Das macht den technischen Aufbau einfacher aber reduziert trotzdem die Anfragen an die API und damit auch an den SQL Server.

Es war jedenfalls sinnvoll das hier mal zu diskutieren und andere Ansätze zu hören. Wieder etwas dazu gelernt was es alles an Möglichkeiten gibt 🙂

T
2.224 Beiträge seit 2008
vor 3 Jahren

Je nachdem wie euer System aufgebaut ist, könntest du schauen ob lokale Caches reichen oder ob du, wie von schon angesprochen z.B. Redis eine Möglichkeit wäre.
Wir nutzen Redis sowohl als verteilten Cache als auch über Pub/Sub zur Kommunikation zwischen den Prozessen bei Cache Änderungen.
Falls ein einfacher lokaler Cache reichen sollte, was wir auch bei unseren Apis nutzen, dann wäre Redis vermutlich etwas viel 🙂

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.835 Beiträge seit 2008
vor 3 Jahren

Wir haben hier intern auch nochmal diskutiert und werden die Lösung von FZelle umsetzen. )

In meinem Vorschlag ist Fzelles Umsetzung enthalten; mein Text behandelt aber auch noch das Drumherum.
SignalR an der Stelle ist ja nur die ausliefernde Komponente - Du brauchst aber auch alles vorher.

Die API hosten wir in Azure und ist daher an sich schon skalierbar aber der SQL Server muss leider erstmal On-Premise bleiben.

Es heisst On-Premises bzw in der Kurzform On-Prem aber niemals On-Premise.
Das tut so weh in den Augen - und es machen so viele falsch... 🙂

premise ist die Voraussetzung während premises das Grundstück ist.

Nur weil Du eine API auf Azure hast, hast Du nicht pauschal eine skalierende Anwendung.
Azure macht es Dir nur sehr viel einfacher eine skalierende API zur Verfügung zu stellen.

Ich seh oft Azure Implementierungen bei Kunden, die 1:1 umgesetzt sind wie On-Prem-Anwendungen (einfach weil man es so kennt und sich nicht mit Azure beschäftigt hat) und skaliert am Ende nicht.
Will ich Dir jetzt nicht unterstellen; Du hast das sicher gemacht. Wollts nur gesagt haben.

Wenn Du ne hybride Umgebung hast - also Cloud App und Azure DB - dann ist das Networking besonders wichtig und oft der Flaschenhals.
Sofern es bei euch möglich ist würde ich dann einen Cluster konfigurieren und die schreibenden Operationen gegen On-Prem laufen lassen und die lesenden Operationen gegen Azure SQL.
Damit senkst Du die Last und vor allem die Latenz auf Deine Main-DB extrem und skalierst viel besser.