Java Magazin   8.2021 - Event Streaming

Preis: 9,80 €

Erhältlich ab:  Juli 2021

Umfang:  100

Autoren / Autorinnen: 
Dominik Mohilo ,  
Tam Hanna ,  
Benjamin Buick ,  
Benjamin Buick ,  
Robin de Silva Jayasinghe ,  
Tam Hanna ,  
Karine VardanyanStephan Rauh ,  
Tim ZöllerIngo Küper ,  
Martin Lippert ,  
Arne Limburg ,  
Dr. Veikko Krypczyk ,  
Manfred Steyer ,  
Natalie RampKatrin Bertschy ,  
Markus Stroh ,  
Roger Butenuth ,  

Die Physik und Metaphysik nach Aristoteles sieht in der Kinesis, also der Bewegung, nicht nur die physische Bewegung von A nach B, sondern auch die Veränderung. Auch andere griechische Philosophen, etwa Heraklit und Platon, befassten sich schon mit der steten Veränderung. Dem ein oder anderen wird dahingehend die Flusslehre vielleicht in Erinnerung geblieben sein. Diese ist – stark vereinfacht – zu einem geläufigen Sprichwort geworden, das besagt: Man kann nicht zweimal in den gleichen Fluss steigen.

Veränderung ist auch, um hier den Bogen über die Philosophie zur Weltliteratur zu spannen, das zentrale Motiv in Kafkas epochaler Erzählung „Die Verwandlung“. Wundert es da, dass das Framework Apache Kafka sich mit Datenströmen befasst? Vermutlich nicht. Und dass Amazons Antwort auf Apache Kafka unter dem Namen AWS Kinesis veröffentlicht wurde, passt also ebenfalls hervorragend in die Terminologie und das Narrativ der Verarbeitung sogenannter Data Streams.

„Panta rhei“ sagte Heraklit einst, und so war und ist es auch für diese Seite, das Java Magazin und mich selbst: Alles fließt. Daher möchte ich zum Schluss noch einige persönliche Worte an Sie, liebe Leser*innen und Autor*innen richten. In den vergangenen Jahren habe ich die erfüllende Aufgabe wahrnehmen dürfen, die inhaltliche und konzeptionelle Ausrichtung des Java Magazins gemeinsam mit unseren Autoren und Autorinnen sowie meinen lieben Kollegen zu prägen. Da aber nun einmal alles in Bewegung und Veränderung unvermeidbar ist, endet meine Zeit als Redakteur des Java Magazins mit dieser Ausgabe.

Ich möchte mich für die allseits wunderbare Zusammenarbeit mit unseren Expert:innen über diese lange Zeit herzlichst bedanken. Und natürlich bedanke ich mich auch bei Ihnen, liebe Leserinnen und Leser, für die Treue und dass Sie mir den ein oder anderen Schabernack hier an dieser Stelle nachgesehen haben. Und haben Sie keine Angst: Der Fluss an Fachartikeln wird nicht versiegen, es wird nur nicht mehr der gleiche sein wie der, in den Sie die letzten Jahre gestiegen sind.

mohilo_dominik_sw.tif_fmt1.jpgDominik Mohilo | Redakteur

Mail Website Twitter

In dieser Artikelserie werden wir in Zukunft ein Clojure-Feature pro Ausgabe unter die Lupe nehmen – von „A wie assoc“ bis „Z wie zipper“. Das Ziel dabei ist, interessante Aspekte der Sprache in kleinen Häppchen vorzustellen, die sich leicht verdauen lassen. Zum Auftakt schauen wir uns heute die Funktion assoc und ihr Umfeld an.

Wie bei jeder Clojure-Funktion ist es sinnvoll, zuerst einen Blick in die Dokumentation [1] zu werfen. Die Signatur (assoc map key val) bzw. (assoc map key val & kvs) besagt, dass die Funktion assoc mit den Parametern map, key und val aufgerufen wird. Optional können weitere key- und value-Paare am Ende der Parameterliste hinzugefügt werden. Des Weiteren ergibt sich aus der Dokumentation, dass die Funktion auf eine Map oder einen Vektor angewandt werden kann. Wenn sie auf eine Map angewandt wird, gibt sie eine neue Map desselben Typs zurück, die zusätzlich die Zuordnung vom Value val zum Schlüssel key enthält. Bei Vektoren verhält es sich ähnlich, nur dass der key den Index des Vektors bestimmt, an dem der neue Wert eingesetzt wird. Dabei muss der Index kleiner/gleich der Länge des Vektors sein. Damit lässt sich ein Element direkt am Ende des Vektors hinzufügen, aber nicht darüber hinaus. Dass jeweils eine neue Datenstruktur zurückgegeben wird, liegt in der Natur von Clojure: Datenstrukturen sind in der Regel unveränderlich. Den Umgang von Clojure mit unveränderlichen Datenstrukturen und die Gründe, die für unveränderliche Datenstrukturen sprechen, werden wir im Teil „P wie Persistenz“ dieser Reihe genauer beleuchten. Wie sich die Benutzung der Funktion gestaltet, lässt sich in Listing 1 sehen. Zeilen, die mit => beginnen, gehören hierbei nicht zum eigentlichen Programmcode, sondern sollen das Ergebnis der vorangegangenen Evaluation im Code visualisieren.

Listing 1

(let [example-map {:a 1}]
 
  ; neuer Schlüssel :b mit dem Wert 2 hinzugefügt
  (assoc example-map :b 2) 
    => {:a 1, :b 2}
 
  ; Bestehende Assoziationen werden überschrieben
  (assoc example-map :a 2 :b 3 :c 4)) 
    => {:a 2, :b 3, :c 4}
 
; Dem Vektor wird das neue Element '4' an Index 3 hinzugefügt
(assoc [1 2 3] 3 4) 
  => [1 2 3 4]

In den gezeigten Beispielen wird assoc benutzt, um eindimensionale Maps oder Vektoren um neue Assoziationen zu ergänzen (genau genommen lässt sich assoc auf alle Datenstrukturen anwenden, die das Interface clojure.lang.Associative implementieren). Die Funktion auf verschachtelte Vektoren oder Maps anzuwenden, gestaltet sich schwierig. Glücklicherweise existiert eine Hilfsfunktion assoc-in, die Entwicklern exakt diese Funktion bereitstellt. Der Blick in die Dokumentation [2] zeigt uns die Signatur: (assoc-in m [k & ks] v). Der erste Parameter ist wieder eine assoziative Datenstruktur, gefolgt von einem Vektor mit beliebig vielen, aber mindestens einem Schlüsselwert. Der letzte Parameter enthält den zu assoziierenden Wert. In Listing 2 sind einige Codebeispiele zu dieser Funktion enthalten.

Listing 2

(let [person {:name {:first "Homer"
                     :second "Simpson"}
              :age 40}]
  ; Ein neuer Wert wird in der zweiten Ebene ergänzt
  (assoc-in person [:name :middle] "Jay")
    => {:name {:first "Homer"
               :second "Simpson"
               :middle "Jay"}
        :age 40}
 
  ; Existiert eine verschachtelte Assoziation noch nicht, wird eine neue Map hinzugefügt
  (assoc-in person [:address :street] "Evergreen Terrace"))
    => {:name {:first "Homer"
               :middle "Jay"}
        :age 40
        :address {:street "Evergreen Terrace"}}
 
; Ein Wert wird in der zweiten Ebene des Vektors neu zugewiesen
(assoc-in [[1 2] [2 3]] [1 0] 0)
  => [[1 2][0 3]]
 
; Mischformen aus Maps und Vektoren sind möglich
(assoc-in [{:name "Holger"} {:name "Fritz"}] [0 :age] 40)
  => [{:name "Holger" :age 40} {:name "Fritz"}]

Die Werte einer assoziativen Datenstruktur lassen sich natürlich auch extrahieren. Hierfür ist die Funktion get [3] vorgesehen. Die Nutzung lässt sich an ihrer Signatur ableiten: Mit (get map key) lässt sich der zum Schlüssel key zugeordnete Wert aus einer assoziativen Datenstruktur map ermitteln. Als optionaler vierter Parameter not-found wird ein Wert übergeben, welcher zurückgegeben wird, wenn das eigentliche Ergebnis des Funktionsaufrufs nil wäre. Ähnlich wie assoc lässt sich get nur komfortabel auf flachen Datenstrukturen anwenden. Um im Falle verschachtelter Datenstrukturen einen mehrfachen Aufruf von get zu verhindern, bietet es sich an, die Funktion get-in [4] mit der Signatur (get-in m ks) bzw. (get-in m ks not-found) zu verwenden. Die Verwendung von get und get-in wird in Listing 3 verdeutlicht.

Listing 3

(let [person {:name {:first "Homer"
                     :second "Simpson"}
              :age 40}]
 
  (get person :name)
    => {:first "Homer", :second "Simpson"}
 
  ; Der dritte Parameter definiert einen Defaultwert
  (get person :height 180)
    => 180
 
  ; Verschachtelte Aufrufe für verschachtelte Datenstrukturen
  (get (get person :name) :second)
    => "Simpson"
 
  ; get-in vereinfacht den Zugriff auf verschachtelte Datenstrukturen
  (get-in person [:name :second])
    => "Simpson"
 
  (get [:a :b :c] 2)
    => :c
 
  (get-in [[:a :b] [:c :d]] [1 0]))
    => :c

Um existierende Assoziationen in einer Datenstruktur zu aktualisieren, könnte man assoc bzw. assoc-in und get bzw. get-in kombinieren, da die Funktion assoc ja bereits bestehende Assoziationen überschreibt. Auch hier bietet Clojure einen schöneren und vor allem sprechenderen Weg an. Mit update bzw. update-in ist es möglich, eine Funktion auf einen Teil einer Datenstruktur anzuwenden. Dabei sind sämtliche Arten von Funktionen erlaubt, unabhängig von ihrer Signatur. Einige Beispiele dazu sind in Listing 4 aufgeführt. Interessant ist in diesem Beispiel, dass das Symbol + auf eine Funktion referenziert und nicht, etwa wie in Java, auf einen Operator. Mehr dazu wird es im Laufe der Serie beim Thema „O wie Operator“ geben.

Listing 4

(let [person {:name {:first "Homer"
                     :second "Simpson"}
              :age 40}]
 
  ; inc erhöht einen numerischen Wert um 1
  (update person :age inc)
    => {:name {:first "Homer", :second "Simpson"}, :age 41}
 
  ; Wendet die Funktion + mit dem zusätzlichen Parameter auf dem mit :age assoziierten Wert an
  (update person :age + 10)
    => {:name {:first "Homer", :second "Simpson"}, :age 50}
 
  ; String-Konkatenation auf dem verschachtelten Vornamen
  (update-in person [:name :first] str "Jay"))
    => {:name {:first "Homer Jay", :second "Simpson"}, :age 40}

Möchte man in einer Datenstruktur eine existierende Assoziation entfernen – quasi dissoziieren – bietet sich die Funktion dissoc [5] an. Die Signatur erinnert an die der Funktion assoc, mit dem Unterschied, dass natürlich kein Wert angegeben werden kann, nur der zu dissoziierende Schlüssel. Ein weiterer Unterschied zu assoc ist, dass dissoc nur auf Maps angewandt werden kann, nicht auf Vektoren. Wenn man genau darüber nachdenkt, ist das auch logisch: Eine Assoziation aus einem Vektor zu entfernen, würde weitere Effekte auslösen, beispielsweise das Verschieben aller folgenden Elemente um eine Position nach vorne, um die entstandene Lücke zu schließen. Ein Gegenstück zu assoc-in, also etwa dissoc-in, existiert nicht, lässt sich jedoch mit der bereits erwähnten Funktion update-in [6] umsetzen. Ein Anwendungsbeispiel findet sich in Listing 5.

Listing 5

(let [person {:name {:first "Homer"
                     :second "Simpson"}
              :age 40}]
  ; Entfernt die Assoziation des Schlüssels :age aus der Map
  (dissoc person :age)
    => {:name {:first "Homer", :second "Simpson"}
 
  ; Wendet (dissoc map :second) auf die verschachtelte Map an, welche mit :name assoziiert ist
  (update-in person [:name] dissoc :second))
    => {:name {:first "Homer"} 
        :age 40}

In diesem Teil der Serie haben wir grundlegende Funktionen zum Hinzufügen, Lesen, Aktualisieren und Entfernen von Werten in und aus assoziativen Datenstrukturen vorgestellt. Dabei lässt sich vor allem an den Funktionen update und update-in erahnen, dass Clojures funktionaler Ansatz sehr mächtig ist. Wir hoffen, dass wir mit dem ersten Artikel die Lust auf mehr geweckt haben und freuen uns darauf, euch auch im weiteren Verlauf der Serie einige Ansätze der Sprache näherbringen zu können.

zoeller_tim_sw.tif_fmt1.jpgTim Zöller arbeitet seit über 13 Jahren als Softwareentwickler, davon über 10 Jahre mit Java. Mit der von ihm gegründeten Firma lambdaschmiede GmbH in Freiburg unterstützt er Kunden in Softwareprojekten und entwickelt eigene Software mit Clojure.

Mail

kueper_ingo_sw.tif_fmt1.jpgIngo Küper studierte Angewandte Mathematik und Informatik, ist geschäftsführender Gesellschafter der doctronic GmbH & Co. KG, bildet seit 20 Jahren Fachinformatiker aus und blickt auf über 35 Jahre Erfahrung in der Softwareentwicklung und -architektur zurück.

Mail

Vielerorts wird Kafka schon als das Allheilmittel für alle Kommunikationsprobleme in verteilten Systemen angesehen. Ein bisschen erinnert das an die späten 2000er Jahre, wo in vielen Unternehmen nach der Einführung von SOA auch ESBs eingesetzt wurden, um die SOA-Probleme zu lösen. Auch bei Kafka und anderen Streamingplattformen ist es so: Der falsche Einsatz kann zu einem architektonischen Desaster werden. Aber warum eigentlich? Und worauf muss man achten, um diesem Desaster zu entkommen?

Als Netflix anfing, seine Applikation in viele kleine Services zu zerschneiden, die dann miteinander kommunizieren sollten, war zunächst der Plan, die Kommunikation synchron über REST laufen zu lassen. Recht schnell war allerdings klar, dass synchrone Kommunikation an ihre Grenzen stößt, wenn man viele kleine Services unabhängig entwickeln und laufen lassen will. Bei jedem Aufruf muss damit gerechnet werden, dass der aufgerufene Service gerade nicht verfügbar ist und das selbst, wenn man gar keine Antwort erwartet, sondern den Service nur informieren will. Als Aufrufer muss man dann aufwendige Retry-Mechanismen implementieren (je nach Anforderung müssen die Nachricht ggf. mehrere Tage vorgehalten werden, um die Zustellung erneut zu versuchen). Da bietet es sich natürlich an, eine Messaginginfrastruktur einzusetzen, die hochverfügbar ist und die Nachricht zunächst annimmt, sodass der Consumer sie eigenständig abholen kann, wenn er verfügbar ist. Auf diese Weise erreicht man eine Entkopplung von Producer und Consumer. Letzterer kann dann selbst entscheiden, ob er verpasste Nachrichten verwerfen oder später verarbeiten will. Asynchrone Verarbeitung über Messages bringt aber neue Probleme mit sich, auf die ich in dieser Ausgabe der Kolumne eingehe.

Two hard Problems

Mathias Verraes tweetete bereits 2015: „There are two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery“ [1]. Diese Abwandlung der „Only two Problems“-Sprüche bringt die Herausforderungen verteilter Systeme sehr gut auf den Punkt.

Bei asynchroner Kommunikation ist es praktisch unmöglich, sicher zu sein, dass die Nachrichten in der abgesendeten Reihenfolge beim Empfänger ankommen. Spätestens wenn die Events von mehreren Instanzen des sendenden Service verschickt werden, kann eine bestimmte Reihenfolge nicht mehr garantiert werden. Die Messaginginfrastruktur kann zwar gewisse Garantien geben, sie ist aber auch nur ein Aspekt des Systems. Wenn die Reihenfolge wirklich wichtig ist, muss sie auf sendender Seite, in der Infrastruktur und auf der empfangenden Seite sichergestellt werden. Da ist es in der Regel einfacher, im Code sicherzustellen, dass die Messages unabhängig von der Reihenfolge abgearbeitet werden können.

Dasselbe Problem tritt auf, wenn es darum geht, dass eine Nachricht nur genau einmal verarbeitet werden soll. Zwar stellen Messaginginfrastrukturen wie Kafka in der Regel mittlerweile Mechanismen zur Verfügung, um die einmalige Auslieferung sicherzustellen, aber auch hier gilt: Das ist nur sinnvoll, wenn Sender und Empfänger das auch können – und das ist häufig gar nicht so einfach. Zwar gibt es Lösungen, wie z. B. das Outbox-Pattern [2], aber das bringt eben eine Komplexität mit sich, die die Frage aufwirft, ob Exactly-once Delivery wirklich so wichtig ist, oder ob das System nicht besser damit umgehen können sollte, dass Nachrichten auch mal zweimal ausgeliefert werden.

Wenn die Reihenfolge der Nachrichten und/oder die exakt einmalige Auslieferung in einem System tatsächlich wichtig sind, ist es eventuell sinnvoll, eine Messaginginfrastruktur zu wählen, die diese Garantien geben kann (z. B. über gemeinsame Transaktionen mit der Datenbank), sodass der Aufwand des Sicherstellens eben nicht bei den fachlichen Services liegt, die sich eher darum kümmern sollen, die von ihnen erwartete Fachlichkeit zu realisieren. Mit Kafka lassen sich leider keine gemeinsamen Transaktionen mit einer Datenbank realisieren. Eine solche Möglichkeit bietet in der Enterprise-Java-Welt nur das XA-Protokoll (siehe [2]), das in klassischen JEE-Servern zur Verfügung steht und von JMS-Providern als Messaginginfrastruktur unterstützt wird.

Herausforderungen und Lösungen

Wenn es also unterschiedliche technologische Möglichkeiten einzelner Messaginginfrastrukturen gibt, welche architektonischen Überlegungen sollten dann vor der Wahl der Messaginginfrastruktur angestellt werden?

Der erste wichtige Punkt ist, dass nicht versucht werden sollte, architektonische und softwaredesigntechnische Herausforderungen technologisch zu lösen. Den Fehler haben viele Unternehmen bereits während des SOA-Hypes gemacht, als Enterprise-Services-Bus-Systeme (ESBs) eingesetzt wurden, um die architektonischen Herausforderungen, die durch SOA entstanden, zu lösen. Die architektonischen Herausforderungen, die man damals mit ESBs lösen wollte, waren z. B. die Frage danach, wer mit wem kommuniziert, wer wen kennt und wer welches Datenmodell zur Kommunikation nutzt. Der ESB sollte jede dieser Fragen verstecken, indem er ein Mapping zwischen Service-Aufrufen und Datenmodellen vornahm. Falls ein aufgerufener Service ein anderes Datenmodell (oder eine andere Version) verwendete, lag es im Aufgabenbereich des ESB, dieses zu transformieren. Wenn die Quelle einer Information von einem Service zum anderen wechselte, lag es im Aufgabenbereich des ESB, das Mapping entsprechend zu ändern, sodass die Clients davon nichts mitbekamen. Das Ergebnis waren sehr komplexe ESB-Konfigurationen, die schwer wartbar waren und deren Anpassung bei neuen Deployments schnell zu Bottlenecks wurden.

In Microservices-Architekturen gibt es andere Pattern, die diese Probleme adressieren und das unabhängig von der (Messaging-)Infrastruktur. Ein grundsätzlicher Unterschied zu den Architekturen mit ESB ist das Pattern „Smart Endpoints, dump Pipes“ [3]. Ziel ist es, die Mappinglogik eben nicht in einer Infrastrukturkomponente abzubilden, sondern in den Services selbst. Ähnliches gilt für das Tolerant-Reader-Pattern bei der Kompatibilität von Datenmodellen.

Makroarchitektur mit Messagingplattformen

Grundsätzlich gibt es zwei Gründe, warum man in einer verteilten Architektur auf asynchrone Kommunikation über Messaging setzten möchte. Der erste Grund ist die bereits beschriebene Entkopplung. Einerseits möchte ich als Aufrufer nicht davon abhängig sein, ob der Empfänger meiner Nachricht gerade in der Lage ist, sie entgegenzunehmen oder nicht. Eine Messaginginfrastruktur schafft hier eine geeignete Indirektion. Voraussetzung dafür ist allerdings, dass die Messaginginfrastruktur selbst hochverfügbar ist. Entkopplung geht (vor allem aus Architekturperspektive) aber noch einen Schritt weiter: Eventuell möchte ich mich nicht einmal darum kümmern, wer meine Nachricht entgegennimmt.

Und damit kommen wir direkt zum zweiten Grund, warum Messaging anstelle von synchroner Kommunikation sinnvoll sein kann: In einer verteilten Architektur möchte ich nicht, dass jeder jeden kennen muss. Vielmehr ist es so, dass ich in einer Makroarchitektur (also der Architektur, die die Abhängigkeiten zwischen den Services beschreibt) auch klare Regeln definieren möchte, welcher Service welchen kennen darf. Auch hier sind z. B. zirkuläre Abhängigkeiten ein „Architecture Smell“. Wie kann ich aber designen, wenn zwei Services gegenseitig Informationen austauschen müssen? Zunächst muss ich entscheiden, welcher der beiden Services den anderen kennen (also aufrufen) darf. Falls Service 1 Service 2 kennen darf, kann er sich für Events von Service 2 registrieren und dessen Nachrichten bekommen. Service 2 muss nicht wissen, wer seine Events verarbeitet. Möchte jetzt Service 1 zusätzlich Informationen an Service 2 übermitteln, so kann das aus architektonischer Sicht nicht über Events passieren, weil sich dazu Service 2 bei Service 1 registrieren müsste, den er ja gar nicht kennen soll. Die Lösung wären hier Commands. Service 2 könnte also eine Command-Schnittstelle anbieten, die von Service 1 aufgerufen wird. Diese Schnittstelle könnte entweder über REST realisiert werden oder – um die oben genannte Entkopplung dennoch hinzubekommen – auch über die Messaginginfrastruktur. Aus architektonischer Sicht müsste bei übermittelten Messages dann zwischen Events und Commands unterschieden werden. Einige Messaginginfrastrukturen unterscheiden die beiden Varianten technologisch. Bei JMS-Providern wird z. B. zwischen Topics und Queues unterschieden. Architektonisch würde man über Topics Events verschicken (mit beliebig vielen Empfängern) und über Queues Commands (genau ein Empfänger).

Kafka unterscheidet die beiden Varianten technologisch nicht. Wird also Kafka als Messaginginfrastruktur gewählt, muss auf andere Weise klargestellt werden, ob es sich bei einer Message um ein Event oder ein Command handelt. Architekturen, die das nicht tun, drohen im Chaos zu versinken, weil niemand nachvollziehen kann, wer eigentlich welche Nachricht sendet und/oder empfängt. Grundsätzlich sollte immer festgelegt sein, dass es pro Event genau einen Service gibt, der dieses versendet. Bei Commands sollte es immer genau einen Service geben, der es empfängt.

Entkopplung der Architektur von der Technologie

Wenn ich bestimmte Features (wie verteilte Transaktionen von JMS oder aber das Speichern und Replay von Messages bei Kafka) für meine Architektur voraussetze, binde ich mich mit meiner Architektur automatisch an die jeweils zugrunde liegende Technologie. Das Ergebnis könnte in ein paar Jahren sein, dass ich an einer Legacy-Technologie festhänge und nicht in der Lage bin, sie abzulösen.

Wie kann ich das verhindern? Aus architektonischer Sicht ist es sinnvoll, die tatsächlichen Anforderungen von den Fähigkeiten der Messaginginfrastruktur zu lösen. Wenn ein Service ein Event versendet, sollten die Eigenschaften des Events (und vor allem die Eigenschaften des Versendens) beschrieben werden, ohne sich auf die zugrundeliegende Messaginginfrastruktur zu beziehen, z. B. „Dieses Event wird erst gesendet, wenn der Fall tatsächlich erfolgreich war“. Ob diese Eigenschaft durch die Messaginginfrastruktur (verteilte Transaktionen), durch das Outbox-Pattern oder durch eine Kombination transaktionaler (Kafka-)Topics erreicht wird, ist aus architektonischer Sicht uninteressant. Gleiches gilt für die Anforderung „Events sind erneut abspielbar“. Man kann das natürlich mit Kafka recht einfach realisieren, aber natürlich auch, indem man die Events zusätzlich in einer Datenbank speichert.

Im gleichen Zuge sollte sich natürlich auch die Sprach- und Denkweise der Entwickler ändern: Es sollte nicht heißen „Unser Service empfängt Kafka-Events“, sondern „Unser Service empfängt Events von Service XY“. Dass der Kanal „zufällig“ Kafka ist, sollte dabei keine Rolle spielen.

Fazit

Kafka ist ein sehr gutes Produkt, wenn es um das Streamen großer Mengen an Messages geht. Diese können dabei noch für einen längeren Zeitraum gespeichert werden und man kann sie sogar erneut einspielen. All diese Features verleiten dazu, Kafka als Allzweckwaffe zur Kommunikation zwischen Services zu verwenden. Dadurch entstehen mehrere Herausforderungen. Neben den erläuterten architektonischen Herausforderungen gibt es auch zwei technologische. Die erste Herausforderung ist die Reihenfolge der Nachrichten. Wenn sie wichtig ist, ist eine Messaginglösung generell zu hinterfragen und synchrone Aufrufe sind eventuell die bessere Wahl. Die zweite Herausforderung ist der Missbrauch als Bussystem. Wenn ein Service irgendeine Information benötigt, braucht er sich nur an das Bussystem anzukoppeln und wird informiert, egal woher die Information kommt.

Wichtig ist, die Fehler, die bei der Einführung von ESBs gemacht wurden, nicht zu wiederholen und stattdessen die Technologie von der (Makro-)Architektur zu trennen. Wenn das gelingt, können Streamingplattformen wie Kafka in einer Service-orientierten Plattform eine sinnvolle Ergänzung zu synchroner Kommunikation sein.

In diesem Sinne – stay tuned!

limburg_arne_sw.tif_fmt1.jpgArne Limburg ist Softwarearchitekt bei der OPEN KNOWLEDGE GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.

Desktop Tablet Mobile
Desktop Tablet Mobile