Message Queues mit AMQP und Node.js

Architektur/Methoden  –  0 Kommentare

Message Queues erleichtern das Entwickeln entkoppelter, autonomer Dienste in verteilten Architekturen. Als Protokoll kommt hierfür häufig AMQP zum Einsatz, das sich auch in Verbindung mit RabbitMQ und Node.js verwenden lässt. Allerdings gilt es, dabei einige Stolperfallen zu beachten.

Die Entwicklung von Webanwendungen ist naturgemäß verteilt. Schon die Kombination aus Webbrowser und -server stellt ein verteiltes System dar. Aus dem Grund müssen Entwickler auch ohne die Integration zusätzlicher Webdienste einige Besonderheiten verteilter Systeme beachten: Hierzu zählt unter anderem der Umgang mit einer langsamen oder gar abgebrochenen Verbindung.

Damit der Ausfall einer einzelnen Komponente nicht den Rest der Webanwendung beeinträchtigt, muss jede Komponente autonom arbeiten können. Es gilt also, sie zu entkoppeln. Eine gangbare Maßnahme hierfür ist der Verzicht auf Pull-Verfahren: Da eine Abfrage ein funktionierendes und erreichbares Gegenüber voraussetzt, liegt es nahe, dass dieses Vorgehen in verteilten Systemen gewisse Nachteile
birgt. Als Alternative bieten sich Push-Verfahren an, die die zu übertragenden Nachrichten bei einer fehlenden oder fehlerhaften Verbindung zunächst lokal zwischenspeichern, um sie dann zu einem späteren Zeitpunkt auszuliefern. Der Nachteil der Methode ist, dass sie sich nicht für alle Szenarien gleichermaßen eignet: Eine Suchmaschine wie Google ist beispielsweise kaum als Push-Dienst denkbar. Architekturen wie Command Query Responsibility Segregation (CQRS) beweisen allerdings, dass sich das Pushen von Nachrichten als Standardvorgehen für ein verteiltes Systems erfolgreich einsetzen lässt.

Eine Message Queue verwenden

Um den Versand von Nachrichten durchzuführen, benötigt man neben einem Transportkanal zumindest einen lokalen Puffer, der nicht versandfähige Nachrichten gegebenenfalls zwischenspeichert. Beide Aspekte kann man hervorragend in einem gemeinsamen Dienst kapseln, der anschließend den übrigen Komponenten zur Verfügung steht.

Genau das ist die primäre Aufgabe einer Message Queue: Sie nimmt Nachrichten von anderen Komponenten entgegen und speichert sie zwischen, bis die Zielkomponente verfügbar ist. Die versendende Komponente wird dabei als Publisher bezeichnet, die empfangende als Consumer (siehe
Abbildung 1).

Die einfachste Struktur von Message Queues entkoppelt zwei Komponenten, den Publisher und den Consumer, durch einen Puffer. (Abb. 1)


Darüber hinaus nehmen Message Queues häufig noch weitere Aufgaben wahr, unter anderem das Routing an Consumer und das garantierte Zustellen von Nachrichten.

Um unterschiedliche Implementierungen von Message Queues einheitlich ansprechen zu können, wurden in den vergangenen Jahren mehrere Protokolle entwickelt – unter anderem das Advanced Message Queuing Protocol (AMQP), das Stream Text Oriented Messaging Protol (STOMP) und Message Queue Telemetry Transport (MQTT). Sie unterscheiden sich bezüglich ihrer Ausrichtung und ihrer Fähigkeiten teilweise deutlich voneinander: Während AMQP primär im Unternehmensbereich zum Einsatz kommt, stellt MQTT den De-facto-Standard für den IoT-Bereich (Internet of Things) dar.

Eine weit verbreitete Message Queue ist RabbitMQ, die VMware in der Programmiersprache Erlang entwickelt und kostenfrei unter einer Open-Source-Lizenz (Mozilla Public License) zur Verfügung stellt. RabbitMQ beherrscht von Haus aus das AMQP-Protokoll, lässt sich jedoch mit Plug-ins um die Unterstützung für STOMP und MQTT erweitern.

Exchanges, Queues & Co.

Im Unterschied zu der in Abbildung 1 dargestellten Struktur entkoppelt RabbitMQ den Publisher nochmals von der eigentlichen Queue: Dazu dient der sogenannte Exchange (siehe Abbildung 2).

Der Exchange entkoppelt den Producer von der eigentlichen Queue und eröffnet so unterschiedliche Möglichkeiten für das Routing von Nachrichten. (Abb. 2)

Er ermöglicht zwei Szenarien der Zustellung von Nachrichten:

  • Verwenden mehrere Consumer eine gemeinsame Queue, verteilt Letztere die Nachrichten auf die einzelnen Consumer. Auf die Weise ist es zum Beispiel möglich, Aufgaben auf mehrere Arbeitsprozesse zu verteilen (siehe Abbildung 3).
  • Verfügt hingegen jeder Consumer über eine eigene Queue, die aber alle der gleiche Exchange bedient, erhält jeder Consumer alle Nachrichten. Dies dient der Implementierung eines Publish-/Subscriber-Modells (siehe Abbildung 4).
Teilen sich mehrere Consumer eine Queue, verteilt RabbitMQ die Nachrichten auf die einzelnen Consumer. (Abb. 3)
Verfügt jeder Consumer hingegen über eine eigene Queue, erhält er alle Nachrichten, die über den Exchange versendet werden. (Abb. 4)

Allerdings ist solch ein Verhalten ebenfalls konfigurierbar, denn ein Exchange lässt sich in unterschiedlichen Modi betreiben. Standardmäßig verwenden Exchanges in RabbitMQ den "Direct"-Modus, alternativ kann man sie allerdings auch in den "Fanout"- oder den "Topic"-Modus schalten. Sie unterscheiden sich wie folgt:

  • Der "Fanout"-Modus ist die einfachste der drei Varianten: Befindet sich ein Exchange in diesem Modus, verteilt er alle eingehenden Nachrichten stets an alle ihm angehängten Queues. Auf den ersten Blick unterscheidet sich das nicht von der bisherigen Verwendung eines Exchanges im "Direct"-Modus. Das ändert sich, sobald man Nachrichten um einen sogenannten Routingschlüssel ergänzt: In dem Fall leitet der Exchange Nachrichten nur noch an jene Queues weiter, die sich auf eben diesen Schlüssel registriert haben. Auf die Weise lassen sich Nachrichten kategorisieren und entsprechend ausliefern (siehe Abbildung 5).
  • Ein Exchange im "Topic"-Modus verhält sich prinzipiell ähnlich zu einem, der im "Direct"-Modus betrieben wird. Der Unterschied besteht in der Form des Routingschlüssels. Entspricht er im "Direct"-Modus lediglich einem statischen Begriff, erlaubt der "Topic"-Modus hierarchische Routingschlüssel, wobei Queues für ihre Registrierung am Exchange Platzhalter verwenden können.
Im "Direct"-Modus berücksichtigt ein Exchange den Routingschlüssel von Nachrichten, um sie gezielt an bestimmte Queues auszuliefern. (Abb. 5)

Zusätzlich zu der Konfiguration der Exchanges verfügt RabbitMQ über einige weitere Optionen zur Konfiguration. Beispielsweise lässt sich einstellen, ob die Lebensdauer einer Queue an die Lebensdauer des zugehörigen Consumers zu binden ist: Trennt der Consumer die Verbindung, kann RabbitMQ die zugehörige Queue auf Wunsch automatisch entfernen.

Außerdem lassen sich Exchanges und Queues als "durable" kennzeichnen, was bewirkt, dass RabbitMQ sämtliche Nachrichten persistiert. Aktiviert man die Option, überleben noch nicht ausgelieferte Nachrichten einen Absturz oder Neustart von RabbitMQ. Zugleich sinkt allerdings deren Leistungsfähigkeit, da Lese- und Schreibzugriffe auf ein physisches Laufwerk weitaus langsamer sind als jene auf den Arbeitsspeicher.

Zu guter Letzt lässt sich auch eine Zustellbestätigung anfordern: Normalerweise entfernt RabbitMQ Nachrichten aus Queues, sobald sie sie an den zugehörigen Consumer übertragen hat. Stürzt der Consumer anschließend jedoch ab, ohne die Nachricht vollständig zu verarbeiten, ist sie verloren. Dem lässt sich mit Zustellbestätigungen ein einfacher Riegel vorschieben: Verliert eine Queue die Verbindung zu ihrem Consumer, liefert sie bei der nächsten Verbindung all die Nachrichten erneut aus, deren Verarbeitung noch nicht bestätigt wurde.