Verteilte Stream Processing Frameworks für Fast Data & Big Data – Ein Meer an Möglichkeiten

Keine Kommentare

Spark Streaming, Flink, Storm, Kafka Streams – das sind nur die populärsten Vertreter einer stetig wachsenden Auswahl zur Verarbeitung von Streaming-Daten in großen Mengen. In diesem Artikel soll es um die wesentlichen Konzepte hinter diesen Frameworks gehen und die drei Apache-Projekte Spark Streaming, Flink und Kafka Streams kurz eingeordnet werden.

Warum Stream Processing?

Die Verarbeitung von Streaming-Daten gewinnt durch die stetig wachsende Anzahl von Datenquellen, die durchgehend Daten produzieren und zur Verfügung stellen, zunehmend an Bedeutung. Neben dem omnipräsenten Internet of Things sind dies zum Beispiel Klickstreams, Daten im Werbegeschäft oder auch Geräte- und Serverlogs.

Nun sind unendliche und kontinuierliche Daten kein neues Phänomen. Auch jetzt entsprechen schon viele Daten diesem Schema. Zum Beispiel treten auch Änderungen an Stammdaten kontinuierlich auf, allerdings nur in geringer Frequenz. Stammdaten werden nach dem klassischen Request/Response verarbeitet. Bei zeitunkritischen Änderungen oder größeren Volumen werden die Daten auch gerne gesammelt gespeichert und dann regelmäßig durch Batchprozesse verarbeitet. Diese laufen dann beispielsweise jede Nacht oder auch in kürzeren Intervallen.

Tägliche Intervalle reichen aber häufig nicht mehr aus. Gefragt ist Geschwindigkeit: Analysen und Auswertungen werden zeitnah erwartet und nicht Minuten oder gar Stunden später. An dieser Stelle kommt das Stream Processing ins Spiel: Daten werden verarbeitet, sobald sie dem System bekannt sind. Begonnen hat dies mit der Lambda Architektur (vgl. [1]), bei der die Stream- und Batch-Verarbeitung parallel erfolgen, da die Stream-Verarbeitung keine konsistenten Ergebnisse garantieren konnte. Mit den heutigen Systemen ist es auch möglich, nur mit Streaming-Verarbeitung konsistente Ergebnisse nahezu in Echtzeit zu erreichen. (vgl. [2])

Time Matters

Ein wichtiger Aspekt beim Streaming ist die Zeit. Dabei kann im Wesentlichen zwischen drei Zeiten unterschieden werden:

  • Eventzeit: Zeitpunkt, zu dem ein Event tatsächlich auftrat
  • Ingestionzeit: Zeitpunkt, zu dem das Event im System beobachtet wurde
  • Verarbeitungszeit: Zeitpunkt, zu dem das Event vom System verarbeitet wurde

Abb. 1: Exemplarische Darstellung von Eventzeit und Verarbeitungszeit. Mit verspäteten (Gelb, Grün, Rot) und Out-of-order Events (Blau)

In der Praxis ist vor allem Eventzeit im Vergleich zur Ingestion- & Verarbeitungszeit interessant. Die Differenz zwischen der Eventzeit und der Verarbeitungszeit kann stark schwanken. Die Gründe dafür sind vielfältig: Netzwerk-Latenzen, verteilte Systeme, Hardware-Ausfälle oder auch eine unregelmäßige Datenanlieferung. Wenn nach der Verarbeitungszeit verarbeitet wird, ist dies nicht wichtig: Die Daten werden auf Basis der Systemzeit der Verarbeitung analysiert: Wenn ein Event um 12 Uhr eintrifft, ist es irrelevant, dass es bereits um 11 Uhr aufgetreten ist.

Der normale Use Case ist dies aber nicht: Wenn ein Event um 11 Uhr auftritt, möchte ich es in der Regel auch zeitlich so betrachten. Die Frage hier ist dann: Wann weiß ich, dass ich alle Events bis 11 Uhr bekommen habe? Wie lange warte ich auf Events? Hier helfen Strategien wie Watermarks, Trigger und Akkumulatoren:

  • Watermarks: Wann habe ich alle Daten zusammen?
  • Trigger: Wann soll ich die Berechnung auslösen?
  • Akkumulation: Wie füge ich einzelne Berechnungen zusammen, beispielsweise wenn nachträglich Daten folgen?

Über diese drei Konzepte ließe sich problemlos ein eigener Artikel schreiben. Tyler Akidau, der Kopf hinter Streaming bei Google, hat dies bereits hervorragend zusammengefasst. Deshalb sei an dieser Stelle für Details sein Artikel empfohlen [3].

State & Window

Jede nicht triviale Anwendung wird eingehende Events miteinander korrelieren. Dafür ist ein Zustand nötig, in dem vorherige Events zwischengespeichert werden. Dieser State kann unendlich gespeichert werden oder explizit zeitlich begrenzt. Ein Beispiel für einen unendlichen gespeicherten State ist eine Lookup-Tabelle mit Metadaten. Ein zeitlich begrenzt State ist beispielsweise ein Window.

Bei einem Window werden Daten für einen bestimmten Zeitraum zusammengefasst und analysiert. Dies ist in fast jeder Anwendung nötig, da der Datenstrom ja nie endet. Dabei gibt es verschiedene Typen von Windows:

  • Tumbling Window: nicht überlappende, fixe Zeitabschnitte
  • Sliding Window: überlappende, fixe Zeitabschnitte
  • Session Window: nicht überlappende Zeitabschnitte unterschiedlicher Länge; definiert durch bestimmte Events oder durch Überschreiten einer bestimmten Zeit zwischen zwei Events

Abb. 2: Tumbling und Sliding Window bei einem Zeitfenster von vier Sekunden und ein Sliding Intervall von zwei Sekunden beim Sliding Window. Innerhalb eines jeden Fensters werden die Werte summiert.

Abb. 3: Sessionwindows bei einer Inaktivität von mindestens zwei Minuten zwischen zwei Events für einen Key.

Für die Definition von Windows ist die Unterscheidung zwischen Event- und Verarbeitungszeit wichtig: Windows basierend auf Verarbeitungszeit sind sehr einfach zu realisieren, Windows basierend auf Eventzeit benötigen die oben genannten Strategien zur Eventzeit, um nicht unendlich zu wachsen.

API & Laufzeitumgebung

Erste Unterschiede bei den Frameworks lassen sich bei der API und dem generellen Verarbeitungsmodell feststellen. Unterscheiden lässt sich zwischen einem nativen Streaming-Ansatz und dem Microbatching. Beim nativen Streaming werden eingehende Daten direkt verarbeitet wohingegen beim Microbatching die eingehenden Daten zunächst für eine bestimmte Zeit (typischerweise 1 – 30s) gesammelt und anschließend zusammen verarbeitet werden. Der nächste Microbatch kann dann entweder direkt nach dem Abschluss des vorherigen Batches gestartet werden oder erst nach Verstreichen des fixen Intervalls. In beiden Fällen erhöht Microbatching die Latenz, dafür ist das Fehlerhandling etwas einfacher zu realisieren. Der früher häufig genannte Vorteil des sehr hohen Durchsatzes kann heute aber auch von nativen Streaming Frameworks erreicht werden. Zudem bieten diese mehr Flexibilität bei Windows und States.

Sichtbar für den Entwickler ist vor allem die API. Auch hier kann zwischen zwei Varianten unterschieden werden: einer komponentenbasierten und einer deklarativen, high-level API. Bei ersterer wird der Fluss durch verschiedene Komponenten beschrieben (Quelle -> Verarbeitung 1 -> Verarbeitung 2 -> Senke), bei letzterer werden die Operationen auf Daten beschrieben ( map, filter, reduce), ähnlich wie bei Scala Collections oder Java 8 Streams. Die Beschreibung von Komponenten bietet mehr Flexibilität bei der Verteilung der Datenströme, während die deklarative API häufig bereits höherwertige Funktion bereitstellt und automatisch Optimierungen vornehmen kann.

Zuletzt bleibt noch die Frage: Wo werden die Anwendungen ausgeführt? Auch hier kann man – Überraschung 🙂 – zwei grundsätzliche Alternativen unterscheiden. Einige Frameworks brauchen ein spezielles Cluster bestehend aus Master Nodes und Worker Nodes. Diese Cluster kümmern sich dann auch um das Ressourcenmanagement und Fehlerbehandlung, können dies aber auch auslagern an andere Tools (zum Beispiel YARN oder Mesos). Andere Frameworks kommen als einfache Bibliothek daher, die sich in die eigene Anwendung einbinden lässt. Das Ausführen und Skalieren der Anwendung muss dann von anderen Tools übernommen werden. Hier hat man die volle Flexibilität vom Ausführen eines Jar Files über Docker-Lösungen bis hin zu Mesos & Co.

Verteilte Systeme sind unzuverlässig!

Alle drei Frameworks sind spezialisiert auf die Verarbeitung großer Datenmenge und lösen dies durch horizontale Skalierung. Diese verteilten Systeme sind inhärent unzuverlässig: Einzelne Nodes können ausfallen, das Netzwerk ist inkonstant oder die Datenbank, in der die Ergebnisse geschrieben werden sollen, ist nicht erreichbar.

Aus diesem Grund hat jedes Framework unterschiedliche Mechanismen, um bestimmte Garantien zu erreichen. Diese reichen vom Microbatching, bei dem kleine Batches wiederholt werden, über Acknowledges für einzelne Datensätze bis hin zu transaktionalen Updates auf Quelle und Senke. Die erreichten Garantien sind dann in der Regal At-Least-Once, also mindestens einmal verarbeitet, oder Exactly-Once, genau einmal verarbeitet. Da Exactly-Once häufig nur schwierig und mit großem Aufwand zu erreichen ist, sind At-Least-Once-Garantien mit idempotenten Operationen häufig ausreichend sowohl in Bezug auf Geschwindigkeit als auch auf Fehlertoleranz.

Gibt’s da nichts von Apache?

Zeithandling, State & Windows, eine Laufzeitumgebung und das alles in verteilten Systemen: Streaming-Anwendungen sind komplex. Es gibt eine Reihe von Projekten die bei diesen Problemen helfen sollen. Drei davon kurz vorgestellt:

Apache Spark (Streaming)
Apache Spark ist aktuell eines der populärsten der Projekte im Streaming-Bereich. Gestartet als besseres MapReduce folgte später auch eine Unterstützung für Streaming-Daten. Spark Streaming setzt dabei auf Microbatching mit einer deklarativen API. Aktuell wird dabei nur die Verarbeitungszeit vollständig unterstützt, mit der neuen Structured Streaming API wird seit der Version 2.0 allerdings auch die Unterstützung für Eventzeit-Verarbeitung sukzessive ausgebaut. Das gleiche gilt für die Unterstützung von Windows. Der State wird lokal in Memory oder auf Disk gehalten und per Checkpointing regelmäßig gesichert. Da Spark inzwischen mit jeder Hadoop Distribution ausgeliefert wird, ist die Verbreitung sehr hoch. Ebenso existiert ein großes Ökosystem mit vielen Tools und Konnektoren.

Apache Flink
Wenn es um Eventzeit-Verarbeitung geht, ist Apache Flink aktuell die erste Wahl. Unterstützt werden Watermarks und Trigger ebenso wie unterschiedliche Window-Operationen. Flink verfolgt dabei einen nativen Streaming-Ansatz und erreicht somit niedrige Latenzen. Ebenso wie bei Spark Streaming wird eine deklarative API genutzt, mit der Möglichkeit sogenannte Rich Functions zu nutzen, in denen beispielsweise ein State genutzt wird. Im Gegensatz zu Spark können verschiedene State-Implementierungen genutzt werden: In-Memory, Festplatte oder RocksDB. Flink ist etwas jünger als Spark, gewinnt aber zunehmend an Verbreitung. Ebenso wachsen die Community und das Ökosystem stetig, sind allerdings noch nicht so groß wie bei Spark.

Apache Kafka Streams
Das Streaming Framework aus dem Kafka-Ökosystem ist der jüngste Vertreter in dieser Übersicht. Es basiert auf vielen Konzepten, die bereits in Kafka enthalten sind, wie beispielsweise die Skalierung durch Partitionierung der Topics. Auch aus diesem Grund kommt es als leichtgewichtige Bibliothek daher, die in eine Anwendung eingebunden werden kann. Die Anwendung kann dann nach belieben betrieben werden: Standalone, in einem Applikationsserver, als Docker Container oder über einen Resourcen Manager wie Mesos. Flink & Spark hingegen benötigen immer ein Cluster, entweder ein mit den Boardmitteln der Frameworks gebautes oder aber YARN/Mesos. Kafka Streams ist allerdings beschränkt auf Kafka als Quelle und auch als Senke. Die Konnektivität zu anderen Systemen wird dann über Kafka Connect hergestellt. Ansonsten besitzt Kafka Streams neben einer deklarativen auch eine komponentenorientierte API, eine rudimentäre Unterstützung von Eventtime sowie RocksDB als State-Implementierung. Während Kafka selbst schon sehr reif ist und häufig auch in Verbindung mit Flink und Spark genutzt wird, ist die Streaming-Komponente noch recht jung. So ist auch die Community eher klein und die Verbreitung eher gering. Es ist aber zu erwarten, dass beides zeitnah wachsen wird.

Update:

Kafka Streams nutzt die Konzepte des Beam Models, um den Herausforderungen des Eventzeit Handlings zu begegnen. Streams wird entwickelt auf dem Konzept von KTables und KStreams, welches genutzt wird, um Eventzeit Verarbeitung zu unterstützen.

Und was passt zu mir?

Nun bleibt zum Schluss die Frage: Welches Framework passt zu mir? Wenn Eventzeit-Verarbeitung benötigt wird, führt aktuell fast kein Weg an Flink vorbei. Ein weiterer Pluspunkt ist die niedrige Latenz. Die wichtigsten Umsysteme (Kafka, Cassandra, Elasticsearch, SQL-Datenbanken) können relativ einfach integriert werden.

Die niedrige Latenz und einen einfach zu nutzenden Eventzeit Support ermöglicht auch Kafka Streams. Wenn also Kafka bereits im Einsatz ist und die Verarbeitung eher einfach ist, ohne komplexe Anforderungen an Eventzeit-Verarbeitung, ist Kafka Streams eine gute Alternative. Dafür muss ich hier noch die Umsysteme über Kafka Connect anbinden und mich um die Laufzeitumgebung kümmern. Dies kann aber auch ein Vorteil sein, wenn ich vorhandene Tools, zum Beispiel aus dem Docker-Ökosystem, nutzen kann.

Und Spark? Wenn Eventzeit nicht relevant ist und auch Latenzen im Sekundenbereich akzeptabel sind, ist Spark die erste Wahl. Es ist stabil und fast jedes beliebige Umsystem kann einfach eingebunden werden. Außerdem ist es bei Hadoop-Installationen schon vorhanden. Zudem kann der Code, der für Batch-Anwendungen genutzt wird, bei Bedarf auch für die Streaming-Anwendungen verwendet werden, da die API dieselbe ist.

Lediglich bei sehr großen States im Terrabyte-Bereich kann es bei Spark zu Problemen kommen. Die Unterstützung für Eventzeit wird mit Spark 2.1 deutlich erweitert.

Fazit

Stream Processing Frameworks vereinfachen die Verarbeitung großer Datenmengen signifikant. Die vorgestellten Frameworks lösen dabei vor allem Probleme im Bereich der verteilten Verarbeitung wodurch einfach zu skalierende Lösungen entwickelt werden können. Ebenso wichtig sind die unterschiedlichen Aspekte der Zeitverarbeitung, die alle Frameworks unterstützen.

Hier unterscheiden sich Systeme auch am deutlichsten von Bibliotheken wie Akka Streams, RxJava oder Vert.x. Die vorgestellten Frameworks sind vor allem im Big- und Fast-Data-Bereich angesiedelt, während mit den Bibliotheken auch einfach kleinere reaktive Anwendungen gebaut werden können – dann allerdings in der Regel ohne native Unterstützung für Eventzeit und Clustering.

So bleibt festzuhalten, dass die vorgestellten Framework allesamt bei aktuellen Herausforderungen im Fast-Data-Bereich unterstützen können und dabei auch neue Architekturen jenseits der bekannten Lambda-Architektur unterstützen. Dabei ist die Komplexität dieser verteilten System allerdings keinesfalls zu unterschätzen. Dennoch ist davon auszugehen, dass die Verbreitung der Systeme ebenso wie die Funktionalität weiter zunehmen wird.

Dieser Artikel erschien zuerst im Softwerker, dem kostenfreien Magazin der codecentric.

Links

[1] http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html

[2] https://www.oreilly.com/ideas/questioning-the-lambda-architecture

[3] https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102

Matthias Niehoff

Matthias beschäftigt sich insbesondere mit Big-Data- und Streaming-Anwendungen auf Basis von Apache Cassandra und Apache Spark. Dabei verliert er aber auch andere Tools und Werkzeuge im Big-Data-Bereich nicht aus den Augen. Matthias teilt seine Erfahrungen auf Konferenzen, Meet-ups und Usergroups.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.