This article is also available in English

TL;DR

Wir haben uns mit sieben Personen aus sechs unterschiedlichen Entwicklungsprojekten unterhalten. Die Projekte wurden überwiegend in SCS und Microservices basierten Architekturen durchgeführt. Zwei häufig verwendete Ansätze sind der Einsatz von HTTP-Feeds und Apache Kafka als Message-oriented Middleware. Je nach nicht funktionalen Anforderungen des Projektes, wie zum Beispiel Entkopplung, Kapazität für hohen Datendurchsatz oder hohe Resilienz der Systeme, wurden unterschiedliche Entscheidungen getroffen.

Begriffsklärung

Allgemein wird bei asynchroner Service-zu-Service-Kommunikation häufig zwischen eventgetriebenen und datenreplizierenden Systemen unterschieden. Einfach gesagt, kann es bei ersterem reichen, einen Objektzustand ausschließlich über Änderungen des Objekts zu beschreiben, während bei der Datenreplikation im Fall eines Updates idealerweise das gesamte Datenaggregat neu veröffentlicht wird.

Unabhängig davon, ob die Services Events oder Aggregate austauschen, resultiert aus der Kommunikation über ein Netzwerk eine unvermeidbare Latenz, bis die Zustandsänderungen bei den Konsumenten erfasst und verarbeitet werden. Datenkonsistenz wird zeitnah angestrebt, um aber zu verdeutlichen, dass es zwischenzeitlich zu Inkonsistenzen kommen kann, wird hier von einer Eventual Consistency gesprochen. Abhängig vom Use-Case ist es nötig, die Konsistenz langsamer oder schneller zu erreichen, was wiederum von Pull- und Pushraten oder aber auch von der angestrebten Skalierbarkeit des Gesamtsystems abhängt.

Apache Kafka

Abb.1: Kafka Architekur aus der Vogelperspektive
Abb.1: Kafka Architekur aus der Vogelperspektive

Nicht nur in INNOQ-Projekten ist Apache Kafka eine beliebte Middleware, die eingesetzt wird, um hoch skalierbare und robuste Datenströme bereitzustellen. Kafka verwendet ein verteiltes und fehlertolerantes Architekturmuster, das es ermöglicht, große Mengen an Daten effizient zu verarbeiten und gleichzeitig eine hohe Verfügbarkeit zu gewährleisten. Abbildung 1 zeigt, wie Nachrichtenproduzenten Informationen an einen Kafka-Cluster übergeben können. Dieser verteilt die Informationen transparent auf seine Broker in sogenannten Topics. Konsumenten können die Nachrichten dann vom Cluster zur weiteren Verarbeitung abholen.

Kafka implementiert eine große Menge an Features, die für den Umgang mit Events und Datenreplikation hilfreich sind. Da das aber auch unweigerlich zu einer steilen Lernkurve bei den Entwicklungsteams führt, ist es ratsam, nur jene Features zu nutzen, die für den Use-Case erforderlich sind und bei der Auswahl der Features auf die Expertise des Entwicklungsteams zu achten.

Der Betrieb von Apache Kafka kann anspruchsvoll sein, da Skalierung, Konfiguration und Datensicherheit eine sorgfältige Planung und Überwachung erfordern. Zusätzlich müssen Faktoren wie Überwachung, Fehlertoleranz und Integration mit anderen Systemen berücksichtigt werden. Alternativ kann in vielen Fällen auf eine verwaltete Lösung wie Amazons Managed Streaming for Apache Kafka oder Confluent Cloud zurückgegriffen werden.

HTTP-Feeds

Abb.2: HTTP-Feed Architektur aus der Vogelperspektive
Abb.2: HTTP-Feed Architektur aus der Vogelperspektive

HTTP-Feeds bauen auf Standards des Hypertext Transfer Protocols auf, was jedes Web-Framework unterstützt. Der Server stellt hierbei die Datensätze oder Events in chronologischer Reihenfolge bereit, die dann von mehreren Konsumenten abgerufen werden können. Wie in Abbildung 2 zu sehen ist, wird explizit auf eine zentrale Middleware verzichtet, was den Einsatz sehr schlank macht, da Dienste lose gekoppelt werden können, ohne dass eine zusätzliche Infrastrukturkomponente eingeführt werden muss. Der konsumierende Service ist hierbei dafür zuständig, sich zu merken, bis zu welcher Stelle der Feed ausgelesen wurde.

Es gibt kein standardisiertes Vorgehen bei der Implementierung von HTTP-Feeds. Die Implementierung ist abhängig vom Use-Case beziehungsweise den Anforderungen des Software-Systems. Es kann etwa für ein Event-basiertes System sehr sinnvoll sein, einen starken Fokus auf Cachebarkeit zu setzen, um den Konsumenten Event-Sourcing ohne erneuten Zugriff auf den Feed zu ermöglichen.

Eine Übersicht über die Themen, die bei der Spezifikation eines HTTP-Feed Protokolls zu beachten sind, liefert Jochen Christ auf http-feeds.org.

Projektüberblick

Wir haben uns sechs verschiedene Projekte aus den Bereichen E-Commerce, Einzelhandel und Industrie-Automation angesehen, in denen INNOQ involviert war. Allen Projekten ist gemeinsam, dass sie in einer serviceorientierten Architektur mit SCS umgesetzt wurden. Überall wird Spring Boot mit Java oder Kotlin verwendet.

Gemessen an Mitarbeiter:innen, waren die Projektgrößen mit 8 bis 80 Personen sehr divers. Die Projektgröße scheint insofern relevant für die Technologiewahl, als dass in größeren Projekten tendenziell bereits eine Kafka-Installation durch die Organisation bereitgestellt wird. Das macht den Einsatz wesentlich unkomplizierter und dadurch naheliegender, weil für die erforderliche Infrastruktur und den Betrieb bereits gesorgt wird. Außerdem sind Setup und Betrieb in größeren Projekten eher zu bewältigen als in kleineren Projekten, in denen es teilweise auch einfach an Mitarbeiter-Kapazität dafür fehlen kann.

Die genannten Gründe für die Integration von Self-Contained-Systems mittels asynchroner Kommunikation sind wiederum überall gleich. Hauptsächlich geht es um die Vermeidung von Laufzeit-Abhängigkeiten bei der Inter-Service-Kommunikation und das Profitieren von damit einhergehenden Vorteilen wie bessere Skalierbarkeit gegenüber synchroner Requests und stärkerer Resilienz. Die genannten Technologien werden in den betrachteten Projekten zusätzlich immer zur Datenreplikation eingesetzt.

Für HTTP-Feeds wurden das Fehlen der Abhängigkeit zu einer zentralen Middleware und die Interoperabilität zwischen Netzwerken als Gründe genannt. Als Gründe für die Entscheidung für Kafka wurden zusätzliche Features genannt, die von der Software out-of-the-box mitgebracht werden. Ein Beispiel hierfür sind Consumer Groups, d.h. die Aufteilung der Datenströme in parallel verarbeitbare Teilbereiche und die damit verbundene vertikale Skalierbarkeit der Konsumentenprozesse, wie in Abbildung 3 skizziert.

Abb.3: Consumer Groups
Abb.3: Consumer Groups

Projektdetails

Die Herausforderungen sind in den Projekten so unterschiedlich wie die Personen, die daran arbeiten. Natürlich gibt es deshalb auch verschiedene Lösungen für die Integration von Services. Im Folgenden soll ein Überblick über die Besonderheiten der einzelnen Projekte gegeben werden.

Projekt A: HTTP-Feeds für die SCS-Integration, Kafka gegen Race Conditions

Projektumfeld

Es gibt 5 Self-Contained Systems, die jeweils von 1 bis 3 Teams mit ca. 8 Personen betreut werden. Das Team der interviewten Person war für ein SCS mit ca. 10 Microservices verantwortlich, als sie aus dem Projekt ausgeschieden ist.

Architekturlösung

Es wurden verschiedene Richtlinien für die Makroarchitektur festgelegt. So gab es z.B. Anforderungen an die Benutzeroberfläche, aber auch an die Kommunikation zwischen den Systemen. Das System sollte möglichst ereignisgesteuert arbeiten, wobei die Dienste pull-basiert über HTTP-Feeds integriert werden sollten. Die Gründe dafür waren Schlankheit und Flexibilität, da keine zusätzliche Middleware verwendet werden muss.

Innerhalb ihres SCS verwendet das interviewte Team Kafka. Es entstand die teaminterne Anforderung, dass Ereignisse skalierbar in der richtigen Reihenfolge verarbeitet werden müssen, was durch das Serialisierungs- und Partitionierungskonzept von Kafka gewährleistet wird. Dadurch werden Events, die einen bestimmten Vorgang betreffen, nacheinander verarbeitet und Race Conditions können vermieden werden.

Zufriedenheit mit der gewählten Lösung

Bei der Batch-Verarbeitung traten einige Probleme auf, insbesondere bei einem bestimmten Importprozess. Es gab Millionen von gleichzeitigen Datenaktualisierungen, was zu Verzögerungen von bis zu einer Stunde führte. Ursprünglich hatte das Team vorgeschlagen, AWS Lambda Serverless zu verwenden, was möglicherweise eine gute Alternative gewesen wäre. Dies hätte wahrscheinlich das Batch-Problem gelöst, aber die Entwicklung insgesamt verlangsamt. Insgesamt ist das Team jedoch sehr zufrieden mit ihrer Architekturlösung.

Projekt B: Datenreplikation mit HTTP-Feeds und Amazon S3

Projektumfeld

Das Team besteht in der Regel aus insgesamt 9 Personen. Die gesamte Anwendung setzt sich aus ca. 21 SCS zusammen.

Architekturlösung

Nicht-funktionale Anforderungen sind lose Kopplung, hohe Performance, Datenreplikation und Einfachheit der Lösungen. Daher wird die Kommunikation zwischen den Services hauptsächlich über HTTP-Feeds abgebildet, wobei immer vollständige Snapshots der aktuellen Datensätze in den HTTP-Feed geschrieben werden. Aufgrund einer Richtlinie zur Integration von bereits existierenden Unternehmensanwendungen in einem anderen Rechenzentrum gibt es jedoch Ausnahmen, bei denen Daten in XML- oder JSON-Form von einem System in ein AWS S3-Bucket geschrieben und von einem anderen System gelesen werden. Hier zeigt sich, dass andere Ansätze möglicherweise robuster sind, da z.B. eine Schemavalidierung nur schwer zu implementieren ist.

Zufriedenheit mit der gewählten Lösung

Die Projektbeteiligten würden diese Lösung wieder wählen. Die Integration über HTTP-Feeds wird von allen Entwicklern gut angenommen. Aufgrund von Schemaproblemen bei der filebasierten Lösung ist die allgemeine Zufriedenheit jedoch etwas zurückhaltender, aber immer noch gut.

Projekt C: HTTP-Feeds mit Log-Compaction und SharedDB

Projektumfeld

Zweck der Software ist die Verwaltung von Produkten und die Verteilung von Bestellungen an ein Händlernetz. Dabei wird das bestehende ERP-System schrittweise abgelöst.

Das Team ist im Laufe der Zeit stetig gewachsen und besteht zeitweise aus 10 Personen. Es gibt insgesamt 6 geschlossene SCS, die jeweils mehrere Microservices enthalten, in der Regel 1 bis 3 pro Komponente.

Architekturlösung

Die Integration des SCS erfolgt über HTTP-Feeds. Die Gründe für die Wahl von HTTP-Feeds waren Entkopplung, Ausfallsicherheit, Datenkonsistenz und geringe Komplexität. Es handelt sich um eine schlanke Lösung, die Datenreplikation ermöglicht. Eventuelle Inkonsistenzen wurden in Kauf genommen, da ein Polling-Intervall von 10 Sekunden ausreichend ist.

Zur weiteren Vereinfachung des Feeds wurde eine Nachbildung der Kafka-Log-Compaction entwickelt. Der Implementierungsaufwand für dieses Feature war bei der HTTP-Feed-Implementierung in diesem Projekt sehr gering, da die Feeds immer komplette Datensätze veröffentlichen und somit vergangene Ereignisse einfach über den Schlüssel des referenzierten Datensatzes gelöscht werden können.

Innerhalb eines Microservices wird eine gemeinsam genutzte Datenbank (SharedDB) verwendet, wobei Daten über Read-Only-Tabellen an Prozesse übergeben werden. Wichtig ist hierbei, dass diese Prozesse auf der gleichen Codebasis laufen, also als Teil des Microservices verstanden werden können und auch gleichzeitig deployt werden. Von einer Integration mehrerer Services über dieses Pattern wird abgeraten, da sonst Schemaänderungen ein gleichzeitiges Deployment verschiedener Services erfordern.

Zufriedenheit mit der gewählten Lösung

Es gab ein paar Probleme mit Schema-Versionen, die auf eine fehlerhafte Schnittstelle zurückzuführen waren. Insgesamt sind die Beteiligten sehr zufrieden mit der Lösung. Der Einsatz einer Middleware wie Kafka wäre für diesen Anwendungsfall ein nicht zu rechtfertigender Overhead gewesen.

Projekt D: Lösung für Schemaänderungen in Kafka Topics

Projektumfeld

Das Projekt umfasst insgesamt 5 Bereiche, wobei jedes Team aus 8 bis 10 Personen besteht. Die 5 Projektbereiche bestehen in der Regel aus 3 bis 4 Microservices.

Architekturlösung

Die asynchrone Kommunikation zwischen den Diensten war mehr oder weniger durch das SCS-Architekturmodell vorgegeben, da diese Form der Kopplung hier bevorzugt wird. Aufgrund seiner Komplexität wurde Kafka evaluiert, aber zunächst nicht ausgewählt. Als das Projekt zu einem späteren Zeitpunkt auf AWS migriert wurde und mit Amazon MSK ein Managed Kafka zur Verfügung stand, wurde dieses aufgrund seiner Fähigkeit, große Datenmengen zu handhaben, schließlich eingesetzt.

Im Interview haben wir auch über Schemaänderungen innerhalb von Kafka Topics gesprochen. Um eine fehlerfreie Kommunikation zu gewährleisten, müssen sich Konsument und Produzent auf ein eindeutiges, gemeinsames Nachrichtenschema einigen. Ob der Produzent oder der Konsument das Nachrichtenschema vorgibt, kann sich beispielsweise aus der Projektsituation, der Arbeitsweise der beteiligten Teams, aber auch aus technischen Vorgaben ergeben. Dies wird vor allem dann interessant, wenn es zu inkompatiblen Schemaänderungen kommt. Bestehende Konsumenten der Nachrichten müssen diese weiterhin verarbeiten können, bis sie die Änderungen angepasst haben, um eine lose Kopplung der Systeme zu gewährleisten. Ein flexibler und im Projekt bewährter Ansatz ist in Abbildung 4 dargestellt, bei dem die Nachrichten mit einer Schema-Version versehen und die Konsumenten so implementiert werden, dass sie nur die Versionen lesen, für die sie implementiert wurden. Im Falle von Änderungen werden die Nachrichten dann vom Produzenten sowohl in der alten als auch in der neuen Version so lange verteilt, bis sich alle Konsumenten an das neue Schema angepasst haben.

Abb.4: Mögliche Behandlung von Schemaänderungen
Abb.4: Mögliche Behandlung von Schemaänderungen

Wenn du mehr über dieses Thema erfahren möchtest, Goran hat kürzlich einen Blogbeitrag über Schema Evolution mit Avro geschrieben.

Zufriedenheit mit der gewählten Lösung

Die gewählte Architekturlösung wird als sehr gut bewertet. Zwar gab es anfangs viele Fragen und einen hohen Aufwand für das Onboarding der Entwickler, da keine umfangreiche Erfahrung mit Kafka vorhanden war, doch hat sich das System im Laufe der Zeit bewährt. Die Konsequenzen der Eventual Consistency und auch der beschriebene Umgang mit Schemaänderungen erforderten ein gewisses Umdenken, aber mittlerweile laufen die Systeme stabil. Die Beteiligten sind mit der Lösung sehr zufrieden und würden sich wieder dafür entscheiden.

Projekt E: HiveMQ als MQTT Gateway für Kafka und cachebare HTTP-Feeds

Projektumfeld

Das Team besteht aus einer Mischung von 8–10 Personen von INNOQ und internen Kolleg:innen. Es ist verantwortlich für die Entwicklung und den Betrieb von 3 Servicekomponenten. In diesem Projekt werden Daten von einer großen Anzahl von IoT-Geräten verarbeitet.

Architekturlösung

Einzelne Geräte senden MQTT-Nachrichten über einen HAProxy an HiveMQ, von wo sie mittels Importer an Kafka und schließlich an die Datenbank übergeben werden. Die Datenverarbeitung von und zu Kafka, sowie die Anreicherung der Daten wird unter neben Plain-Java-Implementierungen mit dem Actor-Framework Akka realisiert, das eine robuste Nebenläufigkeit garantiert. Der Kafka-Cluster wurde mithilfe eines Operators in Kubernetes selbst betrieben.

Die Gründe für den Einsatz von Kafka waren Zuverlässigkeit, Skalierbarkeit und Robustheit. So können einzelne Komponenten unabhängig voneinander ausgeliefert werden. Ausfallzeiten durch z.B. Deployments führen nicht zu Datenverlusten. Damit einzelne Komponenten unterschiedlich skalieren können, wurden sie als Competing Consumer implementiert. Die zeitliche Entkopplung und die vermeintlich unendliche Pufferung geben die Sicherheit, Fehler machen zu können und Daten notfalls wieder einspielen zu können - auch wenn das bisher nicht vorgekommen ist.

Die Kommunikation zwischen den Komponenten des Dienstes erfolgt über HTTP-Feeds. Bei der Implementierung von HTTP-Feeds wird explizit darauf geachtet, dass diese unveränderlich und damit gut im Cache speicherbar sind, um für Event Sourcing verwendet werden zu können. Dies bedeutet, dass ein Dienst jederzeit den Zustand eines anderen Dienstes idempotent wiederherstellen kann, indem der Feed von Anfang an ausgewertet wird. Dies kann z.B. für Desaster Recovery nützlich sein.

Eine Kundenanforderung ist, dass die Anwendungen providerunabhängig sind und in einer hybriden Cloud-Umgebung gehostet werden können. Gerade für die Integration von Diensten zwischen verschiedenen VPCs eignen sich HTTP-Feeds hervorragend, da die Schnittstelle einfach und sicher öffentlich zugänglich gemacht werden kann.

Zufriedenheit mit der gewählten Lösung

Statt Akka könnte Kafka Streaming als Alternative betrachtet werden. Kafka Streams ist eine Java-Bibliothek, die als Teil des Apache Kafka Ecosystems entwickelt wurde und es Entwicklern ermöglicht, Echtzeit-Datenverarbeitungsanwendungen zur Verarbeitung, Transformation und Analyse von Datenströmen zu erstellen. Diese Technologie war aber zum Zeitpunkt der Implementierung noch nicht verfügbar.

Die Architekturlösung an sich wird als sehr gut bewertet, was bedeutet, dass die Architekten auch im Nachhinein mit den getroffenen Entscheidungen sehr zufrieden sind. Die Zufriedenheit mit der Software an sich wurde als ausgezeichnet bewertet. Dies bedeutet, dass die Software einwandfrei funktioniert und ihren Zweck erfüllt.

Erkenntnis: Fehlerbehandlung als besondere Herausforderung

Eine häufige Herausforderung bei asynchroner Kommunikation ist das Fehlerhandling. In den meisten Interviews wurde darauf eingegangen, weshalb wir hier eine Zusammenfassung unserer Erkenntnisse mit euch teilen wollen.

Im Gegensatz zum Conversation Pattern, das von REST implementiert wird, kann bei einem Messaging Pattern keine direkte Antwort, wie z.B. ein HTTP-Status, an den Producer zurückgegeben werden. Gerade bei Event-getriebenen Systemen ist die Kommunikation nicht auf einen expliziten Konsumenten ausgerichtet. Obwohl dieses Verhalten also gewollt ist, kann dennoch der Wunsch nach einem Rückkanal aufkommen, um den Produzenten beispielsweise über fehlerhafte Datensätze zu informieren.

Auch die Fehlerbehandlung auf Konsumentenseite ist eine besondere Herausforderung. Auf Fehler können Nachrichtenkonsumenten auf verschiedene Arten reagieren. Bei einer sequentiellen Abarbeitung eines HTTP-Feeds kann unter anderem die Verarbeitung schlichtweg angehalten werden, bis der Produzent den Fehler behoben hat. Da hierbei die Kopplung an die Quelle erhöht wird, bietet sich das nur an, wenn Zugriff auf das Produzentensystem besteht. Der Vorteil bei diesem Vorgehen ist, dass die Verarbeitungsreihenfolge für die Nachrichten garantiert bleibt.

Eine andere Möglichkeit ist die Verwendung einer sogenannten Dead-Letter-Queue, kurz DLQ. In diese werden, wie in Abbildung 5 skizziert, alle Nachrichten geschrieben, die nicht verarbeitet werden können. Im Falle von Kafka wäre das ein eigenes Topic, an welches die Nachricht im Zuge der Fehlerbehandlung geschickt werden würde. Diese Nachricht kann dann analysiert und weiterverarbeitet werden. DLQs sollten immer einen Bezug zum Nachrichtenkonsumenten herstellen, um die Fehlernachverfolgung und -behebung zu erleichtern.

Abb.5: Dead Letter Queue
Abb.5: Dead Letter Queue

Da eine direkte Antwortmöglichkeit an einen Produzenten nicht existiert, ist ein gutes Monitoring umso wichtiger. Hierfür gibt es verschiedene Optionen, die natürlich auch kombiniert werden können. Beispielsweise kann eine Notification per E-Mail oder an einen Slack Channel ausgelöst werden, wenn die Anzahl von Fehlerlogs einen bestimmten Threshold überschreitet. Hierfür kann auch ein dedizierter Error Service wie Sentry eingesetzt werden, der auch zusätzlich bei Fehleranalysen helfen kann. Wenn man mit DLQs arbeitet, gibt es die Möglichkeit, mit einem Tool wie Grafana eine Metrik zu erstellen, die die Größe der einzelnen DLQs ausliest. In jedem Fall sollte auf ein ausreichendes Alerting geachtet werden. Normalerweise sollten Alerts an den Consumer gerichtet sein, wenn der Anwendungsfall es erfordert, dass zusätzlich auch automatisierte Notifications an einen Produzenten denkbar sind.

Fazit

Asynchrone Kommunikation spielt eine entscheidende Rolle bei der Integration von Services in einer modernen Architektur. Durch ihren Einsatz können Unternehmen die Laufzeit-Abhängigkeit reduzieren und auf robuste Weise Daten anderer Services replizieren.

Ein Ansatz zur asynchronen Kommunikation ist die Verwendung von HTTP-Feeds. Diese Methode ermöglicht es den Services, Ereignisse und Nachrichten über standardisierte HTTP-Anfragen und -Antworten auszutauschen. Der Vorteil von HTTP-Feeds liegt darin, dass keine zentrale Infrastrukturkomponente erforderlich ist und die Lernkurve für Entwickler relativ flach ist. Dies macht es einfach, die Kommunikation zwischen den Services aufzubauen und zu skalieren.

Eine weitere leistungsstarke Lösung für eventbasierte Integration über einen asynchronen Kanal ist Apache Kafka. Kafka bietet eine Reihe von Vorteilen, darunter eine geringere Latenz, höheren Durchsatz und eine robuste, skalierbare Architektur. Mit Kafka können Services Ereignisse in Echtzeit verarbeiten und gleichzeitig auf eine Vielzahl von existierenden Features und Integrationen zurückgreifen. Dies macht Kafka zu einer beliebten Wahl für Unternehmen, die eine skalierbare und zuverlässige Integration ihrer Services benötigen.

Insgesamt bietet die eventbasierte Integration über eine asynchrone Service-zu-Service-Kommunikation eine Vielzahl von Vorteilen. Sie ermöglicht es Unternehmen, Services effizienter zu integrieren, die individuelle Skalierbarkeit durch lose Kopplung und Pattern wie Consumer Groups zu verbessern und Flexibilität in der Kommunikation zu erhöhen. Ob durch den Einsatz von HTTP-Feeds oder die Verwendung von Kafka, die Wahl des richtigen Ansatzes hängt von den spezifischen Anforderungen und der Komplexität des Projektes ab. Durch die richtige Implementierung und das Verständnis der verschiedenen Optionen können Unternehmen jedoch von den Vorteilen der eventbasierten Integration durch asynchrone Kommunikation stark profitieren.