Versionierung
Die Notwendigkeit, Services in mehreren Versionen anbieten zu können, ist bedingt durch die Vielzahl an Service-Nutzern, die bei Änderung an einem Service nicht alle zeitgleich auf die neue Version eines Service umschalten können. Daher ist es notwendig, dass in einem, wenn möglich, kurz zu haltenden Übergangszeitraum mehrere Versionen eines Service parallel betrieben werden können.
Die Versionierung wird auf der Ebene von Services, nicht Service-Operationen ausgeführt, da diese Ebene von ihrer Granularität zu den üblichen fachlichen Änderungen passt.
Es kann vorkommen, dass in einem Systemrelease neue Versionen von mehreren Services ausgeliefert werden.
| Eine Version beschreibt die evolutionäre Weiterentwicklung desselben fachlichen Konzepts. Wird ein anderes fachliches Konzept umgesetzt, ist keine Versionierung, sondern eine neue Schnittstelle erforderlich. |
1. Abwärtskompatibilität
Als Abwärtskompatibilität wird die Verwendbarkeit bzw. Kompatibilität neuerer oder erweiterter Versionen eines Services bzw. einer Nachricht oder eines Events zu den Anwendungsbedingungen einer früheren Version bezeichnet. Diese liegt vor, wenn bestehende Kommunikationsteilnehmer eine neue Version eines Services, einer Nachricht oder eines Events ohne Anpassung ihres Codes oder ihrer Konfiguration weiterhin korrekt nutzen können
| Diese Definition basiert auf dem Wikipedia-Artikel zur Abwärtskompatibilität. |
Abwärtskompatibilität umfasst sowohl technische als auch fachliche Aspekte. Technische Kompatibilität liegt vor, wenn die Struktur und Datentypen weiterhin verarbeitet werden können. Fachliche Kompatibilität liegt vor, wenn die Semantik der Daten unverändert bleibt. Eine Änderung kann technisch kompatibel, aber fachlich inkompatibel sein und stellt in diesem Fall einen Breaking Change dar. Abwärtskompatibilität ermöglicht Übergangszeiträume, in denen sich die Kommunikationsteilnehmer schrittweise an die Änderungen anpassen können, ohne dass es zu Unterbrechungen oder Fehlern in der Kommunikation kommt.
Die Verantwortung für Änderungen und damit für die Abwärtskompatibilität liegt in der Regel beim Provider von Services bzw. beim Producer/Publisher von Nachrichten und Events, da diese die Schnittstelle bzw. das Format der Nachrichten und Events definieren und somit für die Consumer festlegen. Ausnahmen bilden Szenarien, in denen ein Consumer viele Producer/Publisher orchestriert und aus diesem Grund das Format der Nachrichten oder Events vorgibt, zum Beispiel im Bereich Telemetrie oder Protokollierung.
Inkompatible Änderungen (englisch: breaking changes) sind dagegen Änderungen an der Schnittstelle eines Services bzw. am Format einer Nachricht oder eines Events, welche die Anwendungsbedingungen einer früheren Version verletzen. Sie führen zu Fehlern oder Unterbrechungen in der Kommunikation, wenn die Kommunikationsteilnehmer nicht gleichzeitig auf die neue Version wechseln können und müssen daher genau geplant und kommuniziert werden.
1.1. Abwärtskompatible Änderungen
Für die Gewährleistung von Abwärtskompatibilität wird ein tolerantes Verhalten der Kommunikationsteilnehmer vorausgesetzt:
-
Consumer müssen unbekannte Felder ignorieren können.
-
Producer dürfen bestehende Felder nicht entfernen oder in ihrer Bedeutung verändern.
Bei asynchroner Kommunikation ist Abwärtskompatibilität dann gegeben, wenn bestehende Consumer Nachrichten oder Events weiterhin verarbeiten können, ohne dass Fehlverhalten entsteht.
Diese Grundsätze gelten auch für synchrone Kommunikation. Bei asynchroner Kommunikation ist ein tolerantes Verhalten der Consumer zwingend erforderlich, da Producer und Consumer zeitlich und technisch entkoppelt sind.
Bei synchroner Kommunikation wird Abwärtskompatibilität dadurch erreicht, dass bestehende Service-Nutzende neue Versionen ohne Anpassung weiterhin korrekt nutzen können. Dies setzt insbesondere voraus, dass zusätzliche Felder die Verarbeitung bestehender Clients nicht beeinträchtigen. Die Schnittstelle muss so gestaltet sein, dass bestehende Clients weiterhin funktionieren.
Die folgenden Beispiele stellen häufig vorkommende, abwärtskompatible Änderungen dar.
| Änderungstyp | Auswirkung auf die Kommunikation |
|---|---|
Optionales Feld hinzufügen |
Die Kommunikationsteilnehmer können das Feld bei Bedarf setzen bzw. berücksichtigen. |
Neues Feld mit Standardwert hinzufügen |
Die Kommunikationsteilnehmer können das Feld bei Bedarf mit einem eigenen Wert belegen bzw. berücksichtigen. |
Optionale neue Operation / Nachricht / Event hinzufügen |
Die Kommunikationsteilnehmer können die Operation bei Bedarf aufrufen, bzw. die Nachricht oder das Event produzieren oder konsumieren. |
Zusätzliche Response-Felder |
Die Kommunikationsteilnehmer können zusätzliche Felder ignorieren, sofern sie diese nicht auswerten. |
1.2. Inkompatible Änderungen
Die folgenden Beispiele stellen häufig vorkommende, inkompatible Änderungen (Breaking Changes) dar.
| Änderungstyp | Auswirkung auf die Kommunikation |
|---|---|
Neues Pflichtfeld hinzufügen |
Die Kommunikationsteilnehmer müssen das neue Feld verwenden. Eine Übergangsphase mit einer frühzeitigen Einführung des Feldes als optional kann die Auswirkungen dieses Breaking Changes mindern. |
Feld umbenennen / entfernen |
Die Kommunikationsteilnehmer können Services nicht mehr nutzen oder keine Nachrichten / Event mehr produzieren oder konsumieren. Flexible Validierungen und Verarbeitungslogiken können die Auswirkungen eines solchen Breaking Changes mindern. |
Typ oder Semantik eines Felds ändern |
Werte werden von den Kommunikationsteilnehmern falsch interpretiert oder führen zu Konvertierungsfehlern, auch wenn die Struktur gleich bleibt. Diese Änderungen sollten vermieden werden, da sie schwer zu kommunizieren und zu behandeln sind. Stattdessen sollte ein neues Feld eingeführt und das alte Feld zu einem späteren Zeitpunkt entfernt werden. |
Änderung des Fehlerverhaltens oder von Statuscodes |
Die Kommunikationsteilnehmer können auf unerwartete Fehler reagieren oder bestehende Fehlerbehandlungen greifen nicht mehr. |
2. Entscheidungsmodell zur Versionierung
Aufbauend auf der Definition der Abwärtskompatibilität legt die Referenzarchitektur ein verbindliches Entscheidungsmodell für die Versionierung von Services, Nachrichten und Events fest.
Das Entscheidungsmodell beantwortet, wann
-
eine bestehende Schnittstelle erweitert werden sollte und wann eine neue Version bereitzustellen ist,
-
ein neuer Kanal erforderlich ist und
-
ein neues Backend sinnvoll ist.
Die Entscheidung über die Notwendigkeit einer neuen Version ist von ihrer technischen Umsetzung zu trennen. Eine neue Service-Version bedeutet nicht zwingend eine neue Serviceschnittstelle oder ein neues Backend. Die Einführung einer neuen Service-Version erzwingt also keine bestimmte technische Realisierungsform. Sie kann beispielsweise innerhalb derselben Serviceschicht und desselben Backends umgesetzt werden. In der Regel wird eine neue Service-Version als eigene Serviceschnittstelle innerhalb derselben Serviceschicht realisiert.
2.1. Entscheidungsregeln
2.1.1. Abwärtskompatible Änderungen
Ist eine Änderung abwärtskompatibel, so wird die bestehende Schnittstelle erweitert. Dies bedeutet:
-
keine neue Service-Version,
-
kein neuer Kanal,
-
kein neues Backend.
Abwärtskompatible Änderungen sollen innerhalb derselben Version weiterentwickelt werden, um unnötige Parallelität und Versionsvielfalt zu vermeiden.
2.1.2. Nicht abwärtskompatible Änderungen an synchronen Services
Ist eine Änderung an einem synchronen Service nicht abwärtskompatibel, so ist eine neue Service-Version bereitzustellen. Die bestehende Version bleibt für einen Übergangszeitraum parallel verfügbar, sofern bestehende Service-Nutzende nicht zeitgleich migriert werden können.
2.1.3. Nicht abwärtskompatible Änderungen an Nachrichten und Events
Ist eine Änderung an einer Nachricht oder einem Event nicht abwärtskompatibel, so ist eine neue Version des Formats bereitzustellen. Können bestehende Consumer mehrere Versionen nicht sicher parallel verarbeiten, ist zusätzlich ein neuer Kanal vorzusehen, zum Beispiel eine eigene Queue, ein eigenes Topic oder ein eigener Event-Typ. Dieser Kanal ist dann für die neue Version der Consumer vorgesehen. Dies bedeutet: Wenn bestehende Consumer eine geänderte Nachricht oder ein Event nicht mehr korrekt verarbeiten können, dann darf das bestehende Format nicht einfach verändert werden, vielmehr muss ein neues, getrenntes Nachrichtenformat definiert werden.
2.1.4. Neues Backend
Ein neues Backend ist nicht die Standardmaßnahme bei Versionierung. Es ist dann sinnvoll, wenn
-
sich die fachliche Verarbeitung zwischen den Versionen nicht mehr mit vertretbarem Aufwand gemeinsam abbilden lässt,
-
mehrere inkompatible Versionen langfristig parallel betrieben werden müssen oder
-
nicht-funktionale Anforderungen eine getrennte Bereitstellung erfordern.
In allen anderen Fällen sollen mehrere Versionen durch dasselbe Backend unterstützt werden.
2.2. Entscheidungsraster
| Kommunikationsmuster | Änderungstyp | Empfohlene Maßnahme |
|---|---|---|
Synchroner Service |
abwärtskompatibel |
Bestehende Schnittstelle erweitern |
Synchroner Service |
nicht abwärtskompatibel |
Neue Service-Version bereitstellen (In der Regel über eine zusätzliche Service-Schnittstelle realisiert.) |
Nachricht/ Event |
abwärtskompatibel |
Bestehendes Nachrichtenformat/ Event erweitern |
Nachricht/ Event |
nicht abwärtskompatibel |
Neue Version des Event-/ Nachrichtenformats bereitstellen; bei fehlender Parallelverarbeitbarkeit zusätzlicher neuer Kanal |
2.3. Einordnung in die Referenzarchitektur
Gemäß der Referenzarchitektur der IsyFact werden Versionierungsentscheidungen für synchrone Services in der Regel innerhalb einer Serviceschicht umgesetzt.
Daraus folgen für synchrone Services die folgenden Grundsätze:
-
Eine neue Service-Version wird in der Regel als eigene Service-Schnittstelle in der Serviceschicht umgesetzt.
-
Verschiedene Service-Versionen verwenden denselben Anwendungskern.
-
Versionsspezifische Transformationen erfolgen grundsätzlich in der Service-Schnittstelle des Backends.
-
Ein Service-Gateway übernimmt keine Fachlogik zur Behandlung unterschiedlicher Versionen.
3. Versionierung synchroner Services
Dieses Kapitel konkretisiert das im Entscheidungsmodell zur Versionierung definierte Modell für synchrone Services.
Bei synchronen Services wird die veröffentlichte Schnittstelle eines Services versioniert. Die fachliche Identität des Services bleibt dabei erhalten. Eine neue Version ist nur dann eine Version desselben Services, wenn weiterhin dasselbe fachliche Konzept bereitgestellt wird. Eine neue Schnittstellenversion erfordert kein neues Backend. Im Regelfall werden mehrere Schnittstellenversionen durch dasselbe Backend bereitgestellt. Ein neues Backend ist nur dann vorzusehen, wenn fachliche Verantwortung, Datenhoheit oder Betriebsanforderungen ein eigenes IT-System erfordern.
3.1. Zuordnung zum Entscheidungsmodell
Die Auswahl eines Musters erfolgt auf Basis des Entscheidungsmodell zur Versionierung:
Diese Zuordnung ist im Systementwurf zu dokumentieren und zu begründen.
3.2. Zuordnung der Entscheidungsdimensionen zu Mustern
| Muster | Konsequenz | Kurzbeschreibung |
|---|---|---|
Kompatible Weiterentwicklung |
Kompatible Weiterentwicklung bestehender Schnittstelle im bestehenden Backend |
Die bestehende Service-Schnittstelle wird kompatibel erweitert. Keine Parallelversion notwendig. |
Fachliche Schnittstellenerweiterung |
Neue fachliche Schnittstelle im bestehenden Backend |
Es entsteht eine neue fachliche Verantwortung, der bestehende Anwendungskern kann aber weiterverwendet werden. Keine Versionierung einer bestehenden Schnittstelle, sondern Ergänzung einer Neuen. |
Fachliche Entkopplung in eigenem Backend |
Neues Backend mit fachlicher Schnittstelle |
Es entsteht eine neue fachliche Verantwortung, die nicht mehr durch den bestehenden Anwendungskern abgebildet werden kann. |
Schnittstellenversionierung mit gemeinsamem Anwendungskern |
Neue versionierte Service-Schnittstelle im bestehenden Backend (DTOs und Mapper je Version) |
Die fachliche Verantwortung bleibt gleich, der bestehende Anwendungskern ist weiter nutzbar, aber die technische Schnittstelle ändert sich jedoch inkompatibel. |
Implementierungsversionierung im gemeinsamen Backend |
Versionierte Service-Implementierung im bestehenden Backend mit getrennten Anwendungskomponenten |
Die fachliche Verantwortung bleibt gleich, aber eine sich unterscheidende fachliche Kernlogik muss parallel betrieben werden. |
Vollständig entkoppelte Service-Version |
Neue versionierte Service-Schnittstelle in neuem Backend |
Die fachliche Verantwortung bleibt gleich, aber Datenhoheit, Lifecycle oder Betrieb erfordern vollständige Entkopplung. |
3.3. Kompatible Weiterentwicklung eines Services
Ein Beispiel für die hier behandelte Änderungen ist das Hinzufügen eines optionalen Attributs zu einer bestehenden Operation.
Abwärtskompatible Änderungen werden innerhalb derselben Service-Version umgesetzt. Die bestehende Service-Schnittstelle wird erweitert, ohne bestehende Nutzende zu beeinträchtigen. Dies ist möglich, wenn die Änderungen vollständig abwärtskompatibel sind, die Consumer unbekannte Fehler ignorieren können und keine Änderung der fachlichen Semantik bei bestehenden Feldern vorgenommen wurde.
Hierfür ist die bestehende Service-Schnittstelle zu erweitern. Für neue Felder sind in der Serviceschicht Defaultwerte einzutragen.
Mit diesem Vorgehen ist kein Parallelbetrieb erforderlich. Dadurch ergibt sich minimaler Betriebsaufwand und keine zusätzliche Komplexität. Die Änderungsfreiheit ist jedoch eingeschränkt.
3.4. Neue versionierte Service-Schnittstelle im gemeinsamen Backend
Ein Beispiel für die hier behandelte Änderungen ist die Aufspaltung eines Feldes in mehrere Felder bei gleichbleibender fachlicher Logik. Das Backend stellt mehrere Versionen der veröffentlichten Service-Schnittstelle bereit. Hierfür ist eine zusätzliche Service-Schnittstelle in der Serviceschicht vorzusehen. Versionsspezifische Unterschiede sind in der Serviceschicht zu behandeln. Dazu gehören versionierte Schnittstellenendpunkte, versionierte Request- und Response-DTOs, Mapper zwischen Schnittstellenmodell und internem Modell und Defaultwerte sowie technische Adaptierungen. Mehrere Versionen werden parallel betrieben und nutzen denselben Anwendungskern.
Dies ist das angemessene Vorgehen für Änderung, die nicht abwärtskompatibel sind, aber die fachliche Logik gleich bleibt. Ein Mapping zwischen Versionen ist möglich.
Mit diesem Vorgehen wird eine kontrollierte Migration möglich. Eine Duplikation der Fachlogik ist nicht notwendig und der Infrastrukturaufwand bleibt gering. Die Komplexität in der Serviceschicht ist aber höher da eine Mapping-Logik notwendig ist. Zu beachten ist, dass bei vielen Versionen die Wartungskosten steigen.
3.5. Versionierte Service-Implementierung im bestehenden Backend
Ein Beispiel für die hier behandelten Anpassungen ist eine unterschiedliche fachliche Validierungslogik zwischen Versionen. Die Änderung ist nicht abwärtskompatibel oder die fachliche Logik beginnt zu divergieren damit ist die vollständige Wiederverwendung nicht mehr sinnvoll. Das Backend bleibt dieselbe Systemeinheit. Innerhalb des Backends werden jedoch getrennte Implementierungspfade für die Versionen geführt. Ein gemeinsamer Anwendungskern ist nur zulässig, solange die Versionen dieselbe fachliche Semantik abbildet. Versionsspezifische Fachlogik darf nicht in der Service-Schnittstelle implementiert werden. Sie ist in getrennten Anwendungskomponenten oder getrennten Implementierungspfaden des Anwendungskerns zu kapseln. Die Service-Schicht vermittelt nur zur jeweils passenden Implementierung. Eine angemessene Wartbarkeit bei wachsender Divergenz wird durch die Nutzung möglicher gemeinsame Infrastrukturkomponenten erreicht und die Komplexität pro Version verringert. Die erhöhte Komplexität im Backend und eine teilweise Duplikation von Logik, was sich in steigendem Testaufwand niederschlägt, ist bei diesem Vorgehen als negativ zu betrachten. Bei zunehmender fachlicher Divergenz ist die Minimierung horizontaler Komplexität wichtiger als die Vermeidung technischer Redundanz.
3.5.1. Auswirkungen
-
Deployment: ein Deployment mit differenzierter Logik
-
Betrieb: gemeinsame Laufzeit, Monitoring jeder Version notwendig
-
Komplexität: mittel bis hoch
| Das Muster "Versionierte Service-Implementierung im bestehenden Backend" darf nicht zu versionsabhängigen Verzweigungen im Anwendungskern führen. |
Zulässig sind getrennte Implementierungsklassen, getrennte Anwendungskomponenten, explizite Auswahl der Implementierung durch die Service-Schnittstelle und eine gemeinsame Nutzung technischer Infrastruktur.
Nicht zulässig sind weder verstreute if/else-Logik nach Version, fachliche Regeln in DTO-Mappern, langfristige Kopplung divergierender Fachmodelle noch implizite Unterscheidung über optionale Felder.
3.6. Neue Version als eigenes Backend
Beispiel: Technologiewechsel oder grundlegende fachliche Neuausrichtung eines Services.
Eine neue Service-Version wird als eigenständiges Backend realisiert. Dies ist nur bei hoher fachlicher oder technischer Divergenz sinnvoll, insbesondere wenn ein langfristiger Parallelbetrieb erforderlich ist. Jedes Backend besitzt eine eigene Serviceschicht, einen eigenen Anwendungskern, ein eigenes Deployment und eigene Betriebsanforderungen. Über die Datenhaltung ist explizit zu entscheiden. Die gemeinsame direkte Nutzung derselben Datenbank durch mehrere Backend-Versionen ist grundsätzlich zu vermeiden. Sie koppelt die Backends über das Datenmodell, verhindert unabhängige Weiterentwicklung und macht Änderungen riskant.
Zulässig sind nur bewusst begründete Übergangslösungen, etwa während einer Migration. Diese sind mit Bezug auf Umfang und Dauer im Systementwurf zu dokumentieren.
Als Vorteil ergibt sich eine maximale Entkopplung und eine unabhängige Weiterentwicklung mit klaren Systemgrenzen.
Ein neues Backend ist keine Standardmaßnahme für Versionierung. Es ist eine bewusste Architekturentscheidung und im Systementwurf zu begründen.
4. Versionierung von Events
Dieses Kapitel beschreibt bewährte Praktiken im Umgang mit Änderungen von Nachrichten in eventbasierten Systemen wie z.B. Kafka. Nach der Lektüre sollte der Leser in der Lage sein, diese richtig einzuordnen und mithilfe der definierten Entscheidungsmatrix die geeignete Vorgehensweise bei der Versionierung zu finden.
4.1. Fachliche Änderungen
In der asynchronen Kommunikation werden Events genutzt, um über geschäftsrelevante Ereignisse innerhalb der Anwendungslandschaft zu informieren. Aus der Sicht von konsumierenden Systemen sind Events wichtige historische Ereignisse (Fakten), die für die Weiterverarbeitung benötigten Daten enthalten.
Ihre Semantik wird beim Domain Driven Design bereits zu Beginn beschrieben (z.B. im Rahmen von Event Storming Sitzungen). Es ist wichtig diese auch in der Dokumentation für alle Beteiligten Teams verbindlich festzuhalten (Design By Contract).
Folgende Tabelle zeigt vereinfacht im Stil einer Karteikarte, wie die Definition der Semantik eines Events für eine aufgegebene Bestellung aussehen könnte.
Name |
BestellungAufgegeben |
Version |
1 |
Domäne |
Bestellung |
Eigentümer |
Team A |
Beschreibung |
Der Kunde hat eine Bestellung verbindlich aufgegeben. Die Bestellung wurde erfolgreich im System persistiert. |
Auslöser |
Das Event wird erzeugt, nachdem der Kunde den Kauf bestätigt hat und die Bestellung erfolgreich gespeichert wurde. |
Felder |
|
artikelId |
Eindeutige Kennung des Artikels, um den bestellten Artikel zu identifizieren. Typ String. |
anzahl |
Anzahl des bestellten Artikels. Typ Integer. |
preis |
Einzelpreis des Artikels. Typ Float. |
Fachliche Änderungen wirken sich auf die Semantik aus und führen immer zu Breaking Changes. Im Gegensatz zu technisch inkompatiblen Änderungen können sie nicht automatisch erkannt werden.
Folgende Tabelle zeigt beispielhaft die semantische Änderung des Feldes preis. Zuvor war der Preis für den einzelnen Artikel gemeint, nun soll das Feld den Gesamtbetrag für alle Artikel enthalten. Da Feldname und Datentyp dieselben sind, wissen Consumer von der Änderung nichts und berechnen weiterhin den Gesamtpreis durch Multiplikation mit der Anzahl.
Name |
BestellungAufgegeben |
Version |
2 |
… |
|
Felder |
|
… |
|
preis |
Gesamtbetrag der bestellten Artikel. Typ Float. |
4.2. Technische Änderungen
Änderungen an der Struktur von Nachrichten bei bleibender Semantik werden als technische Änderungen bezeichnet. Technische Änderungen sind in kompatible und inkompatible technische Änderungen untergliedert. Strukturelle Breaking Changes können bei der Nachrichtenvalidierung automatisch erkannt werden.
4.3. Nachrichtenvalidierung
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.
Folgendes Listing in der Definitionssprache Avro zeigt, wie das Event BestellungAufgegeben aufgebaut ist.
Darin ist z.B. das Feld artikelId als String definiert und ist verpflichtend in den daraus abgeleiteten Events.
BestellungAufgegeben (Version 1){
"type": "record",
"name": "BestellungAufgegeben",
"fields": [
{ "name": "artikelId", "type": "string" },
{ "name": "anzahl", "type": "int" }
]
}
| Avro wird wegen der leichten Verständlichkeit auch in den Folgebeispielen genutzt. Die Version wird in Klammern als fortlaufende Ganzzahl zwecks Verweis angegeben. |
4.3.1. Abwärts- vs. Vorwärtskompatibilität
In Literatur und Dokumentationen kommt es gelegentlich zu Verwechslungen zwischen Abwärts- und Vorwärtskompatibilität. Daher im Folgenden eine kurze Definition der beiden zur Abgrenzung.
Ein Schema ist abwärtskompatibel, wenn bestehende Consumer unter Verwendung der alten Schema-Version in der Lage sind, Nachrichten korrekt zu verarbeiten, die mit der neuen Schema-Version erzeugt wurden. Vereinfacht formuliert: alte Consumer können neue Nachrichten lesen.
Ein Schema ist hingegen vorwärtskompatibel, wenn aktualisierte Consumer unter Verwendung der neuen Schema-Version in der Lage sind, Nachrichten korrekt zu verarbeiten, die mit der alten Schema-Version erzeugt wurden. Vereinfacht formuliert: neue Consumer können alte Nachrichten lesen.
5. Datenkonsistenz
In Event-basierten Systemen wie Kafka werden Nachrichten in Logdateien in der Regel für einen längeren Zeitraum persistiert (Event-Log). Daher müssen im Vergleich zu Queue-basierten Systemen Consumer in der Lage sein, auch alte Nachrichten zu lesen, wenn sie wieder eingespielt werden (Replay), etwa beim Eintritt einer neuen Consumer-Group oder Zurücksetzen des Eventstroms (Offset Reset).
Eine standardisierte Schema-Verwaltung spielt für den Erhalt der Datenkonsistenz eine wichtige Rolle.
5.1. Replay und Idempotenz
Neue Versionen von Events müssen so aufgebaut sein, dass weiterhin ein Verarbeiten von alten Nachrichten (Replay) 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 weiterhin Duplikate anhand einer eindeutigen Event-ID erkennen und ignorieren, um Mehrfachverarbeitungen zu vermeiden.
5.2. Schema Management
Schemata ändern sich fortlaufend mit neuen Anforderungen. Um Wildwuchs von Versionen zu vermeiden, werden sie an einer zentralen Stelle z.B. Wiki oder git-Repository von Entwicklerteams gemeinsam verwaltet.
In der Praxis hat sich der Einsatz von Schema Registries wie Apicurio Registry oder Confluent Schema Registry bewährt. Der große Vorteil den Schema Registries mit sich bringen 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.
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.
| Ob Entwicklerteams sich für ein einfaches Schema Management mit Wiki oder einer Schema Registry entscheidet, ist Abwägungssache. Wichtig ist nur, dafür eine zentrale Stelle zu verwenden, um Schemabrüche und Versionswildwuchs zu vermeiden. |
5.3. Schema Evolution vs. Schemabruch
In den meisten Fällen handelt es sich bei den Änderungen an Event-Typen um abwärtskompatible Änderungen.
Kommt beispielsweise das optionale Feld kommentar zur Version 1 des Event-Typs BestellungAufgegeben hinzu, entsteht eine neue Version aber kein neuer Event-Typ.
Eine fortschreitende Versionierung von Event-Typen, die im Rahmen der Kompatibilität bleibt, wird als Schema Evolution bezeichnet.
Im Beispiel verwenden alte Consumer das Schema in der Version 1 beim Lesen von Nachrichten. Nachrichten, welche mit der Version 2 erzeugt wurden, enthalten zusätzlich das Feld kommentar.
Da der alte Consumer dieses nicht kennt, wird es einfach ignoriert und die Verarbeitung geht normal weiter.
In Nachrichten, die mit der alten Schema-Version erzeugt wurden, fehlt das Feld kommentar.
Das ist aber für neue Consumer kein Problem, da es optional ist und einfach mit dem Wert null belegt wird.
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 }
]
}
In Fällen bei denen Änderungen zum Bruch der Kompatibilität führen (Breaking Change) reicht eine neue Schema-Version nicht mehr aus, d.h. es liegt ein Schemabruch vor. Als Folge muss ein neues Schema für einen neuen Event-Typ erstellt werden.
Im Beispiel-Event BestellungAufgegeben könnte ein neues Pflichtfeld preis dazukommen. Bei fortschreitender Versionierung könnten neue Consumer neue Nachrichten zwar lesen, aber bei den alten Nachrichten würde der Preis fehlen und der Consumer wüsste nicht, welcher Wert gemeint ist. Deswegen gibt es ein neues Schema für den Event-Typ BestellungMitPreisAufgegeben.
| 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. |
BestellungMitPreisAufgegeben mit neuem Pflichtfeld (Version 1){
"type": "record",
"name": "BestellungMitPreisAufgegeben",
"fields": [
{ "name": "artikelId", "type": "string" },
{ "name": "anzahl", "type": "int" },
{ "name": "preis", "type": "float" }
]
}
5.4. Entscheidungsmatrix neuer Event-Typ vs. Versionierung
Folgende Tabelle führt als Entscheidungshilfe auf, wann ein neuer Event-Typ in Abhängigkeit der Änderung nötig ist oder eine neue Version ausreicht.
| Änderung | Abwärtskompatibel | Konsequenz |
|---|---|---|
fachlich |
egal |
neuer Event-Typ |
technisch |
nein |
neuer Event-Typ |
technisch |
ja |
neue Version (Schema Evolution) |
Folgende Aussagen treffen zu:
-
Fachliche Änderungen erfordern immer einen neuen Event-Typ
-
Breaking Changes erfordern immer einen neuen Event-Typ
-
Nur kompatible technische Änderungen können mit Schema Evolution versioniert werden
6. Kanalstrategien bei Asynchroner Kommunikation
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.
Beispiel:
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.
BestellungIdLongErstellt mit geändertem Feldtyp (Version 1){
"type": "record",
"name": "BestellungIdLongErstellt",
"fields": [
{ "name": "artikelId", "type": "long" },
{ "name": "anzahl", "type": "int" }
]
}
6.2. Migration von Topics/Queues
Die Strategie zur Migration von Topics hängt vom konkreten Anwendungsfall und den jeweiligen Anforderungen ab:
-
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?
-
Sind verlorene oder fehlerhaft verarbeitete Nachrichten während der Migration hinnehmbar?
-
Darf der laufende Betrieb des Systems während der Wartung unterbrochen werden?
-
Soll die Zustellreihenfolge der Nachrichten erhalten bleiben?
-
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 den Phasen Vorbereitung, Einfrieren der bestehenden Producer, Entleeren des bestehenden Topics, Umstellung auf das neue Topic und Dekommissionierung des alten Topics.
-
Neues Topic
antrag.v2wird angelegt (Phase: Vorbereitung). -
Neue Producer und Consumer werden erstellt.
-
Wartungsfenster wird aktiviert.
-
Alte Producer werden eingestellt, damit der Datenbestand nicht weiter befüllt wird (Phase: Einfrieren).
-
Bestehende Consumer arbeiten verbleibende Nachrichten in
antrag.v1vollständig ab (Phase: Entleeren). -
Neue Producer und Consumer werden gestartet und arbeiten ausschließlich mit dem neuen Topic
antrag.v2(Phase: Umstellung). -
Nach einer Stabilitätsphase geht das System wieder in Betrieb.
-
Topic
antrag.v1kann gelöscht oder archiviert werden (Phase: Dekommissionierung).
Das Bild Big Bang Migration eines Topics veranschaulicht die wesentlichen Schritte der einfachen Migration mit Wartungsfenster.
6.2.2. Migration nach dem Strangler Pattern (schrittweise, im laufenden Betrieb)
In kritischen Systemen wie Banksystemen 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 den Phasen Vorbereitung, Parallelisierung des bestehenden Datenstroms, schrittweise Umstellung der Consumer, Umstellung der Producer und optional Dekommissionierung des alten Topics.
Zu Beginn wird ein neues Topic mit dem aktualisierten Schema bereitgestellt (Phase: Vorbereitung). Anschließend wird der alte Datenstrom vollständig in das neue Topic transformiert und repliziert (Phase: Parallelisierung). 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.
Zeitgleich werden neue Consumer schrittweise auf das neue Topic umgestellt (Phase: schrittweise Umstellung der Consumer). 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).
Sobald alle Consumer erfolgreich auf das neue Topic migriert sind, werden die Producer auf das neue Topic umgestellt (Phase: Umstellung der Producer). Die Umstellung der Producer muss atomar erfolgen, um Duplikate zu vermeiden.
Nach einer Stabilitätsphase kann das alte Topic außer Betrieb genommen, gelöscht oder archiviert werden (Phase: Dekommissionierung). Die Transformationskomponente wird anschließend ebenfalls entfernt.
Das Bild Migration eines Topics nach dem Strangler Pattern veranschaulicht nochmal die wichtigen Phasen der Migration 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.
7. Architektur
Die in Entscheidungsmodell zur Versionierung beschriebenen Entscheidungen werden für synchrone Services in der Referenzarchitektur der IsyFact in der Regel innerhalb einer Service-Schicht umgesetzt.
Erfordert eine nicht abwärtskompatible Änderung eine neue Service-Version, so wird diese im Regelfall als eigene Service-Schnittstelle in der Serviceschicht des Backends bereitgestellt. Mehrere Service-Versionen können dabei denselben Anwendungskern verwenden, sofern sich die fachliche Verarbeitung mit vertretbarem Aufwand gemeinsam abbilden lässt.
Die für die Versionierung notwendigen Transformationen sind Teil der jeweiligen Service-Schnittstelle. Hierzu gehören insbesondere das Ergänzen von Standardwerten, die Abbildung unterschiedlicher Datenstrukturen sowie die Behandlung versionsspezifischer Unterschiede. Fachlogik des Anwendungskerns soll dabei nicht dupliziert werden.
Eine Trennung innerhalb des Anwendungskerns oder die Bereitstellung eines eigenen Backends kann sinnvoll sein, wenn sich die fachliche Semantik zwischen den Versionen wesentlich unterscheidet, mehrere inkompatible Verarbeitungslogiken langfristig parallel unterstützt werden müssen oder die gemeinsame Realisierung zu unverhältnismäßig ist.
Die Entscheidung hierfür ist im Systementwurf zu dokumentieren.
Externe Services werden durch Service-Gateways bereitgestellt.
Es ist möglich, pro Service-Version ein eigenes Service-Gateway zu erstellen, sofern dies aus technischen oder betrieblichen Gründen erforderlich ist.
| Für den Standardfall einer nicht abwärtskompatible Änderung, bei gleichbleibender fachlicher Logik, die kein neues Backend erforderlich macht, ergibt sich folgende Architektur: |
8. Abwärtskompatible Erweiterung eines Services
Ein Backend stellt einen Service bereit, mit dem Personendaten gemeldet werden können. Parameter dieser Meldung sind Vor- und Nachname sowie das Geburtsdatum. Dazu gibt es einen Meldung-Service in der Version 1.0. Dieser wird in der Serviceschicht des Backends implementiert. Ab einem Stichtag soll zusätzlich noch der Geburtsort gemeldet werden. Im bisherigen Datenbestand wird dieses neue Attribut auf den Wert "unbekannt" gesetzt. Der bestehende Service wird um dieses Attribut erweitert. Da es sich um eine abwärtskompatible Änderung handelt, bleibt es bei derselben Service-Version. Anwendungskern und Persistenzschicht müssen ebenfalls erweitert werden.
Die Serviceschicht stellt sicher, dass Aufrufe ohne Angabe des Geburtsorts weiterhin korrekt verarbeitet werden können, indem für das fehlende Attribut der Wert "unbekannt" ergänzt wird.
Wird der Service durch ein Service-Gateway nach außen verfügbar gemacht, bleibt das Gateway auf dieselbe Service-Schnittstelle geroutet. Innerhalb des Gateways findet keine fachliche Abbildung statt. Die Behandlung fehlender optionaler Werte erfolgt im Backend.
9. Inkompatible Veränderung eines Services
Ist eine Änderung an einem Service nicht abwärtskompatibel, ist gemäß dem Entscheidungsmodell zur Versionierung eine neue Service-Version bereitzustellen. Kann die neue Version mit vertretbarem Aufwand auf dieselbe fachliche Verarbeitung abgebildet werden, so können mehrere Service-Versionen denselben Anwendungskern nutzen. In diesem Fall enthält die Serviceschicht für jede Version eine eigene Service-Schnittstelle mit der jeweils erforderlichen Transformationslogik.
Wird in so einem Fall ein neuer Service eingeführt, während der alte Service noch verfügbar bleiben muss, müssen die inkompatiblen Verarbeitungslogiken im Anwendungskern parallel unterstützt werden. Bei starken fachlichen oder technischen Unterschieden kann es sinnvoll sein, die neue Version in einem eigenen Backend bereitzustellen. Auch in diesem Fall enthält das Service-Gateway keine Fachlogik.
| Eine neue Version ist dann erforderlich, wenn eine Änderung nicht abwärtskompatibel ist und bestehende Nutzende nicht ohne Anpassung weiterarbeiten können. |
Führen Änderungen zu einer wesentlichen fachlichen Neuauslegeung der Schnittstelle, soll keine weitere Version derselben Schnittstelle entstehen. In diesem Fall ist zu prüfen, ob es sich fachlich noch um dasselbe Konzept handelt. Wenn Versionen nicht mehr durch einfache Transformation verbunden werden können, handelt es sich nicht mehr um Versionen desselben fachlichen Konzepts, sondern die Einführung eines neuen fachlichen Konzepts. In diesem Fall ist zu prüfen, ob eine eigenständige neue Schnittstelle oder ein eigenes Backend die fachlich und technisch sauberere Lösung darstellt.
9.1. Lifecycle und Deprecation von Schnittstellenversionen
Jede neue nicht abwärtskompatible Schnittstellenversion muss eine Deprecation-Policy für die Vorgängerversion definieren.
Diese umfasst mindestens:
-
Datum der Einführung der neuen Version,
-
Datum der Deprecation der alten Version,
-
Mindestunterstützungszeitraum,
-
geplantes Abschaltdatum,
-
betroffene Clients,
-
Migrationsanleitung,
-
technische Metrik zur Nutzung der alten Version.