AMQP im Kontext einer REST-Architektur

AMQP im Kontext einer REST-Architektur
In komplexen Enterprise-Szenarien bietet sich eine serviceorientierte Architektur an, bei der mehrere kleine Anwendungen miteinander kommunizieren. Dieser Blogbeitrag zeigt anhand eines Fallbeispiels, wie in solchen Fällen eine nachrichtenorientiere Middleware in Form von AMQP eine bestehende REST-Architektur sinnvoll erweitern kann und asynchrone Kommunikationswege ermöglicht.

Ausgangslage: Eine serviceorientierte Architektur via REST

Grundlage für das Fallbeispiel ist eine bestehende serviceorientierte Architektur, bei der Web-Anwendungen via REST-Schnittstellen miteinander kommunizieren. Die Anwendungen können dabei grob in zwei Bereiche gegliedert werden: Stammdatenanwendungen stellen Daten bereit, benötigen selbst jedoch wenig oder keinen Zugriff auf andere Systeme, Clientanwendungen greifen per REST-API auf die Stammdaten zu. Nachfolgend ein Teilausschnitt einer bestehenden REST-Architektur:

Diagramm: Datenfluss zwischen Stammdatenanwendungen & Clientanwendungen per Rest

Das Fallbeispiel besteht aus zwei Stammdatenanwendungen, die Artikel- und Kundendaten bereitstellen sowie zwei Clientanwendungen. Die Auftragsverwaltung benötigt Artikel- und Kundendaten, die Vertragsverwaltung lediglich Kundendaten.

Herausforderung: Benachrichtigung bei Datenänderungen

Neben der bestehenden Verknüpfung via REST besteht die zusätzliche Herausforderung an die Architektur, dass die Clientanwendungen auf Änderungen in den Stammdaten reagieren müssen, beispielsweise sollen ausstehende Aufträge storniert werden, wenn ein Kunde mit seinen Zahlungen zu weit in Rückstand gerät. Die Lösung hierfür muss mehrere Bedingungen erfüllen:

  • Lose Kopplung der Systeme: Es soll möglich sein, neue Clientanwendungen zu integrieren, ohne dass bestehende Anwendungen hierfür verändert werden. Die Stammdatenanwendungen dürfen also nicht wissen, welche Anwendungen bei Änderungen benachrichtigt werden müssen.
  • Asynchrone Ausführung der Änderungen: Nutzer der Stammdatenänderungen dürfen nicht durch die Anbindung der anderen Anwendungen beeinträchtigt werden, die Änderungen müssen also asynchron erfolgen. Hierbei wird eine kurzzeitige Inkonsistenz der Daten zwischen den Systemen bewusst in Kauf genommen.
  • Zuverlässige Zustellung der Daten: Anwendungen sollen Änderungen auch dann erhalten, wenn sie zum Zeitpunkt der Änderung gerade nicht erreichbar sind.

Ein zentraler Publish-Subscribe-Mechanismus kann die ersten beiden Anforderungen erfüllen. Hierbei können die Clientanwendungen selbst entscheiden, welche Änderungen sie abonnieren wollen, ohne dass die Stammdatenanwendung davon Kenntnis haben muss. Die dritte Anforderung führt zum Ausschluss von “einfachen” Systemen wie Redis Pub/Sub, da hier die Daten nur an laufende Anwendungen weitergereicht werden. AMQP hingegen kann alle Anforderungen erfüllen, wie im nächsten Teil beschrieben wird.

Lösungsansatz: AMQP als zentraler Kommunikationspunkt

AMQP (Advanced Messaging Queuing Protocol) ist ein offenes Netzwerkprotokoll für eine nachrichtenorientierte Middleware, das unabhängig von der gewählten Programmiersprache einsetzbar ist. Hierfür gibt es mehrere kommerzielle und freie Server-Implementierungen; wir haben uns für die in Erlang geschriebene Open-Source-Implementierung RabbitMQ entschieden.

AMQP besteht strukturell aus drei Teilen:

  • Exchanges empfangen Nachrichten von externen Anwendungen (“Publisher”, im Fallbeispiel sind das die Stammdatenanwendungen)
  • Queues liefern Nachrichten aus. Externe Anwendungen (“Consumer”, in unserem Fall die Clientanwendungen) registrieren sich hier und können die Nachrichten verarbeiten. Ist kein Consumer eingetragen, werden die Nachrichten in der Queue zwischengespeichert.
  • Routes verbinden Exchanges mit Queues und definieren, unter welchen Bedingungen eine Nachricht an eine Queue zugestellt werden soll.

Diese Teile können auf verschiedene Arten konfiguriert werden; eine gute Übersicht hierzu liefert https://www.rabbitmq.com/tutorials/amqp-concepts.html. Die nächste zeigt die von uns gewählte Konfiguration, im Nachfolgenden werden die einzelnen Teile und die Gründe für die Auswahl erklärt.

Diagramm: Abbildung einer AMQP-Struktur mit einem Exchange und drei Queues(KundeVertrag, KundeAuftrag, ArtikelAuftrag) verbunden mit drei Routen.

Ein zentraler Exchange vom Typ headers empfängt die Daten. Bei diesem Typ werden die Nachrichten basierend auf mehreren Attributen einer Nachricht an Queues weitergeleitet. Für jeden Datenfluss von einer Stammdatenanwendung zu einer Clientanwendung legen wir eine eigene Queue an, die per Routing mit dem zentralen Exchange verbunden wird.

Den eigentlichen Aufbau dieser Struktur und weitere Funktionalitäten zu Einhaltung der gewählten Konventionen kapseln wir in ein selbst geschriebenes Ruby-Gem (AmqpInfrastructure), welches auf dem Ruby-Gem bunny basiert. Hierdurch bekommen die Anwendungen selbst ein einfaches Interface geboten, welches vom internen Aufbau der AMQP-Infrastruktur abstrahiert.

Eine Stammdatenanwendung kann per

Datenänderungen bekanntgeben (das Beispiel zeigt eine mögliche Integration mit ActiveRecord), hierbei sorgt das Gem dafür, dass die Nachricht an den zentralen Exchange geschickt wird und dass die Nachricht die korrekten Header-Informationen enthält:

  • service enthält den Namen des Services (im Beispiel entspricht ein Service einer Anwendung; es können aber auch verschiedene Services in einer Anwendung angeboten werden). Ein Service entspricht hier einer REST-Ressource des bestehenden Systems
  • action enthält den Typ der Nachricht (im Beispiel ‘update’)
  • id enthält die ID der geänderten Ressource

In Clientanwendungen ermöglicht das Gem den einfachen Aufbau einer Queue sowie des Routings basierend auf den zur Konvention passenden Headern:

Die Abarbeitung der Nachrichten erfolgt nun in separaten Prozessen (Subscriber), die beispielsweise per Rake-Task gestartet werden können. Die folgende Abbildung zeigt nun noch einmal den kompletten Ablauf einer Nachricht

Diagramm: Erneute Abbildung einer AMQP-Struktur mit einem Exchange und drei Queues(KundeVertrag, KundeAuftrag, ArtikelAuftrag) verbunden mit drei Routen. Mit einer Nachricht "Artikel" die durch den Exchange, über die Route {service: article, action: update} zur Queue ArtikelAuftrag zur Abarbeitung durch einen Subscriber der Queue geleitet wird.
  • Der Artikelstamm schickt eine Änderungsnachricht an den zentralen Exchange. Die Header der Nachricht sind { service: “article”, action: “update”, id: 42 }
  • Der header-Mechanismus von AMQP sorgt dafür, dass die Nachricht in alle passenden Queues weitergeleitet wird; im Beispiel matcht nur das Routing { service: “article”, action: “update” }. Die Nachricht wird in die Queue ArtikelAuftrag geschickt.
  • Die Subscriber-Prozesse der Auftragsverwaltung erhalten die Nachrichten und arbeiten sie ab.

Weitere Nutzungsmöglichkeiten der Middleware

Neben dem beschriebenen Anwendungsfall kann die Middleware auch auf weitere Arten genutzt werden:

  • Asynchrone Ausführung von Code innerhalb einer Anwendung: Durch die bestehende Architektur ist es leicht möglich, anwendungsinterne Queues anzulegen und langlaufenden Code asynchron auszuführen. Hierdurch kann auf zusätzliche Tools wie das auf Redis basierende sidekiq verzichtet werden. Hierfür bietet sich die Nutzung des default exchange an, durch den Nachrichten direkt an eine Queue adressiert werden können
  • Parallele Ausführung von Code: Wenn mehrere Consumer auf die gleiche Queue lauschen, sorgt RabbitMQ dafür, dass Nachrichten sinnvoll verteilt werden. Hierdurch ist eine parallele Abarbeitung von Nachrichten einfach möglich.
  • Auslagerung von Anwendungsteilen: Die Auftragsverwaltung erlaubt neben der manuellen Eingabe von Aufträgen auch weitere Datenquellen, z.B. EDI oder strukturierte E-Mails. Hier kann die Anwendung verschlankt werden, indem dieser Teil in eine eigene Anwendung extrahiert wird, die auf die verschiedenen Quellen lauscht und die eingegangenen Aufträge dann strukturiert per AMQP-Nachricht weiterleitet.

Fallstricke und Herausforderungen

Die Einführung einer zusätzlichen Komponente in eine bestehende Architektur kommt bekanntlich nie ohne Nebenwirkungen daher. Auch beim Einsatz von RabbitMQ sollte man daher einige Dinge beachten:

Eine zusätzliche Komponente bedeutet einen höheren Administrationsaufwand des Systems. RabbitMQ muss in die bestehende Infrastrukur eingebunden werden; vor allem das Monitoring muss sinnvoll erweitert werden. Außerdem muss ein Update-Prozess für den Server vorgesehen werden.

Die nachrichtenorientierte Kommunikation zwischen den Systemen macht es erforderlich, sich inhaltlich mit den gegebenen Domänen-Events zu beschäftigen. Im Beispiel wurde eine einfache Implementierung gezeigt, bei der jede Änderung des Artikels von allen empfangenden Anwendungen verarbeitet wird. In der Praxis ist es aber notwendig, dass die Events weiter spezifiziert werden und von empfangenden Anwendungen gezielt verarbeitet werden können. Beispielsweise ist eine Korrektur eines Rechtschreibfehlers für die Auftragsverwaltung irrelevant, eine Status-Änderung des Artikels aber wichtig. Hierdurch kann die Auslastung der Systeme verringert werden.

Fazit und Ausblick

Durch die beschriebene Architektur wird das Ziel erreicht, dass Clientanwendungen zuverlässig auf Stammdatenänderungen lauschen können, ohne die Stammdatenanwendung zu beeinflussen. Durch die Nutzung eines Gems geschieht dies, ohne dass die Anwendungen den internen Aufbau von AMQP kennen müssen. Unsere Erfahrung zeigt, dass RabbitMQ und auch das von uns genutzte Ruby-Gem Bunny sehr zuverlässig funktionieren. Zusätzlich bietet RabbitMQ eine Administrations-Oberfläche per Web-Interface, über das jederzeit die Auslastung beobachtet werden kann, somit können Lastspitzen schon frühzeitig erkannt und analysiert werden. Insgesamt ist die Verwendung einer AMQP-basierten nachrichtenorientierten Middleware ein gutes Mittel, eine REST-zentrierte Architektur um asynchrone Kommunikation zu ergänzen.

Sie suchen den richtigen Partner für erfolgreiche Projekte?
Nehmen Sie Kontakt mit uns auf →