Optimieren Sie die Softwareintegration: Ein Apache Camel-Tutorial

Veröffentlicht: 2022-03-11

Software existiert selten, wenn überhaupt, in einem Informationsvakuum. Das ist zumindest die Annahme, die wir Softwareingenieure für die meisten Anwendungen, die wir entwickeln, treffen können.

Unabhängig von der Größenordnung kommuniziert jede Software – auf die eine oder andere Weise – aus verschiedenen Gründen mit einer anderen Software: um Referenzdaten von irgendwoher zu erhalten, Überwachungssignale zu senden, mit anderen Diensten in Kontakt zu bleiben und gleichzeitig Teil einer verteilten Umgebung zu sein System und mehr.

In diesem Tutorial erfahren Sie, was einige der größten Herausforderungen bei der Integration großer Software sind und wie Apache Camel diese mit Leichtigkeit löst.

Das Problem: Architekturdesign für die Systemintegration

Möglicherweise haben Sie Folgendes in Ihrem Leben als Softwareentwickler mindestens einmal getan:

  • Identifizieren Sie ein Fragment Ihrer Geschäftslogik, das das Senden von Daten initiieren soll.
  • Schreiben Sie in derselben Anwendungsschicht die Datentransformationen gemäß den Erwartungen des Empfängers.
  • Verpacken Sie die Daten in einer Struktur, die für die Übertragung und das Routing über ein Netzwerk geeignet ist.
  • Öffnen Sie mit einem geeigneten Treiber oder einem Client-SDK eine Verbindung zu einer Zielanwendung.
  • Senden Sie die Daten und verarbeiten Sie die Antwort.

Warum ist das eine schlechte Vorgehensweise?

Während Sie nur wenige Verbindungen dieser Art haben, bleibt es überschaubar. Mit einer wachsenden Zahl von Beziehungen zwischen Systemen vermischt sich die Geschäftslogik der Anwendung mit der Integrationslogik, bei der es darum geht, Daten anzupassen, technologische Unterschiede zwischen zwei Systemen auszugleichen und Daten mit SOAP, REST oder exotischeren Anfragen an das externe System zu übertragen .

Wenn Sie mehrere Anwendungen integrieren würden, wäre es unglaublich schwierig, das ganze Bild der Abhängigkeiten in einem solchen Code nachzuvollziehen: Wo werden die Daten produziert und welche Dienste verbrauchen sie? Sie werden viele Stellen haben, an denen die Integrationslogik dupliziert wird, um zu booten.

Obwohl die Aufgabe technisch erfüllt ist, kommt es bei einem solchen Ansatz zu großen Problemen mit der Wartbarkeit und Skalierbarkeit der Integration. Eine schnelle Reorganisation von Datenflüssen in diesem System ist so gut wie unmöglich, ganz zu schweigen von tieferen Problemen wie fehlender Überwachung, Unterbrechung von Stromkreisen, mühsamer Datenwiederherstellung usw.

Dies alles ist besonders wichtig bei der Integration von Software im Rahmen eines beträchtlich großen Unternehmens. Sich mit der Unternehmensintegration zu befassen bedeutet, mit einer Reihe von Anwendungen zu arbeiten, die auf einer Vielzahl von Plattformen laufen und an verschiedenen Standorten vorhanden sind. Der Datenaustausch in einer solchen Softwarelandschaft ist durchaus anspruchsvoll. Es muss die hohen Sicherheitsstandards der Branche erfüllen und eine zuverlässige Möglichkeit zur Datenübertragung bieten. In einer Unternehmensumgebung erfordert die Systemintegration ein separates, gründlich ausgearbeitetes Architekturdesign.

Dieser Artikel stellt Ihnen die einzigartigen Schwierigkeiten bei der Softwareintegration vor und bietet einige erfahrungsbasierte Lösungen für Integrationsaufgaben. Wir werden uns mit Apache Camel vertraut machen, einem nützlichen Framework, das die schlimmsten Kopfschmerzen eines Integrationsentwicklers lindern kann. Wir folgen mit einem Beispiel dafür, wie Camel dazu beitragen kann, die Kommunikation in einem Cluster von Microservices aufzubauen, die von Kubernetes unterstützt werden.

Integrationsschwierigkeiten

Ein weit verbreiteter Ansatz zur Lösung des Problems besteht darin, eine Integrationsschicht in Ihrer Anwendung zu entkoppeln. Es kann innerhalb derselben Anwendung oder als eigenständig laufende dedizierte Software vorhanden sein – im letzteren Fall als Middleware bezeichnet.

Auf welche Probleme stoßen Sie normalerweise bei der Entwicklung und Unterstützung von Middleware? Im Allgemeinen haben Sie die folgenden Schlüsselelemente:

  • Alle Datenkanäle sind teilweise unzuverlässig. Probleme, die sich aus dieser Unzuverlässigkeit ergeben, treten möglicherweise nicht auf, wenn die Datenintensität gering bis mäßig ist. Jede Speicherebene, vom Anwendungsspeicher bis hin zu niedrigeren Caches und Geräten darunter, ist einem potenziellen Ausfall ausgesetzt. Einige seltene Fehler treten nur bei großen Datenmengen auf. Sogar ausgereifte, produktionsreife Anbieterprodukte haben ungelöste Bugtracker-Probleme im Zusammenhang mit Datenverlust. Ein Middleware-System sollte in der Lage sein, Sie über diese Datenverluste zu informieren und die Nachrichten rechtzeitig erneut zuzustellen.
  • Anwendungen verwenden unterschiedliche Protokolle und Datenformate. Dies bedeutet, dass ein Integrationssystem ein Vorhang für Datentransformationen und Adapter zu anderen Teilnehmern ist und eine Vielzahl von Technologien verwendet. Dazu können einfache REST-API-Aufrufe gehören, aber auch der Zugriff auf einen Queue-Broker, das Senden von CSV-Aufträgen über FTP oder das Batch-Pulling von Daten in eine Datenbanktabelle. Das ist eine lange Liste und sie wird nicht kürzer.
  • Änderungen in Datenformaten und Routing-Regeln sind unvermeidlich. Jeder Schritt im Entwicklungsprozess einer Anwendung, der die Datenstruktur ändert, führt normalerweise zu Änderungen in den Integrationsdatenformaten und Transformationen. Manchmal sind Infrastrukturänderungen mit neu organisierten Unternehmensdatenflüssen erforderlich. Diese Änderungen können beispielsweise bei der Einführung einer einzigen Stelle zur Validierung von Referenzdaten auftreten, die alle Stammdateneinträge im gesamten Unternehmen verarbeiten muss. Bei N Systemen haben wir am Ende maximal fast N^2 Verbindungen zwischen ihnen, sodass die Anzahl der Stellen, an denen Änderungen angewendet werden müssen, ziemlich schnell wächst. Es wird wie eine Lawine sein. Um die Wartbarkeit aufrechtzuerhalten, muss eine Middleware-Schicht ein klares Bild von Abhängigkeiten mit vielseitigem Routing und Datentransformation bieten.

Diese Ideen sollten beim Design der Integration und der Auswahl der am besten geeigneten Middleware-Lösung berücksichtigt werden. Eine Möglichkeit, damit umzugehen, ist die Nutzung eines Enterprise Service Bus (ESB). Aber ESBs von großen Anbietern sind im Allgemeinen zu schwer und bereiten oft mehr Ärger, als sie wert sind: Es ist fast unmöglich, mit einem ESB schnell zu beginnen, es hat eine ziemlich steile Lernkurve und seine Flexibilität wird einer langen Liste geopfert von Funktionen und integrierten Tools. Meiner Meinung nach sind leichtgewichtige Open-Source-Integrationslösungen weitaus überlegen – sie sind flexibler, lassen sich einfach in der Cloud bereitstellen und einfach skalieren.

Softwareintegration ist nicht einfach. Wenn wir heute Microservices-Architekturen aufbauen und uns mit Schwärmen kleiner Services befassen, haben wir auch hohe Erwartungen daran, wie effizient sie kommunizieren sollten.

Unternehmensintegrationsmuster

Wie zu erwarten ist, beinhaltet die Entwicklung von Datenrouting und -transformation, wie die Softwareentwicklung im Allgemeinen, sich wiederholende Vorgänge. Die Erfahrungen in diesem Bereich wurden von Fachleuten, die sich seit geraumer Zeit mit Integrationsproblemen befassen, zusammengefasst und systematisiert. Das Ergebnis ist eine Reihe von extrahierten Vorlagen, sogenannte Unternehmensintegrationsmuster, die zum Entwerfen von Datenflüssen verwendet werden. Diese Integrationsmethoden wurden in dem gleichnamigen Buch von Gregor Hophe und Bobby Wolfe beschrieben, das dem bedeutenden Buch der Gang of Four ähnelt, jedoch im Bereich der Klebesoftware angesiedelt ist.

Beispielsweise führt das Normalisierungsmuster eine Komponente ein, die semantisch gleiche Nachrichten mit unterschiedlichen Datenformaten auf ein einziges kanonisches Modell abbildet, oder der Aggregator ist ein EIP, der eine Folge von Nachrichten zu einer kombiniert.

Da es sich um etablierte technologieunabhängige Abstraktionen handelt, die zur Lösung von Architekturproblemen verwendet werden, helfen EIPs beim Schreiben eines Architekturdesigns, das sich nicht mit der Codeebene befasst, sondern die Datenflüsse ausreichend detailliert beschreibt. Eine solche Notation zur Beschreibung von Integrationswegen macht nicht nur das Design prägnant, sondern setzt auch eine gemeinsame Nomenklatur und eine gemeinsame Sprache, die im Zusammenhang mit der Lösung einer Integrationsaufgabe mit Teammitgliedern aus verschiedenen Unternehmensbereichen von großer Bedeutung sind.

Wir stellen Apache Camel vor

Vor einigen Jahren baute ich eine Unternehmensintegration in einem riesigen Lebensmitteleinzelhandelsnetz mit Geschäften an weit verteilten Standorten auf. Ich begann mit einer proprietären ESB-Lösung, die sich als zu umständlich in der Wartung herausstellte. Dann stieß unser Team auf Apache Camel, und nachdem wir einige „Proof-of-Concept“-Arbeiten durchgeführt hatten, schrieben wir schnell alle unsere Datenflüsse in Camel-Routen um.

Apache Camel kann als „Vermittlungsrouter“ beschrieben werden, ein nachrichtenorientiertes Middleware-Framework, das die Liste der EIPs implementiert, mit denen ich mich vertraut gemacht habe. Es nutzt diese Muster, unterstützt alle gängigen Transportprotokolle und verfügt über eine Vielzahl nützlicher Adapter. Camel ermöglicht die Handhabung einer Reihe von Integrationsroutinen, ohne eigenen Code schreiben zu müssen.

Abgesehen davon würde ich die folgenden Apache Camel-Funktionen hervorheben:

  • Integrationsrouten werden als Pipelines geschrieben, die aus Blöcken bestehen. Es schafft ein völlig transparentes Bild, um die Datenflüsse aufzuspüren.
  • Camel hat Adapter für viele gängige APIs. Beispielsweise das Abrufen von Daten von Apache Kafka, das Überwachen von AWS EC2-Instanzen, die Integration mit Salesforce – all diese Aufgaben können mit sofort einsatzbereiten Komponenten gelöst werden.

Apache Camel-Routen können in Java oder Scala DSL geschrieben werden. (Eine XML-Konfiguration ist ebenfalls verfügbar, wird aber zu ausführlich und hat schlechtere Debugging-Fähigkeiten.) Sie erlegt dem Tech-Stack der kommunizierenden Dienste keine Einschränkungen auf, aber wenn Sie in Java oder Scala schreiben, können Sie stattdessen Camel in eine Anwendung einbetten es standalone laufen zu lassen.

Die von Camel verwendete Routing-Notation kann mit dem folgenden einfachen Pseudocode beschrieben werden:

 from(Source) .transform(Transformer) .to(Destination)

Source , Transformer und Destination sind Endpunkte, die über ihre URIs auf Implementierungskomponenten verweisen.

Was ermöglicht es Camel, die zuvor beschriebenen Integrationsprobleme zu lösen? Werfen wir einen Blick. Erstens leben Routing- und Transformationslogik jetzt nur noch in einer dedizierten Apache Camel-Konfiguration. Zweitens ergibt sich durch das prägnante und natürliche DSL in Verbindung mit der Nutzung von EIPs ein Bild von Abhängigkeiten zwischen Systemen. Es besteht aus verständlichen Abstraktionen und die Routing-Logik ist leicht anpassbar. Und schließlich müssen wir nicht haufenweise Transformationscode schreiben, da entsprechende Adapter wahrscheinlich bereits enthalten sind.

Integrationen

Ich sollte hinzufügen, Apache Camel ist ein ausgereiftes Framework und wird regelmäßig aktualisiert. Es hat eine große Community und eine beträchtliche kumulative Wissensbasis.

Es hat seine eigenen Nachteile. Camel sollte nicht als komplexe Integrationssuite betrachtet werden. Es ist eine Toolbox ohne High-Level-Features wie Geschäftsprozessmanagement-Tools oder Aktivitätsmonitore, aber es kann verwendet werden, um solche Software zu erstellen.

Alternative Systeme könnten zum Beispiel Spring Integration oder Mule ESB sein. Obwohl Spring Integration als leichtgewichtig angesehen wird, kann sich das Zusammenstellen und Schreiben vieler XML-Konfigurationsdateien meiner Erfahrung nach als unerwartet kompliziert herausstellen und ist kaum ein einfacher Ausweg. Mule ESB ist ein robustes und sehr funktionales Toolset, aber wie der Name schon sagt, ist es ein Enterprise Service Bus, also gehört es zu einer anderen Gewichtsklasse. Mule kann mit Fuse ESB verglichen werden, einem ähnlichen Produkt, das auf Apache Camel basiert und über eine Vielzahl von Funktionen verfügt. Für mich ist die Verwendung von Apache Camel für Klebedienste heute ein Kinderspiel. Es ist einfach zu bedienen und erstellt eine klare Beschreibung dessen, was wohin gehört – gleichzeitig ist es funktional genug, um komplexe Integrationen zu erstellen.

Schreiben einer Beispielroute

Beginnen wir mit dem Schreiben des Codes. Wir beginnen mit einem synchronen Datenfluss, der Nachrichten von einer einzelnen Quelle an eine Liste von Empfängern weiterleitet. Routing-Regeln werden in Java DSL geschrieben.

Wir verwenden Maven, um das Projekt zu erstellen. Fügen Sie zunächst die folgende Abhängigkeit zur pom.xml hinzu:

 <dependencies> ... <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-core</artifactId> <version>2.20.0</version> </dependency> </dependencies>

Alternativ kann die Anwendung auf dem camel-archetype-java Archetyp aufgebaut werden.

Kamelroutendefinitionen werden in der RouteBuilder.configure Methode deklariert.

 public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); from("file:orders?noop=true").routeId("main") .log("Incoming File: ${file:onlyname}") .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List<OrderItem> .split().simple("body.items") // split list to process one by one .to("log:inputOrderItem") .choice() .when().simple("${body.type} == 'Drink'") .to("direct:bar") .when().simple("${body.type} == 'Dessert'") .to("direct:dessertStation") .when().simple("${body.type} == 'Hot Meal'") .to("direct:hotMealStation") .when().simple("${body.type} == 'Cold Meal'") .to("direct:coldMealStation") .otherwise() .to("direct:others"); from("direct:bar").routeId("bar").log("Handling Drink"); from("direct:dessertStation").routeId("dessertStation").log("Handling Dessert"); from("direct:hotMealStation").routeId("hotMealStation").log("Handling Hot Meal"); from("direct:coldMealStation").routeId("coldMealStation").log("Handling Cold Meal"); from("direct:others").routeId("others").log("Handling Something Other"); }

In dieser Definition erstellen wir eine Route, die Datensätze aus der JSON-Datei abruft, sie in Elemente aufteilt und basierend auf dem Nachrichteninhalt an eine Reihe von Handlern weiterleitet.

Lassen Sie uns es mit vorbereiteten Testdaten ausführen. Wir erhalten die Ausgabe:

 INFO | Total 6 routes, of which 6 are started INFO | Apache Camel 2.20.0 (CamelContext: camel-1) started in 10.716 seconds INFO | Incoming File: order1.json INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Drink', name='Americano', qty='1'}] INFO | Handling Drink INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='French Omelette', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='Lasagna', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='Rice Balls', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Dessert', name='Blueberry Pie', qty='1'}] INFO | Handling Dessert

Wie erwartet leitete Camel Nachrichten an die Ziele weiter.

Auswahlmöglichkeiten bei der Datenübertragung

Im obigen Beispiel ist die Interaktion zwischen den Komponenten synchron und erfolgt über den Anwendungsspeicher. Es gibt jedoch viele weitere Kommunikationsmöglichkeiten, wenn wir es mit separaten Anwendungen zu tun haben, die keinen gemeinsamen Speicher haben:

  • Dateiaustausch. Eine Anwendung erstellt Dateien mit gemeinsam genutzten Daten, die von der anderen verwendet werden. Hier lebt der Geist der alten Schule. Diese Art der Kommunikation hat eine Fülle von Folgen: fehlende Transaktionen und Konsistenz, schlechte Performance und isolierte Koordination zwischen Systemen. Viele Entwickler schrieben schließlich hausgemachte Integrationslösungen, um den Prozess mehr oder weniger überschaubar zu machen.
  • Gemeinsame Datenbank. Lassen Sie die Anwendungen die Daten, die sie teilen möchten, in einem gemeinsamen Schema einer einzelnen Datenbank speichern. Das Entwerfen eines einheitlichen Schemas und das Handhaben des gleichzeitigen Zugriffs auf die Tabellen sind die wichtigsten Herausforderungen dieses Ansatzes. Wie beim Dateiaustausch kann dieser leicht zu einem dauerhaften Engpass werden.
  • Remote-API-Aufruf. Stellen Sie eine Schnittstelle bereit, damit eine Anwendung mit einer anderen laufenden Anwendung interagieren kann, wie bei einem typischen Methodenaufruf. Anwendungen teilen sich die Funktionalität über API-Aufrufe, koppeln sie dabei jedoch eng aneinander.
  • Nachrichten. Lassen Sie jede Anwendung eine Verbindung zu einem gemeinsamen Nachrichtensystem herstellen und Daten austauschen und Verhalten asynchron mithilfe von Nachrichten aufrufen. Weder der Absender noch der Empfänger müssen gleichzeitig aktiv sein, damit die Nachricht zugestellt wird.

Es gibt mehr Arten der Interaktion, aber wir sollten im Hinterkopf behalten, dass es im Großen und Ganzen zwei Arten der Interaktion gibt: synchrone und asynchrone. Die erste ist wie das Aufrufen einer Funktion in Ihrem Code – der Ausführungsablauf wartet, bis er ausgeführt wird und einen Wert zurückgibt. Bei einem asynchronen Ansatz werden dieselben Daten über eine zwischengeschaltete Nachrichtenwarteschlange oder ein Abonnementthema gesendet. Als Request-Reply-EIP kann ein asynchroner Remote-Function-Call implementiert werden.

Asynchrones Messaging ist jedoch kein Allheilmittel; es beinhaltet gewisse Einschränkungen. Sie sehen selten Messaging-APIs im Web; Synchrone REST-Dienste sind weitaus beliebter. Messaging-Middleware wird jedoch häufig im Unternehmensintranet oder in der Back-End-Infrastruktur verteilter Systeme verwendet.

Verwenden von Nachrichtenwarteschlangen

Machen wir unser Beispiel asynchron. Ein Softwaresystem, das Warteschlangen und Abonnementthemen verwaltet, wird als Nachrichtenbroker bezeichnet. Es ist wie ein RDBMS für Tabellen und Spalten. Warteschlangen dienen der Punkt-zu-Punkt-Integration, während Themen der Publish-Subscribe-Kommunikation mit vielen Empfängern dienen. Wir verwenden Apache ActiveMQ als JMS-Nachrichtenbroker, weil es solide und integrierbar ist.

Fügen Sie die folgende Abhängigkeit hinzu. Manchmal ist es übertrieben, activemq-all , das alle ActiveMQ-Jars enthält, zum Projekt hinzuzufügen, aber wir werden die Abhängigkeiten unserer Anwendung unkompliziert halten.

 <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-all</artifactId> <version>5.15.2</version> </dependency>

Starten Sie dann den Broker programmgesteuert. In Spring Boot erhalten wir dafür eine Autokonfiguration, indem wir die Maven-Abhängigkeit spring-boot-starter-activemq .

Führen Sie einen neuen Message Broker mit den folgenden Befehlen aus und geben Sie dabei nur den Endpunkt des Connectors an:

 BrokerService broker = new BrokerService(); broker.addConnector("tcp://localhost:61616"); broker.start();

Und fügen Sie das folgende Konfigurations-Snippet zum Hauptteil der configure -Methode hinzu:

 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); this.getContext().addComponent("activemq", ActiveMQComponent.jmsComponent(connectionFactory));

Jetzt können wir das vorherige Beispiel mithilfe von Nachrichtenwarteschlangen aktualisieren. Die Warteschlangen werden automatisch bei der Nachrichtenzustellung erstellt.

 public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); this.getContext().addComponent("activemq", ActiveMQComponent.jmsComponent(connectionFactory)); from("file:orders?noop=true").routeId("main") .log("Incoming File: ${file:onlyname}") .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List<OrderItem> .split().simple("body.items") // split list to process one by one .to("log:inputOrderItem") .choice() .when().simple("${body.type} == 'Drink'") .to("activemq:queue:bar") .when().simple("${body.type} == 'Dessert'") .to("activemq:queue:dessertStation") .when().simple("${body.type} == 'Hot Meal'") .to("activemq:queue:hotMealStation") .when().simple("${body.type} == 'Cold Meal'") .to("activemq:queue:coldMealStation") .otherwise() .to("activemq:queue:others"); from("activemq:queue:bar").routeId("barAsync").log("Drinks"); from("activemq:queue:dessertStation").routeId("dessertAsync").log("Dessert"); from("activemq:queue:hotMealStation").routeId("hotMealAsync").log("Hot Meals"); from("activemq:queue:coldMealStation").routeId("coldMealAsync").log("Cold Meals"); from("activemq:queue:others").routeId("othersAsync").log("Others"); }

In Ordnung, jetzt ist die Interaktion asynchron geworden. Potenzielle Verbraucher dieser Daten können darauf zugreifen, wenn sie dazu bereit sind. Dies ist ein Beispiel für lose Kopplung, die wir in einer reaktiven Architektur zu erreichen versuchen. Die Nichtverfügbarkeit eines der Dienste wird die anderen nicht blockieren. Darüber hinaus kann ein Verbraucher parallel skalieren und aus der Warteschlange lesen. Die Warteschlange selbst kann skaliert und partitioniert werden. Persistente Warteschlangen können die Daten auf der Festplatte speichern und darauf warten, verarbeitet zu werden, selbst wenn alle Teilnehmer ausgefallen sind. Folglich ist dieses System fehlertoleranter.

Eine erstaunliche Tatsache ist, dass CERN Apache Camel und ActiveMQ verwendet, um die Systeme des Large Hadron Collider (LHC) zu überwachen. Es gibt auch eine interessante Masterarbeit, die die Wahl einer geeigneten Middleware-Lösung für diese Aufgabe erläutert. Also, wie sie in der Keynote sagen: „Kein JMS – keine Teilchenphysik!“

Überwachung

Im vorherigen Beispiel haben wir den Datenkanal zwischen zwei Diensten erstellt. Es ist ein zusätzlicher potenzieller Fehlerpunkt in einer Architektur, also müssen wir uns darum kümmern. Werfen wir einen Blick darauf, welche Überwachungsfunktionen Apache Camel bietet. Grundsätzlich stellt es statistische Informationen über seine Routen durch die MBeans bereit, auf die JMX zugreifen kann. ActiveMQ legt Warteschlangenstatistiken auf die gleiche Weise offen.

Schalten wir den JMX-Server in der Anwendung ein, damit er mit den Befehlszeilenoptionen ausgeführt werden kann:

 -Dorg.apache.camel.jmx.createRmiConnector=true -Dorg.apache.camel.jmx.mbeanObjectDomainName=org.apache.camel -Dorg.apache.camel.jmx.rmiConnector.registryPort=1099 -Dorg.apache.camel.jmx.serviceUrlPath=camel

Führen Sie nun die Anwendung aus, damit die Route ihre Aufgabe erfüllt hat. Öffnen Sie das Standardtool jconsole und verbinden Sie sich mit dem Bewerbungsprozess. Verbinden Sie sich mit dem URL service:jmx:rmi:///jndi/rmi://localhost:1099/camel . Gehen Sie zur Domäne org.apache.camel im MBeans-Baum.

Screenshot 1

Wir können sehen, dass alles über das Routing unter Kontrolle ist. Wir haben die Anzahl der In-Flight-Nachrichten, die Fehleranzahl und die Nachrichtenanzahl in den Warteschlangen. Diese Informationen können an ein Überwachungs-Toolset mit umfangreichen Funktionen wie Graphana oder Kibana weitergeleitet werden. Sie können dies tun, indem Sie den bekannten ELK-Stack implementieren.

Es gibt auch eine austauschbare und erweiterbare Webkonsole namens hawt.io, die eine Benutzeroberfläche zum Verwalten von Camel, ActiveMQ und vielen mehr bietet.

Screenshot 2

Testrouten

Apache Camel verfügt über eine recht breite Funktionalität zum Schreiben von Testrouten mit Scheinkomponenten. Es ist ein leistungsstarkes Tool, aber das Schreiben separater Routen nur zum Testen ist ein zeitaufwändiger Prozess. Es wäre effizienter, Tests auf Produktionsrouten durchzuführen, ohne ihre Pipeline zu ändern. Camel verfügt über diese Funktion und kann mithilfe der AdviceWith-Komponente implementiert werden.

Lassen Sie uns in unserem Beispiel die Testlogik aktivieren und einen Beispieltest ausführen.

 <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-test</artifactId> <version>2.20.0</version> <scope>test</scope> </dependency>

Die Testklasse ist:

 public class AsyncRouteTest extends CamelTestSupport { @Override protected RouteBuilder createRouteBuilder() throws Exception { return new AsyncRouteBuilder(); } @Before public void mockEndpoints() throws Exception { context.getRouteDefinition("main").adviceWith(context, new AdviceWithRouteBuilder() { @Override public void configure() throws Exception { // we substitute all actual queues with mock endpoints mockEndpointsAndSkip("activemq:queue:bar"); mockEndpointsAndSkip("activemq:queue:dessertStation"); mockEndpointsAndSkip("activemq:queue:hotMealStation"); mockEndpointsAndSkip("activemq:queue:coldMealStation"); mockEndpointsAndSkip("activemq:queue:others"); // and replace the route's source with test endpoint replaceFromWith("file://testInbox"); } }); } @Test public void testSyncInteraction() throws InterruptedException { String testJson = "{\"id\": 1, \"order\": [{\"id\": 1, \"name\": \"Americano\", \"type\": \"Drink\", \"qty\": \"1\"}, {\"id\": 2, \"name\": \"French Omelette\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 3, \"name\": \"Lasagna\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 4, \"name\": \"Rice Balls\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 5, \"name\": \"Blueberry Pie\", \"type\": \"Dessert\", \"qty\": \"1\"}]}"; // get mocked endpoint and set an expectation MockEndpoint mockEndpoint = getMockEndpoint("mock:activemq:queue:hotMealStation"); mockEndpoint.expectedMessageCount(3); // simulate putting file in the inbox folder template.sendBodyAndHeader("file://testInbox", testJson, Exchange.FILE_NAME, "test.json"); //checks that expectations were met assertMockEndpointsSatisfied(); } }

Führen Sie nun Tests für die Anwendung mit mvn test . Wir können sehen, dass unsere Route erfolgreich mit dem Testhinweis ausgeführt wurde. Es werden keine Nachrichten durch die eigentlichen Warteschlangen geleitet und die Tests wurden bestanden.

 INFO | Route: main started and consuming from: file://testInbox <...> INFO | Incoming File: test.json <...> INFO | Asserting: mock://activemq:queue:hotMealStation is satisfied

Verwenden von Apache Camel mit Kubernetes-Cluster

Eines der heutigen Integrationsprobleme ist, dass Anwendungen nicht mehr statisch sind. In einer Cloud-Infrastruktur haben wir es mit virtuellen Diensten zu tun, die auf mehreren Knoten gleichzeitig laufen. Es ermöglicht die Microservices-Architektur mit einem Netz aus kleinen, leichtgewichtigen Diensten, die untereinander interagieren. Diese Dienste haben eine unzuverlässige Lebensdauer, und wir müssen sie dynamisch entdecken.

Das Zusammenkleben von Cloud-Diensten ist eine Aufgabe, die mit Apache Camel gelöst werden kann. Es ist besonders interessant wegen des EIP-Geschmacks und der Tatsache, dass Camel viele Adapter hat und eine breite Palette von Protokollen unterstützt. Die aktuelle Version 2.18 fügt die ServiceCall-Komponente hinzu, die eine Funktion zum Aufrufen einer API und zum Auflösen ihrer Adresse über Cluster-Discovery-Mechanismen einführt. Derzeit unterstützt es Consul, Kubernetes, Ribbon usw. Einige Codebeispiele, bei denen ServiceCall mit Consul konfiguriert ist, können leicht gefunden werden. Wir verwenden hier Kubernetes, weil es meine bevorzugte Clustering-Lösung ist.

Das Integrationsschema sieht wie folgt aus:

Schema

Der Order und der Inventory werden ein paar triviale Spring Boot-Anwendungen sein, die statische Daten zurückgeben. Wir sind hier nicht an einen bestimmten Tech-Stack gebunden. Diese Dienste produzieren die Daten, die wir verarbeiten möchten.

Bestellservice-Controller:

 @RestController public class OrderController { private final OrderStorage orderStorage; @Autowired public OrderController(OrderStorage orderStorage) { this.orderStorage = orderStorage; } @RequestMapping("/info") public String info() { return "Order Service UU/orders") public List<Order> getAll() { return orderStorage.getAll(); } @RequestMapping("/orders/{id}") public Order getOne(@PathVariable Integer id) { return orderStorage.getOne(id); } }

Es erzeugt Daten im Format:

 [{"id":1,"items":[2,3,4]},{"id":2,"items":[5,3]}]

Der Inventory Service Controller ist dem Order absolut ähnlich:

 @RestController public class InventoryController { private final InventoryStorage inventoryStorage; @Autowired public InventoryController(InventoryStorage inventoryStorage) { this.inventoryStorage = inventoryStorage; } @RequestMapping("/info") public String info() { return "Inventory Service UU/items") public List<InventoryItem> getAll() { return inventoryStorage.getAll(); } @RequestMapping("/items/{id}") public InventoryItem getOne(@PathVariable Integer id) { return inventoryStorage.getOne(id); } }

InventoryStorage ist ein generisches Repository, das Daten enthält. In diesem Beispiel werden statische vordefinierte Objekte zurückgegeben, die in das folgende Format gemarshallt werden.

 [{"id":1,"name":"Laptop","description":"Up to 12-hours battery life","price":499.9},{"id":2,"name":"Monitor","description":"27-inch, response time: 7ms","price":200.0},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9},{"id":4,"name":"Mouse","description":"Designed for comfort and portability","price":19.0},{"id":5,"name":"Keyboard","description":"Layout: US","price":10.5}]

Lassen Sie uns eine Gateway-Route schreiben, die sie verbindet, aber in diesem Schritt ohne ServiceCall:

 rest("/orders") .get("/").description("Get all orders with details").outType(TestResponse.class) .route() .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .to("http4://localhost:8082/orders?bridgeEndpoint=true") .unmarshal(formatOrder) .enrich("direct:enrichFromInventory", new OrderAggregationStrategy()) .to("log:result") .endRest(); from("direct:enrichFromInventory") .transform().simple("${null}") .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .to("http4://localhost:8081/items?bridgeEndpoint=true") .unmarshal(formatInventory);

Stellen Sie sich nun vor, dass jeder Dienst keine spezifische Instanz mehr ist, sondern eine Wolke von Instanzen, die als eine fungieren. Wir verwenden Minikube, um den Kubernetes-Cluster lokal zu testen.

Konfigurieren Sie die Netzwerkrouten, um Kubernetes-Knoten lokal anzuzeigen (das angegebene Beispiel gilt für eine Mac/Linux-Umgebung):

 # remove existing routes sudo route -n delete 10/24 > /dev/null 2>&1 # add routes sudo route -n add 10.0.0.0/24 $(minikube ip) # 172.17.0.0/16 ip range is used by docker in minikube sudo route -n add 172.17.0.0/16 $(minikube ip) ifconfig 'bridge100' | grep member | awk '{print $2}' # use interface name from the output of the previous command # needed for xhyve driver, which I'm using for testing sudo ifconfig bridge100 -hostfilter en5

Verpacken Sie die Dienste in Docker-Containern mit einer Dockerfile-Konfiguration wie dieser:

 FROM openjdk:8-jdk-alpine VOLUME /tmp ADD target/order-srv-1.0-SNAPSHOT.jar app.jar ADD target/lib lib ENV JAVA_OPTS="" ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar

Erstellen Sie die Service-Images und übertragen Sie sie per Push in die Docker-Registrierung. Führen Sie nun die Knoten im lokalen Kubernetes-Cluster aus.

Kubernetes.yaml-Bereitstellungskonfiguration:

 apiVersion: extensions/v1beta1 kind: Deployment metadata: name: inventory spec: replicas: 3 selector: matchLabels: app: inventory template: metadata: labels: app: inventory spec: containers: - name: inventory image: inventory-srv:latest imagePullPolicy: Never ports: - containerPort: 8081

Stellen Sie diese Bereitstellungen als Dienste im Cluster bereit:

 kubectl expose deployment order-srv --type=NodePort kubectl expose deployment inventory-srv --type=NodePort

Jetzt können wir überprüfen, ob Anfragen von zufällig ausgewählten Knoten aus dem Cluster bedient werden. Führen Sie curl -X http://192.168.99.100:30517/info mehrmals nacheinander aus, um auf minikube NodePort für exponierten Dienst zuzugreifen (unter Verwendung Ihres Hosts und Ports). In der Ausgabe sehen wir, dass wir den Anforderungsausgleich erreicht haben.

 Inventory Service UUID = 22f8ca6b-f56b-4984-927b-cbf9fcf81da5 Inventory Service UUID = b7a4d326-1e76-4051-a0a6-1016394fafda Inventory Service UUID = b7a4d326-1e76-4051-a0a6-1016394fafda Inventory Service UUID = 22f8ca6b-f56b-4984-927b-cbf9fcf81da5 Inventory Service UUID = 50323ddb-3ace-4424-820a-6b4e85775af4

Fügen Sie der pom.xml des Projekts die Abhängigkeiten camel-kubernetes und camel-netty4-http . Konfigurieren Sie dann die ServiceCall-Komponente so, dass sie die Kubernetes-Master-Node-Discovery verwendet, die für alle Dienstaufrufe in den Routendefinitionen geteilt wird:

 KubernetesConfiguration kubernetesConfiguration = new KubernetesConfiguration(); kubernetesConfiguration.setMasterUrl("https://192.168.64.2:8443"); kubernetesConfiguration.setClientCertFile("/Users/antongoncharov/.minikube/client.crt"); kubernetesConfiguration.setClientKeyFile("/Users/antongoncharov/.minikube/client.key"); kubernetesConfiguration.setNamespace("default”); ServiceCallConfigurationDefinition config = new ServiceCallConfigurationDefinition(); config.setServiceDiscovery(new KubernetesClientServiceDiscovery(kubernetesConfiguration)); context.setServiceCallConfiguration(config);

Das ServiceCall EIP ergänzt Spring Boot gut. Die meisten Optionen können direkt in der Datei application.properties konfiguriert werden.

Stärken Sie die Camel-Route mit der ServiceCall-Komponente:

 rest("/orders") .get("/").description("Get all orders with details").outType(TestResponse.class) .route() .hystrix() .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .serviceCall("customer-srv","http4:customer-deployment?bridgeEndpoint=true") .unmarshal(formatOrder) .enrich("direct:enrichFromInventory", new OrderAggregationStrategy()) .to("log:result") .endRest(); from("direct:enrichFromInventory") .transform().simple("${null}") .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .serviceCall("order-srv","http4:order-srv?bridgeEndpoint=true") .unmarshal(formatInventory);

Wir haben auch Circuit Breaker in der Route aktiviert. Es handelt sich um einen Integrations-Hook, der das Anhalten von Remote-Systemaufrufen im Falle von Übermittlungsfehlern oder Nichtverfügbarkeit des Empfängers ermöglicht. Dies dient dazu, einen Ausfall des Kaskadensystems zu vermeiden. Die Hystrix-Komponente trägt dazu bei, indem sie das Circuit Breaker-Muster implementiert.

Lassen Sie es uns ausführen und eine Testanforderung senden. Wir erhalten die Antwort von beiden Diensten aggregiert.

 [{"id":1,"items":[{"id":2,"name":"Monitor","description":"27-inch, response time: 7ms","price":200.0},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9},{"id":4,"name":"Mouse","description":"Designed for comfort and portability","price":19.0}]},{"id":2,"items":[{"id":5,"name":"Keyboard","description":"Layout: US","price":10.5},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9}]}]

Das Ergebnis ist wie erwartet.

Andere Anwendungsfälle

Ich habe gezeigt, wie Apache Camel Microservices in einen Cluster integrieren kann. Was sind andere Verwendungen dieses Frameworks? Im Allgemeinen ist es überall dort nützlich, wo regelbasiertes Routing eine Lösung sein könnte. For instance, Apache Camel can be a middleware for the Internet of Things with the Eclipse Kura adapter. It can handle monitoring by ferrying log signals from various components and services, like in the CERN system. It can also be an integration framework for enterprise SOA or be a pipeline for batch data processing, although it doesn't compete well with Apache Spark in this area.

Fazit

You can see that systems integration isn't an easy process. We're lucky because a lot of experience has been gathered. It's important to apply it correctly to build flexible and fault-tolerant solutions.

To ensure correct application, I recommend having a checklist of important integration aspects. Must-have items include:

  • Is there a separate integration layer?
  • Are there tests for integration?
  • Do we know the expected peak data intensity?
  • Do we know the expected data delivery time?
  • Does message correlation matter? What if a sequence breaks?
  • Should we do it in a synchronous or asynchronous way?
  • Where do formats and routing rules change more frequently?
  • Do we have ways to monitor the process?

In this article, we tried Apache Camel, a lightweight integration framework, which helps save time and effort when solving integration problems. As we showed, it can serve as a tool, supporting the relevant microservice architecture by taking full responsibility for data exchange between microservices.

If you're interested in learning more about Apache Camel, I highly recommend the book “Camel in Action” by the framework's creator, Claus Ibsen. Official documentation is available at camel.apache.org.