Event-basierte Systeme: Probleme beim Behandeln von doppelten Nachrichten

Bei meinem aktuellen Kunden existieren verschiedene Mikroservices, die in der Regel asynchron über Nachrichten miteinander kommunizieren. Die Services senden dabei Nachrichten über fachliche Ereignisse (wie bspw. „Rechnung gebucht“ oder „Wareneingang gebucht“) ohne dass sie wissen, für welche anderen Microservices diese relevant sind. Letztere können sich dann auf diese Ereignisse registrieren und diese behandeln, bspw. um nach einem Wareneingang den Bestand für einen Artikel zu erhöhen. Hier fängt das Problem schon an, denn man kann nicht davon ausgehen, dass eine Nachricht nur genau einmal zugestellt wird. Die Nachrichten bei meinem Kunden werden mindestens einmal zugestellt. Damit bspw. eine Nachricht über einen Wareneingang, die mehrmals verarbeitet wird, den Bestand trotzdem nur einmal erhöht., müssen diese dedupliziert werden. Unsere Idee war, diese Deduplizierung über fachliche Attribute, die wir in der Nachricht sowieso mitsenden, durchzuführen. Das klang und funktionierte lange gut, aber im Folgenden soll es darum gehen, warum dieses am Ende zu Problemen führen kann.

Wie ist die fachliche Deduplizierung umgesetzt?

Für die Deduplizierung werden die fachlichen Attribute aus der Nachricht in der Datenbank persistiert und durch eine Unique Constraint wird sichergestellt, dass dieselbe Kombination aus fachlichen Attributen nur einmal vorkommt. Bspw. wird sich bei den Bestandsveränderungen gemerkt, durch welchen Vorgang diese zustande gekommen sind. Also bspw. die Id der Wareneingangsposition und dass es ein Wareneingang war. Dass die Id zu einem Wareneingang gehört ist wichtig, da Bestandsveränderungen bspw. auch durch Kassiervorgänge auftreten können. Über diese beiden fachlichen Attribute (Id und Typ) ist eine Unique Constraint definiert. Wird eine Nachricht mehrmals verarbeitet, tritt durch diese Unique Constraint ein Fehler beim Speichern auf. Daraufhin wird geprüft, ob die Nachricht mehrfach zugestellt und bereits konsumiert wurde. Ist das der Fall, kann sie ignoriert werden, ansonsten wird der Fehler weitergereicht. Mögliche Fehlerbehandlungen kann dann z.B. das Rückrollen der Transaktion, ein Alert oder die Einsortierung in eine Dead Letter Queue sein

Wann führt die fachliche Deduplizierung zu einem Problem?

Lange ging es gut, aber in zwei Fällen führte dieses Verfahren zu einem Problem:

  •  Manchmal ist es gar nicht so einfach, so einen fachlichen Schlüssel zu finden. Beispielsweise wird bei einem Wareneingang nicht nur der Bestand erhöht, es wird auch noch geschaut, ob es eine Kundenbestellung gibt, die durch diese gerade eingetroffene Ware bedient werden kann. Diese Zuordnung zwischen Wareneingangsposition und Position einer Kundenbestellung merken wir uns in einer dedizierten Tabelle. Dabei müssen die Mengen nicht übereinstimmen. Durch eine Wareneingangsposition können auch mehrere Positionen aus Kundenbestellungen bedient werden (und umgekehrt). Es ist folglich eine m:n Beziehung. Zusätzlich zu der Id der Wareneingangsposition muss also auch die Id der Bestellposition Teil der Unique Constraint sein, da nur diese Kombination eindeutig ist. Aber selbst diese Unique Constraint reicht für eine Deduplizierung der Nachrichten nicht aus. Das Problem ist in unserem Fall, dass es gar nicht zu einer Unique Constraint Verletzung käme, wenn dieselbe Nachricht über einen Wareneingang ein zweites Mal zugestellt wird. Grund hierfür ist, dass in diesem Fall der Algorithmus, der die Zuordnung vornimmt, erkennt, dass die Kundenbestellpositionen schon durch den ersten Empfang der Nachricht bedient wurde. Beim zweiten Empfang derselben Nachricht würde er andere Positionen zuordnen. Da dann die Kombination aus Wareneingangsposition Id und Id der Kundenbestellposition eine andere ist, kommt es nicht zu einer Unique Constraint Verletzung.
  • Ein Problem kann man auch bekommen, wenn ein Service verschiedene Nachrichten von verschiedenen Services konsumiert, um dann dieselbe Logik auszuführen. Beispielsweise führen bei meinem Kunden verschiedene Prozesse (Rechnung erstellen, Wareneingang, Kassiervorgänge usw.) dazu, dass Reservierungen für einen Artikelbestand vorgenommen oder aufgelöst werden. Wenn man hier nun eine Deduplizierung über mehrere fachliche Attribute vornimmt, kann es sein, dass zuerst alles gut klappt. Und plötzlich muss man Nachrichten von einem weiteren Service konsumieren, der diese fachlichen Werte vllt. gar nicht mehr liefern kann. Dann passt der bisher gut funktionierende Deduplizierungsmechanismus nicht mehr und muss umgebaut werden.

Unsere Deduplizierung musste also umgebaut werden.

Wie kann es gelöst werden?

Die optimale Lösung sieht so aus, dass die Nachrichten Idempotent verarbeitet werden können, so dass man gar keine Deduplizierung benötigt.

Dieses war bei uns nicht ohne weiteres möglich und wir litten bei einem Service unter den oben genannten Problemen. Daher haben wir uns dann entschieden, die Deduplizierung über ein technisches Attribut (einer eindeutigen Id für die Message) vorzunehmen. Dazu könnte man die Id nehmen, die die Message Broker in der Regel automatisch für die Nachricht generieren, sofern diese auch bei mehrmaligen Zustellversuchen derselben Nachricht konstant bleibt. Alternativ ist es immer möglich, diese beim Versenden der Nachricht selber zu generieren und als Meta Information mitzuversenden. Diese Id wird dann in einer extra Datenbanktabelle persistiert, auf der auch eine Unique Constraint definiert ist. Das restliche Verfahren ist dann analog zur fachlichen Deduplizierung, d.h. kommt es dabei bspw. zu einem Unique Constraint Fehler so weiß man, dass man die Nachricht schon erhalten hat, und sie kann verworfen werden.

Und nun ist immer alles gut?

Ganz so einfach umzusetzen war es dann doch nicht. Wir hatten uns schon gewundert, dass es bei den von uns verwendeten Projekten Spring Boot und Spring Cloud AWS nichts gab, was man einfach aktivieren muss, und schon funktioniert es mit der Deduplizierung über ein technisches Id Attribut. Aber es generisch für alle Nachrichten umzusetzen, ist nicht so einfach, da es davon abhängt, was die Nachricht auslöst bzw. bewirkt:

  • Wenn nur in einer Transaktion etwas in der Datenbank des Services geändert wurde, so kann bei einem Unique Constraint Fehler einfach die Transaktion zurückgerollt werden und schon ist alles wie vorher.
  • Sind aber externe Systeme beteiligt, so ist dieses nicht mehr so einfach möglich, da diese bspw. schon eine E-Mail versendet haben könnten und dieses nun nicht mehr zurückgerollt werden kann (wenngleich es auch dafür natürlich Lösungen gibt).
  • Selbst wenn kein externes System beteiligt ist, muss man sich den Prozess genau anschauen, da es ggf. sein könnte, dass der Service bereits durch eine separate Transaktion etwas persistiert hat.
  • Auch gab es bei meinem Kunden den Fall, dass ein externes System gebuchte Bons per Rest Request gemeldet hat. Diese Rest Requests wurden dann direkt als Nachrichten an weitere System gesendet. Hier kann es dazu kommen, dass das externe System Bons mehrmals per Rest meldet. Dieses kann bspw. vorkommen, wenn nach einem Verbindungsabbruch ein Rest-Request erneut gesendet wird. In diesem Fall gäbe es also zwei Nachrichten die inhaltlich denselben Bon beinhalten. Da diese Nachrichten unterschiedliche technische Ids enthalten, reicht dann eine technische Deduplizierung nicht mehr aus, sondern es muss fachlich geprüft werden, ob der Bon bereits verarbeitet wurde.

Eine generische Lösung, die immer für alle Nachrichten und für alle Benutzer solcher generischen Bibliotheken transparent funktioniert ist also schon ziemlich komplex. Daher gibt es wahrscheinlich auch nichts out of the box (oder wir haben es nur nicht gefunden 😉 ). Wir haben so etwas nun selber in einer Lib umgesetzt und für viele unserer Anwendungsfälle funktioniert diese Lösung ganz gut. Wo dieses nicht reicht, haben wir noch zusätzliche fachliche Validierungen eingebaut.

Schlusswort

Grundsätzlich finde ich eine fachliche Deduplizierung ok, in unserem Anwendungsfall sind wir damit lange gut ausgekommen. Normalerweise kann eine Deduplizierung über eine technische Id dann später auch noch nachgerüstet werden. Ein Grund für eine fachliche Deduplizierung kann bspw. sein, dass man die fachlichen Attribute eh hat und den initialen Aufwand für eine technische Deduplizierung scheut. Aber auch eine fachliche Deduplizierung bekommt man nicht umsonst, bspw. weil die Attribute vll. noch gar nicht in der Nachricht drin sind. Manchmal stellt man auch fest, dass eine technische Deduplizierung nicht ausreicht und man (ggf. zusätzlich?) eine fachliche braucht. Auf jeden Fall sollte man sich darüber Gedanken machen was passiert, wenn Nachrichten mehrmals zugestellt werden und diesen Fall vernünftig behandeln.