Softwarearchitektur verstehen und entscheiden

Aaron KreisÜber mich
Nachrichten und Ereignisse

Message Ordering

Reihenfolge von Nachrichten sicherstellen

Die Idee und ihr Nutzen

Der Schadendienst einer Versicherung meldet anderen Programmen zuerst „Schaden 4711 angelegt“ und kurz danach „Schaden 4711 geändert“. Verarbeitet ein Empfänger die zweite Nachricht zuerst, kennt er Schaden 4711 noch gar nicht und kann die Änderung nicht zuordnen. Die Nachrichten laufen über eine Nachrichtenplattform, ein Programm, das Nachrichten annimmt, aufbewahrt und an die Empfänger weitergibt. Damit die Reihenfolge stimmt, legt die Nachrichtenplattform alle Nachrichten mit derselben Schadennummer in dieselbe Partition, eine Ablage, in der die Nachrichten in der Reihenfolge ihrer Annahme stehen. Genau ein Empfänger liest diese Partition der Reihe nach. Nachrichten zu anderen Schäden können in anderen Partitionen liegen und gleichzeitig von weiteren Exemplaren desselben Empfängers verarbeitet werden. Dieses Vorgehen heißt Message Ordering, auf Deutsch Reihenfolge von Nachrichten.

Bausteine: Schadendienst: schickt jede Nachricht mit der Schadennummer; Partition A: alle Nachrichten zu Schaden 4711, in Annahmereihenfolge; Partition B: alle Nachrichten zu Schaden 4712, in Annahmereihenfolge; Empfänger 1: liest Partition A der Reihe nach; Empfänger 2: liest Partition B der Reihe nach. Verbindungen: Schadendienst → Partition A (4711); Schadendienst → Partition B (4712); Partition A → Empfänger 1; Partition B → Empfänger 2. Alle Nachrichten zu einem Schaden liegen in derselben Partition und werden dort der Reihe nach gelesen.

Wie funktioniert es?

Zuerst schickt der Schadendienst „4711 angelegt“ mit der Schadennummer 4711 als Schlüssel, also als Angabe, wozu die Nachricht gehört. Die Nachrichtenplattform legt die Nachricht in Partition A. Danach schickt der Schadendienst „4711 geändert“ mit demselben Schlüssel, und die Nachricht landet ebenfalls in Partition A, hinter „4711 angelegt“. Die Nachricht „4712 angelegt“ trägt den Schlüssel 4712 und landet in Partition B. Empfänger 1 und Empfänger 2 sind zwei Exemplare desselben Programms, das die Nachrichten zu Schäden verarbeitet. Weil beide Exemplare dieselbe Verarbeitung ausführen, kann jedes Exemplar jede Partition lesen, und die Nachrichtenplattform teilt jedem Exemplar eigene Partitionen zu. Empfänger 1 liest Partition A der Reihe nach. Zuerst liest Empfänger 1 „4711 angelegt“ und legt Schaden 4711 bei sich an. Erst nach dieser vollständigen Verarbeitung liest Empfänger 1 die nächste Nachricht und ordnet die Änderung dem vorhandenen Schaden 4711 zu. Gleichzeitig liest Empfänger 2 Partition B und verarbeitet „4712 angelegt“ unabhängig von Empfänger 1.

Jede Partition hat genau einen Empfänger, der sie der Reihe nach liest. Wie viele Empfänger gleichzeitig arbeiten können, hängt deshalb von der Zahl der Partitionen ab. Eine einzige feste Reihenfolge für alle Nachrichten würde verhindern, dass mehrere Empfänger gleichzeitig arbeiten, und die Verarbeitung stark verlangsamen. Eine solche Reihenfolge ist auch nicht nötig. Die Nachricht „4711 geändert“ setzt voraus, dass „4711 angelegt“ schon verarbeitet ist. Für die Verarbeitung von „4712 angelegt“ spielen beide Nachrichten keine Rolle. Die Reihenfolge gilt deshalb nur je Schlüssel, nicht über alle Schäden hinweg. Zusätzlich kann jede Nachricht eine fortlaufende Versionsnummer je Schaden tragen. „4711 angelegt“ trägt dann Version 1 und „4711 geändert“ Version 2. An der Versionsnummer erkennt der Empfänger, ob eine Nachricht fehlt, doppelt kommt oder schon von einer neueren überholt ist. Die Entwickler des Empfängers legen fest, ob der Empfänger dann wartet, die fehlenden Angaben beim Schadendienst nachlädt oder die veraltete Nachricht verwirft. Die Nachrichtenplattform sichert nur die Reihenfolge, in der sie Nachrichten angenommen hat. Der Schadendienst muss die Nachrichten eines Schadens deshalb selbst in der richtigen Reihenfolge abschicken.

Scheitert die Verarbeitung einer Nachricht, wiederholt der Empfänger die Verarbeitung oder hält an, statt die nächste Nachricht derselben Partition zu lesen. Macht der Empfänger einfach mit der nächsten Nachricht weiter, ist die Reihenfolge trotz geordneten Lesens verloren. Überspringt Empfänger 1 zum Beispiel „4711 angelegt“ bewusst, kennt Empfänger 1 den Schaden 4711 nicht, wenn „4711 geändert“ kommt. Für diesen Fall braucht der Empfänger eine festgelegte Regel, etwa den ganzen Schaden beim Schadendienst nachzuladen.

Reihenfolge je Schlüssel: Anlage vor Änderung

Beteiligte: Schadendienst: schickt Nachrichten mit der Schadennummer als Schlüssel; Partition A: alle Nachrichten mit Schlüssel 4711, in Annahmereihenfolge; Empfänger 1: liest Partition A der Reihe nach; Partition B: Nachrichten mit Schlüssel 4712; Empfänger 2: liest Partition B gleichzeitig. Ablauf: 1. Der Schadendienst schickt „4711 angelegt“ mit dem Schlüssel 4711. Die Nachrichtenplattform legt die Nachricht in Partition A. 2. Der Schadendienst schickt „4711 geändert“ mit demselben Schlüssel. Die Nachricht landet in Partition A, hinter „4711 angelegt“. 3. Der Schadendienst schickt „4712 angelegt“ mit dem Schlüssel 4712. Die Nachricht landet in Partition B. 4. Empfänger 1 liest Partition A der Reihe nach, zuerst „4711 angelegt“. 5. Empfänger 1 legt Schaden 4711 bei sich an. 6. Erst nach der vollständigen Verarbeitung von „4711 angelegt“ liest Empfänger 1 die nächste Nachricht. 7. Empfänger 1 ordnet die Änderung dem vorhandenen Schaden 4711 zu. 8. Empfänger 2 liest gleichzeitig Partition B und verarbeitet „4712 angelegt“. Wie viele Empfänger gleichzeitig arbeiten, hängt von der Zahl der Partitionen ab. 9. Empfänger 2 arbeitet unabhängig von Empfänger 1. Die Reihenfolge gilt je Schlüssel, nicht über alle Schäden hinweg.

Wann ist es sinnvoll und wann nicht?

Message Ordering lohnt sich

  • Nachrichten beschreiben Änderungen, die aufeinander aufbauen. Ohne gesicherte Reihenfolge kommen „Betrag auf 2.000 Euro geändert“ und „Betrag auf 1.500 Euro geändert“ womöglich vertauscht an. Beim Empfänger steht dann am Ende 2.000 statt 1.500 Euro.
  • Täglich entstehen Nachrichten zu sehr vielen Schäden, und mehrere Empfänger sollen gleichzeitig arbeiten, damit die Verarbeitung mit der Menge Schritt hält. Nachrichtenplattformen verteilen Nachrichten dafür auf mehrere Partitionen und mehrere Empfänger. Ohne Schlüssel geht dabei die Reihenfolge leicht verloren. Erhält Empfänger 1 die Nachricht „4711 angelegt“ und Empfänger 2 die Nachricht „4711 geändert“, kann Empfänger 2 die Änderung verarbeiten, bevor Empfänger 1 Schaden 4711 angelegt hat.
  • Zusammengehörige Nachrichten tragen eine gemeinsame Nummer, die sich als Schlüssel eignet, etwa die Schadennummer oder die Vertragsnummer. Ohne einen solchen Schlüssel kann die Nachrichtenplattform zusammengehörige Nachrichten nicht derselben Partition zuordnen.

Message Ordering ist nicht das Richtige

  • Die Nachrichten sind voneinander unabhängig. Keine Nachricht setzt voraus, dass eine frühere schon verarbeitet ist, und eine gesicherte Reihenfolge bringt nichts. Dann schickt der Schadendienst die Nachrichten ohne Schlüssel, und die Nachrichtenplattform verteilt sie gleichmäßig auf alle Partitionen. Scheitert die Verarbeitung einer Nachricht, müssen die folgenden Nachrichten nicht auf sie warten, und kein häufiger Schlüssel füllt eine einzelne Partition.
  • Eine Reihenfolge über alle Schäden hinweg wird gebraucht. Ein Beispiel ist eine lückenlose Reihenfolge aller Buchungen. Message Ordering sichert die Reihenfolge aber nur je Schlüssel, also je Schaden. Dann richten die Entwickler für diese Nachrichten eine einzige Partition ein, die nur ein Empfänger liest. Alle Nachrichten stehen so in einer einzigen Reihenfolge, und niemand muss einen Schlüssel wählen. Die Verarbeitung ist dann aber nicht schneller als dieser eine Empfänger.
  • Jede Nachricht enthält den vollständigen aktuellen Stand des Schadens samt Versionsnummer. Dann verwirft der Empfänger jede Nachricht, deren Versionsnummer kleiner ist als die bereits gespeicherte. Kommt „4711 geändert“ mit Version 2 zuerst an, legt der Empfänger Schaden 4711 gleich mit dem vollständigen Stand aus Version 2 an und verwirft das später eintreffende „4711 angelegt“ mit Version 1. Die Nachrichtenplattform muss die Reihenfolge nicht sichern und kann die Nachrichten beliebig auf Partitionen und Empfänger verteilen.
  • Die Nachrichtenplattform kennt keine Partitionen, und Message Ordering wie hier beschrieben setzt Partitionen voraus. Dann stellt der Empfänger die Reihenfolge selbst wieder her. Ein Zwischenspeicher beim Empfänger hält Nachrichten zurück, die zu früh kommen, und gibt sie anhand der Versionsnummer in der richtigen Reihenfolge weiter. Gregor Hohpe und Bobby Woolf nennen diesen Zwischenspeicher in ihrem Buch Enterprise Integration Patterns Resequencer. Die Versicherung muss die Nachrichtenplattform dafür nicht wechseln. Der Zwischenspeicher braucht zusätzlichen Arbeitsspeicher und eine Regel für Nachrichten, die nie ankommen.

Nachteile und typische Fehler

Sollen mehr Empfänger gleichzeitig arbeiten, muss die Zahl der Partitionen steigen. Nach dieser Umstellung können neue Nachrichten zu Schaden 4711 in einer anderen Partition landen als die älteren. Die Entwickler müssen die Umstellung deshalb so planen, dass die Empfänger die älteren Nachrichten vorher gelesen haben. Die Nachrichtenplattform prüft nicht, ob der Schadendienst die Nachrichten in der richtigen Reihenfolge abschickt und ob die Empfänger Fehler so behandeln, dass die Reihenfolge erhalten bleibt.

Typische Fehler und woran man sie erkennt:

  • Blockierte Partition: Empfänger 1 kann eine fehlerhafte Nachricht zu Schaden 4711 nicht verarbeiten und versucht es immer wieder. Alle Nachrichten hinter dieser Nachricht in Partition A warten. Das betrifft auch Nachrichten zu anderen Schäden, die zufällig in Partition A liegen. Erkennbar ist das an einem wachsenden Rückstand in einer einzigen Partition, während die anderen leer laufen.
  • Überlasteter Schlüssel: Sehr viele Nachrichten tragen denselben Schlüssel. Nimmt der Schadendienst zum Beispiel die Kundennummer als Schlüssel, landen die Nachrichten zu allen Schäden eines Großkunden mit Tausenden Verträgen in derselben Partition. Diese Partition liest nur ein Empfänger, und weitere Exemplare des Empfängers können ihm dabei nicht helfen. Erkennbar ist das an einem Empfänger, der dauerhaft ausgelastet ist, während die übrigen kaum zu tun haben.
  • Durcheinander nach Wiederholung: Empfänger 1 kann „4711: Betrag auf 2.000 Euro geändert“ nicht verarbeiten und sortiert die Nachricht aus. Danach verarbeitet Empfänger 1 die neuere Nachricht „4711: Betrag auf 1.500 Euro geändert“. Später beheben die Entwickler den Fehler und stellen die aussortierte Nachricht wieder in Partition A ein. Dort steht sie jetzt hinter der neueren Nachricht, und Empfänger 1 setzt den Betrag auf 2.000 Euro zurück. Erkennbar ist das an Schäden, deren Stand beim Empfänger auf eine ältere Version zurückspringt.
  • Zwei Absender: Neben dem Schadendienst schickt ein zweites Programm Nachrichten zu Schaden 4711, etwa ein Portal, in dem Gutachter Beträge ändern. Der Schadendienst schickt Version 3 ab, das Portal kurz danach Version 4. Die Nachricht des Portals kommt zuerst bei der Nachrichtenplattform an. Die Nachrichtenplattform legt die Nachrichten in der Reihenfolge ihres Eintreffens ab, also Version 4 vor Version 3. Erkennbar ist das an Versionsnummern, die in einer Partition nicht aufsteigend aufeinander folgen.
  • Geänderte Zahl der Partitionen: Die Entwickler erhöhen die Zahl der Partitionen von zwei auf drei. Die Nachrichtenplattform berechnet die Partition aus dem Schlüssel und der Zahl der Partitionen. Neue Nachrichten zu Schaden 4711 landen deshalb in Partition C, während ältere Nachrichten zu Schaden 4711 noch ungelesen in Partition A liegen. Empfänger 1 liest Partition A, ein drittes Exemplar des Empfängers liest Partition C. Das dritte Exemplar kann dann eine Änderung an Schaden 4711 verarbeiten, bevor Empfänger 1 die Anlage von Schaden 4711 gelesen hat. Erkennbar ist das an Änderungen, die kurz nach der Umstellung keinem vorhandenen Schaden zugeordnet werden können.

Weiterführend

Verwandte Ansätze

  • Partitioning: beschreibt die Aufteilung von Daten nach einem Schlüssel, auf der die Reihenfolge je Partition beruht.
  • Competing Consumers: mehrere Empfänger teilen sich eine Warteschlange. Ohne Partitionen geht dabei die Reihenfolge verloren.
  • Idempotency: sorgt dafür, dass doppelt eintreffende Nachrichten keine doppelte Wirkung haben. Doppelte Nachrichten kommen auch bei richtiger Reihenfolge vor.
  • Dead Letter Queue: eine Ablage für Nachrichten, die sich nicht verarbeiten lassen. Wer Nachrichten dorthin aussortiert, muss festlegen, was mit den nachfolgenden Nachrichten desselben Schadens geschieht.

Werkzeuge und Umsetzungen

  • Apache Kafka: sichert die Reihenfolge innerhalb einer Partition und legt Nachrichten anhand ihres Schlüssels in die Partitionen.
  • Apache Pulsar: bietet mit dem Modus Key Shared mehrere gleichzeitige Empfänger bei gesicherter Reihenfolge je Schlüssel.
  • RabbitMQ: hält die Reihenfolge je Warteschlange ein, wenn nur ein Empfänger gleichzeitig liest, etwa über die Einstellung Single Active Consumer.
  • NATS JetStream: speichert Nachrichten geordnet und erlaubt Empfänger, die sie der Reihe nach lesen.

Quellen

  • Apache Kafka: Introduction: beschreibt, dass Nachrichten mit demselben Schlüssel in dieselbe Partition geschrieben werden und jeder Empfänger eine Partition genau in der Reihenfolge liest, in der sie geschrieben wurde.
  • Microsoft: Message sessions in Azure Service Bus: beschreibt, wie zusammengehörige Nachrichten über eine gemeinsame Kennung in fester Reihenfolge von genau einem Empfänger verarbeitet werden, während andere Gruppen parallel laufen, und warum die bloße Reihenfolge in der Warteschlange nicht genügt.
  • Gregor Hohpe, Bobby Woolf: Enterprise Integration Patterns, Resequencer: beschreibt einen Zwischenspeicher, der zu früh eintreffende Nachrichten zurückhält und in der richtigen Reihenfolge weitergibt.