Versionierung von Nachrichten und Events

Dieses Kapitel konkretisiert das Entscheidungsmodell zur Versionierung für Nachrichten und Events.

Grundsätzlich gilt: Die Struktur von Nachrichten bzw. Events darf nur so geändert werden, dass Producer und Consumer weiterhin unabhängig voneinander betrieben und migriert werden können.

1. Zuordnung zum Entscheidungsmodell

Vor der Umsetzung ist jede Änderung als kompatibel oder inkompatibel einzustufen. Eine Änderung gilt für bestehende Kanäle (d.h. Queues und Topics) nur dann als kompatibel, wenn sie vollständig kompatibel ist, also Abwärts- und Aufwärtskompatibilität gegeben sind oder die betroffenen Producer und Consumer kontrolliert in einer abgestimmten Reihenfolge migriert werden.

Abwärts- und Aufwärtskompatibilität

In Literatur und Dokumentationen kommt es gelegentlich zu Verwechslungen zwischen Abwärts- und Aufwärtskompatibilität. Daher im Folgenden eine kurze Definition der beiden zur Abgrenzung im Kontext von Nachrichten und Events.

Ein Schema ist abwärtskompatibel, wenn bestehende Consumer unter Verwendung der alten Schema-Version in der Lage sind, Nachrichten/Events korrekt zu verarbeiten, die mit der neuen Schema-Version erzeugt wurden.
Vereinfacht formuliert: alte Consumer können neue Nachrichten lesen.

Ein Schema ist hingegen aufwärtskompatibel, wenn aktualisierte Consumer unter Verwendung der neuen Schema-Version in der Lage sind, Nachrichten/Events korrekt zu verarbeiten, die mit der alten Schema-Version erzeugt wurden.
Vereinfacht formuliert: neue Consumer können alte Nachrichten lesen.

Die Begriffe Abwärtskompatibilität und Rückwärtskompatibilität werden häufig synonym verwendet, ebenso wie Aufwärtskompatibilität und Vorwärtskompatibilität. Die IsyFact verwendet konsistent das Begriffspaar Abwärtskompatibilität und Aufwärtskompatibilität.

Die folgende Tabelle beschreibt die Muster knapp und verweist auf die entsprechenden Abschnitte, in denen die Muster ausführlich beschrieben werden.

Tabelle 1. Zuordnung der Entscheidungsdimensionen zu Mustern
Muster / Abschnitt Kurzbeschreibung

Kompatible Weiterentwicklung

Nachrichten / Events werden kompatibel auf demselben Kanal erweitert. Der Einsatz von Schema Evolution wird empfohlen.

Inkompatible Weiterentwicklung

Neue Typen der Nachrichten / Events werden auf einem neuen Kanal angeboten. Eine Kanalstrategie zur Migration wird empfohlen.

Die folgenden Beispiele verwenden die Nachricht bzw. das Event BestellungAufgegeben. Darin ist zum Beispiel das Feld artikelId als String definiert und verpflichtend in den daraus abgeleiteten Nachricht bzw. Events.

Listing 1. Schema für BestellungAufgegeben (Version 1)
{
  "type": "record",
  "name": "BestellungAufgegeben",
  "fields": [
    { "name": "artikelId", "type": "string" },
    { "name": "anzahl", "type": "int" }
  ]
}
Dieses und weitere Listings nutzen die Definitionssprache Avro™ wegen ihrer leichten Verständlichkeit. Avro™ ist ein Serialisierungsformat für Events, das Schema-Evolution unterstützt und über zahlreiche Implementierungen verfügt.

2. Kompatible Weiterentwicklung

Vollständig kompatible Änderungen können auf demselben Topic, derselben Queue oder demselben Nachrichten- oder Event-Typ umgesetzt werden. Hierfür empfiehlt sich die Verwendung von Schema Evolution. Fehlerhafte Schema Evolution kann zu Fehlern bei der Deserialisierung, blockierten Kanälen, fehlerhaften Replays oder stillen fachlichen Fehlinterpretationen führen. Das relevanteste Risiko ist nicht ein offensichtliches Systemversagen, sondern "stille semantische Fehler". Der Consumer verarbeitet eine formal gültige Nachricht, interpretiert sie aber fachlich falsch. Die fachliche Kompatibilität ist daher zusätzlich zur technischen Schema-Kompatibilität zu prüfen.

3. Inkompatible Weiterentwicklung

Inkompatible Änderungen, also Breaking Changes, dürfen nicht durch eine Änderung des bestehenden Nachrichtenformats auf demselben Kanal eingeführt werden. Die folgenden Änderungen sind typische Breaking Changes:

Tabelle 2. Typische Breaking Changes bei Nachrichten und Events
Änderung Erläuterung

Pflichtfeld hinzufügen

Alte Nachrichten und alte Producer enthalten es nicht.

Pflichtfeld entfernen

Bestehende Consumer können neue Nachrichten nicht mehr korrekt verarbeiten, wenn sie das Feld fachlich oder technisch voraussetzen.

Feld umbenennen

Entspricht technisch dem Entfernen des alten Feldes und dem Hinzufügen eines neuen Feldes. Bestehende Consumer finden das erwartete Feld nicht mehr.

Feldtyp ändern

Consumer erwarten den alten Typ.

Semantik eines Feldes ändern

Technisch gleich, fachlich anders.

Einheit ändern

Beispiel: amount von Euro zu Cent.

Enum-Wert entfernen

Consumer können alte Werte nicht mehr interpretieren.

Enum-Wert hinzufügen

Bestehende Consumer können den neuen Wert nur dann sicher verarbeiten, wenn sie unbekannte Enum-Werte explizit behandeln. Andernfalls ist die Änderung nicht kompatibel.

Bedeutung eines Events ändern

Consumer reagieren fachlich falsch.

Kardinalität ändern

Beispiel: Id wird zu Ids.

Struktur verschachteln oder abflachen

Feldpfade ändern sich.

Reihenfolge/Key-Semantik ändern

Besonders kritisch bei Kafka-Partitionierung.

Standardmäßig ist hierfür ein neuer Event-Typ, ein neuer Routing-Key oder ein neuer Kanal vorzusehen. Es liegt ein Schemabruch vor. Inkompatible Änderungen sollten durch ein Schema Management kontrolliert vorgenommen werden. Im Fall von Events sind deren Besonderheiten zu beachten. Wird ein neuer Kanal eingeführt, so sollte ein Vorgehen für die Migration festgelegt werden.

4. Schema Management

Schemata ändern sich fortlaufend mit neuen Anforderungen. Um Wildwuchs von Versionen zu vermeiden, müssen sie an einer zentralen Stelle verwaltet werden. Dies kann mithilfe eines Wikis oder Git-Repositories geschehen. In der Praxis hat sich jedoch der Einsatz einer Schema Registry bewährt.

Der große Vorteil einer Schema Registry ist die Erkennung von Schemabrüchen während der Schema-Validierung. Damit sind Breaking Changes durch Anpassungen am Schema praktisch ausgeschlossen, weil nur kompatible Änderungen zugelassen werden. Eine Schema Registry ist vor allem dann sinnvoll, sobald eine verteilte Entwicklung stattfindet oder mehr als ein Consumer an der Kommunikation beteiligt ist.

Die Vorteile von Schema Registries erkauft man sich u.a. durch die gestiegene Komplexität und Aufwände für den Betrieb eines zusätzlichen Systems in der Anwendungslandschaft.

Zur weiteren Vertiefung, und um die Vorteile und Herausforderungen von Schema Registries zu verstehen, können folgende Produkte als Beispiele herangezogen werden: Apicurio Registry (Open Source) und Confluent Schema Registry (kommerziell).

4.1. Schema Evolution

In den meisten Fällen handelt es sich bei den Änderungen an Nachrichten- und Event-Typen um abwärtskompatible Änderungen. Kommt beispielsweise das optionale Feld kommentar zur Version 1 des Typs BestellungAufgegeben hinzu, entsteht eine neue Version, aber kein neuer Typ. Eine fortschreitende Versionierung von Typen, die im Rahmen der Kompatibilität bleibt, wird als Schema Evolution bezeichnet.

Beispiel 1. Schema Evolution bei kompatiblen Änderungen

Alte Consumer verwenden das Schema in der Version 1 beim Lesen von Nachrichten/Events. Nachrichten/Events, welche mit der Version 2 erzeugt wurden, enthalten zusätzlich das Feld kommentar. Da die alten Consumer dieses nicht kennen, wird es einfach ignoriert und die Verarbeitung geschieht weiterhin ohne Fehler.

In Nachrichten, die mit der alten Schema-Version erzeugt wurden, fehlt das Feld kommentar. Das ist für neue Consumer kein Problem, da es optional ist und mit dem Wert null belegt wird.

Listing 2. Schema für BestellungAufgegeben mit optionalem Kommentarfeld (Version 2)
{
  "type": "record",
  "name": "BestellungAufgegeben",
  "fields": [
    { "name": "artikelId", "type": "string" },
    { "name": "anzahl", "type": "int" },
    { "name": "kommentar", "type": ["null" ,"string"], "default": null }
  ]
}

4.2. Schemabrüche

In Fällen, bei denen Änderungen zum Bruch der Kompatibilität führen (Breaking Change) reicht eine neue Schema-Version nicht mehr aus. Es liegt ein Schemabruch vor. Als Folge muss ein neues Schema für einen neuen Nachrichten- oder Event-Typ erstellt werden.

Beispiel 2. Schemabruch bei inkompatiblen Änderungen

Im Beispiel BestellungAufgegeben kommt ein neues Pflichtfeld preis hinzu. Bei fortschreitender Versionierung könnten neue Consumer neue Nachrichten zwar lesen, aber bei alten Nachrichten würde der Preis fehlen und dem Consumer fehlt diese Information. Deswegen gibt es ein neues Schema für den Event-Typ BestellungMitPreisAufgegeben.

Listing 3. Schema für BestellungMitPreisAufgegeben (Version 1)
{
  "type": "record",
  "name": "BestellungMitPreisAufgegeben",
  "fields": [
    { "name": "artikelId", "type": "string" },
    { "name": "anzahl", "type": "int" },
    { "name": "preis", "type": "float" }
  ]
}
Im Falle von Breaking Changes wird dringend empfohlen, für das neue Schema auf generische Namen wie BestellungAufgegebenV1 zu verzichten, um eine Vermischung von verschiedenen Versionskonzepten zu vermeiden, insbesondere wenn fortschreitende Versionen manuell z.B. über Header gesetzt werden.

5. Versionierung von Events

Die Versionierung von Events unterliegt besonderen Anforderungen. Die Grundlagen von Event Streaming sind im entsprechenden Kommunikationsmuster umrissen.

Events werden genutzt, um über geschäftsrelevante Ereignisse innerhalb der Anwendungslandschaft zu informieren. Aus der Sicht der konsumierenden IT-Systeme sind Events wichtige historische Ereignisse (Fakten), die für die Weiterverarbeitung benötigten Daten enthalten.

Ihre Semantik wird bereits zu Beginn des Entwicklungsprozesses beschrieben (beim Domain Driven Design zum Beispiel im Rahmen von Event Storming). Es ist wichtig, diese auch in der Dokumentation für alle Beteiligten verbindlich festzuhalten (Design By Contract).

IT-Systeme tauschen Events in einem wohldefinierten Format untereinander aus. Wie die Struktur einer Nachricht konkret aufgebaut ist (Syntax), wird in einem Schema festgehalten. Das vereinbarte Schema wird von Producern verwendet, um vor dem Versenden die Nachricht zu validieren. Der Consumer verwendet es für die Validierung der empfangenen Nachricht (Design by Contract).

Die Verwendung von Schemata stellt die korrekte Kommunikation zwischen den Teilnehmern und damit die Funktion insbesondere von Consumern sicher.

5.1. Event Replay

In Event-basierten Systemen werden Nachrichten in einem Event-Log in der Regel für einen längeren Zeitraum persistiert. Daher müssen Consumer im Vergleich zu Queue-basierten Systemen in der Lage sein, auch alte Nachrichten zu lesen, wenn sie wieder eingespielt werden (Replay), etwa beim Eintritt in eine neue Consumer-Group oder Zurücksetzen des Eventstroms (Offset Reset).

Neue Versionen von Events müssen so aufgebaut sein, dass weiterhin ein Replay von alten Events möglich ist. Ein Event muss dazu in sich abgeschlossen und vollständig bleiben und es darf nicht von der externen Systemumgebung abhängen. So wird sichergestellt, dass durch Replay keine fachlichen Inkonsistenzen entstehen.

Bei einer erneuten Nachrichtenzustellung muss die Verarbeitungslogik der neuen Consumer idempotent sein. Sie müssen Duplikate anhand einer eindeutigen Event-ID erkennen und ignorieren, um Mehrfachverarbeitungen zu vermeiden.

6. Kanalstrategien bei inkompatiblen Änderungen

Bei inkompatiblen Änderungen an der Struktur von Nachrichten können bestehende oder neue Consumer alte Nachrichten nicht mehr korrekt verarbeiten. Um Stabilität und Datenkonsistenz zu gewährleisten, wird ein neues Topic für inkompatible Schemaänderungen erstellt. Im nächsten Schritt können Producer und Consumer auf das neue Topic migriert werden.

6.1. Versionierung von Queues/Topics

Für die Benennung von Topic-Versionen hat sich ein Muster etabliert, das auch bei REST-APIs für Breaking Changes verwendet wird.

Listing 4. Beispielhafte Benennung von versionierten Topics
bestellung.erstellt.v1
bestellung.erstellt.v2
...

Eine Vergabe von Versionen nach dem Semantic Versioning sollte vermieden werden, da sie zu einer Vermischung von API- und Datenstromsemantik führt. Beispielsweise würde die Benennung eines Topics mit bestellung.erstellt.v1.2 eine kompatible Änderung suggerieren, ein neues Topic wird jedoch ausschließlich bei inkompatiblen Änderungen eingeführt. Versionen für kompatible Änderungen werden im Rahmen von Schema Evolution durch die Schema Registry verwaltet.

Wenn beispielsweise im Schema für Bestellungen der Datentyp des Feldes artikelId geändert wird, entsteht ein neues Schema mit einem Breaking Change. Mit der Verwendung des neuen Schemas befinden sich die alten Nachrichten weiterhin im alten Topic bestellung.erstellt.v1 und die mit dem neuen Schema erzeugten Nachrichten im Topic bestellung.erstellt.v2.

Listing 5. Schema BestellungIdLongErstellt mit geändertem Feldtyp (Version 1)
{
  "type": "record",
  "name": "BestellungIdLongErstellt",
  "fields": [
    { "name": "artikelId", "type": "long" },
    { "name": "anzahl", "type": "int" }
  ]
}

6.2. Migration von Queues/Topics

Die Strategie zur Migration von Topics hängt vom konkreten Anwendungsfall und den jeweiligen Anforderungen ab:

  1. Was passiert mit bestehenden Events - können diese gelöscht werden oder werden sie noch für spätere Analysen gebraucht, gegebenenfalls im neuen Format?

  2. Sind verlorene oder fehlerhaft verarbeitete Nachrichten während der Migration hinnehmbar?

  3. Darf der laufende Betrieb des Systems während der Wartung unterbrochen werden?

  4. Soll die Zustellreihenfolge der Nachrichten erhalten bleiben?

  5. Dürfen Nachrichten mehrfach verarbeitet werden (Duplikate)?

6.2.1. Einfache Migration (Big-Bang, mit Wartungsfenster)

Wenn eine kurzzeitige Unterbrechung des Betriebs akzeptabel ist (z.B. weil nachts wenige Anträge eingehen), kann die vollständige Migration während eines definierten Wartungsfensters in einem einzigen Schritt erfolgen, sofern sie mit den beteiligten Entwicklerteams und Abteilungen abgestimmt ist.

Während einer Wartungszeit werden bestehende Producer zuerst eingestellt, sodass keine Nachrichten mehr in das alte Topic geschrieben werden. Anschließend arbeiten die bestehenden Consumer alle noch im System befindlichen Nachrichten im alten Topic vollständig ab. Sobald alle Nachrichten im alten Topic vollständig verarbeitet sind, werden die neuen Producer und Consumer gestartet und arbeiten ausschließlich mit dem neuen Topic.

Der Ablauf gliedert sich in die folgenden fünf Phasen.

Vorbereitung
  1. Neues Topic antrag.v2 wird angelegt.

  2. Neue Producer und Consumer werden erstellt.

  3. Wartungsfenster wird aktiviert.

Einfrieren der bestehenden Producer
  1. Alte Producer werden eingestellt, damit der Datenbestand nicht weiter befüllt wird.

Entleeren des bestehenden Topics
  1. Bestehende Consumer arbeiten verbleibende Nachrichten in antrag.v1 vollständig ab.

Umstellung auf das neue Topic
  1. Neue Producer und Consumer werden gestartet und arbeiten ausschließlich mit dem neuen Topic antrag.v2.

  2. Nach einer Stabilitätsphase geht das System wieder in Betrieb.

Dekommissionierung des alten Topics
  1. Topic antrag.v1 kann gelöscht oder archiviert werden.

Die Abbildung Big Bang Migration eines Topics veranschaulicht die wesentlichen Schritte der einfachen Migration mit Wartungsfenster.

big bang topic migration.dn
Abbildung 1. Big Bang Migration eines Topics

6.2.2. Migration nach dem Strangler Pattern (schrittweise, im laufenden Betrieb)

In kritischen Systemen sind Ausfälle und Datenverluste inakzeptabel. Hier ist eine Strategie erforderlich, die eine Migration während des laufenden Betriebs ermöglicht, ohne bestehende Funktionalität zu beeinträchtigen. Dies ist in der Praxis der empfohlene Weg.

Das Strangler Pattern dient dazu, ein bestehendes System schrittweise durch ein neues zu ersetzen. Dabei werden neue Komponenten parallel zum Bestandssystem aufgebaut, die inkrementell dessen Funktionalität übernehmen, bis das Bestandssystem abgelöst werden kann.

Dieses Prinzip lässt sich auf Topics/Queues mit inkompatiblen Änderungen übertragen.

Der Ablauf gliedert sich in die folgenden fünf Phasen.

Vorbereitung

Zu Beginn wird ein neues Topic mit dem aktualisierten Schema bereitgestellt.

Parallelisierung des bestehenden Datenstroms

Anschließend wird der alte Datenstrom vollständig in das neue Topic transformiert und repliziert. Dies erfolgt über eine Transformationskomponente, die Nachrichten aus dem alten Topic liest, in das neue Format überführt und in das neue Topic schreibt. Technisch kann dies beispielsweise durch ein Consumer-/Producer-Paar oder mittels Kafka Streams umgesetzt werden.

Schrittweise Umstellung der Consumer

Zeitgleich werden neue Consumer schrittweise auf das neue Topic umgestellt. Voraussetzung hierfür ist, dass sowohl alte Daten als auch laufende Events im neuen Topic vollständig verfügbar sind. Während der gesamten Übergangsphase existieren altes und neues Topic parallel, wobei der vollständige Datenbestand in beiden Systemen vorliegt (Replikation).

Umstellung der Producer

Sobald alle Consumer erfolgreich auf das neue Topic migriert sind, werden die Producer auf das neue Topic umgestellt. Die Umstellung der Producer muss atomar erfolgen, um Duplikate zu vermeiden.

Dekommissionierung des alten Topics

Nach einer Stabilitätsphase kann das alte Topic außer Betrieb genommen, gelöscht oder archiviert werden. Die Transformationskomponente wird anschließend ebenfalls entfernt.

Die Abbildung Migration eines Topics nach dem Strangler Pattern veranschaulicht nochmal die wichtigen Phasen der Migration nach dem Strangler Pattern.

strangler topic migration.dn
Abbildung 2. Migration eines Topics nach dem Strangler Pattern

Dieses Vorgehen ermöglicht eine Migration mit minimalem Risiko, ohne den laufenden Betrieb zu stören und mit kontrollierter Umstellung aller beteiligten Systeme. Es ist jedoch zu berücksichtigen, dass durch den Parallelbetrieb von neuem und altem Topic und der zusätzlichen Komponente Transformator Komplexität der Infrastruktur und Systemlast ansteigen. Änderungen sind daher mit den Teams für Betrieb und Monitoring abzustimmen.