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:
- Code deployen, der sowohl mit altem als auch neuem Schema funktioniert
- Die Migration ausführen
- Code deployen, der nur mit dem neuen Schema funktioniert
- 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):
- Neue Spalte hinzufügen:
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);
- Code deployen, der in beide Spalten schreibt:
await db.query(
'UPDATE users SET name = $1, full_name = $1 WHERE id = $2',
[name, userId]
);
- Bestehende Daten nachträglich befüllen:
UPDATE users SET full_name = name WHERE full_name IS NULL;
-
Code deployen, der aus der neuen Spalte liest, aber in beide schreibt
-
Code deployen, der nur die neue Spalte verwendet
-
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:
- Eine Check-Bedingung hinzufügen (sperrt nicht):
ALTER TABLE users ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
- Die Bedingung validieren (durchsucht, sperrt aber nicht):
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
- 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
- Produktionsschema dumpen (nicht die Daten)
- Migration lokal anwenden
- Anwendungstests ausführen
Staging-Testen
- Aktuelles Produktions-Backup ins Staging wiederherstellen
- Migration ausführen
- Funktionsfähigkeit der Anwendung prüfen
- 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:
- Additive Änderungen: Benötigen meist keinen Rollback (neue Spalten werden ignoriert)
- Daten-Migrationen: Alte Spalte behalten, bis Sicherheit besteht
- 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:
- Bedingung ohne Validierung hinzufügen:
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
NOT VALID;
- 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:
- strong_migrations (Ruby)
- squawk (PostgreSQL)
Integration ins Deployment
Machen Sie Migrationen zu einem Teil Ihrer Deployment-Pipeline:
- Migrationen vor dem Deployment des neuen Codes ausführen
- Erfolg der Migrationen überprüfen
- 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.