paint-brush
So erstellen Sie eine Event-Streaming-Anwendung in .NETvon@bbejeck
2,692 Lesungen
2,692 Lesungen

So erstellen Sie eine Event-Streaming-Anwendung in .NET

von Bill Bejeck14m2023/02/13
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Stream-Verarbeitung ist ein Ansatz zur Softwareentwicklung, der Ereignisse als primäre Eingabe oder Ausgabe einer Anwendung betrachtet. In diesem Blogbeitrag erstellen wir eine Event-Streaming-Anwendung mit Apache Kafka, den.NET Producer- und Consumer-Clients und der Task Parallel Library (TPL) von Microsoft. Der Kafka-Client und TPL kümmern sich um die meiste Arbeit; Sie müssen sich nur auf Ihre Geschäftslogik konzentrieren.
featured image - So erstellen Sie eine Event-Streaming-Anwendung in .NET
Bill Bejeck HackerNoon profile picture
0-item


Wenn Sie innehalten und über den Alltag nachdenken, können Sie alles leicht als Ereignis betrachten. Betrachten Sie die folgende Reihenfolge:


  1. Die Anzeige „Kraftstoffmangel“ Ihres Autos leuchtet auf
  2. Daher halten Sie an der nächsten Tankstelle an, um zu tanken
  3. Wenn Sie Benzin in das Auto pumpen, werden Sie aufgefordert, dem Prämienclub des Unternehmens beizutreten, um einen Rabatt zu erhalten
  4. Sie gehen hinein, melden sich an und erhalten eine Gutschrift für Ihren nächsten Einkauf


Wir könnten hier endlos weitermachen, aber ich habe es auf den Punkt gebracht: Das Leben ist eine Abfolge von Ereignissen. Wie würden Sie angesichts dieser Tatsache heute ein neues Softwaresystem entwerfen? Würden Sie unterschiedliche Ergebnisse sammeln und in beliebigen Abständen verarbeiten oder mit der Verarbeitung bis zum Ende des Tages warten? Nein, das würdest du nicht tun; Sie möchten auf jedes Ereignis reagieren, sobald es eintritt. Sicherlich kann es Fälle geben, in denen Sie nicht sofort auf individuelle Umstände reagieren können. Stellen Sie sich zum Beispiel vor, dass Sie einen Dump der Transaktionen eines ganzen Tages auf einmal erhalten. Dennoch würden Sie sofort nach Erhalt der Daten handeln, was, wenn Sie so wollen, eine große Pauschalmaßnahme darstellt.


Wie implementiert man also ein Softwaresystem für die Arbeit mit Veranstaltungen? Die Antwort ist Stream-Verarbeitung.


Was ist Stream-Verarbeitung?

Als De-facto-Technologie für den Umgang mit Ereignisdaten ist die Stream-Verarbeitung ein Ansatz für die Softwareentwicklung, bei dem Ereignisse als primäre Eingabe oder Ausgabe einer Anwendung betrachtet werden. Es macht beispielsweise keinen Sinn, darauf zu warten, auf Informationen zu reagieren oder auf einen potenziell betrügerischen Kreditkartenkauf zu reagieren. In anderen Fällen geht es möglicherweise darum, einen eingehenden Fluss von Datensätzen in einem Microservice zu verarbeiten, und die effizienteste Verarbeitung dieser Datensätze ist für Ihre Anwendung am besten.

Was auch immer der Anwendungsfall sein mag, man kann mit Sicherheit sagen, dass ein Event-Streaming-Ansatz der beste Ansatz für den Umgang mit Ereignissen ist.


In diesem Blogbeitrag erstellen wir eine Event-Streaming-Anwendung mit Apache Kafka®, den .NET Producer- und Consumer-Clients und der Task Parallel Library (TPL) von Microsoft. Auf den ersten Blick kann man nicht alle drei davon als wahrscheinliche Kandidaten für eine Zusammenarbeit bezeichnen. Klar, Kafka und die .NET-Clients sind ein tolles Paar, aber wo passt TPL ins Bild?


In den meisten Fällen ist der Durchsatz eine Schlüsselanforderung. Um Engpässe aufgrund von Impedanzinkongruenzen zwischen der Nutzung von Kafka und der nachgelagerten Verarbeitung zu vermeiden, empfehlen wir im Allgemeinen eine prozessinterne Parallelisierung, wann immer sich die Möglichkeit bietet.


Lesen Sie weiter, um zu erfahren, wie alle drei Komponenten zusammenarbeiten, um eine robuste und effiziente Event-Streaming-Anwendung zu erstellen. Das Beste daran ist, dass der Kafka-Client und TPL den Großteil der schweren Arbeit übernehmen; Sie müssen sich nur auf Ihre Geschäftslogik konzentrieren.


Bevor wir uns mit der Anwendung befassen, geben wir eine kurze Beschreibung der einzelnen Komponenten.

Apache Kafka

Wenn die Stream-Verarbeitung der De-facto-Standard für die Verarbeitung von Ereignisströmen ist, dann ist Apache Kafka der De-facto-Standard für die Erstellung von Event-Streaming-Anwendungen. Apache Kafka ist ein verteiltes Protokoll, das hoch skalierbar, elastisch, fehlertolerant und sicher bereitgestellt wird. Kurz gesagt: Kafka verwendet Broker (Server) und Clients. Die Broker bilden die verteilte Speicherschicht des Kafka-Clusters, die sich über Rechenzentren oder Cloud-Regionen erstrecken kann. Clients bieten die Möglichkeit, Ereignisdaten aus einem Broker-Cluster zu lesen und zu schreiben. Kafka-Cluster sind fehlertolerant: Wenn ein Broker ausfällt, übernehmen andere Broker die Arbeit, um einen kontinuierlichen Betrieb sicherzustellen.

Konfluente .NET-Clients

Ich habe im vorherigen Absatz erwähnt, dass Clients entweder in einen Kafka-Broker-Cluster schreiben oder daraus lesen. Apache Kafka lässt sich mit Java-Clients bündeln, es sind jedoch auch mehrere andere Clients verfügbar, nämlich der .NET Kafka-Produzent und -Konsumer, der das Herzstück der Anwendung in diesem Blogbeitrag darstellt. Der .NET-Produzent und -Konsumer stellt dem .NET-Entwickler die Leistungsfähigkeit des Event-Streamings mit Kafka zur Verfügung. Weitere Informationen zu den .NET-Clients finden Sie in der Dokumentation .

Task-Parallelbibliothek

Die Task Parallel Library ( TPL ) ist „eine Reihe öffentlicher Typen und APIs in den Namespaces System.Threading und System.Threading.Tasks“, die das Schreiben gleichzeitiger Anwendungen vereinfacht. Die TPL macht das Hinzufügen von Parallelität zu einer einfacher zu handhabenden Aufgabe, indem sie die folgenden Details behandelt:


1. Handhabung der Arbeitspartitionierung 2. Planung von Threads im ThreadPool 3. Details auf niedriger Ebene wie Abbruch, Statusverwaltung usw.


Das Fazit ist, dass Sie mit der TPL die Verarbeitungsleistung Ihrer Anwendung maximieren und sich gleichzeitig auf die Geschäftslogik konzentrieren können. Konkret verwenden Sie die Teilmenge der Dataflow-Bibliothek der TPL.


Die Dataflow-Bibliothek ist ein akteurbasiertes Programmiermodell, das prozessinterne Nachrichtenübermittlungs- und Pipeline-Aufgaben ermöglicht. Die Dataflow-Komponenten bauen auf den Typen und der Planungsinfrastruktur der TPL auf und lassen sich nahtlos in die C#-Sprache integrieren. Das Lesen aus Kafka ist normalerweise recht schnell, aber die Verarbeitung (ein DB-Aufruf oder RPC-Aufruf) stellt normalerweise einen Engpass dar. Alle Parallelisierungsmöglichkeiten, die wir nutzen können, um einen höheren Durchsatz zu erzielen, ohne dass die Bestellgarantien darunter leiden, sind eine Überlegung wert.


In diesem Blogbeitrag nutzen wir diese Dataflow-Komponenten zusammen mit den .NET Kafka-Clients, um eine Stream-Verarbeitungsanwendung zu erstellen, die Daten verarbeitet, sobald sie verfügbar sind.

Datenflussblöcke

Bevor wir uns mit der Anwendung befassen, die Sie erstellen werden: Wir sollten einige Hintergrundinformationen darüber geben, was die TPL Dataflow Library ausmacht. Der hier beschriebene Ansatz ist am besten anwendbar, wenn Sie CPU- und E/A-intensive Aufgaben haben, die einen hohen Durchsatz erfordern. Die TPL-Datenflussbibliothek besteht aus Blöcken, die eingehende Daten oder Datensätze puffern und verarbeiten können, und die Blöcke fallen in eine von drei Kategorien:


  1. Quellblöcke – dienen als Datenquelle und andere Blöcke können daraus lesen.

  2. Zielblöcke – Ein Datenempfänger oder eine Senke, auf die von anderen Blöcken geschrieben werden kann.

  3. Propagatorblöcke – Verhalten sich sowohl als Quell- als auch als Zielblock.


Sie nehmen die verschiedenen Blöcke und verbinden sie, um entweder eine lineare Verarbeitungspipeline oder einen komplexeren Verarbeitungsgraphen zu bilden. Betrachten Sie die folgenden Abbildungen:



Jeder Knoten im Diagramm stellt eine andere Verarbeitungs- oder Rechenaufgabe dar.



Die Dataflow-Bibliothek stellt mehrere vordefinierte Blocktypen bereit, die in drei Kategorien fallen: Pufferung, Ausführung und Gruppierung. Wir verwenden die Pufferungs- und Ausführungstypen für das für diesen Blogbeitrag entwickelte Projekt. Der BufferBlock<T> ist eine Allzweckstruktur, die Daten puffert und sich ideal für den Einsatz in Producer/Consumer-Anwendungen eignet. Der BufferBlock verwendet eine First-In-First-Out-Warteschlange für die Verarbeitung eingehender Daten.


Der BufferBlock (und die Klassen, die ihn erweitern) ist der einzige Blocktyp in der Dataflow-Bibliothek, der das direkte Schreiben und Lesen von Nachrichten ermöglicht; andere Typen erwarten, Nachrichten von Blöcken zu empfangen oder an Blöcke zu senden. Aus diesem Grund haben wir einen BufferBlock als Delegaten verwendet, als wir den Quellblock erstellt und die ISourceBlock Schnittstelle implementiert haben, sowie den Senkenblock, der die ITargetBlock Schnittstelle implementiert hat.


Der andere in unserer Anwendung verwendete Datenflussblocktyp ist ein TransformBlock <TInput, TOutput> . Wie bei den meisten Blocktypen in der Datenflussbibliothek erstellen Sie eine Instanz des TransformBlocks, indem Sie einen Func<TInput, TOutput> bereitstellen, der als Delegat fungiert, den der Transformationsblock für jeden empfangenen Eingabedatensatz ausführt.


Zwei wesentliche Merkmale von Dataflow-Blöcken bestehen darin, dass Sie die Anzahl der gepufferten Datensätze und den Grad der Parallelität steuern können.


Durch Festlegen einer maximalen Pufferkapazität übt Ihre Anwendung automatisch Gegendruck aus, wenn die Anwendung irgendwann in der Verarbeitungspipeline auf eine längere Wartezeit stößt. Dieser Gegendruck ist notwendig, um eine übermäßige Datenansammlung zu verhindern. Sobald das Problem behoben ist und der Puffer kleiner wird, werden wieder Daten verbraucht.


Die Möglichkeit, die Parallelität für einen Block festzulegen, ist entscheidend für die Leistung. Wenn ein Block eine CPU- oder E/A-intensive Aufgabe ausführt, besteht eine natürliche Tendenz zur Parallelisierung der Arbeit, um den Durchsatz zu erhöhen. Das Hinzufügen von Parallelität kann jedoch zu Problemen bei der Verarbeitungsreihenfolge führen. Wenn Sie der Aufgabe eines Blocks Threading hinzufügen, können Sie die Ausgabereihenfolge der Daten nicht garantieren. In manchen Fällen spielt die Reihenfolge keine Rolle, aber wenn es darauf ankommt, muss ein schwerwiegender Kompromiss in Betracht gezogen werden: höherer Durchsatz bei Parallelität im Vergleich zur Verarbeitung der Auftragsausgabe. Glücklicherweise müssen Sie diesen Kompromiss mit der Dataflow-Bibliothek nicht eingehen.


Wenn Sie die Parallelität eines Blocks auf mehr als eins festlegen, garantiert das Framework, dass die ursprüngliche Reihenfolge der Eingabedatensätze beibehalten wird (beachten Sie, dass die Beibehaltung der Reihenfolge mit Parallelität konfigurierbar ist, wobei der Standardwert „true“ ist). Wenn die ursprüngliche Reihenfolge der Daten A, B, C lautet, lautet die Ausgabereihenfolge A, B, C. Skeptisch? Das weiß ich, also habe ich es getestet und festgestellt, dass es wie angekündigt funktioniert. Wir werden etwas später in diesem Beitrag über diesen Test sprechen. Beachten Sie, dass die Erhöhung der Parallelität nur bei zustandslosen oder zustandsbehafteten Operationen erfolgen sollte, die assoziativ und kommutativ sind, was bedeutet, dass eine Änderung der Reihenfolge oder Gruppierung von Operationen keinen Einfluss auf das Ergebnis hat.


An diesem Punkt können Sie sehen, wohin das führt. Sie haben ein Kafka-Thema, das Ereignisse darstellt, die Sie so schnell wie möglich bearbeiten müssen. Sie erstellen also eine Streaming-Anwendung, die aus einem Quellblock mit einem .NET KafkaConsumer, Verarbeitungsblöcken zur Umsetzung der Geschäftslogik und einem Senkenblock mit einem .NET KafkaProducer zum Zurückschreiben der Endergebnisse in ein Kafka-Thema besteht. Hier ist eine Darstellung einer allgemeinen Ansicht der Anwendung:




Der Antrag hat folgenden Aufbau:


  1. Quellblock: Umschließen eines .NET KafkaConsumer und eines BufferBlock Delegaten
  2. Transformationsblock: Deserialisierung
  3. Transformationsblock: Eingehende JSON-Daten dem Kaufobjekt zuordnen
  4. Transformationsblock: CPU-intensive Aufgabe (simuliert)
  5. Transformationsblock: Serialisierung
  6. Zielblock: Umschließen eines .NET KafkaProducer- und BufferBlock Delegaten


Als nächstes folgt eine Beschreibung des Gesamtablaufs der Anwendung und einiger kritischer Punkte zur Nutzung von Kafka und der Dataflow-Bibliothek zum Aufbau einer leistungsstarken Event-Streaming-Anwendung.


Eine Event-Streaming-Anwendung

Hier ist unser Szenario: Sie haben ein Kafka-Thema, das Datensätze zu Käufen von Ihrem Online-Shop empfängt, und das eingehende Datenformat ist JSON. Sie möchten diese Kaufereignisse verarbeiten, indem Sie ML-Rückschlüsse auf die Kaufdetails anwenden. Darüber hinaus möchten Sie die JSON-Datensätze in das Protobuf-Format umwandeln, da dies das unternehmensweite Format für Daten ist. Natürlich ist der Durchsatz für die Anwendung entscheidend. Die ML-Vorgänge sind CPU-intensiv, daher benötigen Sie eine Möglichkeit, den Anwendungsdurchsatz zu maximieren, damit Sie die Vorteile der Parallelisierung dieses Teils der Anwendung nutzen können.


Verbrauchen von Daten in die Pipeline

Lassen Sie uns die kritischen Punkte der Streaming-Anwendung besichtigen, beginnend mit dem Quellblock. Ich habe die Implementierung der ISourceBlock Schnittstelle bereits erwähnt, und da BufferBlock auch ISourceBlock implementiert, verwenden wir sie als Delegaten, um alle Schnittstellenmethoden zu erfüllen. Die Quellblockimplementierung umschließt also einen KafkaConsumer und den BufferBlock. Innerhalb unseres Quellblocks haben wir einen separaten Thread, dessen einzige Verantwortung darin besteht, dass der Verbraucher die von ihm verbrauchten Datensätze an den Puffer weiterleitet. Von dort leitet der Puffer Datensätze an den nächsten Block in der Pipeline weiter.


Vor der Weiterleitung des Datensatzes an den Puffer wird der ConsumeRecord (der vom Consumer.consume Aufruf zurückgegeben wird) von einer Record umhüllt, die zusätzlich zum Schlüssel und Wert die ursprüngliche Partition und den ursprünglichen Offset erfasst, was für die Anwendung von entscheidender Bedeutung ist – und Ich werde gleich erklären, warum. Es ist auch erwähnenswert, dass die gesamte Pipeline mit der Record Abstraktion arbeitet, sodass alle Transformationen zu einem neuen Record Objekt führen, das den Schlüssel, den Wert und andere wichtige Felder wie den ursprünglichen Offset umschließt und sie in der gesamten Pipeline beibehält.


Verarbeitungsblöcke

Die Anwendung unterteilt die Verarbeitung in mehrere verschiedene Blöcke. Jeder Block ist mit dem nächsten Schritt in der Verarbeitungskette verknüpft, sodass der Quellblock mit dem ersten Block verknüpft ist, der die Deserialisierung übernimmt. Während der .NET KafkaConsumer die Deserialisierung von Datensätzen verarbeiten kann, lassen wir den Consumer die serialisierte Nutzlast weitergeben und in einem Transform-Block deserialisieren. Die Deserialisierung kann CPU-intensiv sein. Wenn wir sie also in den Verarbeitungsblock einfügen, können wir den Vorgang bei Bedarf parallelisieren.


Nach der Deserialisierung fließen die Datensätze in einen anderen Transform-Block, der die JSON-Nutzlast in ein Purchase-Datenmodellobjekt im Protobuf-Format konvertiert. Der interessantere Teil kommt, wenn die Daten in den nächsten Block gelangen, was eine CPU-intensive Aufgabe darstellt, die erforderlich ist, um die Kauftransaktion vollständig abzuschließen. Die Anwendung simuliert diesen Teil und die bereitgestellte Funktion schläft mit einer zufälligen Zeitspanne zwischen einer und drei Sekunden.


In diesem simulierten Verarbeitungsblock nutzen wir die Leistungsfähigkeit des Dataflow-Block-Frameworks. Wenn Sie einen Dataflow-Block instanziieren, stellen Sie eine Delegate-Func-Instanz bereit, die auf jeden gefundenen Datensatz angewendet wird, sowie eine ExecutionDataflowBlockOptions Instanz. Ich habe die Konfiguration der Dataflow-Blöcke bereits erwähnt, aber wir werden sie hier noch einmal kurz durchgehen. ExecutionDataflowBlockOptions enthält zwei wesentliche Eigenschaften: die maximale Puffergröße für diesen Block und den maximalen Parallelisierungsgrad.


Während wir die Puffergrößenkonfiguration für alle Blöcke in der Pipeline auf 10.000 Datensätze festlegen, bleiben wir bei der Standardparallelisierungsstufe von 1, mit Ausnahme unserer simulierten CPU-Intensivstufe, wo wir sie auf 4 setzen. Beachten Sie, dass die Standardgröße des Dataflow-Puffers beträgt unbegrenzt. Wir werden die Auswirkungen auf die Leistung im nächsten Abschnitt besprechen, aber vorerst vervollständigen wir die Anwendungsübersicht.


Der intensive Verarbeitungsblock wird an einen Serialisierungstransformationsblock weitergeleitet, der den Senkenblock speist, der dann einen .NET KafkaProducer umschließt und die Endergebnisse für ein Kafka-Thema erzeugt. Der Sink-Block verwendet außerdem einen Delegaten BufferBlock und einen separaten Thread zum Produzieren. Der Thread ruft den nächsten verfügbaren Datensatz aus dem Puffer ab. Dann ruft es die KafkaProducer.Produce Methode auf und übergibt einen Action Delegaten, der den DeliveryReport umschließt – der Producer-E/A-Thread führt den Action Delegaten aus, sobald die Produktionsanforderung abgeschlossen ist.


Damit ist die allgemeine Anleitung der Anwendung abgeschlossen. Lassen Sie uns nun einen entscheidenden Teil unseres Setups besprechen – den Umgang mit Commit-Offsets – der von entscheidender Bedeutung ist, da wir Datensätze vom Verbraucher per Pipeline weiterleiten.


Festschreiben von Offsets

Wenn Sie Daten mit Kafka verarbeiten, schreiben Sie regelmäßig Offsets (ein Offset ist die logische Position eines Datensatzes in einem Kafka-Thema) der Datensätze fest, die Ihre Anwendung bis zu einem bestimmten Punkt erfolgreich verarbeitet hat. Warum legt man also die Offsets fest? Diese Frage lässt sich leicht beantworten: Wenn Ihr Verbraucher entweder auf kontrollierte Weise oder aufgrund eines Fehlers herunterfährt, nimmt er die Verarbeitung ab dem letzten bekannten festgeschriebenen Offset wieder auf. Durch das regelmäßige Festschreiben der Offsets verarbeitet Ihr Verbraucher Datensätze nicht oder zumindest nur in minimalem Umfang erneut, falls Ihre Anwendung nach der Verarbeitung einiger Datensätze, aber vor dem Festschreiben heruntergefahren wird. Dieser Ansatz wird als „mindestens einmalige Verarbeitung“ bezeichnet und stellt sicher, dass Datensätze mindestens einmal verarbeitet werden und im Falle von Fehlern möglicherweise einige von ihnen erneut verarbeitet werden. Dies ist jedoch eine gute Option, wenn die Alternative darin besteht, einen Datenverlust zu riskieren. Kafka bietet auch Garantien für eine genau einmalige Verarbeitung, und obwohl wir in diesem Blogbeitrag nicht auf Transaktionen eingehen, können Sie mehr über Transaktionen in Kafka in lesen dieser Blogbeitrag .


Es gibt zwar verschiedene Möglichkeiten, Offsets festzuschreiben, die einfachste und grundlegendste ist jedoch der Ansatz der automatischen Festschreibung. Der Verbraucher liest Datensätze und die Anwendung verarbeitet sie. Nach Ablauf einer konfigurierbaren Zeitspanne (basierend auf Datensatzzeitstempeln) übernimmt der Verbraucher die Offsets der bereits verbrauchten Datensätze. Normalerweise ist die automatische Festschreibung ein sinnvoller Ansatz. In einer typischen Consume-Process-Schleife kehren Sie erst dann zum Consumer zurück, wenn Sie alle zuvor konsumierten Datensätze erfolgreich verarbeitet haben. Bei einem unerwarteten Fehler oder einer unerwarteten Abschaltung kehrt der Code nie zum Verbraucher zurück, sodass kein Commit erfolgt. Aber in unserer Anwendung hier verwenden wir Pipelining – wir nehmen verbrauchte Datensätze, schieben sie in einen Puffer und kehren zurück, um mehr zu verbrauchen – es gibt kein Warten auf eine erfolgreiche Verarbeitung.


Wie stellen wir mit dem Pipelining-Ansatz eine mindestens einmalige Verarbeitung sicher? Wir nutzen die Methode IConsumer.StoreOffset , die einen einzelnen Parameter – einen TopicPartitionOffset – verarbeitet und ihn (zusammen mit anderen Offsets) für den nächsten Commit speichert. Beachten Sie, dass dieser Ansatz zur Offset-Verwaltung im Gegensatz dazu steht, wie das automatische Festschreiben mit der Java-API funktioniert.


Die Festschreibungsprozedur funktioniert also folgendermaßen: Wenn der Senkenblock einen Datensatz zur Produktion an Kafka abruft, stellt er ihn auch dem Aktionsdelegierten zur Verfügung. Wenn der Produzent den Rückruf ausführt, übergibt er den ursprünglichen Offset an den Verbraucher (die gleiche Instanz im Quellblock) und der Verbraucher verwendet die StoreOffset-Methode. Sie haben weiterhin die automatische Festschreibung für den Konsumenten aktiviert, aber Sie stellen die Offsets zum Festschreiben bereit, anstatt den Konsumenten blind die letzten Offsets festschreiben zu lassen, die er bis zu diesem Punkt verbraucht hat.



Festschreiben von Offsets


Obwohl die Anwendung also Pipelining verwendet, führt sie erst einen Commit durch, nachdem sie eine Bestätigung vom Broker erhalten hat. Dies bedeutet, dass der Broker und der Mindestsatz an Replikat-Brokern den Datensatz gespeichert haben. Auf diese Weise kann die Anwendung schneller voranschreiten, da der Verbraucher die Pipeline kontinuierlich abrufen und mit Daten versorgen kann, während die Blöcke ihre Arbeit ausführen. Dieser Ansatz ist möglich, weil der .NET-Consumer-Client threadsicher ist (einige Methoden sind nicht threadsicher und werden als solche dokumentiert), sodass unser einzelner Consumer sicher sowohl im Quell- als auch im Senkenblock-Thread arbeiten kann.


Bei jedem Fehler während der Produktionsphase protokolliert die Anwendung den Fehler und legt den Datensatz wieder im verschachtelten BufferBlock ab, sodass der Produzent erneut versucht, den Datensatz an den Broker zu senden. Diese Wiederholungslogik wird jedoch blind ausgeführt, und in der Praxis werden Sie wahrscheinlich eine robustere Lösung wünschen.

Auswirkungen auf die Leistung

Nachdem wir uns nun mit der Funktionsweise der Anwendung befasst haben, werfen wir einen Blick auf die Leistungszahlen. Alle Tests wurden lokal auf einem macOS Big Sur (11.6)-Laptop durchgeführt, daher kann Ihr Kilometerstand in diesem Szenario variieren. Der Aufbau des Leistungstests ist unkompliziert:


  1. Erstellen Sie 1 Million Datensätze zu einem Kafka-Thema im JSON-Format. Dieser Schritt wurde vorab durchgeführt und floss nicht in die Testmessungen ein.

  2. Starten Sie die Kafka Dataflow-fähige Anwendung und setzen Sie die Parallelisierung aller Blöcke auf 1 (Standardeinstellung).

  3. Die Anwendung wird ausgeführt, bis 1 Mio. Datensätze erfolgreich verarbeitet wurden, und wird dann heruntergefahren

  4. Notieren Sie die Zeit, die für die Verarbeitung aller Datensätze benötigt wurde


Der einzige Unterschied für die zweite Runde bestand darin, den MaxDegreeOfParallelism für den simulierten CPU-intensiven Block auf vier zu setzen.

Hier sind die Ergebnisse:


Anzahl der Datensätze

Parallelitätsfaktor

Zeit (Minuten)

1M

1

38

1M

4

9


Durch einfaches Festlegen einer Konfiguration haben wir den Durchsatz erheblich verbessert und gleichzeitig die Reihenfolge der Ereignisse beibehalten. Wenn wir also einen maximalen Parallelitätsgrad von vier aktivieren, erhalten wir die erwartete Beschleunigung um einen Faktor von mehr als vier. Der entscheidende Teil dieser Leistungsverbesserung besteht jedoch darin, dass Sie keinen gleichzeitigen Code geschrieben haben, was schwierig wäre, ihn korrekt auszuführen.


Zu Beginn des Blogbeitrags habe ich einen Test erwähnt, um zu überprüfen, ob die Parallelität mit Dataflow-Blöcken die Ereignisreihenfolge beibehält. Lassen Sie uns jetzt darüber sprechen. Der Prozess umfasste die folgenden Schritte:


  1. Erzeugen Sie 1 Million Ganzzahlen (0–999.999) für ein Kafka-Thema

  2. Ändern Sie die Referenzanwendung, um mit Ganzzahltypen zu arbeiten

  3. Führen Sie die Anwendung mit einer Parallelitätsstufe von eins für den simulierten Remote-Prozessblock aus – produzieren Sie zu einem Kafka-Thema

  4. Führen Sie die Anwendung mit einer Parallelitätsstufe von vier erneut aus und erzeugen Sie die Zahlen für ein anderes Kafka-Thema

  5. Führen Sie ein Programm aus, um die Ganzzahlen aus beiden Ergebnisthemen zu verarbeiten und sie in einem Array im Speicher zu speichern

  6. Vergleichen Sie beide Arrays und stellen Sie sicher, dass sie in identischer Reihenfolge sind


Das Ergebnis dieses Tests war, dass beide Arrays die Ganzzahlen in der Reihenfolge von 0 bis 999.999 enthielten, was beweist, dass die Verwendung eines Dataflow-Blocks mit einem Parallelitätsgrad von mehr als eins die Verarbeitungsreihenfolge der eingehenden Daten beibehielt. Ausführlichere Informationen zur Dataflow-Parallelität finden Sie in der Dokumentation .

Zusammenfassung

In diesem Beitrag haben wir vorgestellt, wie man .NET Kafka-Clients und die Task Parallel Library verwendet, um eine robuste Event-Streaming-Anwendung mit hohem Durchsatz zu erstellen. Kafka bietet leistungsstarkes Event-Streaming und die Task Parallel Library bietet Ihnen die Bausteine zum Erstellen gleichzeitiger Anwendungen mit Pufferung, um alle Details zu verwalten, sodass sich Entwickler auf die Geschäftslogik konzentrieren können. Auch wenn das Szenario für die Anwendung etwas kompliziert ist, können Sie hoffentlich den Nutzen der Kombination der beiden Technologien erkennen. Versuche es- Hier ist das GitHub-Repository .



Auch hier veröffentlicht.