Schach-Webanwendung mit Kafka und WebSockets

  • von Autor

Niels Oliver Sperling, Softwareentwickler bei sidion

Apache Kafka ist ein Service, der Datenströme als Event Log speichert und die Verarbeitung sowohl in Echtzeit als auch asynchron erlaubt. Wir haben bei unserem Code Camp Kafka eingesetzt, um eine Webanwendung für Schach zu bauen, mit der Spieler über das Internet gegeneinander spielen können und beliebig viele Zuschauer ein Spiel in Echtzeit in ihrem Browser verfolgen können.

 

Kafka

Sie wollen einen Event Stream von möglicherweise tausenden von Nachrichten pro Sekunde in einem ausfallsicheren, redundant ausgelegten Data Store speichern, um diese Events dann beispielweise zu transformieren und in andere Systeme einzuspielen oder statistische Auswertungen darauf auszuführen? Ihnen ist dabei wichtig, die Events auch noch mal ganz vom Anfang an oder von einem beliebigen Offset an abspielen zu können, falls Events falsch verarbeitet oder ausgelassen wurden? Dann dürfte Sie Apache Kafka interessieren: Dies ist ein skalierbarer Service (ein verteilter Cluster), der es ermöglicht, Nachrichten in sogenannten Topics zu speichern (die, falls gewünscht, auf mehreren Nodes repliziert werden können) und diese dann über ein cleveres API zugänglich zu machen, das die genannten Use Cases unterstützt. So können die Nachrichten in Echtzeit mit nur wenigen Millisekunden Latenz verarbeitet werden, aber auch erst Tage später. Die hohe Performance wird dabei vor allem durch linearen IO erzielt: Die Nachrichten werden im Prinzip einfach hintereinander weg in ein Logfile geschrieben und können auch nur in dieser Reihenfolge wieder gelesen werden (Random Access ist also nicht möglich). Das Lesen kann jedoch bei einem beliebigen Offset begonnen werden.

Use Case

Es gibt viele mögliche Anwendung für ein derartiges System (https://kafka.apache.org/uses). Aktuell verwenden wir Kafka beispielweise in einem Projekt, um sämtliche Änderungen in einer Datenbank zu erfassen und diese dann (nach einer Transformation) in eine andere Datenbank einzuspielen (um den Übergang von einer Altanwendung auf ein neues System ohne Big Bang zu ermöglichen). Hier soll es jedoch um den Einsatz von Kafka in einer Schach Webanwendung gehen, wo wir sowohl von der Fähigkeit für Echtzeitzugriff Gebrauch machen, als auch von der Möglichkeit, alte Daten noch einmal zu lesen.

Anforderungen

  • Zwei Personen an unterschiedlichen Orten spielen über eine Webanwendung miteinander Schach, weitere Personen können das Spiel über das Internet live verfolgen.
  • Macht ein Spieler einen Zug, wird dieser ohne nennenswerte Verzögerung für den anderen Spieler und die Zuschauer sichtbar (ohne die Seite neu zu laden).
  • Das Spiel kann jederzeit unterbrochen und später fortgesetzt werden, der bisherige Spielverlauf wird dann neu geladen. Auch Zuschauer können jederzeit kommen und gehen.
  • Der Anwendungsserver ist stateless.
  • Die Anwendung ist beliebig skalierbar (mehrere Instanzen).

Unsere Lösung

Wir haben ein System  gebaut, dass neben Kafka drei Komponenten beinhaltet: Ein React Frontend und zwei Spring Boot Microservices, einen „Producer“-Service zum Schreiben der Spielzüge in den Kafka und einen „Consumer“-Service zum Lesen und Veröffentlichen derselben (die Trennung ist nicht unbedingt erforderlich, ermöglicht aber die unabhängige Skalierung des Consumer-Services im Fall von sehr vielen Zuschauern).

Spielbeginn

Wenn ein Spieler über das Frontend ein neues Spiel anlegt, wird ein Request an den Producer-Service geschickt. Dieser legt im Kafka ein neues Topic mit einer Partition an (innerhalb einer Partition werden Nachrichten in der Reihenfolge des Eingangs gespeichert).

Spielzüge schreiben

Wann immer ein Spieler einen Zug macht, schickt das Frontend einen Request an den Producer-Service (eine JSON Nachricht, die Informationen über den Spielzug und den aktuellen Spielstand in Forsyth–Edwards-Notation enthält). Der Producer-Service schreibt diese Nachricht in das Topic des Spiels über das Producer API von Kafka.

Spielzüge veröffentlichen

Der Consumer-Service bietet eine WebSocket-Schnittstelle für die Clients an. Hier registrieren sich die Clients (Spieler und Zuschauer), um über neue Spielzüge zu einem bestimmten Spiel informiert zu werden. Der Consumer-Service erhält die Nachrichten aller Spiel-Topics vom Kafka (über das Consumer API, welches über die Spring Kafka Integration verwendet wird) und leitet diese Nachrichten über die WebSockets an die jeweils interessierten Clients weiter. Auf diese Weise werden die Clients in Echtzeit über Spielzüge informiert, die dann im Frontend dargestellt werden. Was aber, wenn ein Zuschauer erst verspätet hinzustößt oder das Spiel unterbrochen und an einem anderen Tag fortgesetzt wird? Für diesen Fall bietet der Consumer-Service noch einen weiteren Endpunkt, über den ein Client den gesamten bisherigen Spielverlauf abrufen kann, bevor er dann wieder auf die aktuellen Events hört (auch der Spielverlauf wird im Frontend dargestellt). Der Consumer-Service liest den Spielverlauf aus dem Kafka, indem er von Offset Null bis zum aktuell letzten Offset alle Nachrichten aus dem Topic des Spiels ausliest (bei direkter Verwendung des Consumer APIs mit einem separaten Consumer, unabhängig vom dem, der auf Nachrichten in Echtzeit hört).

Fazit

Dank Kafka und der Spring Kafka Integration konnten wir die zunächst gar nicht so trivial erscheinenden Anforderungen mit sehr wenig Code umsetzen und eine im Prinzip beliebig skalierbare Anwendung schaffen. (Man muss dazusagen, dass wir für die eigentliche Spiellogik und Darstellung im Frontend fertige Komponenten eingebunden haben und die Einhaltung der Spielregeln im Backend nicht verifiziert wurde.)

Von dem schnellen linearen IO konnten wir allerdings nicht wirklich profitieren, da dieser nur wirklich zum Tragen kommt, wenn sehr viel IO auf einem einzelnen Topic stattfindet. Wir haben ja für jedes Spiel ein eigenes Topic angelegt. Hätten wir stattdessen ein großes Topic für alle Spiele angelegt, hätten wir die Möglichkeit verloren, gezielt den Spielverlauf eines bestimmten Spiels auszulesen. (Man hätte dafür dann sämtliche Spielzüge aller Spiele von Beginn an lesen müssen, was offensichtlich nicht skaliert.)

Dieser letzte Punkt lässt auch Zweifel an der Eignung von Kafka für Event Sourcing aufkommen: Hier will man schließlich in der Regel auch nicht einen großen globalen Event Stream, sondern beispielweise für jeden User oder jede Order einen, den man gezielt lesen kann. Das aber geht mit Kafka nur, wenn man sehr viele kleine Topics (oder sehr viele Partitionen innerhalb eines Topics) anlegt, womit man dann aber die hohe Effizienz von linearem IO gar nicht nutzt, da so für ein einzelnes Topic nur wenig IO zu erwarten ist.

Weitere Argumente, die Kafka als ungeeignet für Event Sourcing erscheinen lassen, finden sich hier: https://medium.com/serialized-io/apache-kafka-is-not-for-event-sourcing-81735c3cf5c

Zurück