Softwarearchitektur verstehen und entscheiden

Aaron KreisÜber mich
Konsistenz und Transaktionen

Outbox Pattern

Datenänderung und ausgehende Nachricht gemeinsam sichern

Die Idee und ihr Nutzen

Ein Schadendienst, also das Programm einer Versicherung für die Bearbeitung von Schäden, gibt den Schadenfall 4711 zur Auszahlung frei. Er speichert die Freigabe in seiner Datenbank und soll außerdem dem Zahlungsdienst, dem Programm für Auszahlungen, eine Nachricht schicken, damit der Zahlungsdienst die Auszahlung vorbereitet. Stürzt der Schadendienst zwischen Speichern und Senden ab, erfährt der Zahlungsdienst nie von der Freigabe. Deshalb speichert der Schadendienst die Freigabe und die Nachricht gemeinsam in seiner Datenbank, die Nachricht in einer Tabelle namens Outbox (Postausgang). Die Datenbank speichert dann entweder beides oder nichts. Ein eigenes Programm, der Versandprozess, liest die Nachricht danach aus der Outbox und sendet sie. Diese Bauweise heißt Outbox Pattern.

Bausteine: Schadendienst: gibt Fall 4711 zur Auszahlung frei; Fallstatus: Fall 4711 „freigegeben“; Outbox: Nachricht an den Zahlungsdienst; Versandprozess: liest die Outbox und sendet neue Einträge; Nachrichtenplattform: nimmt Nachrichten an und stellt sie zu; Zahlungsdienst: bereitet die Auszahlung vor, erkennt Doppelte. Verbindungen: Schadendienst — Fallstatus; Schadendienst — Outbox; Outbox → Versandprozess; Versandprozess → Nachrichtenplattform; Nachrichtenplattform → Zahlungsdienst. Der Schadendienst speichert Freigabe und Nachricht in derselben Datenbanktransaktion, und der Versandprozess sendet die Nachricht danach aus der Outbox.

Wie funktioniert es?

Der Schadendienst gibt Fall 4711 frei und schreibt dafür zwei Änderungen gemeinsam in die Datenbank des Schadendienstes. Die erste Änderung setzt den Status von Fall 4711 auf „freigegeben“, die zweite legt in der Outbox einen Eintrag mit der Kennung e-5531 und dem Inhalt „Schadenfall 4711 freigegeben“ an. Ein solcher gemeinsamer Speichervorgang heißt Datenbanktransaktion. Die Datenbank des Schadendienstes bestätigt beide Änderungen gemeinsam. Danach fragt der Versandprozess die Outbox nach Einträgen, die noch nicht gesendet sind, und erhält den Eintrag e-5531. Der Versandprozess soll den Eintrag an die Nachrichtenplattform übergeben, ein eigenes Programm, das Nachrichten annimmt und an Empfänger weiterleitet. Die Nachrichtenplattform ist aber für zehn Minuten nicht erreichbar. Der Sendeversuch scheitert, und der Eintrag e-5531 bleibt ungesendet in der Outbox. Der Versandprozess versucht es in Abständen erneut und übergibt e-5531 nach der Störung an die Nachrichtenplattform. Die Nachrichtenplattform bestätigt, dass sie die Nachricht sicher gespeichert hat. Erst danach markiert der Versandprozess den Eintrag als gesendet. Die Nachrichtenplattform stellt die Nachricht dem Zahlungsdienst zu, und der Zahlungsdienst prüft die Kennung e-5531 und bereitet die Auszahlung vor.

Der Schadendienst sendet die Nachricht also nicht selbst. Eine Datenbanktransaktion speichert alles oder nichts, aber nur innerhalb der Datenbank des Schadendienstes; die Nachrichtenplattform gehört nicht dazu. Deshalb liegt die Nachricht zuerst in der Outbox. Der Versandprozess, oft Relay genannt, ist ein eigenes Programm, das neue Einträge aus der Outbox liest und an die Nachrichtenplattform übergibt. Von neuen Einträgen erfährt der Versandprozess, indem er die Outbox in kurzen Abständen abfragt oder das Änderungsprotokoll der Datenbank mitliest, in dem die Datenbank jede gespeicherte Änderung festhält. Nach der Bestätigung der Nachrichtenplattform kann der Versandprozess den Eintrag auch löschen, statt ihn als gesendet zu markieren. Jede Nachricht trägt eine eindeutige Kennung wie e-5531, an der der Zahlungsdienst erkennt, ob er die Nachricht schon verarbeitet hat. Die Auszahlung selbst bleibt Sache des Zahlungsdienstes.

Das Outbox Pattern stellt sicher, dass keine Freigabe ohne vorgemerkte Nachricht gespeichert wird und dass die Nachricht mindestens einmal gesendet wird, solange der Versandprozess läuft. Dass die Nachricht genau einmal ankommt, stellt es nicht sicher. Der Versandprozess kann eine Nachricht senden und abstürzen, bevor er den Eintrag als gesendet markiert hat. Nach dem Neustart findet er den Eintrag weiter ungesendet in der Outbox und sendet dieselbe Nachricht noch einmal. Ebenso kann nur die Bestätigung der Nachrichtenplattform ausbleiben. Der Versandprozess weiß dann nicht, ob die Nachricht angekommen ist, und sendet sie erneut. Kommt e-5531 so ein zweites Mal beim Zahlungsdienst an, erkennt der Zahlungsdienst die Kennung wieder und bereitet keine zweite Auszahlung vor. Während die Nachrichtenplattform nicht erreichbar ist, kann der Schadendienst weiter Fälle freigeben. Die Nachrichten zu diesen Fällen bleiben bis zum Ende der Störung in der Outbox gespeichert, auch wenn der Schadendienst in der Zwischenzeit neu startet.

Outbox: Freigabe und Nachricht gemeinsam sichern

Beteiligte: Schadendienst: gibt Fall 4711 zur Auszahlung frei; Datenbank des Schadendienstes: speichert Fallstatus und Outbox-Tabelle; Versandprozess: liest die Outbox und sendet neue Einträge; Nachrichtenplattform: nimmt Nachrichten an und stellt sie zu; Zahlungsdienst: bereitet die Auszahlung vor, erkennt Doppelte. Ablauf: 1. Der Schadendienst schreibt den Status „freigegeben“ für Fall 4711 und den Outbox-Eintrag e-5531 in derselben Datenbanktransaktion. 2. Die Datenbank des Schadendienstes bestätigt beide Änderungen gemeinsam. Eine Freigabe ohne vorgemerkte Nachricht kann es jetzt nicht mehr geben. 3. Der Versandprozess fragt die Outbox nach Einträgen, die noch nicht gesendet sind. 4. Die Datenbank des Schadendienstes liefert den Eintrag e-5531 mit dem Inhalt „Schadenfall 4711 freigegeben“. 5. Der Versandprozess will e-5531 an die Nachrichtenplattform übergeben, doch die Nachrichtenplattform ist zehn Minuten lang nicht erreichbar. Der Eintrag e-5531 bleibt ungesendet in der Outbox und geht nicht verloren. 6. Sobald die Nachrichtenplattform wieder erreichbar ist, versucht es der Versandprozess erneut und übergibt e-5531 an die Nachrichtenplattform. 7. Die Nachrichtenplattform bestätigt, dass sie die Nachricht sicher gespeichert hat. 8. Erst nach dieser Bestätigung markiert der Versandprozess den Eintrag e-5531 als gesendet. 9. Die Nachrichtenplattform stellt e-5531 dem Zahlungsdienst zu. 10. Der Zahlungsdienst prüft die Kennung e-5531. Kommt e-5531 später noch einmal, erkennt der Zahlungsdienst die Kennung wieder und bereitet keine zweite Auszahlung vor.

Wann ist es sinnvoll und wann nicht?

Das Outbox Pattern lohnt sich

  • Ein Dienst speichert eine Änderung in seiner Datenbank, und andere Dienste müssen zuverlässig davon erfahren. Ohne Outbox erfährt der Zahlungsdienst nie von der Freigabe, wenn der Schadendienst zwischen Speichern und Senden abstürzt. Mit Outbox bleibt die Nachricht in der Outbox gespeichert, bis der Versandprozess sie gesendet hat.
  • Ohne Outbox hilft es auch nicht, die Nachricht zuerst zu senden. Scheitert danach das Speichern, bereitet der Zahlungsdienst eine Auszahlung für einen Fall vor, der gar nicht freigegeben ist. In beiden Reihenfolgen passen die Daten der beiden Dienste dauerhaft nicht zusammen, wenn niemand sie später korrigiert. Im einen Fall fehlt zur Freigabe die vorbereitete Auszahlung, im anderen Fall fehlt zur vorbereiteten Auszahlung die Freigabe.
  • Die Dienste haben jeweils eine eigene Datenbank und arbeiten nur über Nachrichten zusammen. Eine gemeinsame Transaktion über Datenbank und Nachrichtenplattform steht nicht zur Verfügung oder soll nicht verwendet werden, weil der Schadendienst dann bei einer Störung der Nachrichtenplattform nichts mehr speichern kann.
  • Ein Dienst informiert andere Dienste regelmäßig über seine Änderungen. Beispiele sind neue Aufträge oder geänderte Adressen. Ohne Outbox kann bei jeder dieser Änderungen ein Absturz zwischen Speichern und Senden eine Nachricht verlieren. Mit Outbox verschickt derselbe Versandprozess alle diese Nachrichten.
  • Eine verlorene Nachricht hätte für Kunden spürbare Folgen. Erfährt der Zahlungsdienst nichts von der Freigabe, wartet der Kunde vergeblich auf sein Geld. Bei der Versicherung muss jede Freigabe am Ende zu genau einer Auszahlung führen. Mit Outbox geht die Nachricht nicht verloren, und der Zahlungsdienst erkennt doppelte Nachrichten an der Kennung.

Das Outbox Pattern ist nicht das Richtige

  • Eine verlorene Nachricht schadet nicht. Ein Beispiel ist die Zahl der offenen Fälle, die der Schadendienst jede Minute neu meldet. Geht eine dieser Meldungen verloren, ersetzt die Meldung der nächsten Minute sie. Dann sendet der Schadendienst die Nachricht selbst an die Nachrichtenplattform. Outbox-Tabelle und Versandprozess entfallen, und die Nachricht wartet nicht in der Outbox, bis der Versandprozess sie liest.
  • Der Dienst hat keine Datenbank, die Transaktionen unterstützt. Änderung und Outbox-Eintrag lassen sich dann nicht gemeinsam speichern. Stattdessen sendet der Dienst die Änderung zuerst als Nachricht an die Nachrichtenplattform und liest sie wie jeder andere Empfänger wieder von dort. Erst dann speichert der Dienst die Änderung bei sich. Scheitert das Speichern, liest der Dienst dieselbe Nachricht noch einmal, bis das Speichern gelingt. So gibt es keine gespeicherte Änderung ohne Nachricht und keine Nachricht, deren Änderung dauerhaft fehlt.
  • Der Sachbearbeiter, der einen Fall im Schadendienst freigibt, soll sofort sehen, ob der Zahlungsdienst die Auszahlung angenommen hat. Über die Outbox kommt keine Antwort des Zahlungsdienstes zurück, und die Nachricht erreicht den Zahlungsdienst erst, wenn der Versandprozess sie sendet. Dann ruft der Schadendienst den Zahlungsdienst direkt auf und speichert die Freigabe erst nach der Bestätigung des Zahlungsdienstes. Scheitert der Aufruf oder bricht der Schadendienst dabei ab, sieht der Sachbearbeiter sofort einen Fehler und gibt den Fall noch einmal frei. Der Zahlungsdienst erkennt den wiederholten Aufruf an der Fallnummer und bereitet keine zweite Auszahlung vor. Outbox-Tabelle und Versandprozess entfallen.
  • Datenbank und Nachrichtenplattform unterstützen beide eine gemeinsame Transaktion, und eine Verzögerung der Nachricht ist nicht hinnehmbar. Dann speichert der Schadendienst Freigabe und Nachricht mit einem Two Phase Commit: Datenbank und Nachrichtenplattform stimmen erst beide zu und bestätigen dann gemeinsam. Die Nachricht liegt damit sofort auf der Nachrichtenplattform, ohne Umweg über die Outbox. Der Two Phase Commit macht aber beide Systeme voneinander abhängig: Ist die Nachrichtenplattform gestört, kann der Schadendienst nichts speichern.
  • Der Schadendienst ist ein Altsystem, das nicht geändert werden darf. Der Schadendienst kann dann keinen Outbox-Eintrag schreiben. Stattdessen liest ein zusätzliches Programm die Änderungen aus dem Änderungsprotokoll der Datenbank des Schadendienstes und sendet sie an die Nachrichtenplattform. Dieses Vorgehen heißt Change Data Capture. Der Code des Schadendienstes bleibt dabei unverändert. Der Zahlungsdienst erhält dann aber geänderte Tabellenzeilen statt einer Nachricht wie „Schadenfall 4711 freigegeben“ und muss den Aufbau der Tabellen des Schadendienstes kennen.

Nachteile und typische Fehler

Nachrichten kommen verzögert beim Empfänger an. Die Verzögerung hängt davon ab, wie oft der Versandprozess die Outbox liest und wie lange Störungen der Nachrichtenplattform dauern. Der Versandprozess ist ein zusätzliches Programm, das überwacht werden muss. Fragt der Versandprozess die Outbox in kurzen Abständen ab, belastet er die Datenbank des Schadendienstes mit zusätzlichen Abfragen. Liest er stattdessen das Änderungsprotokoll mit, braucht die Datenbank dafür besondere Einstellungen und ein weiteres Programm wie Change Data Capture. Weil dieselbe Nachricht mehrfach gesendet werden kann, muss jeder Empfänger doppelte Nachrichten erkennen und dafür Kennungen speichern.

Typische Fehler und woran man sie erkennt:

  • Stillstehender Versandprozess: Steht der Versandprozess still, sammeln sich die Nachrichten in der Outbox, ohne dass es jemand bemerkt. Erkennbar ist das an einer wachsenden Zahl ungesendeter Einträge und an freigegebenen Fällen, zu denen der Zahlungsdienst keine Auszahlung vorbereitet hat.
  • Doppelte Auszahlung: Der Versandprozess sendet e-5531 nach einem Absturz ein zweites Mal, und der Zahlungsdienst prüft die Kennung nicht. Der Zahlungsdienst bereitet dann für Fall 4711 zwei Auszahlungen vor. Erkennbar ist das an mehreren Auszahlungen mit derselben Kennung e-5531.
  • Zu früh markiert: Der Versandprozess markiert den Eintrag als gesendet, sobald er das Senden gestartet hat, statt auf die Bestätigung der Nachrichtenplattform zu warten. Fällt die Nachrichtenplattform aus, bevor sie die Nachricht gespeichert hat, sendet der Versandprozess den Eintrag nie wieder, weil der Eintrag schon als gesendet markiert ist. Die Nachricht ist verloren. Erkennbar ist das an Einträgen, die als gesendet gelten, deren Nachricht aber nie beim Zahlungsdienst angekommen ist.
  • Getrennte Transaktionen: Der Schadendienst schreibt den Status und den Outbox-Eintrag in zwei getrennten Datenbanktransaktionen. Stürzt der Schadendienst nach der ersten Transaktion ab, ist Fall 4711 freigegeben, aber in der Outbox steht kein Eintrag, und der Zahlungsdienst erfährt nie von der Freigabe. Erkennbar ist das an freigegebenen Fällen ohne zugehörigen Outbox-Eintrag.
  • Wachsende Outbox: Niemand entfernt die gesendeten Einträge. Gibt der Schadendienst 2.000 Fälle am Tag frei, liegen nach einem Jahr rund 730.000 gesendete Einträge in der Outbox. Der Versandprozess fragt diese Tabelle in kurzen Abständen nach ungesendeten Einträgen ab, und jede Abfrage dauert mit der wachsenden Tabelle länger. Erkennbar ist das an wachsender Tabellengröße und steigender Verzögerung beim Versand.

Weiterführend

Verwandte Ansätze

  • Inbox Pattern: das Gegenstück beim Empfänger, der sich die Kennungen bereits verarbeiteter Nachrichten merkt.
  • Delivery Semantics: beschreibt, ob eine Nachricht höchstens einmal, mindestens einmal oder in der Wirkung genau einmal ankommt. Mit einer Outbox kommt eine Nachricht mindestens einmal an, sofern Versandprozess und Nachrichtenplattform wie beschrieben arbeiten.
  • Event Driven Architecture: eine Bauweise, in der Dienste über Nachrichten zu Ereignissen zusammenarbeiten und deshalb auf zuverlässig versendete Nachrichten angewiesen sind.

Werkzeuge und Umsetzungen

  • Debezium Outbox Event Router: liest neue Outbox-Einträge aus dem Änderungsprotokoll der Datenbank und veröffentlicht sie auf der Nachrichtenplattform.
  • Spring Modulith: Java-Bibliothek, die Ereignisse zusammen mit der fachlichen Änderung in der Datenbank vermerken kann. Laut der Dokumentation von Spring Modulith ist dieses eingebaute Verfahren aber keine vollständige Outbox. Für eine vollständige Outbox kann Spring Modulith den Versand an die Zusatzbibliothek Namastack Outbox übergeben, derzeit nur bei relationalen Datenbanken.
  • Eventuate Tram: Java-Rahmenwerk mit Outbox und Versandprozess.
  • Wolverine: Bibliothek für die Plattform .NET mit eingebauter Outbox.

Quellen

  • Chris Richardson: Pattern: Transactional outbox: beschreibt das Speichern der Nachricht in derselben Transaktion wie die fachliche Änderung, den getrennten Versandprozess mit Abfragen oder Mitlesen des Änderungsprotokolls und die Folge, dass Empfänger doppelte Nachrichten verkraften müssen.
  • Debezium: Outbox Event Router: Dokumentation eines Werkzeugs, das neue Outbox-Einträge aus dem Änderungsprotokoll liest und weiterleitet; die eindeutige Kennung jedes Eintrags lässt sich laut Dokumentation nutzen, um doppelte Nachrichten zu entfernen.