8 Min

Schemaänderungen ohne Ausfallzeit mit dem Expand/Contract-Muster

Planen und veröffentlichen Sie Schemaänderungen ohne Ausfallzeit mit dem Expand/Contract-Muster, sicheren Backfills, kompatiblen Releases, Prüfung und Rollback.

Schemaänderungen ohne Ausfallzeit mit dem Expand/Contract-Muster

Warum Schemaänderungen Ausfälle verursachen

Schemaänderungen führen zu Ausfällen, wenn Anwendungsversionen, Hintergrund-Worker und die Datenbank nicht mehr darin übereinstimmen, welche Strukturen und Werte gültig sind. Der Fehler kann sofort sichtbar werden, etwa wenn jede Anfrage einen Fehler zurückgibt. Er kann sich auch schleichend zeigen, etwa durch steigende Abfragelatenz, fehlgeschlagene Schreibvorgänge, Replica-Lag und eine Warteschlange von Jobs, die erneut ausgeführt werden müssen.

Ein Produktions-Deployment ändert selten jeden Prozess gleichzeitig. Bei Rolling Releases laufen alte und neue Anwendungsinstanzen parallel. Langlebige Worker können stundenlang einen älteren Build behalten, mobile Clients monatelang aktiv sein, und Berichts- oder Integrationsjobs können Tabellen nutzen, ohne durch die Hauptanwendung zu gehen. Sie alle teilen sich eine Datenbank.

Häufige Fehlerbilder sind:

  • Neuer Code schreibt in eine Spalte, bevor die Migration zum Erstellen der Spalte abgeschlossen ist.
  • Alter Code liest eine Tabelle oder Spalte, die ein späteres Release umbenannt oder entfernt hat.
  • Ein Table Rewrite, Backfill oder Indexaufbau verbraucht genug I/O und CPU, um den normalen Datenverkehr zu verlangsamen.
  • Ein Schema-Befehl wartet auf eine Sperre, während sich Anfragen dahinter stauen.
  • Ein neues Constraint lehnt Schreibvorgänge eines Prozesses ab, der noch nicht aktualisiert wurde.

Der gefährliche Teil ist oft das Erlangen einer Sperre, nicht die eigentliche Ausführungszeit. Ein schnelles ALTER TABLE kann hinter einer langen Transaktion warten. Währenddessen können spätere Abfragen hinter der ausstehenden Schema-Sperre in eine Warteschlange geraten. So wird aus einer kleinen Migration ein Stillstand der gesamten Anwendung.

Keine Ausfallzeit verlangt, dass jeder Zwischenzustand der Datenbank für jede Anwendungsversion nutzbar bleibt, die noch laufen kann. Fügen Sie zuerst kompatible Strukturen hinzu, verschieben Sie Datenverkehr und Daten in kontrollierten Schritten und entfernen Sie den alten Pfad erst, wenn sein letzter Consumer verschwunden ist.

Dieser Aufwand lohnt sich bei Systemen mit Live-Traffic, Rolling Deployments, strengen Verfügbarkeitszielen oder aufwendigen Wiederherstellungsabläufen. Ein kleines internes Tool mit einer ruhigen Datenbank ist mit einem getesteten Wartungsfenster möglicherweise besser bedient. Entscheidend sind die Kosten eines Ausfalls und die operative Komplexität der Migration.

Expand/Contract einfach erklärt

Das Expand/Contract-Muster macht aus einer inkompatiblen Änderung eine Folge kompatibler Releases. Die Datenbank unterstützt vorübergehend zwei Darstellungen, während Code und Daten von der alten zur neuen wechseln.

Die Abfolge hat drei Teile:

  • Expand: Fügen Sie Spalten, Tabellen, Indizes oder Constraints hinzu, ohne etwas zu entfernen, das der aktuelle Code braucht.
  • Übergang: Stellen Sie kompatiblen Code bereit, übertragen Sie historische Daten und leiten Sie Lese- und Schreibzugriffe auf die neue Darstellung.
  • Contract: Löschen Sie alten Code und Datenbankobjekte erst, wenn Prüfungen zeigen, dass sie nicht mehr verwendet werden.

Angenommen, eine PostgreSQL-Tabelle speichert den Namen einer Person in full_name, die Anwendung benötigt aber getrennte Felder first_name und last_name. In der Expand-Phase kommen nullable Spalten hinzu, während full_name erhalten bleibt. Ein kompatibles Release schreibt die Darstellungen, die während des Übergangs nötig sind. Ein Backfill trennt bestehende Werte und folgt dabei einer klaren Regel für Namen, die sich nicht zuverlässig aufteilen lassen. Erst wenn die neuen Felder ausreichend vollständig sind, wechseln die Lesezugriffe. Später entfernt die Contract-Phase full_name.

Diese Reihenfolge passt zu Rolling Deployments, weil der alte Build weiterhin full_name findet und der neue alle drei Spalten vorfindet. Sie erhält auch einen Weg für ein Anwendungs-Rollback. Verhält sich das neue Release falsch, kann der vorherige Build laufen, weil seine Schema-Abhängigkeiten nicht entfernt wurden.

Ein Datenbank-Rollback unterscheidet sich von einem Anwendungs-Rollback. Wenn Sie eine Migration zurückdrehen, nachdem Daten umgewandelt wurden, können Informationen verloren gehen oder veraltete Werte wiederhergestellt werden. Während des Übergangs sollten Sie Anwendungs-Traffic lieber auf die bewährte Darstellung zurückführen und additive Datenbankobjekte bestehen lassen. Korrigieren Sie die vorwärts gerichtete Migration, sobald der Vorfall stabil ist.

Das Muster bedeutet nicht, dass jede Änderung Dual-Write-Code braucht. Eine optionale Spalte, die nur neuer Code nutzt, benötigt möglicherweise nur eine additive Migration und ein Deployment. Umbenennungen, Änderungen der Darstellung, das Aufteilen von Tabellen und Änderungen an Pflichtfeldern brauchen meist mehr Phasen, weil zwei Anwendungsversionen das Schema sonst nicht sicher gemeinsam nutzen können.

Die Änderung vor der Planung einordnen

Der Migrationsplan sollte zu den tatsächlichen Risiken der Operation bei Sperren, Rewrites, Kompatibilität und Datenkonvertierung passen. Jedes ALTER TABLE gleich zu behandeln führt entweder zu unnötigem Aufwand oder zu einem unsicheren Release.

Additive Änderungen sind meist am einfachsten. Eine nullable Spalte, eine separate Tabelle oder ein mit einer Online-Methode erstellter Index lassen sich oft einführen, bevor Anwendungscode sie nutzt. Der Befehl benötigt trotzdem eine Sperre. Testen Sie sein Verhalten deshalb mit einer Tabelle und Transaktionslast, die der Produktion ähneln.

Destruktive Änderungen umfassen das Löschen oder Umbenennen von Spalten, das Einschränken von Typen, das Ersetzen von Tabellen und strengere Constraints. Sie machen eine Annahme ungültig, auf die bestehender Code angewiesen ist. Verschieben Sie sie in die Contract-Phase, nachdem Code-Referenzen und externe Consumer entfernt sind.

Operationen, die Daten verändern, verdienen eine eigene Bewertung. Das Umwandeln von Zeitstempeln, Normalisieren von Telefonnummern, Zusammenführen von Datensätzen oder Aufteilen von Freitext kann Informationen verlieren. Legen Sie vor dem Backfill fest, wie Sie ungültige und mehrdeutige Werte behandeln. Lässt sich eine Umwandlung nicht rückgängig machen, bewahren Sie die Quelle auf, bis das Ergebnis fachliche Prüfungen bestanden hat.

Eine hilfreiche Vorabprüfung beantwortet fünf Fragen:

  • Welche Sperre verlangt jede Anweisung, und wie lange kann sie darauf warten oder sie halten?
  • Schreibt die Operation die Tabelle neu, erzeugt sie viel WAL oder erhöht sie Replica-Lag?
  • Welche Anwendungen, Jobs, Berichte und Change-Data-Capture-Consumer nutzen die betroffenen Objekte?
  • Können aktuelles und geplantes Release mit jedem Übergangszustand laufen?
  • Welches Signal pausiert die Operation, und welcher genaue Zustand bleibt nach dem Anhalten bestehen?

Führen Sie die exakte Migration auf Daten mit realistischem Volumen und realistischer Verteilung aus. Eine Testtabelle mit tausend ordentlich gepflegten Zeilen sagt wenig über eine Produktionstabelle mit hunderten Millionen Zeilen, breiten Tupeln, toten Zeilen, ungleich verteilten Werten und lang laufenden Transaktionen aus.

PostgreSQL sicher erweitern

Eine sichere PostgreSQL-Expand-Phase nutzt kurze Metadatenänderungen, begrenzte Lock-Wartezeiten und getrennte Online-Operationen, wenn die Datenbank sie verlangt. Fügen Sie die neue Struktur hinzu, bevor Sie Code bereitstellen, der von ihr abhängt.

Das Hinzufügen einer nullable Spalte ohne Standardwert ist meist eine kurze Metadatenoperation:

BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Das Timeout verhindert, dass das Release unbegrenzt hinter einer offenen Transaktion wartet. Kann die Sperre nicht zeitnah erlangt werden, lassen Sie die Migration fehlschlagen, untersuchen Sie den Blocker und wiederholen Sie sie zu einem sichereren Zeitpunkt. Wiederholen Sie sie nicht automatisch in einer engen Schleife, denn wiederholte Lock-Anfragen können den Produktionsverkehr weiter stören.

Moderne PostgreSQL-Versionen können eine Spalte mit einem konstanten Standardwert hinzufügen, ohne diesen Wert sofort in jede bestehende Zeile zu schreiben. Diese Optimierung macht nicht jeden Standardwert unbedenklich. Ein flüchtiger Ausdruck kann einen Rewrite erfordern, und ALTER TABLE benötigt weiterhin kurzzeitig eine ACCESS EXCLUSIVE-Sperre. Prüfen Sie das Verhalten für die eingesetzte PostgreSQL-Version und den genauen Ausdruck, statt sich auf eine allgemeine Regel zu verlassen.

Ein normales CREATE INDEX kann Schreibvorgänge blockieren. Nutzen Sie die gleichzeitige Erstellung, wenn die Tabelle beschreibbar bleiben muss:

CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);

CREATE INDEX CONCURRENTLY darf nicht innerhalb eines Transaktionsblocks laufen. Es dauert länger, erzeugt zusätzliche Arbeit und kann auf ältere Transaktionen warten, doch normale Inserts, Updates und Deletes können weiterlaufen. Es verbraucht trotzdem CPU, I/O und WAL. Überwachen Sie daher Datenbanklatenz und Replicas während der Ausführung.

Ein fehlgeschlagener gleichzeitiger Aufbau kann einen ungültigen Index zurücklassen. Prüfen Sie vor dem Wiederholen den Indexzustand und entfernen oder bauen Sie das ungültige Objekt bewusst neu. Migrationstools, die jede Datei in eine Transaktion einschließen, brauchen einen unterstützten nichttransaktionalen Modus für gleichzeitige Indexoperationen.

Neue Tabellen lassen sich oft einfacher einführen als In-Place-Transformationen. Fügen Sie bei einer Eins-zu-viele- oder Viele-zu-viele-Beziehung die Zieltabelle und ihre Indizes hinzu, während die Quellspalte erhalten bleibt. Löschen Sie die Quelle erst, wenn neue Schreibvorgänge, historische Daten, Lesezugriffe und nachgelagerte Consumer umgestellt sind.

Typänderungen erfordern besondere Sorgfalt. Manche betreffen nur Metadaten, andere schreiben jede Zeile neu oder halten eine restriktive Sperre zu lange. Bei einer riskanten Konvertierung fügen Sie eine Spalte mit dem Zieltyp hinzu, füllen sie in Batches, stellen den Anwendungszugriff um und löschen das Original später. So kann das Team auch Konvertierungsfehler festhalten, statt ein großes ALTER COLUMN TYPE nur als Ganzes gelingen oder scheitern zu lassen.

Kompatiblen Code bereitstellen

Kompatibler Anwendungscode kommt mit fehlenden Übergangswerten zurecht und verlangt während desselben Rollouts nie eine destruktive Migration. Die Datenbankerweiterung sollte abgeschlossen sein, bevor die erste Anwendungsinstanz das neue Objekt nutzt.

Paralleles Schreiben ist sinnvoll, wenn beide Darstellungen aktuell bleiben müssen. Führen Sie beide Schreibvorgänge nach Möglichkeit in derselben Datenbanktransaktion aus. Ein asynchroner zweiter Schreibvorgang kann fehlschlagen, nachdem der erste erfolgreich war. Dadurch entsteht eine Abweichung, die spätere Lesezugriffe sichtbar machen können.

Die Dual-Write-Logik braucht außerdem eine eindeutige Quelle der Wahrheit. Wenn phone_e164 aus phone abgeleitet wird, legen Sie fest, welche Eingabe gewinnt, wenn beide geliefert werden, und wenden Sie in API-Handlern, Workern, Importen und Administrations-Tools dieselbe Normalisierung an. Sonst können zwei scheinbar korrekte Codepfade unterschiedliche Ergebnisse speichern.

Lesezugriffe sollten später wechseln als Schreibvorgänge. Lassen Sie Lesezugriffe auf dem etablierten Feld, während neue Schreibvorgänge beide Formen füllen und der Backfill historische Zeilen bearbeitet. Nach der Prüfung stellen Sie einen Lesepfad bereit, der das neue Feld bevorzugt und den alten Wert nur nach einer klar definierten Fallback-Regel nutzt. Messen Sie die Fallback-Nutzung. Ein stiller Fallback kann unvollständige Daten dauerhaft verbergen.

Eine typische Release-Abfolge lautet:

  • Release 1 fügt die neuen Datenbankobjekte hinzu, ohne das Anwendungsverhalten zu ändern.
  • Release 2 schreibt die Übergangsdarstellungen und nutzt weiter die etablierten Lesezugriffe.
  • Release 3 schaltet Lesezugriffe um, nachdem Backfill und Konsistenzprüfungen erfolgreich waren.
  • Release 4 pflegt die alte Darstellung nicht weiter, sobald die Rollback-Kriterien abgelaufen sind.
  • Release 5 entfernt alte Code-Referenzen, später folgt die Bereinigung der Datenbank.

Halten Sie öffentliche API-Verträge getrennt von physischen Schemaänderungen. Eine umbenannte Datenbankspalte verlangt nicht sofort eine Umbenennung eines Felds in Web-, Mobil- oder Integrationsantworten. Ändern Sie solche Verträge nach ihrer eigenen Kompatibilitätsregel, besonders wenn Clients nicht zusammen mit dem Server aktualisiert werden können.

Erfassen Sie jeden Writer. HTTP-Handler sind nur eine Quelle für Änderungen. Queue-Consumer, geplante Jobs, Importskripte, Datenreparatur-Tools, Datenbank-Trigger und direkte Administrationsvorgänge können weiterhin Zeilen im alten Format erzeugen. Kennzeichnen Sie Datenbankverbindungen, wo praktikabel, mit einem Anwendungsnamen und protokollieren Sie die Nutzung von Übergangspfaden, damit übersehene Prozesse sichtbar werden.

Langlebige Prozesse können durch vorbereitete Statements, zwischengespeicherte Metadaten oder eine Object-Relational-Mapping-Schicht veraltete Annahmen bewahren. Testen Sie Rolling Restarts und das Verhalten des Connection Pools vor der Contract-Phase. Ein Prozess, der kürzlich keinen Traffic verarbeitet hat, kann beim ersten seltenen Job trotzdem scheitern.

Daten auffüllen, ohne die Datenbank zu überlasten

Schemas sicherer ändern
Verwandeln Sie Ihren Expand-Contract-Plan in konkrete Aufgaben und Prüfpunkte, bevor Sie die Produktion ändern.

Ein sicherer Backfill aktualisiert kleine, fortsetzbare Batches und wird langsamer, wenn sich der Zustand der Produktion verschlechtert. Er beginnt erst, nachdem Live-Writer die neue Darstellung pflegen können.

Wählen Sie Batches nach Laufzeit und Datenbankauswirkung, nicht nach einer universellen Zeilenzahl. Tausend schmale Zeilen können in Millisekunden fertig sein, während tausend Zeilen mit großen Werten oder aufwendigen Umwandlungen erhebliches I/O erzeugen. Beginnen Sie vorsichtig und zielen Sie auf Transaktionen, die in Sekunden enden. Committen Sie zwischen den Batches, damit sich Sperren und alte Zeilenversionen nicht in einer Transaktion ansammeln.

PostgreSQL unterstützt ORDER BY und LIMIT nicht direkt bei einem einfachen UPDATE. Wählen Sie einen Batch in einem Common Table Expression aus und aktualisieren Sie dann diese Zeilen:

WITH batch AS (
    SELECT id
    FROM my_table
    WHERE id > $1
      AND new_col IS NULL
    ORDER BY id
    LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
  AND target.new_col IS NULL
RETURNING target.id;

Die Anwendung speichert die größte abgeschlossene id als Cursor. Das bedingte Update macht Wiederholungen idempotent. Ein Absturz nach dem Commit beschädigt daher keine bereits bearbeiteten Zeilen. Speichern Sie den Fortschritt sorgfältig genug, damit der Cursor nicht über einen nicht bestätigten Batch hinausläuft.

Ein steigender id-Cursor verhindert, dass der Anfang der Tabelle immer wieder gescannt wird. Er erfasst aber keine späten Korrekturen oder Zeilen, die unterhalb des Cursors eingefügt wurden. Schließen Sie mit einem Nachhol-Lauf über alle verbleibenden NULL-Werte ab. Wenn Kennungen nicht geordnet sind oder Zeilen zwischen Zuständen wechseln können, verwenden Sie eine Arbeitstabelle oder einen anderen expliziten Prüfpunkt, statt anzunehmen, dass ein Vorwärtsscan vollständig ist.

Mehrere Worker können Zeilen mit FOR UPDATE SKIP LOCKED beanspruchen. Parallelität erhöht jedoch den Schreibdruck und erschwert die Fortschrittsverfolgung. Kombinieren Sie übersprungene Zeilen nicht mit einem Cursor, der dauerhaft an ihnen vorbeizieht. Eine Warteschlange mit beanspruchten Kennungen oder ein wiederholter Eignungsscan ist für parallele Worker sicherer.

Drosseln Sie anhand von Produktionsmesswerten wie Abfragelatenz, aktiven Verbindungen, Lock-Wartezeiten, WAL-Erzeugung, Replica-Replay-Verzögerung und Wachstum toter Zeilen. Pausieren Sie bei einem überschrittenen Grenzwert und setzen Sie am Prüfpunkt fort. Feste Pausen sind einfach, doch Feedback aus der Datenbank reagiert besser auf veränderten Traffic.

Ändern Sie nicht jede Zeile, wenn nur einige bearbeitet werden müssen. Filtern Sie nach dem neuen Feld, dem Quellzustand oder einer Migrationsmarkierung. Ist die Transformation aufwendig, berechnen Sie sie dort, wo die Konsistenz es erlaubt, außerhalb der Update-Transaktion und führen Sie danach einen kurzen bedingten Schreibvorgang aus. Führen Sie statt Daten stillschweigend zu erfinden eine Anzahl und Stichprobe abgelehnter Werte.

Autovacuum und Replicas müssen die Arbeit nach jedem Update auffangen. Ein Backfill kann auf dem Primary erfolgreich enden, während Replicas stark zurückfallen oder Table Bloat spätere Abfragen verschlechtert. Rate Limits sollten diese verzögerten Kosten berücksichtigen, nicht nur die unmittelbare Laufzeit eines Batches.

Daten und Produktions-Traffic prüfen

Eine Migration ist erst für die Contract-Phase bereit, wenn Datenprüfungen, Anwendungstelemetrie und Belege zu Abhängigkeiten gemeinsam zeigen, dass der neue Pfad maßgeblich ist. Ein abgeschlossener Jobzähler allein beweist keine Korrektheit.

Beginnen Sie mit Vollständigkeit und Konsistenz. PostgreSQLs IS DISTINCT FROM vergleicht Werte und behandelt NULL ausdrücklich, anders als <>, das bei NULL auf einer der beiden Seiten ein unbekanntes Ergebnis liefert:

SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;

Führen Sie bei einer stark genutzten, sehr großen Tabelle nicht wiederholt einen unindizierten Full-Table-Count aus. Nutzen Sie eine einmalige kontrollierte Validierung, begrenzte Kennungsbereiche, Stichproben oder einen temporären Prüfprozess, der die Tabelle schrittweise durchläuft. Die passende Methode hängt von den Kosten eines Fehlers und den verfügbaren Datenbankreserven ab.

Die Prüfung sollte Folgendes abdecken:

  • Für Zeilen, die das neue Feld benötigen, bleiben keine unerwartet fehlenden Werte.
  • Der neue Wert entspricht der vereinbarten Transformation, einschließlich fehlerhafter und leerer Eingaben.
  • Neue Zeilen und Updates bleiben konsistent, nachdem der historische Durchlauf abgeschlossen ist.
  • Die Nutzung des Lese-Fallbacks hat den geplanten Grenzwert erreicht, bei servergesteuertem Traffic meist null.
  • Fehlerraten, Abfragelatenz, Sperren und Replica-Verzögerung bleiben innerhalb der Release-Grenzen.

Vergleichen Sie auch fachliche Ergebnisse, nicht nur Spalten. Wenn eine Migration Preise, Berechtigungen, Kontostatus oder Kennungen ändert, validieren Sie Summen und Invarianten, von denen Nutzer abhängen. Zwei Spalten können technisch übereinstimmen und dennoch beide die falsche Geschäftsregel abbilden.

Beobachten Sie vor der Bereinigung einen vollständigen Betriebszyklus. Das richtige Intervall ergibt sich aus dem tatsächlichen Systemverhalten, nicht aus einer festen Ein-Wochen-Regel. Dazu können Monatsabschlussprozesse, ein seltener Abrechnungsjob, verzögerte Queue-Wiederholungen oder die maximale Laufzeit eines älteren mobilen Clients gehören. Dokumentieren Sie die Belege dafür, dass jeder Consumer umgestellt ist.

Führen Sie den Wechsel des Lesepfads als Canary aus, wenn die Anwendungsarchitektur es zulässt. Leiten Sie einen kleinen Teil des Traffics auf den neuen Lesepfad, vergleichen Sie die Ergebnisse und erweitern Sie schrittweise. Halten Sie die Rollback-Aktion einfach: Leiten Sie Lesezugriffe auf die etablierte Darstellung zurück, ohne den Backfill umzukehren.

Constraints hinzufügen, wenn die Daten bereit sind

Constraints sollten erst streng werden, wenn alle Writer die Regeln einhalten und bestehende Daten validiert sind. NOT NULL, einen Check oder einen Fremdschlüssel schon in der Expand-Phase durchzusetzen kann Traffic blockieren oder Schreibvorgänge eines älteren Prozesses ablehnen.

PostgreSQL kann ein Check-Constraint als NOT VALID hinzufügen. Damit gilt die Regel für neue oder geänderte Zeilen, ohne sofort alle historischen Zeilen zu scannen. Validieren Sie es nach dem Backfill separat:

ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;

ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;

Nach erfolgreicher Validierung können unterstützte PostgreSQL-Versionen diesen Nachweis nutzen, wenn die Spalte auf NOT NULL gesetzt wird. Das vermeidet einen weiteren vollständigen Tabellenscan. Die abschließende Änderung benötigt weiterhin eine starke Tabellensperre. Nutzen Sie daher ein begrenztes Lock-Timeout und einen Wiederholungsplan:

ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;

ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;

Der temporäre Check kann bestehen bleiben, wenn er einen Nutzen hat. Gleichwertige Constraints erhöhen jedoch nur den Katalogaufwand, ohne die Regel zu ändern.

Fremdschlüssel können mit NOT VALID und VALIDATE CONSTRAINT ähnlich eingeführt werden. Neue Schreibvorgänge werden nach dem Erstellen des Constraints geprüft, historische Daten später validiert. Fügen Sie einen unterstützenden Index bewusst hinzu, wenn das Lösch- oder Update-Verhalten der referenzierten Beziehung sonst teure Scans auslösen würde.

Die Anwendungsvalidierung sollte der Datenbankdurchsetzung vorausgehen, ersetzt sie aber nicht. Code erzeugt verständlichere Fehlermeldungen für Nutzer, während die Datenbank Daten schützt, die über jeden Pfad geschrieben werden. Beobachten Sie während des Rollouts Constraint-Verletzungen, um einen Writer zu finden, den die Abhängigkeitsprüfung übersehen hat.

Den alten Pfad sicher zurückbauen

Kompatiblen Code veröffentlichen
Erstellen Sie eine React- und Go-App und stimmen Sie Migrationen auf die einzelnen Rollout-Schritte ab.

Die Contract-Phase sollte Anwendungsabhängigkeiten entfernen, bevor sie Datenbankobjekte entfernt. Sobald Telemetrie und Prüfungen zeigen, dass der neue Pfad maßgeblich ist, kann die Bereinigung über getrennte Releases erfolgen.

Beenden Sie zuerst das Lesen des alten Felds und entfernen Sie die Fallback-Logik. Deaktivieren Sie anschließend alte Schreibvorgänge und beobachten Sie die Produktion lange genug, um seltene Pfade zu erfassen. Entfernen Sie Feature Flags, Trigger, Kompatibilitäts-Views, Reparaturskripte und geplante Jobs, die die alte Darstellung erwähnen. Durchsuchen Sie exportierten Quellcode und Migrationscode, prüfen Sie aber auch Berichte, Integrationsabfragen und Change-Data-Capture-Konfigurationen außerhalb des Haupt-Repositorys.

Eine sichere Reihenfolge für die Bereinigung ist:

  • Entfernen Sie Fallback-Lesezugriffe und bestätigen Sie, dass sie nicht mehr in der Telemetrie erscheinen.
  • Stoppen Sie alte Schreibvorgänge und löschen Sie Synchronisierungscode.
  • Entfernen Sie Anwendungsreferenzen aus allen bereitstellbaren Versionen.
  • Löschen Sie veraltete Indizes und Constraints mit der passenden Online-Methode.
  • Löschen Sie die alte Spalte oder Tabelle in einem späteren Datenbank-Release.

Das Löschen einer PostgreSQL-Spalte ist hauptsächlich eine Katalogänderung, benötigt aber trotzdem eine ACCESS EXCLUSIVE-Sperre. Daher kann eine kurze Anweisung hinter einer langen Transaktion warten und spätere Arbeit blockieren. Setzen Sie ein Lock-Timeout, prüfen Sie vorab lang laufende Transaktionen und planen Sie den Versuch in einer risikoärmeren Zeit.

Verwenden Sie für einen veralteten Index DROP INDEX CONCURRENTLY, wenn blockierte Schreibvorgänge nicht akzeptabel wären. Wie die gleichzeitige Erstellung darf auch dieser Befehl nicht innerhalb eines Transaktionsblocks laufen und hat Einschränkungen, die Migrationstools berücksichtigen müssen.

Fassen Sie Code-Bereinigung und physisches Löschen nicht in einem Release zusammen. Die Trennung erlaubt der bereinigten Anwendung, gegen eine Datenbank zu laufen, die das ungenutzte Objekt noch enthält. Tritt ein Anwendungsproblem auf, bleibt ein Rollback möglich, ohne das Schema neu anzulegen oder Daten zu rekonstruieren.

Prüfen Sie vor dem Löschen einer Tabelle die Eigentümerschaft von Sequenzen, Views, Funktionen, Berechtigungen, Triggern, Replikations-Publikationen und externen Abfragen. Verwenden Sie CASCADE in einer Produktionsmigration nicht als Abkürzung, denn dadurch können Abhängigkeiten entfernt werden, die nicht Teil der beabsichtigten Änderung waren.

Rollbacks und fehlgeschlagene Schritte behandeln

Die Rollback-Planung sollte für jede Phase eine sichere Aktion festlegen, statt sich auf eine allgemeine Down-Migration zu verlassen. Additive Objekte, Datenverschiebungen, Lesepfadwechsel und Löschungen haben unterschiedliche Wiederherstellungseigenschaften.

Kann die Expand-Phase ihre Sperre nicht erlangen, lassen Sie die Anwendung unverändert und wiederholen Sie den Versuch, nachdem die blockierende Transaktion behoben wurde. Schlägt ein gleichzeitiger Indexaufbau fehl, prüfen Sie, ob er einen ungültigen Index hinterlassen hat, und bereinigen Sie dieses Objekt vor einem neuen Versuch.

Erzeugt ein Backfill Last, pausieren Sie ihn. Bereits bestätigte, idempotente Batches können bestehen bleiben. Verringern Sie die Batchgröße oder Rate, beheben Sie die aufwendige Transformation und setzen Sie am Prüfpunkt fort. Millionen korrekter Updates zurückzudrehen erhöht meist das Risiko, ohne der Produktion bei der Erholung zu helfen.

Liefert ein neuer Lesepfad falsche Ergebnisse, leiten Sie Lesezugriffe auf die alte Darstellung zurück und behalten Sie die neuen Daten zur Diagnose. Setzen Sie Dual Writes nur fort, wenn sie nachweislich korrekt sind. Ist der Writer selbst fehlerhaft, deaktivieren Sie ihn oder rollen Sie die Anwendung zurück, bevor Sie betroffene Zeilen reparieren.

Nach der Contract-Phase kann die Wiederherstellung Datenrestaurierung erfordern, statt nur einen älteren Build bereitzustellen. Definieren Sie den Punkt ohne Rückkehr ausdrücklich. Erstellen Sie das nach der Wiederherstellungsrichtlinie des Systems erforderliche Backup oder den Snapshot, testen Sie die Wiederherstellung vor dem Release und bewahren Sie das alte Objekt, wenn die Speicherkosten es erlauben, für den vereinbarten Aufbewahrungszeitraum auf.

Schema-Befehle können transaktional sein, externe Effekte sind jedoch nicht immer abgedeckt. Gleichzeitige Indexoperationen, Queue-Nachrichten, Cache-Änderungen und Anwendungs-Deployments teilen keine atomare Transaktion. Das Runbook sollte den beobachtbaren Zustand nach jedem Teilfehler und den Befehl beschreiben, der von dort sicher fortsetzt.

Häufige Migrationsfallen vermeiden

Die meisten fehlgeschlagenen Migrationen ohne Ausfallzeit erzwingen den neuen Zustand zu früh oder übersehen einen Consumer des alten Zustands. Die folgenden Fallen verdienen vor der Freigabe eine ausdrückliche Prüfung.

  • NOT NULL hinzufügen, obwohl eine alte Anwendungsinstanz das Feld noch auslassen kann.
  • Einen großen Backfill in einer Transaktion ausführen und Sperren sowie Zeilenversionen zu lange behalten.
  • Eine Spalte wie eine additive Änderung umbenennen, obwohl alter Code noch ihren ursprünglichen Namen nutzt.
  • Lesezugriffe umschalten, bevor alle Schreibpfade und historischen Zeilen die neue Darstellung füllen.
  • Ein erfolgreiches Deployment als Beweis ansehen, dass Berichte, Worker, Replicas und Integrationen kompatibel sind.

Ein weiterer subtiler Fehler entsteht durch bidirektionale Synchronisierung. Ein Trigger kopiert old_col nach new_col, während Anwendungscode new_col zurück nach old_col kopiert. Unterschiede bei der Normalisierung oder Trigger-Reihenfolge können Schleifen erzeugen, beabsichtigte Werte überschreiben oder die Zuständigkeit unklar machen. Bevorzugen Sie eine Richtung und dokumentieren Sie, welche Darstellung in jedem Release maßgeblich ist.

Standardwerte können fehlende Aktualisierungen von Writern verbergen. Erhält eine neue Pflichtspalte einen leeren oder allgemeinen Standardwert, wirkt alter Code kompatibel, speichert aber semantisch ungültige Daten. Nutzen Sie einen nullable Übergang, wenn fehlende Werte für die Diagnose nützlich sind, und setzen Sie die eigentliche Regel erst durch, nachdem jeder Writer einen sinnvollen Wert liefert.

Ein Feature Flag macht einen inkompatiblen Schema-Befehl nicht von selbst sicher. Ein deaktivierter Codepfad kann weiterhin von einem älteren Prozess geladen, vorbereitet oder ausgeführt werden. Das Datenbankobjekt muss bestehen bleiben, bis keine bereitstellbare oder aktive Version es referenziert.

Auch die Zuständigkeit für die Migration zählt. Weisen Sie einer Person oder einem Team die Verantwortung für den Übergang bis zur Contract-Phase zu, einschließlich Prüfung und Entfernungsterminen. Andernfalls können temporäre Spalten, Flags und Synchronisierungsjobs monatelang bestehen bleiben und jede spätere Änderung verteuern.

Eine Telefonspalte ohne Ausfallzeit ersetzen

Backfills in kleinen Batches ausführen
Erstellen Sie einen einfachen Backfill-Job und passen Sie Batchgrößen an, ohne Ihr Team auszubremsen.

Das Ersetzen von customers.phone durch das normalisierte customers.phone_e164 verlangt eine additive Spalte, eine festgelegte Konvertierungsregel, kompatiblen Code, einen begrenzten Backfill, einen Lesepfadwechsel und verzögerte Bereinigung. Die Konvertierungsregel muss vor SQL stehen, weil sich nicht jeder gespeicherte Wert automatisch normalisieren lässt.

Klassifizieren Sie zunächst vorhandene Werte. Gültige Nummern lassen sich konvertieren, wenn der nötige Länderkontext bekannt ist. Leere Werte können NULL werden. Mehrdeutige oder fehlerhafte Nummern sollten in einen Ausnahmebericht kommen, statt geraten zu werden. Entscheiden Sie, ob das Produkt für jeden Kunden eine Telefonnummer verlangt, denn davon hängt ab, ob NOT NULL später sinnvoll ist.

Fügen Sie die Spalte mit einem kurzen Lock-Timeout hinzu:

BEGIN;
SET LOCAL lock_timeout = '2s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Stellen Sie Code bereit, der neue Eingaben normalisiert und phone sowie phone_e164 in einer Transaktion schreibt. Lassen Sie die Lesezugriffe zunächst auf phone. Aktualisieren Sie jeden Writer, einschließlich Kontoimporten, Support-Tools, Worker-Jobs und Tests, die Kunden-Fixtures erstellen.

Füllen Sie geeignete Zeilen in kurzen Transaktionen auf. Erfassen Sie die zuletzt verarbeitete Kennung, die Anzahl konvertierter und übersprungener Werte sowie den Grund jeder Fehlerkategorie. Drosseln Sie den Job anhand der Produktionslatenz und Replica-Verzögerung. Nach dem Vorwärtslauf scannen Sie erneut nach geeigneten NULL-Werten, um gleichzeitige Inserts oder nach einem Neustart übersprungene Zeilen zu erfassen.

Führen Sie Konsistenzprüfungen mit denselben Normalisierungsregeln wie die Anwendung aus und nehmen Sie anschließend manuell internationale Vorwahlen, Durchwahlen, leere Werte, doppelte Kontaktdatensätze und alte importierte Daten in Stichproben. Eine Zeilenanzahl belegt die Abdeckung, nicht aber eine korrekte Telefonnummer.

Stellen Sie einen Lesepfad bereit, der phone_e164 zurückgibt, sofern vorhanden, und phone nur für eine protokollierte Ausnahme nutzt. Überwachen Sie die Fallback-Nutzung und Normalisierungsfehler. Lösen Sie die verbleibenden Ausnahmen, statt den Fallback zu dauerhaftem Verhalten werden zu lassen.

Wenn das neue Feld maßgeblich ist, entfernen Sie den Fallback und beenden das Schreiben von phone. Beobachten Sie seltene Jobs und Integrations-Traffic über einen passenden Betriebszyklus. Fügen Sie das validierte Constraint nur hinzu, wenn die Produktregel es verlangt.

Entfernen Sie zuletzt Code-Referenzen auf phone. Löschen Sie dessen Indizes oder Constraints getrennt und entfernen Sie die Spalte dann in einer späteren Migration mit begrenzter Lock-Wartezeit. Scheitert der Lesepfadwechsel vor diesem Löschen, rollen Sie das Anwendungsverhalten zurück, während beide Spalten verfügbar bleiben.

Dieses Beispiel zeigt auch ein fachliches Problem, das Schema-Mechanik nicht lösen kann: Das Aufteilen oder Normalisieren von Daten, die Menschen eingegeben haben, ist nicht immer verlustfrei. Der Migrationsplan muss Ausnahmen bewahren und einer verantwortlichen Person eine Möglichkeit geben, sie zu lösen.

Jedes Release vor der Veröffentlichung prüfen

Eine Release-Checkliste sollte Kompatibilität belegen, die Auswirkung auf die Produktion begrenzen und die Wiederherstellungsaktion für die aktuelle Phase benennen. Bewahren Sie die Nachweise bei der Änderung auf, damit ein Operator die Absicht nicht während eines Vorfalls rekonstruieren muss.

Bestätigen Sie vor dem Deployment:

  • Die Anwendungsversion funktioniert mit dem Datenbankzustand vor und nach diesem Release.
  • Für Schema-Befehle, die hinter dem Traffic warten könnten, sind Lock- und Statement-Timeouts gesetzt.
  • Der Backfill- oder Validierungsjob verfügt über Fortschritts-, Pausen-, Fortsetzungs- und Rate-Limit-Steuerungen.
  • Dashboards decken Fehler, Latenz, Sperren, Datenbanklast, WAL und Replica-Verzögerung ab.
  • Die Rollback-Aktion wurde getestet und hängt nicht von einem bereits entfernten Objekt ab.

Halten Sie klare Abschlussbedingungen fest. Beispiele sind null neue Konsistenzfehler während eines vollständigen Jobzyklus, null Fallback-Lesezugriffe aus servergesteuertem Traffic, jeder bekannte Consumer aktualisiert und eine erfolgreiche kontrollierte Validierungsabfrage. Der Fortschritt in Prozent hilft während eines Backfills, doch 100 Prozent verarbeitet bedeutet nicht 100 Prozent korrekt.

Prüfen Sie die Reihenfolge der Migration unabhängig vom Code Review. Eine korrekte Sammlung aus SQL- und Anwendungsänderungen kann trotzdem scheitern, wenn das Deployment sie in der falschen Reihenfolge ausführt. Legen Sie fest, welcher Schritt erst beginnen darf, wenn ein anderer abgeschlossen ist.

Abbruchbedingungen sollten nach Möglichkeit numerisch sein. Definieren Sie akzeptable Abfragelatenz, Lock-Wartezeit, Replica-Verzögerung, Fehlerrate und Batchdauer. Wird ein Grenzwert überschritten, sollte der Operator wissen, ob ein Job zu pausieren, ein wartendes Statement abzubrechen oder Lesezugriffe umzuleiten sind, ohne während des Vorfalls erneut eine Freigabe einzuholen.

Die Migration ist erst abgeschlossen, wenn die neue Darstellung Lese- und Schreibvorgänge verarbeitet, historische Daten die Prüfung bestanden haben, das alte Objekt entfernt und die temporäre Betriebslogik beseitigt ist.

Den Prozess wiederholbar machen

Ein wiederverwendbares Migrations-Runbook macht Expand/Contract zu gewöhnlicher Release-Arbeit mit benannten Verantwortlichen und messbaren Freigabepunkten. Es sollte kurz genug für ein Live-Deployment und konkret genug sein, um Teilfehlerzustände zu beschreiben.

Nutzen Sie fünf Abschnitte im Runbook:

  • Erweiterung: genaue Schemaoperationen, erwartete Sperren, Timeouts und Transaktionsanforderungen.
  • Kompatibilität: betroffener Code, Writer, Reader, Flags, Clients und Deployment-Reihenfolge.
  • Backfill: Transformationsregel, Batching, Prüfpunkte, Drosselung und Ausnahmebehandlung.
  • Prüfung: SQL-Checks, fachliche Invarianten, Telemetrie und Abschlussgrenzwerte.
  • Contract: Entfernen von Abhängigkeiten, Beobachtungszeitraum, physische Bereinigung und Wiederherstellungsgrenzen.

Weisen Sie jedem Übergangsobjekt einen Verantwortlichen und ein erwartetes Abschlussdatum zu. Verfolgen Sie Spalten, Indizes, Flags, Trigger und Jobs an derselben Stelle. Bereinigung gehört zur Migration und ist keine optionale Wartung.

Für Teams, die mit Koder.ai entwickeln, kann der Planning Mode diese Phasen und Prüfpunkte festhalten, bevor Änderungen in der Produktion beginnen. Der Export des Quellcodes ermöglicht außerdem, dass Migrations-SQL und Kompatibilitätslogik dieselbe Prüfung wie anderer Anwendungscode erhalten. Koder.ai unterstützt Deployment, Hosting, Snapshots und Rollback. Von einem Anwendungs-Rollback sollte jedoch nicht erwartet werden, dass es eine bestätigte Datentransformation umkehrt. Bewahren Sie die Schema-Kompatibilität, bis der Datenbank-Wiederherstellungsplan nicht mehr von der alten Darstellung abhängt.

Planen Sie schreibintensive Arbeit möglichst für Zeiten mit weniger Traffic ein, verlassen Sie sich aber nicht allein auf das Timing. Begrenzte Transaktionen, feedbackbasierte Drosselung, beobachtbarer Fortschritt und eine getestete Pausenaktion halten eine Online-Migration beherrschbar, wenn sich Traffic oder Daten anders verhalten als erwartet.

FAQ

Warum kann eine Schemaänderung einen Ausfall verursachen?

Schemaänderungen stören den Produktionsbetrieb, wenn alte und neue Anwendungsversionen unterschiedliche Datenbankstrukturen erwarten. Bei einem schrittweisen Deployment können beide Versionen gleichzeitig laufen. Wird eine Spalte zu früh entfernt oder umbenannt, schlagen Lese- oder Schreibvorgänge fehl.

Was ist das Migrationsmuster Expand/Contract?

Expand/Contract teilt eine inkompatible Änderung in sichere Phasen auf. Zuerst kommt die neue Struktur hinzu, dann wechseln Code und Daten dorthin. Erst wenn kein Consumer die alte Struktur mehr nutzt, wird sie entfernt.

Wie kann ich eine Datenbankspalte ohne Ausfallzeit umbenennen oder ersetzen?

Fügen Sie zuerst die neue Spalte hinzu und lassen Sie die alte bestehen. Stellen Sie Code bereit, der mit beiden Feldern arbeiten kann, übertragen Sie bestehende Zeilen in kleinen Batches, schalten Sie die Lesezugriffe nach der Prüfung um und entfernen Sie die alte Spalte erst in einem späteren Release.

Kann ich in PostgreSQL eine Spalte hinzufügen, ohne den Datenverkehr zu blockieren?

Meistens ja. Eine nullable Spalte ohne Standardwert ist in PostgreSQL häufig eine kurze Metadatenänderung, benötigt aber trotzdem eine Tabellensperre. Setzen Sie ein kurzes Lock-Timeout, damit die Migration fehlschlägt statt hinter einer langen Transaktion zu warten.

Wie erstelle ich einen Index, ohne Schreibvorgänge zu blockieren?

Verwenden Sie CREATE INDEX CONCURRENTLY, wenn die Tabelle beschreibbar bleiben muss. Der Vorgang dauert länger, erzeugt Datenbanklast und darf nicht innerhalb eines Transaktionsblocks laufen. Überwachen Sie deshalb Latenz, WAL und Replica-Verzögerung.

Wann sollte eine Anwendung alte und neue Felder parallel schreiben?

Schreiben Sie beide Werte in derselben Datenbanktransaktion, wenn beide Darstellungen aktuell bleiben müssen. Legen Sie fest, welches Feld bei Abweichungen gilt, und verwenden Sie in APIs, Workern, Importen und Support-Tools dieselben Normalisierungsregeln.

Wie führe ich einen Backfill für eine große PostgreSQL-Tabelle sicher aus?

Verarbeiten Sie kurze, fortsetzbare Batches und committen Sie nach jedem Batch. Speichern Sie einen Prüfpunkt, aktualisieren Sie nur Zeilen mit offenem Bedarf und verlangsamen oder pausieren Sie den Job, wenn Abfragelatenz, Lock-Wartezeiten, WAL-Volumen oder Replica-Lag steigen.

Woran erkenne ich, dass ein Backfill vollständig und korrekt ist?

Schalten Sie Lesezugriffe nicht allein deshalb um, weil der Backfill beendet ist. Prüfen Sie erforderliche Werte, vergleichen Sie alte und neue Darstellungen, überwachen Sie Fallback-Lesezugriffe und bestätigen Sie, dass neue Schreibvorgänge nach dem historischen Datenlauf konsistent bleiben.

Wann sollte ich NOT NULL, Check-Constraints oder Fremdschlüssel hinzufügen?

Fügen Sie strikte Constraints erst hinzu, wenn vorhandene Daten geprüft wurden und jeder aktive Writer gültige Werte liefert. PostgreSQL erlaubt es, manche Constraints als NOT VALID hinzuzufügen, sie für neue Zeilen durchzusetzen und historische Zeilen separat zu validieren.

Wann kann ich den alten Schemepfad sicher entfernen?

Entfernen Sie zuerst Fallback-Lesezugriffe, stoppen Sie dann alte Schreibvorgänge und beobachten Sie das System über einen vollständigen Betriebszyklus. Wenn Anwendungen, Jobs, Berichte, Integrationen und Clients das alte Objekt nicht mehr referenzieren, entfernen Sie den zugehörigen Code und löschen die Datenbankspalte oder Tabelle in einem späteren Release.

Related posts