Wer große Datenmengen in Echtzeit verarbeitet, kommt mit Konzepten wie der Stapelverarbeitung nicht mehr zum Ziel. Als Alternative bietet sich Stream Processing an. Dieser Artikel erklärt, wie es funktioniert.
Stream Processing, auch als Datenstromverarbeitung bekannt, entstand als Paradigma in den frühen 1970er-Jahren und wurde der Beschreibung von Automatisierungsprozessen in Fabriken nachempfunden. Ein Programm besteht dabei aus über Datenströme verbundenen Quellen, Knoten/Operatoren und Senken. Quellen lesen Daten von externen Komponenten oder erzeugen sie selbst. Senken übernehmen die Ausgabe der Daten und schreiben sie auf den Bildschirm, in Dateien oder in externe Systeme. Operatoren besitzen mindestens einen Eingang, an dem ein Datenstrom anliegt.
Mit dem Aufkommen von verteiltem Rechnen, der damit einhergehenden Komplexität beim Skalieren von Anwendungen und dem Wunsch, immer schneller auf neue Daten reagieren zu können, hat Stream Processing in den letzten Jahren wieder Fahrt aufgenommen. Es ist insbesondere als Distributed Stateful Stream Processing sehr populär. Diese Form der verteilten, dabei aber zustandsbehafteten Verarbeitung eröffnet fast unendliche Möglichkeiten, komplexe Geschäftslogiken, Analyseprozesse und sogar komplette (reaktive) Anwendungen zu entwickeln.
Dazu tragen nicht zuletzt einige im Open-Source-Umfeld entwickelte Frameworks bei. Der Entwickler, der sie nutzt, braucht sich keine Gedanken über die unteren Netzwerkschichten oder gar die Synchronisation nebenläufiger Prozesse zu machen. Er kann zudem die Fehlertoleranzgarantien des Frameworks ausnutzen, ohne sie selbst zu implementieren.
Stream-Processing-Konzepte
Um die Konzepte von Stateful Stream Processing zu verstehen, stelle man sich folgendes Szenario vor: Ein großer Video-Streaming-Anbieter hat Tausende Videos im Programm. Für den Nutzer ist es schier unmöglich, sie alle zu durchforsten, um das Video zu finden, das seinen Vorlieben und der aktuellen Stimmung entspricht. Deshalb will ihn der Anbieter dabei unterstützen und ihm ein paar personalisierte Vorschläge unterbreiten.
Nachdem sich der Nutzer schon etliche Wochen lang durch das Angebot geklickt hat, kann der Streaming-Provider vorhersehen, welche Filme oder Serien der Kunde mag. Der Anbieter untersucht alle dem Kunden unterbreiteten Vorschläge, verfolgt, wie sich der Kunde weiter durch diese Angebote geklickt hat, und prüft, welches Video er schließlich angesehen hat.
Diese Informationen stehen dem Anbieter in zwei getrennten Datenströmen zur Verfügung: Impressions (angezeigte Vorschläge) und Plays (Videowiedergabe). Sieht man sich die Datenströme für diese Aufgabe genauer an, lässt sich eine Architektur wie in Abbildung 1 ableiten. Sie liest den jeweiligen Strom, bearbeitet ihn noch etwas und führt schließlich beide Ströme zusammen, um das gewünschte Ergebnis zu erzielen.

Abbildung 1: Der logische Datenflussplan einer Stream-Processing-Anwendung zur Datenanalyse und Bestimmung der gezeigten Videovorschläge (Impressions), die einen Nutzer zu einer Videowiedergabe (Plays) führten.
Datenstrom
Bisher war recht abstrakt von einem Datenstrom die Rede – aber was ist das eigentlich genau, und was gilt es dabei zu beachten? Ganz allgemein handelt es sich bei einem Datenstrom erst einmal um eine Sequenz von Daten oder Ereignissen beziehungsweise Nachrichten (Events). Man kann hier weiter zwischen Binärdatenströmen unterscheiden, wie zum Beispiel bei der Musik- oder Videodatenverarbeitung, und Ereignisdatenströmen, von denen im Folgenden die Rede ist.
Auch bei einem Netzwerkkanal handelt es sich erst einmal um einen solchen Datenstrom, der sich Block für Block oder Byte für Byte lesen lässt. Dieser Datenstrom lässt sich anhand seiner logischen Struktur weiter aufteilen, etwa in einzelne HTTP-Anfragen. Als Ereignis gilt dabei eine Anfrage, die sich von allen weiteren getrennt betrachten lässt. Solche Ereignisdatenströme sind das von typischen Stream-Processing-Frameworks benutzte Modell.
Bei genauerer Betrachtung der Ereignissequenzen ergeben sich unterschiedliche Betrachtungsweisen. Erst einmal ist jeder Datenstrom auf natürliche Weise unbegrenzt. Wer also an einer bestimmten Position im gedachten Datenstrom liest, weiß nicht, wie viele weitere Ereignisse es noch gibt. Wenn der Datenstrom in irgendeiner Weise zwischengespeichert wurde, gibt es oft die Möglichkeit, diese Leseposition zu verschieben und so auch in die Vergangenheit zurückzuspringen; was in der Zukunft geschieht, bleibt zum aktuellen Zeitpunkt jedoch unbekannt.
Gewissermaßen ein Spezialfall dieser Sichtweise sind begrenzte Datenströme, also solche mit definiertem Anfang und Ende. Sie entstehen typischerweise, weil jemand künstlich einen eigentlich unbegrenzten Datenstrom aufteilt, um einzelne Blöcke von Ereignissen zusammen zu bearbeiten. Ein Beispiel könnten alle Log-Dateien oder alle Buchungen eines Tages sein. Die Arbeit mit begrenzten Datenströmen nennt man üblicherweise Batch Processing.
Im Video-Szenario gilt es, zwei Quelldatenströme zu verarbeiten: Impressions und Plays. Beide sind komplett unabhängig voneinander und werden separat von den verschiedenen Apps oder Web-Services mit Ereignissen gefüllt. Jeder Datenstrom kann auf dem Weg zu den Quellen der Anwendung in externen und wahrscheinlich verteilten Systemen verschiedene Wege gehen, beispielsweise durch Lastverteilung oder Server-Ausfall. Sie treffen somit zeitversetzt ein – ein Effekt, den man auch innerhalb einer verteilten Anwendung berücksichtigen muss.
Um das zu verdeutlichen, zeigt Abbildung 2 exemplarisch den komplexeren physischen Ausführungsplan der Anwendung aus Abbildung 1, mit je zwei parallelen Instanzen der Quellen und Operatoren und einer Instanz für die Senke. Dabei gibt es verschiedene Verbindungsmuster zwischen diesen Instanzen: einfache Weiterleitungen von je einer Quelle zu dem dazugehörenden Folgeoperator oder komplexere Partitionierungsmuster zwischen Map und Join-Operator.

Abbildung 2: Der physische Ausführungsplan der Stream-Processing-Anwendung aus Abbildung 1 mit je zwei parallelen Instanzen der Quellen und Operatoren, aber nur einer Senke.
Hier gilt es, den Datenstrom zu partitionieren, um sämtliche Anzeigeereignisse eines Nutzers seinen Abspielereignissen zuzuordnen. Lässt sich sicherstellen, dass alle Ereignisse von einem Nutzer immer auf derselben Join-Instanz bearbeitet werden, kann man diese Berechnung trivial parallelisieren. Meist gelingt das durch Hash-Partitionierung, was allerdings eine Verbindung von jeder Impressions-/Plays-Map-Instanz zu jeder Join-Instanz erfordert.
Zeit
Die Zuordnung der Anzeige- zu den Wiedergabeereignissen soll idealerweise alle Anzeigen vor dem Zeitpunkt der Wiedergabe genau so rekonstruieren, wie sie der Nutzer auf seinen Endgeräten gesehen hat. Das ist jedoch leichter gesagt als getan: Eine erste simple Lösung könnte sein, im Join alle Anzeigeereignisse zwischenzuspeichern und für jede Wiedergabe sämtliche zugehörigen Anzeigen zu suchen und dann auszugeben. Was logisch klingt, führt im allgemeinen Fall allerdings zu falschen Ergebnissen.
Man stelle sich Folgendes vor: Alice hat Filmempfehlungen für “Star Wars” und “Scary Movie” gesehen und sich letztlich “Spaceballs” angeschaut. Die dazugehörigen Ereignisse kommen von ihren Endgeräten und wandern durch mehrere verschiedene Schichten der Server des Anbieters. Dann werden sie von den Quellen gelesen und in den Map-Operatoren verarbeitet, etwa durch Entfernen von überflüssigen Details.
Sie liegen nun in den Datenströmen zwischen Map und Join (Abbildung 2). In welcher Reihenfolge der Join-Operator diese drei Ereignisse nun liest, hat entscheidende Konsequenzen für das Ergebnis der Berechnung: Wird “Spaceballs” zuerst gesehen, gibt es gar keine vorherigen Anzeigen. Auf ähnliche Weise könnte nur eine Anzeige vor der Wiedergabe eintreffen, und das Ergebnis wäre nicht vollständig.
Im Gegensatz zur Aufgabe, alle Anzeigen vor dem Zeitpunkt der Wiedergabe so zu rekonstruieren, wie Alice sie auf ihren Endgeräten gesehen hat, verknüpft diese Lösung nur die Anzeigeereignisse miteinander, die vor dem Wiedergabeereignis eintreffen. Das Ergebnis hängt also von der Verarbeitungszeit (Processing Time) ab. Die korrekte Lösung muss sich im Gegensatz zur Lösung auf Grundlage der Verarbeitungszeit auf die Zeitstempel in den Ereignissen beziehen. Das nennt man Event Time Processing – ein essenzielles Konzept jedes Stream-Processing-Frameworks.
Oft kommt in diesem Zusammenhang die “Star-Wars”-Analogie zum Einsatz, um die Unterschiede zwischen diesen beiden Zeitkonzepten zu verdeutlichen und auch zu betonen, dass Ereignisse nicht zwangsläufig nach Event Time sortiert eingehen müssen. Als Processing Time gilt dabei der Zeitpunkt, zu dem George Lucas und sein Team die Ereignisse zu einem Film verarbeitet haben. Die Event Time hingegen ist die Zeit, zu der die Geschichte “vor langer Zeit in einer weit, weit entfernten Galaxis” abläuft (Episode I bis VI und so weiter). Je nach Betrachtungsweise ist das eine oder das andere die richtige Reihenfolge (siehe Tabelle “Event Time vs. Processing Time”).
|
Processing Time |
Episode IV (1977) |
Episode V (1980) |
Episode VI (1983) |
Episode I (1999) |
Episode II (2002) |
Episode III (2005) |
|
Event Time |
Episode I (1999) |
Episode II (2002) |
Episode III (2005) |
Episode IV (1977) |
Episode V (1980) |
Episode VI (1983) |
Die Schwierigkeit liegt beim Event Time Processing darin herauszufinden, wann der eingehende Nachrichtenstrom hinreichend vollständig ist, um Aussagen über ihn zu treffen. Im “Star-Wars”-Kontext lässt sich das anhand der Jahreszahlen verdeutlichen: Sieht jemand die Filme zum Beispiel in der Reihenfolge der Kalenderjahre, wann kann er dann sicher sein, dass er die ganze Geschichte kennt?
1983 ging man zum Beispiel davon aus, dass die Geschichte vollständig sei, allerdings kamen mit einiger Verspätung noch weitere Ereignisse vor der bisher bekannten Geschichte heraus. Wie sieht es 2005 aus? Ist die Geschichte mit Episode I bis VI vollständig? Weiß man das eventuell beim Erscheinen von Episode VII (2015)? In diesem Fall wäre die Antwort ein klares Nein, denn 2016 erschien der Film “Rogue One”, der zwischen Episode III und IV spielt.
Für einen Stream Processor ist das ähnlich knifflig, denn mit jedem eingehenden Ereignis stellt sich ihm die Frage: Muss er noch auf weitere Daten warten (dann wäre sein Wissen umfassender) oder soll er (zeitnah) auf die momentan vorhandenen Informationen reagieren?
Watermarks
Dieser Kompromiss zwischen Vollständigkeit und schneller Reaktion lässt sich beim Stream Processing vom Nutzer nach eigenen Wünschen mit sogenannten Watermarks fein abgestuft regulieren. Ein Watermark(T) enthält einen Zeitstempel T und fließt mit den restlichen Nachrichten im Datenstrom. Das signalisiert, dass es zum Zeitpunkt des Empfangs (wahrscheinlich) keine weiteren Nachrichten mit einer Event Time vor oder einschließlich T im Datenstrom gibt.
Der Stream Processor kann also nun davon ausgehen, dass er alles bis T gesehen hat, und entsprechend reagieren. Sollte dennoch später ein Ereignis mit Zeitstempel kleiner oder gleich T erscheinen, handelt es sich dabei der Vereinbarung nach um ein verspätetes Ereignis. Man kann es entweder nutzen, um die Berechnung zu vervollständigen und ein erneutes Resultat auszugeben, oder es zur Fehleranalyse in einen Seitenkanal schreiben oder es komplett ignorieren. Dies lässt sich je nach Szenario vom Programmierer einstellen.
Doch wie entstehen Watermarks? Da sie den erwähnten Kompromiss zwischen Vollständigkeit und schneller Reaktion definieren, hängen ihre Erstellung und die Bestimmung von T hochgradig von der konkreten Anwendung ab. Typischerweise greift man zu einfachen Mechanismen, die den Grad an Unordnung im Datenstrom definieren. Wer beispielsweise ein Ereignis mit der Event Time X sieht, der hat wahrscheinlich auch alle Ereignisse bis Event Time X**–**4 gesehen und erstellt ein entsprechendes Watermark.
Mit diesem Schema kann man erst bei “Star Wars VII” davon ausgehen, alle Ereignisse bis Episode III gesehen zu haben. Episode I und II waren allerdings schon verspätete Ereignisse, da nach Episode VI schon ein Watermark(II) erstellt werden konnte. Bei einem so starken Grad an Unordnung müsste man also schon sehr lange warten, um sicherzugehen, dass man die Geschichte vollständig kennt.
Für das eingangs erwähnte Beispiel von Alice müsste man mit dem Join für Play(Spaceballs) mit den passenden Impressions also so lange warten, bis man von allen Eingangsdatenströmen ein Watermark erhalten hat, das größer oder gleich dem Zeitstempel des Wiedergabeereignisses ist. Nur so wird die Vereinbarung zu Watermarks in Operatoren eingehalten, die von unterschiedlichen Datenströmen lesen.
Aus Sicht des Operators kann man sich das auch so vorstellen, dass er über eine eigene Event-Time-Uhr verfügt, die ihren Zeiger nur mit dem Minimum aller zuletzt gesehenen Watermarks je Eingangsdatenstrom bewegt. Rückt dieser Zeiger also weiter, können alle zeitbasierten Aktionen bis zu diesem Zeitpunkt abgearbeitet werden.
Datenspeicher
Um nicht nur einfache Verarbeitungen mit Filter und Map zu erlauben, sondern auch komplexere Anwendungen wie das oben genannte Beispiel, braucht ein Stream-Processing-Framework irgendeine Form von Datenspeicher, der den aktuellen Zustand dauerhaft festhält und Zugriffe ermöglicht.
Traditionell kann man diese Funktion in eine externe Komponente auslagern, etwa in eine zentrale Datenbank, oder aber in einen internen beziehungsweise lokalen Datenspeicher. Externe Datenspeicher trennen die Verarbeitung der Daten von deren Speicherung. Das kann von Vorteil sein, führt aber auch zu vielen Nachteilen wie einer hohen Latenz beim Zugriff auf die Daten oder schwierigen Konsistenzmodellen vor allem bei sehr hoch skalierten Anwendungen.
Im Umfeld von Stream Processing haben sich daher lokale Datenspeicher etabliert (Abbildung 3), die den Zustand eines Operators im Arbeitsspeicher halten oder gegebenenfalls auch auf Festplatte/SSD auslagern. In dem Fall ist jeder Zugriff auf den Speicher lokal und damit um einiges schneller. Das Framework kann auch relativ einfach und ressourceneffizient eigene Konsistenzmodelle zur Verfügung stellen, muss als Preis dafür dann allerdings den Zustand des Operators verwalten. Daneben muss es sich um Fehlertoleranz, Ausfallsicherheit, Skalierbarkeit und Erweiterbarkeit kümmern.
Im Szenario mit den Videodaten brauchen wir zum Beispiel einen (lokalen) Datenspeicher, um die Anzeige- und Wiedergabeereignisse innerhalb des Join-Operators zwischenzuspeichern, bis das passende Watermark signalisiert, dass die Eingaben vollständig beziehungsweise vollständig genug sind, um sie zu verarbeiten. Das stellt sicher, dass die Ausgabe sowohl “Star Wars” als auch “Scary Movie” als Anzeigen für die “Spaceballs”-Wiedergabe umfasst und die Personalisierung korrekt abläuft.
Ausfallsicherheit und Konsistenzmodelle
Beim Stream Processing wird üblicherweise ein eingehendes Ereignis nach dem anderen verarbeitet, auch wenn es prinzipiell möglich ist, mehrere Ereignisse zu Gruppen beziehungsweise Batches zusammenzufassen und im Block zu bearbeiten.
Aus diesem Grund hat es sich etabliert, ein Konsistenzmodell um diese Ereignisse herum zu definieren. Dabei betrachtet man, wie dieses Ereignis beziehungsweise dessen Nachricht im Fehlerfall verarbeitet wird: Gibt es keinerlei Garantien dazu, spricht man von “at most once” (maximal einmal) – das heißt, eine Nachricht könnte verarbeitet werden oder auch nicht. Von “at least once” spricht man, wenn das System sicherstellt, dass jede Nachricht mindestens einmal verarbeitet wird, von “exactly once”, wenn das genau einmal geschieht.
Letztlich kann es aber auch keine Rolle spielen, wie oft eine Nachricht verarbeitet wird, solange sie nur die internen Zustände entsprechend dieser Garantien beeinflusst und keine Seiteneffekte verursacht: “Exactly once” bedeutet in dem Fall, dass jede Nachricht den internen Zustand exakt einmal beeinflusst. Es kann also passieren, dass die Nachricht mehrfach verarbeitet wird, aber der Zustand, beispielsweise ein Zähler, bleibt immer konsistent und zählt kein Ereignis doppelt. Getrennt davon lassen sich dann auch die Seiteneffekte mit einbeziehen, etwa an den Senken mit “exactly once end-to-end”, wobei auch die Resultate nur exakt einmal produziert werden.
Diese Formen der Fehlertoleranz setzen zwei Dinge voraus: die Möglichkeit, einen verteilten, konsistenten Snapshot des gesamten laufenden Systems zu erstellen, sowie die Möglichkeit, bei einem Neustart nach einem Fehler diesen Zustand wieder herzustellen und bei den Quellen an exakt der Position weiterzulesen, an der der Snapshot erstellt wurde.
Die unterschiedlichen Stream Processing Frameworks stellen auf verschiedene Weise sicher, diese Garantien zu erfüllen. Das im Detail zu besprechen, würde hier zu weit führen; in den jeweiligen Dokumentationen finden sich aber umfassende Beschreibungen. Nur so viel vorab: Einige bewahren neben den internen Speichern auch jeden Datenstrom innerhalb der Anwendung auf, um dann einfach dort weiterzumachen, wo die Anwendung vor einem Fehler stand. Andere speichern lediglich die internen Datenspeicher und haben stattdessen direkte, schnelle Kommunikationskanäle; sie müssen im Fehlerfall aber einige Nachrichten erneut verarbeiten.
Stream Processing Frameworks
Im Open-Source-Umfeld nutzt man als (Stateful) Stream Processing Frameworks hauptsächlich das Trio Apache Flink, Apache Kafka und Apache Spark. Alle wurden bei der Apache Software Foundation entwickelt und stehen folglich unter der Apache-Lizenz. Alle drei sind in Java und/oder Scala geschrieben und in der JVM zu Hause.
Während Apache Flink als Stream Processor geboren wurde, kam Apache Spark aus dem Batch-Processing-Umfeld und erlangte mit der Zeit Stream-Processing-Fähigkeiten in Form der Bearbeitung in Micro-Batches. Apache Kafka ist eigentlich ein Werkzeug zum Speichern von Datenströmen und als Quelle wie Senke für Stream Processing sehr populär. Es hat in den letzten Jahren aber auch einige Funktionen erhalten, die das Bearbeiten der gespeicherten Daten als Stream Processor ermöglichen.
Auch wenn sich diese Frameworks unterschiedlich entwickelt haben, finden sich in jedem davon alle oben erwähnten Konzepte auf die ein oder andere Weise (oder werden zumindest entwickelt). Die Implementationen dazu und die daraus folgenden Vorteile oder auch Einschränkungen fallen jedoch unterschiedlich aus, wie bereits hinsichtlich der Ausfallsicherheit angerissen.
APIs und Programmierung
Im Programmiermodell geht zwar jedes Stream-Processing-Framework seine eigenen Wege, einige Gemeinsamkeiten lassen sich aber dennoch feststellen. So gibt es zum Beispiel bei allen erwähnten Vertretern Programmierschnittstellen (APIs) mit unterschiedlich starker Ausdrucksfähigkeit. Das reicht von relativ systemnahen APIs mit fein abgestuften Kontroll- und Einflussmöglichkeiten auf Zustand und Zeitmechanismen (wie dem Erstellen eines Timers für einen zukünftigen Zeitpunkt) bis zu höher abstrahierenden Sprachen wie SQL, bei denen das Framework und dessen Optimierer die Details bestimmen. Hier muss sich der Entwickler nur darum kümmern, was für Daten er erhalten möchte, nicht aber um deren Beschaffung.
Wichtig bleibt hierbei jedoch, zwischen Datenbanken und Stream-Processing-Frameworks zu unterscheiden. Zwar kann man eine Anwendung mit SQL beschreiben und dann auf einem Stream Processor laufen lassen, doch das macht diesen nicht zur Datenbank. Hier dient SQL also lediglich als Beschreibungssprache für ein Programm. Im Streaming-Kontext ist das auch üblicherweise eine Anwendung, die kontinuierlich läuft und den unendlichen Datenstrom Stück für Stück bearbeitet. Sobald ein neues Ereignis zur Verfügung steht, passt sie den internen Zustand und die Ausgabe an.
Um überhaupt SQL benutzen zu können, definieren Stream-Processing-Frameworks oft eine gewisse Dualität zwischen einem Datenstrom und einer (dynamischen) Tabelle. Das ist auch gar nicht so abwegig: Sieht man zu einem bestimmten Zeitpunkt auf eine Tabelle und beobachtet ab dann alle Änderungen, hat man schließlich einen Datenstrom. Datenbanken erlauben oft sogar den Zugriff auf diesen Datenstrom, entweder durch eine Art Binär-Log oder in Form eines CDC-Datenstroms (Change Data Capture). Diese Dualität gilt analog auch umgekehrt.
Das eingangs erwähnte und bei den Konzepten bereits thematisierte Beispiel der Verknüpfung der Videowiedergabeereignisse mit den dazugehören Videoanzeigen zur Personalisierung lässt sich in Flinks SQL-API zum Beispiel so implementieren wie in Listing 1.
Listing 1
SQL-Beispiel in Flink
SELECT p.userid, p.title, p.playTime, COLLECT(DISTINCT i.title) AS impressions FROM Plays p, Impressions i WHERE p.userid = i.userid AND i.impressionTime BETWEEN p.playTime - INTERVAL '1' HOUR AND p.playTime GROUP BY p.userid, p.title, p.playTime
In dem Fall muss man nur noch »p.playTime« und »i.impressionTime« als Event-Time-Attribute definieren, inklusive Beschreibung der Watermark-Strategie, und schon hat man ein recht kompaktes Programm, das kontinuierlich für jede Videowiedergabe alle Anzeigeereignisse bis zu einer Stunde vor der Wiedergabe ausgibt.
In Flinks systemnaher DataStream-API (Java oder Scala) würde der Code für dieses Szenario schon etwas komplizierter ausfallen: Hier müsste sich der Programmierer selbst darum kümmern, die Ereignisse beim Lesen der jeweiligen Datenströme temporär zwischenzuspeichern, bis das Watermark signalisiert, dass die Eingabe vollständig ist. Dabei kann man aber auf diverse Hilfsmittel aus dem Framework zurückgreifen, und auch die API ist kein Hexenwerk. Sie ermöglicht durch ihre vielfältigen Funktionen auch noch eine ganz andere Art von Anwendungen, die exakt festlegen können, was bei jedem Ereignis genau passieren soll und wann.
Zu guter Letzt sei hier auch noch eine ganz andere Klasse von APIs erwähnt: Stateful Functions. Damit lassen sich leicht und flexibel ereignisbasierte verteilte Anwendungen erstellen, die als Unterbau einen starken Stream Processor nutzen (mit all den bisher besprochenen Konzepten und Vorteilen), sich aber im Serverless Umfeld wohlfühlen. Ein solches Programm wird nicht als Datenstrom modelliert, sondern als zustandsbehaftete Funktionen für jedes Objekt des Systems, wobei jede Funktion frei mit anderen interagieren kann.
In diesem Modell lassen sich für Flink auch andere Programmiersprachen nutzen als nur Java/Scala und SQL, da diese Funktionen untereinander per HTTP kommunizieren, aber abseits dieses Protokolls vollständig unabhängig voneinander sind. Die Firma Ververica hat für diese Art der Modellierung und Programmierung als eine der ersten im Stream-Processing-Umfeld vor einem Jahr ein Stateful-Functions-API veröffentlicht, das mittlerweile offizieller Bestandteil von Apache Flink ist.
Fazit
Stream Processing erlaubt es, relativ einfach verteilte Echtzeitanwendungen beliebiger Komplexität zu erstellen. Etablierte Open-Source-Frameworks helfen dabei, korrekte Ergebnisse zu produzieren. Sie übernehmen auch schwierige Aufgaben wie die Lastverteilung und Netzwerkkommunikation im darunterliegenden Cluster sowie die Fehlertoleranz bei Ausfällen.
Einige der hier diskutierten Konzepte mögen auf den ersten Blick verwirrend erscheinen, vor allem Event Time und Watermarks, sind aber letztlich natürliche Phänomene, die sich unmittelbar aus der Problemstellung heraus ableiten. Hat man sie erst einmal verinnerlicht, steht der nächsten Stream-Processing-Anwendung nichts mehr im Weg.
Auch wenn es verteilte Stream-Processing-Frameworks noch nicht so lange gibt, ist Stream Processing mittlerweile zu einem ernst zu nehmenden Paradigma geworden, an dem man nur schwer vorbeikommt, wenn man möglichst in Echtzeit viele Daten verteilt verarbeiten möchte. Das ist sicher einer der Bereiche, der weiterhin im Fokus steht, und bei dem sich die Grenzen zwischen geringer Latenz und gleichermaßen hoher Skalierbarkeit sowie Ausfallsicherheit weiter verschieben.
Um Stream Processing einem noch breiteren Publikum zur Verfügung zu stellen, wurden bereits SQL-APIs erstellt, damit nicht nur Software-Ingenieure, sondern auch Datentechniker, -analytiker und -wissenschaftler von den Vorteilen profitieren und Ergebnisse zunehmend in Echtzeit nutzen können. Diese APIs sowie das Ökosystem rundherum werden kontinuierlich weiterentwickelt.
Als dritte Säule für die Zukunft des Stream Processing werden wir sicher immer öfter auch Implementierungen aus dem Serverless-Umfeld sehen. Dabei modelliert man Programme als Funktionen, die untereinander beliebig vernetzt sind, allerdings ansonsten weitgehend unabhängig voneinander existieren. Dabei handelt es sich um eine der spannendsten Erweiterungen des eigentlichen Konzepts, die noch einige Überraschungen bereithalten dürfte. (jcb/jlu)
Der Autor
Dr. Nico Kruber, Committer im Apache-Flink-Project, arbeitet als Solutions Architect bei Ververica. Vor seiner Zeit mit Apache Flink promovierte er am Zuse-Institut Berlin in verteilten Systemen und arbeitete am verteilten, transaktionalen Key-Value-Store Scalaris.







