IBiS' Reise in Richtung Quorum Queues

Letztes Jahr haben wir bei IT-Clouds, einem Konzern, der für die Entwicklung der Cloud-Produkte von Swisscom verantwortlich ist, die Unterstützung von RabbitMQs Quorum Queue [1] in unserer internen PaaS-Lösung aktiviert. Dann haben wir eines unserer Softwaresysteme migriert, um sie in der Produktion zu nutzen. Dieser Beitrag beschreibt unser System, unsere Motivation und die Schritte, die wir für die Migration einer Reihe von Java-/Spring-basierten Anwendungen nur mit Code durchgeführt haben.

Über IBiS

IBiS steht für "Integration of Business Services". Es ist ein wichtiges Backend-System für die Cloud-Angebote von Swisscom (Enterprise Service Cloud, Dynamic Computing Services), das die Abrechnungs- und Berichtsfunktionen unserer Produkte unterstützt. Das System:

  • verfolgt den Lebenszyklus von Ressourcen, die in der Cloud laufen (virtuelle Maschinen, Kubernetes-Cluster usw.).
  • misst und bewertet die Ressourcennutzung (CPU, Speicher, Speicher usw.).
  • hält Bestands- und Geschäftssysteme von Drittanbietern auf dem gleichen Stand (Konfigurationsmanagementsysteme, Abrechnungssysteme usw.).
  • bietet verschiedene Berichtsfunktionen (z. B. Abrechnungs- oder Lizenzierungsberichte für verwendete Betriebssysteme) für das Kundenportal und nachfolgende Geschäftssysteme.

Um seine Ziele zu erreichen, hört IBiS auf alle Arten von Ereignissen, die von Cloud-Diensten kommen, und reagiert entsprechend darauf. Er besteht aus einer Reihe von Komponenten, die einfach und ereignisgesteuert aufgebaut sind:

IBiS wird bei Swisscom entwickelt, ist in modernem Java geschrieben und basiert auf den neuesten Versionen des Spring Frameworks.

RabbitMQ bei IBiS

Anforderungen an zuverlässige Nachrichtenübermittlung

Das Herzstück von IBiS ist RabbitMQ [2], ein Open-Source-Messaging-Broker, der das AMQP-Messaging-Protokoll [3] unterstützt. Wir nutzen RabbitMQ als Dienst innerhalb unserer internen Application Cloud (iAPC), einer PaaS-Lösung auf Basis von Cloud Foundry [4]. Er ist für den zuverlässigen Transport von Ereignissen innerhalb des Systems verantwortlich, von den Komponenten, die sie aufnehmen (z. B. von Apache Kafka [5]), bis zu den Komponenten, die sie durch Anwendung der entsprechenden Geschäftslogik verarbeiten. Eine der wichtigsten Geschäftsanforderungen von IBiS ist die genaue Verfolgung der Vorgänge in den Swisscom Clouds. Um dies zu erreichen, müssen wir sicherstellen, dass die Ereignisse auf zuverlässige Weise geliefert werden. Der Verlust eines Ereignisses kann schlimme Folgen haben und leicht zu Abrechnungs- oder Berichtsfehlern führen (z.B. eine VM wurde in der Cloud gelöscht, aber IBiS hat nie davon erfahren, da das Ereignis verloren ging).

Ursprünglich nutzte IBiS dauerhaft gespiegelte Warteschlangen, um die gewünschte Zuverlässigkeit zu erreichen. Das war lange Zeit eine gute Lösung, aber die neuesten RabbitMQ-Versionen (3.8.0+) unterstützen Quorum-Queues. Quorum-Warteschlangen gelten als die moderne Alternative zu gespiegelten Warteschlangen. Sie konzentrieren sich auf die Datensicherheit und wurden speziell für die Bedürfnisse von Systemen wie IBiS entwickelt, bei denen Zuverlässigkeit eine wichtige Rolle spielt. Da unsere Arbeitslast genau das ist, wofür die Quorum-Warteschlangen gebaut sind, haben wir uns entschieden, von gespiegelten Warteschlangen wegzugehen. Der Rest dieses Artikels beschreibt, wie wir das gemacht haben.

Unser Setup

Bevor wir über die Migration selbst sprechen, ist es wichtig, kurz das RabbitMQ-System zu beschreiben, das IBiS nutzt. Unsere interne Nachrichtenarchitektur basiert auf mehreren Topic-Exchanges [6], an die Nachrichten (Ereignisse) gesendet werden. Jede Nachricht wird von einem Routing-Schlüssel begleitet, der in unserem Fall der Ereignistyp ist. Die Vermittlungsstellen leiten die Nachrichten an eine Reihe von Warteschlangen [7] weiter. Die Warteschlangen werden von unseren Backend-Komponenten erstellt (deklariert), wobei jede Warteschlange zu genau einer Komponente gehört und auf eine Teilmenge von Routing-Schlüsseln hört. Dies stellt im Wesentlichen ein Publish/Subscribe-System dar, bei dem jede Backend-Komponente bestimmte Routing-Schlüssel (Ereignistypen) abonnieren und nur auf die Nachrichten hören kann, die für sie von Interesse sind. Die Architektur eines solchen Austauschs ist in der folgenden Abbildung dargestellt:

Bei der Verarbeitung von Nachrichten müssen wir auch mit Fehlern umgehen (z. B. wenn ein Ereignis falsch geformt ist). Zu diesem Zweck nutzen wir Dead-Letter-Warteschlangen. Wenn eine Nachricht von einer Backend-Komponente nicht verarbeitet werden kann, wird sie an einen Dead-Letter-Austausch [8] gesendet, der sie in eine Dead-Letter-Warteschlange schiebt, die der Komponente gehört, in der das Problem entdeckt wurde. Die Nachricht bleibt dort, bis wir das Problem verstehen und sie manuell bearbeiten können.

Letztendlich lässt sich unsere Architektur wie folgt zusammenfassen.

  • Wir verwenden einen Themenaustausch, an den wir neue Nachrichten senden.
  • Wir verwenden einen Dead Letter Exchange, über den fehlerhafte Nachrichten behandelt werden.
  • Jede Backend-Komponente deklariert zwei Warteschlangen: eine, um neue Nachrichten zu verarbeiten, die vom Themenaustausch kommen, und eine, um fehlerhafte Nachrichten, die vom toten Briefaustausch kommen, erneut zu verarbeiten.

Migration zu Quorum-Queues mit Spring AMQP

Unsere Nachforschungen über die Umstellung von gespiegelten Warteschlangen auf Quorum-Warteschlangen haben schnell ergeben, dass RabbitMQ uns dabei nicht allein helfen kann. Die RabbitMQ-Warteschlangen können nicht einfach ihren Typ ändern. Sie sind unveränderlich, d.h. um zu Quorum-Warteschlangen zu wechseln, müssen wir neue Warteschlangen deklarieren, Nachrichten aus den alten Warteschlangen verschieben und die alten Warteschlangen entfernen. Eine Möglichkeit, dieses Problem zu lösen, wäre die Verwendung des Shovel Plugins [9]. Dies würde jedoch erfordern, dass das RabbitMQ-Team bei Swisscom aktiv wird (Wartung planen, das Plugin installieren, testen usw.), ohne dass es eine Garantie dafür gibt, dass der Ansatz für unseren Anwendungsfall tatsächlich funktioniert. Deshalb haben wir uns entschieden, einen anderen Weg einzuschlagen und alles mit Spring AMQP als Code abzuwickeln.

Spring AMQP

Spring AMQP [10] ist ein Spring-Projekt, das die Entwicklung von AMQP-basierten Lösungen unterstützen soll. Es bietet praktische Abstraktionen, die einfach zu verwenden sind, sich gut in das Spring Framework integrieren lassen und es ermöglichen, das volle Potenzial von AMQP zu nutzen. Auch wenn es den Rahmen dieses Artikels sprengen würde, ausführlich auf Spring AMQP einzugehen, ist es wichtig zu erwähnen, dass es Klassen bietet, mit denen wir verschiedene RabbitMQ-Objekte deklarieren und verwalten können. Wir können zum Beispiel ganz einfach einen neuen Topic-Austausch, Routing-Regeln (Bindungen) und eine neue Warteschlange deklarieren:

Sobald wir unser Setup deklariert haben (das dann automatisch beim Start der Anwendung auf idempotente Weise bereitgestellt wird), können wir mit der Klasse RabbitTemplate auf RabbitMQ zugreifen. Versuchen wir, diese Teile zusammenzufügen, um gespiegelte Warteschlangen in Quorum-Warteschlangen umzuwandeln.

Transparente Migration

Wir können uns notieren, was passieren muss, um transparent und sicher von gespiegelten Warteschlangen zu Quorum-Warteschlangen zu migrieren. Das müssen wir tun:

  1. neue Warteschlangen vom Typ Quorum erstellen.
  2. das Routing zu den bestehenden gespiegelten Warteschlangen beenden.
  3. die in den bestehenden gespiegelten Warteschlangen verbliebenen Nachrichten bearbeiten (sie können noch eintreffen, während wir die Weiterleitung unterbrechen).
  4. die gespiegelten Warteschlangen entfernen, so dass nur noch die Quorum-Warteschlangen vorhanden sind.

Indem wir unsere Messaging-Architektur und Spring AMQP nutzen, können wir diese Schritte in die folgenden übersetzen. Für jede Backend-Komponente wollen wir:

  1. eine neue Quorum-Warteschlange beim Start der Anwendung bereitstellen und die vorherige Warteschlange aus der Konfiguration entfernen. Dadurch wird die neue Warteschlange erstellt, die wir abhören, und das Abhören der alten Warteschlange beendet.
  2. die Bindung der alten Warteschlange an den Themenaustausch aufheben, d.h. die Routing-Regeln entfernen, die dazu führen, dass der Themenaustausch weiterhin Nachrichten an die alte Warteschlange weiterleitet.
  3. alle Nachrichten aus der gespiegelten Warteschlange zu entfernen, indem du sie zurückweist (d.h. einen Fehler signalisierst) und sie somit in die Warteschlange für tote Buchstaben verschiebst.
  4. die Nachrichten aus der Warteschlange für tote Briefe verbrauchen.
  5. die gespiegelte Warteschlange entfernen.

Die oben beschriebenen Schritte sind in dem folgenden Diagramm dargestellt:

Man könnte sich fragen, warum wir uns entschieden haben, die alten Warteschlangen zu entleeren, indem wir die Nachrichten an Dead-Letter-Warteschlangen senden, anstatt sie direkt zu konsumieren. Wir haben diesen Ansatz aus Wartungsgründen gewählt. Da wir uns bei der Behandlung fehlerhafter Nachrichten stark auf Dead-Letter-Warteschlangen verlassen, verfügen wir bereits über robusten, bewährten Code, den wir wiederverwenden können. Ein direkter Abruf der gespiegelten Warteschlangen wäre zwar möglich, birgt aber das Risiko, dass einige Randfälle nicht behandelt werden und somit Nachrichten verloren gehen.

Wir können jetzt den Code durchgehen, den wir für die Migration unserer Warteschlangen verwendet haben.

Lass uns zunächst die Namen der Warteschlangen festlegen. Angenommen, wir haben die neue Quorum-Warteschlange unter dem Namen "my-quorum-queue" deklariert und die vorherige klassische gespiegelte Warteschlange "my-classic-mirrored-queue" ist bereits vorhanden:

Dann können wir das autowired RabbitTemplate-Objekt verwenden (siehe Spring AMQP-Dokumente für weitere Details), um einen Administrations-Client zu erhalten:

Dann prüfen wir zunächst, ob die klassische Warteschlange existiert. Ist das nicht der Fall, haben wir nichts zu tun.

Als Nächstes nutzen wir die Bindungen, die wir in der Konfiguration unserer Anwendung deklarieren. Wenn wir sie ähnlich wie in dem Beispiel aus dem Abschnitt über Spring AMQP erstellen, können wir eine Bindungsliste erstellen und sie verwenden, um die Bindungen aus der alten Warteschlange zu entfernen. Die wichtigste Annahme dabei ist, dass die Bindungen dieselben sind, d. h. die neue Warteschlange hat denselben Satz an Bindungen wie die alte.

Dann gehen wir davon aus, dass das Entfernen der Bindungen einige Zeit in Anspruch nimmt und einige Nachrichten noch eintreffen könnten, während wir vorankommen. Deshalb warten wir ein bisschen.

Zu diesem Zeitpunkt sollte die alte Warteschlange einen konsistenten Zustand erreichen. Es wird davon ausgegangen, dass alle Nachrichten eingegangen sind und dass keine neuen Nachrichten ankommen, da es keine Bindungen gibt. Daher können wir alle Nachrichten zurückweisen und in die Warteschlange für tote Briefe verschieben:

In diesem Moment sollte die alte Warteschlange geleert und alle ausstehenden Nachrichten in die Warteschlange für tote Briefe verschoben werden. Wie bereits erwähnt, enthält unsere IBiS-Codebasis eine Logik für die Warteschlange für tote Briefe. Dies geschieht über das Objekt `deadLetterQueueService` (das unter der Haube lediglich ein paar Dienstprogramm-Methoden bereitstellt, um aus der Warteschlange für tote Briefe zu lesen oder einige Statistiken zu erhalten). Wir verwenden es, um sicherzustellen, dass die Migration erfolgreich war und um die Nachrichten zu verbrauchen:

Zum Schluss entfernen wir die alte Warteschlange:

Wir haben diese Migration als REST-Endpunkt bereitgestellt, der automatisch (nach dem Start der Anwendung) oder manuell (falls wir sie nach einem Fehler wiederholen müssen) ausgelöst wird. Die Migration ist idempotent, d. h., sie kann mehrfach ausgelöst werden und führt immer wieder zum gleichen Ergebnis.

Den vollständigen Code findest du hier(öffnet ein neues Fenster).

Conclusion

In diesem Artikel haben wir beschrieben, wie wir IBiS, eine Softwareplattform, die wir entwickeln, um die Cloud-Angebote von Swisscom in Bezug auf Abrechnung und Reporting zu unterstützen, auf die Quorum-Queues von RabbitMQ umgestellt haben. Wir haben besprochen, wie wir RabbitMQ intern nutzen, welche Anforderungen wir haben und wie wir uns für die Migration entschieden haben. Schließlich haben wir anhand von Codebeispielen aus der Praxis gezeigt, wie eine solche Migration mit Java und dem Spring AMQP-Projekt durchgeführt werden kann.

References

Adam Krajewski

Adam Krajewski

Software Engineer

Mehr getIT-Beiträge

Bereit für Swisscom

Finde deinen Job oder die Karrierewelt, die zu dir passt. In der du mitgestalten und dich weiterentwickeln willst.

Was du draus machst, ist was uns ausmacht.

Zu den Karrierewelten

Zu den offenen Security Stellen