Zero-Downtime-Datenbankmigrationen: Ein praktischer Leitfaden

Einführung

„Wir müssen die Seite für Wartungsarbeiten offline nehmen” ist ein Satz, der 2024 eigentlich ausgestorben sein sollte. Trotzdem sehe ich immer noch Teams, die Ausfallzeiten für Datenbankmigrationen einplanen, die sicher online durchgeführt werden könnten.

Dieser Leitfaden beschreibt die Muster, die ich verwende, um Datenbanken ohne Ausfallzeit zu migrieren – selbst bei Tabellen mit Millionen von Zeilen.

Die goldene Regel

Nehmen Sie niemals eine Änderung vor, die mit dem aktuell laufenden Anwendungscode inkompatibel ist.

Das bedeutet, Migrationen laufen in mehreren Phasen ab:

  1. Code deployen, der sowohl mit altem als auch neuem Schema funktioniert
  2. Die Migration ausführen
  3. Code deployen, der nur mit dem neuen Schema funktioniert
  4. Aufräumen (optional)

Sichere Operationen

Diese Operationen sind in der Regel sicher, ohne Ausfallzeit auszuführen:

Eine nullable Spalte hinzufügen

ALTER TABLE users ADD COLUMN middle_name VARCHAR(100);

Das ist sicher, weil:

  • Bestehender Code die Spalte nicht kennt (ignoriert sie)
  • Neuer Code mit NULL-Werten umgehen kann

Einen Index gleichzeitig hinzufügen

CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

Das Schlüsselwort CONCURRENTLY ist entscheidend – ohne es wird die Tabelle während der Indexerstellung gesperrt.

Eine neue Tabelle hinzufügen

CREATE TABLE user_preferences (
  user_id INTEGER REFERENCES users(id),
  preferences JSONB
);

Neue Tabellen beeinflussen bestehenden Code nicht.

Gefährliche Operationen

Diese erfordern sorgfältige Handhabung:

Eine Spalte umbenennen

Falscher Weg (verursacht Ausfallzeit):

ALTER TABLE users RENAME COLUMN name TO full_name;

Richtiger Weg (keine Ausfallzeit):

  1. Neue Spalte hinzufügen:
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);
  1. Code deployen, der in beide Spalten schreibt:
await db.query(
  'UPDATE users SET name = $1, full_name = $1 WHERE id = $2',
  [name, userId]
);
  1. Bestehende Daten nachträglich befüllen:
UPDATE users SET full_name = name WHERE full_name IS NULL;
  1. Code deployen, der aus der neuen Spalte liest, aber in beide schreibt

  2. Code deployen, der nur die neue Spalte verwendet

  3. Alte Spalte löschen:

ALTER TABLE users DROP COLUMN name;

Eine NOT-NULL-Bedingung hinzufügen

Falscher Weg:

ALTER TABLE users ALTER COLUMN email SET NOT NULL;

Dies durchsucht die gesamte Tabelle und sperrt sie.

Richtiger Weg:

  1. Eine Check-Bedingung hinzufügen (sperrt nicht):
ALTER TABLE users ADD CONSTRAINT users_email_not_null 
  CHECK (email IS NOT NULL) NOT VALID;
  1. Die Bedingung validieren (durchsucht, sperrt aber nicht):
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
  1. In NOT NULL umwandeln (sofort, nutzt bestehende Bedingung):
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_email_not_null;

Spaltentyp ändern

Falscher Weg:

ALTER TABLE orders ALTER COLUMN amount TYPE DECIMAL(10,2);

Richtiger Weg: Gleiches Muster wie beim Umbenennen – neue Spalte hinzufügen, Daten migrieren, umschalten.

Migrationen großer Tabellen

Bei Tabellen mit Millionen von Zeilen erfordern selbst „sichere” Operationen Vorsicht.

Batch-weises Nachbefüllen

Aktualisieren Sie niemals Millionen von Zeilen in einer einzigen Transaktion:

async function backfillInBatches(batchSize = 1000) {
  let processed = 0;
  
  while (true) {
    const result = await db.query(`
      UPDATE users 
      SET full_name = name 
      WHERE id IN (
        SELECT id FROM users 
        WHERE full_name IS NULL 
        LIMIT $1
      )
      RETURNING id
    `, [batchSize]);
    
    processed += result.rowCount;
    console.log(`Processed ${processed} rows`);
    
    if (result.rowCount < batchSize) break;
    
    // Kleine Verzögerung, um die Last zu reduzieren
    await sleep(100);
  }
}

Monitoring während Migrationen

Beobachten Sie diese Kennzahlen:

  • CPU- und I/O-Auslastung der Datenbank
  • Replikations-Lag
  • Abfrage-Latenz
  • Sperr-Wartezeiten

Migrationen testen

Lokales Testen

  1. Produktionsschema dumpen (nicht die Daten)
  2. Migration lokal anwenden
  3. Anwendungstests ausführen

Staging-Testen

  1. Aktuelles Produktions-Backup ins Staging wiederherstellen
  2. Migration ausführen
  3. Funktionsfähigkeit der Anwendung prüfen
  4. Migrationsdauer überprüfen

Trockenlauf in Produktion

Für kritische Migrationen:

BEGIN;
-- Migration ausführen
-- Ergebnisse prüfen
ROLLBACK;

Rollback-Strategie

Haben Sie immer einen Rollback-Plan:

  1. Additive Änderungen: Benötigen meist keinen Rollback (neue Spalten werden ignoriert)
  2. Daten-Migrationen: Alte Spalte behalten, bis Sicherheit besteht
  3. Destruktive Änderungen: Einen Wiederherstellungsplan haben

Praxisbeispiel: Einen Fremdschlüssel hinzufügen

Wir mussten einen Fremdschlüssel von orders zu customers bei einer Tabelle mit 50 Mio. Zeilen hinzufügen.

Vorgehen:

  1. Bedingung ohne Validierung hinzufügen:
ALTER TABLE orders 
ADD CONSTRAINT fk_orders_customer 
FOREIGN KEY (customer_id) REFERENCES customers(id) 
NOT VALID;
  1. In Batches während einer Phase mit geringem Traffic validieren:
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;

Ergebnis: Keine Ausfallzeit, abgeschlossen in 45 Minuten während normalen Traffics.

Tools und Automatisierung

Migrations-Linter

Verwenden Sie Tools, die gefährliche Muster erkennen:

Integration ins Deployment

Machen Sie Migrationen zu einem Teil Ihrer Deployment-Pipeline:

  1. Migrationen vor dem Deployment des neuen Codes ausführen
  2. Erfolg der Migrationen überprüfen
  3. Neuen Anwendungscode deployen

Fazit

Zero-Downtime-Migrationen erfordern mehr Planung und mehrere Deployments, aber der Aufwand lohnt sich. Ihre Nutzer:innen erleben keine Unterbrechungen, und Sie können jederzeit mit Zuversicht deployen.

Die zentralen Prinzipien:

  • Niemals die Kompatibilität mit laufendem Code brechen
  • Änderungen in kleinen, umkehrbaren Schritten vornehmen
  • Gründlich testen vor der Produktion
  • Während und nach Migrationen überwachen

Behandeln Sie Datenbankänderungen mit derselben Sorgfalt wie Anwendungscode, und „geplante Wartung” gehört der Vergangenheit an.