Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Lire en français

Abschnitt 7.7 von 7.8 · Mittelstufe · 25 Min. Lesezeit

Schemaevolution, Migrationen und Kompatibilität

Videolektion

5 Min. · auf Englisch gesprochen, mit Untertiteln
Nur anhören (Audio)
Transkript lesen

Jedes Schema ändert sich, während das System läuft: Datenbanktabellen, API-Nutzdaten und Events gleichermaßen. In dieser Lektion lernen Sie, wie Sie diese Änderungen sicher und ohne Ausfallzeit vornehmen.

Stellen Sie sich ein Krankenhaus vor, das das Papierformular für jede Aufnahme ändert, während weiter Patienten eintreffen. Tauscht es die Formulare einfach über Nacht aus, hält die Nachtschicht das alte Formular in der Hand, während die Tagschicht das neue erwartet. Und die abgelegten Akten nutzen noch das alte Layout. Ein umsichtiges Krankenhaus druckt ein Formular mit den alten und den neuen Feldern und nutzt es, bis alle umgestellt haben und die alten Akten übertragen sind. Erst dann entfernt es die alten Felder. Softwareteams ändern die Struktur ihrer Daten genau auf diese umsichtige, schrittweise Art.

Drei Tatsachen machen das schwierig. Erstens laufen in jedem System, das ohne Ausfallzeit deployt wird, alte und neue Codeversionen nebeneinander, während eines Rolling Deployments und nach jedem Rollback. Also muss jeder Schemazustand mit mindestens zwei Codeversionen funktionieren. Zweitens leben Daten länger als Code. Vor fünf Jahren geschriebene Zeilen, in einem Log aufbewahrte Events und Backups behalten alle die alte Form. Drittens werden Schemas gemeinsam genutzt. Eine Spalte wird vielleicht von Berichten, Extraktionsjobs und anderen Services gelesen. Deshalb ist ein privates Schema hinter einer Schnittstelle so viel leichter zu ändern.

Kompatibilität wird über Leser und Schreiber definiert. Links stehen Daten, die mit dem alten und dem neuen Schema geschrieben wurden, rechts die Leser, die jeweils eines davon nutzen. Gleiche Versionen sind immer unproblematisch. Eine Änderung ist abwärtskompatibel, wenn neue Leser alte Daten lesen können. Ein optionales Feld mit Standardwert hinzuzufügen ist abwärtskompatibel: Der neue Leser setzt den Standardwert ein. Eine Änderung ist aufwärtskompatibel, wenn alte Leser neue Daten lesen können. Dazu müssen alte Leser Felder ignorieren, die sie nicht kennen. Volle Kompatibilität heißt, dass beide Richtungen funktionieren, Leser und Schreiber also in beliebiger Reihenfolge aktualisieren können. Bei einer Datenbank schreibt neuer Code meist schon, bevor jeder Leser neu deployt ist, also brauchen Sie mindestens Aufwärtskompatibilität.

Manche Änderungen sind sicher. Ein optionales Feld oder eine nullable Spalte mit Standardwert hinzuzufügen ist die übliche sichere Änderung. Ein Pflichtfeld ohne Standardwert bricht alte Daten, also fügen Sie es zuerst optional hinzu, füllen es und erzwingen es erst dann. Eine Umbenennung bricht beide Richtungen zugleich, deshalb geschieht sie nie in einem Schritt. Und die gefährlichste Änderung behält die Form, ändert aber die Bedeutung, etwa wenn ein Betrag von Pence auf Pfund umgestellt wird. Jeder Leser liegt unbemerkt falsch. Legen Sie immer ein neues Feld mit neuem Namen an.

Die Technik für inkompatible Änderungen heißt Expand and Contract. Hier benennt die Essenslieferplattform in einer Tabelle mit vier Millionen Zeilen eine Spalte von postcode in postal code um. Eine einzelne Umbenennung würde den laufenden Code brechen. Also zuerst Expand: die neue Spalte leer hinzufügen. Als Nächstes schreibt die Anwendung denselben Wert in beide Spalten, liest aber noch die alte. Ein Hintergrundjob kopiert bestehende Zeilen in kleinen Batches, dann bestätigt eine Prüfung, dass sich keine Zeilen unterscheiden. Jetzt liest die Anwendung die neue Spalte, schreibt aber weiter beide, sodass ein Rollback auf das vorige Release sicher bleibt. Dann schreibt sie die alte Spalte nicht mehr, und das Team wartet, bis kein Rollback mehr nötig sein wird. Zum Schluss Contract: die neue Spalte zur Pflicht machen und die alte löschen.

Hier sind diese sechs deploybaren Schritte hintereinander. Jeder funktioniert mit dem Code aus dem Schritt davor, und jeder lässt sich zurückrollen. Beachten Sie, dass in den Schritten eins bis fünf beide Spalten existieren. Sechs Deployments für eine Umbenennung klingen nach viel, aber kein Release braucht eine Ausfallzeit, und kein Team muss sich abstimmen. Den Contract-Schritt vergessen Teams am häufigsten. Verfolgen Sie ihn ausdrücklich, sonst füllt sich das Schema mit halb migrierten Spalten.

Datenbankänderungen gehören in die Versionsverwaltung neben den Code, als geordnete Folge kleiner Migrationsskripte. Werkzeuge wie Flyway, Liquibase und Alembic wenden sie der Reihe nach an, in Tests, Staging und Produktion. Bearbeiten Sie nie eine Migration, die irgendwo angewendet wurde; schreiben Sie eine neue. Und proben Sie mit Daten in Produktionsgröße, denn eine Anweisung, die im Test fünfzig Millisekunden dauert, kann eine große Tabelle eine Stunde lang sperren. Große Backfills laufen als separate Jobs in kleinen Batches, bremsen, wenn Replikate zurückfallen, und lassen sich sicher neu starten. Für Events prüft eine Schema Registry jede neue Version gegen die Kompatibilitätsregel und weist eine inkompatible Änderung zurück, bevor sie die Produktion erreicht. Geben Sie jedem Feld, das Sie hinzufügen oder entfernen könnten, einen Standardwert.

Der vollständige Abschnitt enthält das SQL für jeden Schritt, mehr zu Event-Formaten und eine Checkliste. Als Nächstes, in Abschnitt sieben Punkt acht, geht es um Analytics und Streaming. Bis gleich.

Was Sie wissen müssen

Warum sich Schemas weiterentwickeln müssen, ohne die Welt anzuhalten

Alle gespeicherten oder übertragenen Daten haben ein Schema: die Tabellen und Spaltentypen einer Datenbank, die Felder einer JSON-API-Antwort, die Datensatzdefinition eines Events in einem Stream. Schemas ändern sich ständig, weil sich das Geschäft ändert: Die Bank führt einen zweiten Kontoinhaber ein, die Essenslieferplattform beginnt, Trinkgelder zu erfassen, eine Aufsichtsbehörde verlangt ein neues Feld. Die architektonische Frage lautet, wie man ein Schema ändert, während das System weiterläuft, obwohl die alte Form noch von Code, gespeicherten Daten und anderen Teams verwendet wird.

Drei Tatsachen machen das schwierig. Erstens laufen in jedem System, das ohne Ausfallzeit deployt wird, alte und neue Codeversionen nebeneinander: Ein Rolling Deployment ersetzt Instanzen nacheinander, ein Canary leitet einen kleinen Teil des Datenverkehrs an die neue Version, und ein Rollback holt die alte Version zurück. Jeder Schemazustand muss daher mit mindestens zwei Codeversionen funktionieren. Zweitens leben Daten länger als Code. Vor fünf Jahren geschriebene Zeilen, dauerhaft aufbewahrte Events in einem Log, in einer Queue wartende Nachrichten und Backups behalten alle die alte Form. Drittens werden Schemas gemeinsam genutzt: Eine Spalte wird von Berichten, Extraktionsjobs und manchmal anderen Services gelesen, und ein Event-Schema wird von Consumern gelesen, die der Producer vielleicht gar nicht kennt. Abschnitt 7.1 hat dafür plädiert, Daten hinter einer Schnittstelle zu besitzen; bei der Schemaevolution zahlt sich diese Disziplin aus, denn ein privates Schema lässt sich mit der Zustimmung eines einzigen Teams ändern.

Abwärts-, Aufwärts- und volle Kompatibilität

Kompatibilität wird über Leser und Schreiber definiert. Eine Änderung ist abwärtskompatibel, wenn Code mit dem neuen Schema Daten lesen kann, die mit dem alten geschrieben wurden. Ein optionales Feld mit Standardwert hinzuzufügen ist abwärtskompatibel: Der neue Leser setzt den Standardwert ein, wenn den alten Daten das Feld fehlt. Eine Änderung ist aufwärtskompatibel, wenn Code mit dem alten Schema Daten lesen kann, die mit dem neuen geschrieben wurden. Das setzt voraus, dass alte Leser Felder ignorieren, die sie nicht kennen; das ist die Disziplin des Tolerant Reader. Eine Änderung ist voll kompatibel, wenn sie beides ist; dann lassen sich Leser und Schreiber in beliebiger Reihenfolge aktualisieren.

Welche Richtung Sie brauchen, ergibt sich daraus, wer zuerst aktualisiert. Aktualisieren Consumer vor Producern (häufig, wenn ein Stream wenige Consumer hat, die Sie kontrollieren), brauchen Sie Abwärtskompatibilität. Aktualisieren Producer zuerst, was der übliche Fall bei einer Datenbank ist (neuer Code beginnt zu schreiben, bevor jeder Leser neu deployt wurde) und bei öffentlichen APIs, brauchen Sie Aufwärtskompatibilität. Wenn Sie die Reihenfolge nicht kontrollieren können oder beide Seiten zurückrollen müssen, streben Sie volle Kompatibilität an. Die Confluent Schema Registry prüft zum Beispiel standardmäßig Abwärtskompatibilität gegen die vorherige Version und bietet transitive Varianten, die gegen jede frühere Version prüfen; das ist wichtig, wenn Consumer alte Daten vom Anfang eines Logs erneut abspielen können.

Ein Wort der Warnung zur Begrifflichkeit. Im API-Design wird abwärtskompatibel oft locker im Sinne von bestehende Clients funktionieren weiter verwendet; in Begriffen von Lesern und Schreibern ist das Aufwärtskompatibilität für die Clients, die Ihre Antworten lesen, und Abwärtskompatibilität für die Anfragen, die Sie von ihnen lesen. Wenn eine Diskussion an dem Begriff hängt, fragen Sie, welche Seite sich ändert und welche Seite weiter funktionieren muss.

Welcher Leser welche Daten lesen kann

Abwärtskompatibilität: neue Leser können alte Daten lesen; Aufwärtskompatibilität: alte Leser können neue Daten lesen; volle Kompatibilität: beidesDaten mitSchema v1 (alt)Daten mitSchema v2 (neu)Leser mitSchema v1 (alt)Leser mitSchema v2 (neu)gleiche Version: immer gutgleiche Version: immer gutABWÄRTSneu liest altAUFWÄRTSalt liest neuVOLL = beide Diagonalen funktionieren; Leser und Schreiber beliebig aktualisierbar
Abwärtskompatibilität schützt neue Leser vor alten Daten; Aufwärtskompatibilität schützt alte Leser vor neuen Daten.

Häufige Änderungen und ihre Kompatibilität

ÄnderungAbwärts (neu liest alt)Aufwärts (alt liest neu)Sicheres Vorgehen
Ein optionales Feld oder eine nullable Spalte mit Standardwert hinzufügenJaJa, wenn Leser unbekannte Felder ignorierenDie übliche sichere Änderung
Ein Pflichtfeld ohne Standardwert hinzufügenNein: Alten Daten fehlt esJaOptional hinzufügen, Backfill, dann erzwingen
Ein Feld entfernenJaNur wenn alte Leser einen Standardwert dafür habenErst überall nicht mehr lesen, dann entfernen
Ein Feld oder eine Spalte umbenennenNeinNeinExpand and Contract: neu hinzufügen, kopieren, umstellen, alt entfernen
Einen Typ ändern (Text zu Integer, int zu long)Hängt vom Format ab: Erweitern ist oft sicher, Einengen nichtHängt davon abEin neues Feld mit dem neuen Typ hinzufügen und migrieren
Bedeutung ändern, ohne die Form zu ändern (Pence zu Pfund)Unbemerkt falschUnbemerkt falschNiemals: ein neues Feld mit neuem Namen anlegen

Expand and Contract: die zentrale Technik

Die meisten inkompatiblen Änderungen lassen sich in eine Folge kompatibler Änderungen zerlegen. Das Muster heißt Expand and Contract oder Parallel Change (Danilo Satos Beschreibung von 2014 auf martinfowler.com nennt die Phasen Expand, Migrate und Contract). Zuerst erweitern (Expand): die neue Struktur neben der alten hinzufügen, sodass alter und neuer Code funktionieren. Dann migrieren (Migrate): die Schreiber so ändern, dass sie beide pflegen, vorhandene Daten übertragen und die Leser nacheinander auf die neue Struktur umstellen. Schließlich zurückbauen (Contract): Sobald nichts mehr die alte Struktur nutzt, wird sie entfernt. Jeder Schritt wird separat deployt, jeder ist mit der Version davor kompatibel, und jeder lässt sich ohne Datenverlust zurückrollen.

Der Preis sind Zeit und vorübergehende Unordnung: Die Änderung erstreckt sich über mehrere Releases, und tage- oder wochenlang trägt das Schema beide Formen. Der Nutzen: Kein Release braucht jemals eine Ausfallzeit, kein Deployment muss teamübergreifend koordiniert werden, und jeder Schritt lässt sich anhalten. Scott Amblers und Pramod Sadalages Refactoring Databases (2006) katalogisiert Dutzende Datenbank-Refactorings, die auf derselben Idee beruhen, mit einer Übergangsphase, in der altes und neues Schema nebeneinander bestehen und von der Anwendung oder von Triggern synchron gehalten werden.

Ausgearbeitetes Beispiel

Ausgearbeitetes Beispiel: eine Spalte ohne Ausfallzeit umbenennen

Die Tabelle restaurants der Essenslieferplattform hat eine Spalte postcode. Die Plattform expandiert in Länder mit anderen Postleitzahlformaten, und das Team möchte sie postal_code nennen. Die Tabelle hat 4 Millionen Zeilen, der Restaurant-Service wird mehrmals täglich mit Rolling Updates deployt, und ein nächtlicher Extraktionsjob liest die Spalte ebenfalls. Ein einzelnes ALTER TABLE ... RENAME COLUMN würde jede laufende Instanz des alten Codes in dem Moment brechen, in dem es committet wird.

Release 1: ExpandEine Migration fügt postal_code text NULL hinzu. Ab PostgreSQL 11 ist das Hinzufügen einer Spalte ohne Standardwert oder mit konstantem Standardwert eine reine Metadatenänderung. Alter Code ignoriert die neue Spalte; nichts bricht.
Release 2: Dual WriteDie Anwendung schreibt bei jedem Insert und Update denselben Wert in beide Spalten und liest weiterhin postcode. (Ein Datenbank-Trigger, der eine Spalte in die andere kopiert, ist eine Alternative, wenn mehrere Anwendungen in die Tabelle schreiben.)
Schritt 3: BackfillEin Hintergrundjob kopiert postcode für bestehende Zeilen in Batches von 5000 nach Primärschlüsselbereich in postal_code, pausiert zwischen den Batches und beobachtet die Replikationsverzögerung. Danach zählt eine Prüfabfrage die Zeilen, in denen sich beide unterscheiden: Sie muss 0 liefern.
Release 4: Lesen umstellenDie Anwendung liest postal_code, schreibt aber weiterhin beide Spalten, sodass ein Rollback auf Release 2 oder 3 sicher bleibt. Der Extraktionsjob wird ebenfalls auf die neue Spalte umgestellt.
Release 5: die alte Spalte nicht mehr schreibenDie Anwendung schreibt nur noch postal_code. Die alte Spalte darf jetzt nicht NOT NULL sein, sonst schlagen Inserts fehl. Warten Sie lange genug, um sicher zu sein, dass kein Rollback auf Release 4 nötig wird.
Release 6: ContractEine Migration erzwingt NOT NULL für postal_code (mit einem NOT VALID-Check-Constraint, der separat validiert wird und den PostgreSQL ab Version 12 nutzen kann, um NOT NULL ohne langen blockierenden Scan zu setzen) und löscht schließlich postcode.

Sechs Deployments für eine Umbenennung klingen übertrieben, aber Schritte lassen sich zusammenlegen, wenn das Team jeden Leser kontrolliert und ein kurzes Rollback-Fenster akzeptiert; entscheidend ist das Prinzip, dass jeder deployte Zustand mit dem Code davor und danach funktioniert.

Den Contract-Schritt vergessen Teams am häufigsten. Verfolgen Sie ihn ausdrücklich (ein Ticket, ein ADR-Status, eine Lint-Regel, die die alte Spalte markiert), sonst füllt sich das Schema mit halb migrierten Spalten.

Die Umbenennung als sechs deploybare Schritte

Spalte umbenennen mit Expand and Contract: neue Spalte hinzufügen, doppelt schreiben, Backfill, Lesen umstellen, alte nicht mehr schreiben, dann löschenEine logische Umbenennung, sechs deploybare Schritte; jeder lässt sich sicher zum vorigen zurückrollen1 Expand+postal_codealt + neu (null)2 Dual Writeschreibt in beideliest postcode3 BackfillUPDATE in TeilenPrüfen: 0 Abw.4 Lesen umst.liest postal_codeschreibt noch beide5 Alt stoppschreibt nur neubeob., warten6 Contractalte SpaltelöschenSpalten vorhandenpostcodepostcode und postal_code existieren (Schritte 1 bis 5)postal_codenach Schritt 6Expand (additiv, kompatibel mit altem Code)Contract (erst, wenn nichts die alte Spalte liest)
In den Schritten 1 bis 5 existieren beide Spalten, sodass jede Codeversion aus dem vorigen Schritt weiterhin laufen kann.
-- Migration V41 (expand): metadata-only change, no table rewrite in PostgreSQL 11+
SET lock_timeout = '3s';            -- fail fast rather than queue behind long queries
ALTER TABLE restaurants ADD COLUMN postal_code text;

-- Backfill job (run repeatedly by a script, not inside one big migration)
UPDATE restaurants
SET    postal_code = postcode
WHERE  id >= :from_id AND id < :from_id + 5000   -- one small batch per transaction
AND    postal_code IS DISTINCT FROM postcode;

-- Verify before switching reads
SELECT count(*) FROM restaurants WHERE postal_code IS DISTINCT FROM postcode;  -- expect 0

-- Migration V44: enforce NOT NULL without a long blocking scan
ALTER TABLE restaurants ADD CONSTRAINT postal_code_nn CHECK (postal_code IS NOT NULL) NOT VALID;
ALTER TABLE restaurants VALIDATE CONSTRAINT postal_code_nn;  -- scans, but allows writes
ALTER TABLE restaurants ALTER COLUMN postal_code SET NOT NULL;  -- PostgreSQL 12+: uses the check, skips scan
ALTER TABLE restaurants DROP CONSTRAINT postal_code_nn;

-- Migration V45 (contract), weeks later, once nothing reads postcode
ALTER TABLE restaurants DROP COLUMN postcode;
PostgreSQL-Anweisungen für die Schritte Expand, Backfill, Prüfen und Contract, so geschrieben, dass lange Sperren vermieden werden.

Migrationen als Code in der Delivery-Pipeline

Datenbankänderungen gehören in die Versionsverwaltung neben den Anwendungscode, als geordnete Folge von Migrationen, jede ein kleines Skript, das das Schema von einer Version zur nächsten bringt. Pramod Sadalages und Martin Fowlers Evolutionary Database Design (erstmals 2003 veröffentlicht, 2016 neu geschrieben) hat die Praxis beschrieben, und die Werkzeuge sind ausgereift. Flyway führt versionierte SQL-Dateien wie V41__add_postal_code.sql der Reihe nach aus und verzeichnet jede in einer Tabelle flyway_schema_history. Liquibase beschreibt Änderungen als Changesets in SQL, XML, YAML oder JSON, erfasst in einer Tabelle DATABASECHANGELOG. Alembic für Python und SQLAlchemy erzeugt Revisionsskripte mit den Funktionen upgrade() und downgrade() und hält die aktuelle Revision in alembic_version fest. Flyway und Liquibase speichern außerdem eine Prüfsumme jeder angewendeten Migration und brechen mit einem Fehler ab, wenn eine davon nachträglich bearbeitet wurde; das schützt Umgebungen davor, unbemerkt auseinanderzulaufen.

In einer Continuous-Delivery-Pipeline laufen dieselben Migrationen in Tests gegen eine frische Datenbank, dann gegen Staging, dann gegen die Produktion, sodass die Produktion nie eine Änderung erhält, die nicht geprobt wurde. Gute Praxis ergänzt einige Regeln. Bearbeiten Sie nie eine Migration, die irgendwo angewendet wurde; schreiben Sie eine neue. Halten Sie Schemaänderungen und den davon abhängigen Code in getrennten Deployments, bei Erweiterungen die Schemaänderung zuerst, bei Rückbauten zuletzt. Testen Sie Migrationen gegen eine Kopie von Daten in Produktionsgröße, denn eine Anweisung, die auf einer Testdatenbank 50 ms dauert, kann eine große Tabelle eine Stunde lang sperren. Rollen Sie lieber mit einer neuen Migration vorwärts, statt Down-Migrationen zu nutzen, die Spalten und damit Daten löschen. Und lassen Sie sich von der Datenbank über Sperren informieren: In PostgreSQL blockiert ein ALTER TABLE, das auf seine Sperre wartet, jede danach eintreffende Abfrage; setzen Sie also ein kurzes lock_timeout und versuchen Sie es erneut, statt sich hinter einem lang laufenden Bericht einzureihen.

Große Backfills und Online-Schemaänderungen

Ein Backfill füllt eine neue Spalte oder Tabelle für bestehende Zeilen. Als ein einziges UPDATE über 400 Millionen Transaktionen hält er stundenlang Sperren, erzeugt riesige Mengen Write-Ahead-Log, die Replikate nachspielen müssen (was Replikationsverzögerung verursacht), und hinterlässt in PostgreSQL tote Zeilenversionen, die Vacuum aufräumen muss. Der professionelle Ansatz ist ein separater, neu startbarer Job, der kleine Batches nach Primärschlüsselbereich verarbeitet, jeden Batch committet, seinen Fortschritt festhält, sich drosselt, wenn Replikationsverzögerung oder Datenbanklast steigen, und idempotent ist, sodass ein erneut ausgeführter Batch keinen Schaden anrichtet. Führen Sie ihn außerhalb des Migrationswerkzeugs aus, damit ein Deployment nie auf ihn wartet.

Manche Änderungen lassen sich nicht ohne Sperren an Ort und Stelle vornehmen. PostgreSQL bietet CREATE INDEX CONCURRENTLY (das nicht innerhalb eines Transaktionsblocks laufen kann, was man Migrationswerkzeugen mitteilen muss) und die oben gezeigte Abfolge NOT VALID, dann VALIDATE CONSTRAINT. Für MySQL bauen Werkzeuge wie gh-ost und pt-online-schema-change eine Schattenkopie der Tabelle mit dem neuen Schema, halten sie mit laufenden Schreibvorgängen synchron und tauschen sie mit einer kurzen Umbenennung ein. Den Typ einer Spalte ändert man meist am besten per Expand and Contract auf eine neue Spalte statt mit einem ALTER an Ort und Stelle, das in den meisten Datenbanken die ganze Tabelle neu schreibt.

Schema Registries, Avro und Protocol Buffers

Für Events und Nachrichten entspricht dem Migrationswerkzeug eine Schema Registry. Producer registrieren jede Schemaversion; die Registry prüft sie gegen die konfigurierte Kompatibilitätsregel und weist eine inkompatible Änderung beim Build oder Deployment zurück statt erst in der Produktion. Jede Nachricht trägt eine kurze Schemakennung, sodass Consumer genau das Schema abrufen können, mit dem sie geschrieben wurde.

Apache Avro macht das Schema des Schreibers explizit: Zum Dekodieren braucht man sowohl das Schema des Schreibers als auch das des Lesers, und Avros Auflösungsregeln ordnen Felder über den Namen zu, ignorieren Felder des Schreibers, die der Leser nicht kennt, und füllen Leserfelder, die in den Daten fehlen, mit ihren deklarierten Standardwerten. Daraus folgt die praktische Regel: Geben Sie jedem Feld, das Sie hinzufügen oder entfernen könnten, einen Standardwert. Im Beispiel unten wurden currency und tip_pence in späteren Versionen mit Standardwerten hinzugefügt, sodass alte Events mit neuen Consumern weiterhin dekodiert werden.

Protocol Buffers identifizieren Felder auf dem Übertragungsweg über ihre Nummer, nicht über den Namen. Felder hinzuzufügen ist sicher, und alte Leser behalten unbekannte Felder beim erneuten Serialisieren. Das Umbenennen eines Felds wirkt sich nicht auf die binäre Kodierung aus (wohl aber auf die JSON-Form). Die obersten Regeln lauten, eine Feldnummer nie zu ändern oder wiederzuverwenden und die Nummern und Namen gelöschter Felder als reserved zu markieren, damit niemand sie versehentlich wiederverwendet. Confluent empfiehlt für Protobuf rückwärts-transitive Kompatibilität (backward transitive), weil das Hinzufügen eines neuen Nachrichtentyps nicht aufwärtskompatibel ist. Auch JSON Schema lässt sich registrieren und prüfen, aber die Offenheit von JSON (ist eine unbekannte Eigenschaft erlaubt oder nicht?) macht die Kompatibilitätsregeln subtiler.

{
  "type": "record",
  "name": "OrderPlaced",
  "namespace": "food.orders.v1",
  "fields": [
    {"name": "order_id",       "type": "string"},
    {"name": "restaurant_id",  "type": "string"},
    {"name": "total_pence",    "type": "long"},
    {"name": "currency",       "type": "string", "default": "GBP"},
    {"name": "tip_pence",      "type": ["null", "long"], "default": null}
  ]
}
Ein Avro-Schema für ein OrderPlaced-Event; nach Version 1 hinzugefügte Felder haben Standardwerte, sodass neue Consumer alte Events lesen können.

API- und Event-Schemas weiterentwickeln

APIs und Events folgen denselben Prinzipien mit anderen Werkzeugen. Bevorzugen Sie bei APIs additive Änderungen, verlangen Sie von Clients, dass sie Tolerant Reader sind, und heben Sie inkompatible Änderungen für eine neue Version auf, die neben der alten läuft, mit Nutzungsmonitoring, um zu sehen, wann die alte Version abgeschaltet werden kann (Abschnitt 6.5 behandelt Versionierungsansätze). Mit Consumer-Driven-Contract-Tests veröffentlicht jeder Consumer die Felder, auf die er sich tatsächlich verlässt, sodass der Provider weiß, welche Änderungen sicher sind.

Events sind schwieriger als APIs, weil sie gespeichert werden: Ein Consumer kann ein Topic von Anfang an erneut abspielen, und ein System mit Event-Sourcing (Abschnitt 7.5) baut seinen Zustand aus Events auf, die vor Jahren geschrieben wurden. Behandeln Sie ein veröffentlichtes Event-Schema als öffentlichen Vertrag, versionieren Sie es in der Registry, und wenn eine Änderung wirklich inkompatibel ist, veröffentlichen Sie für eine gewisse Zeit einen neuen Event-Typ oder ein neues Topic (OrderPlaced.v2) neben dem alten, statt das bestehende zu verändern. Abschnitt 6.4 behandelt das Event-Design allgemeiner.

Checkliste: bevor Sie eine Schemaänderung ausliefern

  • Ist die Änderung additiv?Wenn nicht, zerlegen Sie sie in Expand-, Migrate- und Contract-Schritte, die jeweils für sich deploybar sind.
  • Funktioniert jeder deployte Zustand mit der vorigen und der nächsten Codeversion?Wegen Rolling Deployments und Rollbacks laufen immer zwei Versionen gleichzeitig.
  • Wer liest diese Daten sonst noch?Suchen Sie nach Berichten, Extraktionsjobs, anderen Services und Event-Consumern; prüfen Sie die Abonnenten in der Registry und die Lineage im Datenkatalog.
  • Wurde die Migration mit Daten in Produktionsgröße ausgeführt?Messen Sie Sperrdauer, Tabellen-Rewrites und Log-Volumen; nutzen Sie, wo verfügbar, nebenläufige Indexerstellung und NOT-VALID-Constraints.
  • Ist ein Lock-Timeout gesetzt?Schnell scheitern und erneut versuchen, statt ein wartendes ALTER den gesamten Verkehr blockieren zu lassen.
  • Ist der Backfill separat, in Batches, gedrosselt, idempotent und fortsetzbar?Er sollte nie ein Deployment blockieren oder Replikate überfluten.
  • Haben neue Felder Standardwerte, und werden keine Feldnummern oder -namen wiederverwendet?Unverzichtbar für Avro und Protocol Buffers; durch die Kompatibilitätsprüfung der Registry erzwungen.
  • Hat sich die Bedeutung eines Felds geändert?Änderungen von Einheit, Währung oder Semantik brauchen einen neuen Feldnamen, nie einen wiederverwendeten.
  • Wird der Contract-Schritt nachverfolgt?Legen Sie ein Ticket oder ADR an, damit die alte Struktur tatsächlich entfernt wird, sobald sie ungenutzt ist.

Prüfen Sie Ihr Verständnis

Ein Team fügt einem Avro-Schema ein Pflichtfeld ohne Standardwert hinzu. Welche Kompatibilität bricht das, und warum?

Die Abwärtskompatibilität: Ein Consumer mit dem neuen Schema, der alte Daten liest, findet das Feld nicht und hat keinen Standardwert zum Einsetzen.

Warum geht der Dual-Write-Schritt weiter, nachdem das Lesen auf die neue Spalte umgestellt wurde?

Damit ein Rollback auf das vorige Release, das noch die alte Spalte liest, diese Spalte auf aktuellem Stand vorfindet.

Warum sollte ein großer Backfill nicht als eine einzige UPDATE-Anweisung in einer Migration geschrieben werden?

Eine riesige Transaktion hält lange Sperren, erzeugt einen Log-Schub, der Replikationsverzögerung verursacht, hinterlässt viel Aufräumarbeit, lässt sich nicht fortsetzen und blockiert das Deployment; kleine, committete Batches vermeiden all das.

Was verhindert es, eine Feldnummer in Protocol Buffers als reserved zu markieren?

Es verhindert, dass jemand die Nummer eines gelöschten Felds für ein neues Feld wiederverwendet, wodurch alte Daten als falsches Feld dekodiert würden.

Weiter · 7.8Analytics und StreamingWie analytische Daten architektonisch aufgebaut werden: OLTP und OLAP im Vergleich, Data Warehouses und Sternschemata, ETL und ELT, Data Lakes und Lakehouses, Stream Processing mit Ereigniszeit und Watermarks, Lambda und Kappa sowie Data Mesh.Weiter →

Quellen und weiterführende Literatur

Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.

Fragen: „Wir müssen die Spalte mit dem Kundennamen in einer Tabelle mit 50 Millionen Zeilen ohne Ausfallzeit in Vor- und Nachnamen aufteilen. Welche Schritte und Releases würden Sie planen?“