Apache Kafka und Co – Ein Ausflug
Erste Station RabbitMQ
RabbitMQ ist ein etablierter Message Broker, welcher in Erlang geschrieben ist. Erlang ist für hoch parallele Applikationen interessant, da es jeden Core der CPU mehr als gut ausnutzen kann (1 Thread pro Core – Erlang Prozesse kann man Millionen machen – siehe Actor Modell). Wie funktioniert RabbitMQ?
Quelle: https://www.cloudamqp.com
Dieses Bild zeigt die Funktionsweise:
Eine Nachricht wird an einen Exchange geschickt. Anhand von konfigurierten Bindings wird die Nachricht an eine Queue weitergeleitet:
- Direct Binding: Die Nachricht wird direkt in eine Queue zugestellt
- Topic Binding: Hier kann man einen Routing Key angeben (Wörter mit Punkt separiert) und kann Pattern basiert (abc.*.xyz bzw. # für mehrere Wörter) die Zustellung zu Queues steuern. Die Nachricht kann natürlich in mehreren Queues landen, wenn mehrere Patterns matchen.
- Fanout Binding: hier wird die die Nachricht an alle angebundenen Queues geschickt
RabbitMQ hat ein eigenes Protokoll: AMQP. Allerdings hat es auch Adaptoren zu anderen Protokollen wie STOMP, JMS, MQTT, … Auch sind sehr viele interessant Features verpackt wie u.a. Priority Queue Support.
Interessant ist auch, dass RabbitMQ durchaus in z.B. einem Fanout Szenario die Nachricht (wenn klein genug) in allen Queues speichern kann:
The intent is for very small messages to be stored in the queue index as an optimisation, and for all other messages to be written to the message store. Doku
Prinzipiell gilt aber: Konsumierte Nachrichten sind weg. Es gibt zwar einige Plugins – aber die sind eher dafür gedacht, um das Leben des Entwicklers zu vereinfachen. Auch zu empfehlen ist EasyNetQ. Das ist ein Wrapper um die doch sehr „technische“ RabbiteMQ Client Library. EasyNetQ ist Conventation based – aber mit dem Advanced Bus kann man auch viele Vorteile nutzen und die Namensgebung selbst steuern. Ich hab mir den Code angeschaut – er macht einen sehr guten Eindruck – man kann ihm durchaus trauen.
Ein zusätzlich nettes Feature ist Federated Exchanges: Es kann auf einen entfernten Exchange gebunden werden, der die Nachrichten wie ein lokaler zugestellt bekommt.
Therefore the federated exchange only receives messages for which it has a subscription. Doku
RabbitMQ ist intelligent genug, dass es nur Nachrichten an den entfernten Exchange weiterleitet, für die es ein potentielles Binding gibt.
Sehr interessant fand ich auch die Admin API, da man mit „/api/queues?columns=name,consumers“ eine super Übersicht hat, welche Queues gerade konsumiert werden und welche nicht – eine Überwachung ist daher einfach umzusetzen. RabbitMQ hat eine Heartbeat Implementierung, was eine zuverlässige und schnelle Detektion erlaubt.
Für mich das interessanteste Finding war: Ich habe keine Möglichkeit, Nachrichten nochmals abzuspielen – Gründe könnten sein, dass man Nachrichten analysieren will oder durch das Abspielen einen State wiederherstellen will (Event-Sourcing). Im Falle von RabbitMQ muss ich zum Sender gehen und muss e.g. nach einem Absturz sagen: „Bitte sende mir deinen aktuellen State“. Oft brauche ich dieses Feature nicht – aber wenn ich es brauche, dann ist es mit RabbitMQ durchaus kompliziert zu bauen.
Was unterscheidet also Kafka von RabbitMQ?
Zwischenstation RPC (Remote Procedure Call)
RabbitMQ ist ein sogenannter Broker-Style. D.h. man versucht Komponenten voneinander zu entkoppeln weil
- Sender und Empfänger müssen sich nicht kennen
- Es erfolgt eine zeitliche Entkoppelung, weil der Empfänger auch zeitverzögert abarbeiten kann
- Der Broker ist außerhalb der Applikation und erlaub es daher, e.g. den Sender neu zu starten, ohne dass Nachrichten verloren gehen.
Diese Eigenschaften machen durchaus Sinn – aber eben nicht immer. Viele Details findet man in diesem Buch.
Ich habe inzwischen sehr oft gehört „RPC ist böse“ bzw. „RPC nutzen wir sicher nicht“. Eine fundierte Begründung gab es nie … Was ist RPC nun?
Remote Procedure Invocation applies the principle of encapsulation to integrating applications. If an application needs some information that is owned by another application, it asks that application directly. If one application needs to modify the data of another, then it does so by making a call to the other application. Each application can maintain the integrity of the data it owns. Furthermore, each application can alter its internal data without having every other application be affected. http://www.enterpriseintegrationpatterns.com
Man spricht auch von einer Broker-Less-Architektur. Applikationen sprechen direkt miteinander. Durch ein Directory-Service wie etcd oder Consul findet man andere Applikationen.
All of Google’s services communicate using a Remote Procedure Call (RPC) infra‐ structure named Stubby; an open source version, gRPC, is available. Often, an RPC call is made even when a call to a subroutine in the local program needs to be per‐ formed. [1]
Und ich behaupte es ist fast ein ungeschriebenes Gesetz in der IT: Wenn google was im großen Stil macht, passt es meistens. Ich erspare mir mit RPC viele Network Hops.
Fazit: Problem erkennen, Werkzeug suchen, umsetzen.
Zweite Station Kafka
Bei Kafka spricht man von einer „distributed commit log” (bzw. „distributing streaming platform“). Nachrichten werden zuverlässig in einer Log gespeichert. Kafka gruppiert Nachrichten in sogenannten Topics, welche wiederrum in Partitions unterteilt werden.
Partitions können auf unterschiedlichen physikalischen Hosts liegen und erlauben daher eine gute Skalierung. Partitions werden mit Hilfe von Consumer Groups gelesen:
Wichtig ist dabei, dass innerhalb einer Consumer Group eine Partition nur von einem Consumer gelesen werden kann. Stirbt ein Consumer, so übernimmt ein anderer seine Arbeit („Rebalance“). Um alle Nachrichten zu bekommen, würde man pro Applikation eine Consumer-Group machen.
Zusammengefasst: Producer hängen Nachrichten immer ans Ende der Commit-Log an – Consumer können Nachrichten von einer beliebigen Stelle weg lesen (ein großer Unterschied zu RabbitMQ – hier kann ich schwer wieder von vorne anfangen) – die Nachrichten verschwinden nicht, wenn sie gelesen wurden. Es können viele Consumer parallel die Message-Log lesen, ohne sich gegenseitig zu beeinflussen. Kafka funktioniert extrem performant, da es Linux Features wie den System-Call „sendfile“ nutzt, mit dem ein Pagecache schnell an einen Socket geschickt werden kann. Consumer können ihr letztes Offset auch in Kafka speichern. Somit können sie bei einem Absturz an der letzten Stelle weitermachen.
Auch stellt sich natürlich die Frage, wie lange die Nachrichten tatsächlich in der Log bleiben – weil sonst läuft sie ja irgendwann über. Es gibt zwei Strategien:
- Retention: Hier kann man sagen, dass Nachrichten e.g. für 7 Tage verfügbar sein sollen oder ein Topic maximal 1 GB haben darf.
- Log Compaction: Man kann jeder Message einen Key angeben. Log Compaction entfernt periodisch alte Werte des Keys – d.h. es ist immer garantiert, dass der letzte Wert des Keys verfügbar ist. Sehr interessant für Event-Sourcing.
Kafka Clients pollen den Broker und holen sich dann gleich mehrere Nachrichten – das ist sehr performant (ist alles bis ins letzte Detail kfonigurierbar).
Was ich nicht so toll fand ist, dass es keinen nativen C# Client gibt – es ist nur ein Wrapper um den C-Client. Auch hat er nicht die neuesten Features wie e.g. Stream-Processing. D.h. man kommt wohl schwer um den Referenz Client „Java“ herum. Ebenfalls muss man sich mit Zookeeper auseinandersetzen. Es gibt Docker Images, welche beides für einen „übernehmen“ – trotzdem rennt ein anderes Service mit.
Kafka hat viele Einsatzgebiete – aber die interessantesten sind sicher: das verteilen von Datenbank Änderungen und Stream Processing.
Fazit: Kafka und RabbitMQ aber auch RPC ergänzen sich und haben unterschiedliche Einsatzgebiete. Sich auf ein Tool zu versteifen finde ich persönlich falsch.
[1] Beyer, B., Jones, C., Petoff, J., & Murphy, N. R. (2016). Site Reliability Engineering: How Google Runs Production Systems. " O’Reilly Media, Inc.".