CI/CD
.yml
GitLab · Magento · Datenbank · Zero-Downtime
Datenbank-Migrationen bei Zero-Downtime
Expand/Contract, Kompatibilität und Risiken realistisch bewertet

Zero-Downtime-Deployments scheitern an keiner anderen Stelle so häufig wie an Datenbank-Migrationen. Eine Spalte umbenennen, eine Tabelle löschen, einen NOT NULL-Constraint hinzufügen – das sind Operationen, die mit laufendem Code nicht kompatibel sind. Dieser Artikel erklärt das Expand/Contract-Pattern, wann es funktioniert und wann ein Wartungsfenster die ehrlichere Antwort ist.

14 Min. Lesezeit Expand/Contract · db_schema.xml · Online Schema Change Magento 2.4.x · MySQL 8.x · GitLab 16.x

1. Warum Datenbank-Migrationen Zero-Downtime gefährden

Zero-Downtime-Deployment setzt voraus, dass die Datenbank gleichzeitig von der alten und der neuen Code-Version lesbar und schreibbar ist. Das klingt selbstverständlich, ist es aber nicht: Viele gewöhnliche Schemaänderungen sind nicht kompatibel mit der Code-Version, die noch im Einsatz ist, während die Migration läuft. Ein Beispiel: Der neue Code setzt eine Spalte customer_consent voraus. Der alte Code kennt sie nicht und ignoriert sie – das wäre kompatibel. Aber wenn die Spalte NOT NULL ohne Default ist, schlägt jeder INSERT des alten Codes fehl, weil er die Spalte nicht befüllt. Das ist Downtime, auch wenn kein Wartungsfenster geplant war.

Das Problem ist strukturell: setup:upgrade in Magento führt Migrationen durch, bevor der neue Code vollständig aktiv ist. In einem Zero-Downtime-Modell ohne Wartungsfenster läuft die alte Code-Version während dieser Migration weiter. Die Datenbank ist nach der Migration bereits im neuen Zustand, der Code noch im alten. Jede Schemaänderung, die mit dem alten Code inkompatibel ist, produziert Fehler in diesem Fenster. Das Expand/Contract-Pattern ist der strukturelle Ansatz, um dieses Fenster sicher zu gestalten – aber es erfordert bewusstes Design jeder Schemaänderung, nicht nur schnelles DDL-Schreiben.

2. Das Expand/Contract-Pattern: Theorie und Praxis

Das Expand/Contract-Pattern teilt jede Breaking-Schemaänderung in zwei Phasen: Expand (sichere Änderung, backward compatible) und Contract (Bereinigung, erst wenn der alte Code außer Betrieb ist). Eine Spaltenumbenennung von street zu address_line wird nicht als einzige Migration durchgeführt, sondern aufgeteilt: Expand – neue Spalte address_line hinzufügen (der alte Code ignoriert sie), neuer Code schreibt in beide Spalten. Contract – nach einem oder mehreren erfolgreichen Releases, wenn keine alte Code-Version mehr läuft, wird die Spalte street entfernt.

In der Praxis bedeutet das: Manche Deployments brauchen zwei separate Releases. Das Expand-Release fügt die neue Struktur hinzu und läuft mit dem alten Code. Das Contract-Release entfernt die alte Struktur und läuft nur noch mit dem neuen Code. Zwischen den beiden Releases muss sichergestellt sein, dass kein Rollback auf eine Code-Version stattfindet, die die alte Spalte noch erwartet. Das Expand/Contract-Pattern erhöht den Planungsaufwand, eliminiert aber die Klasse von "Migration kaputt den laufenden Code"-Fehlern systematisch. Für Magento-Teams, die wirklich Zero-Downtime anstreben, ist es kein optionales Pattern, sondern eine Denkweise für jede Schemaänderung.

3. Magento db_schema.xml und setup:upgrade im Zero-Downtime-Kontext

Magento 2.3+ nutzt das deklarative Schema-System mit db_schema.xml. Das System vergleicht den gewünschten Schemastand (in XML beschrieben) mit dem aktuellen Datenbankzustand und generiert die notwendigen DDL-Statements. Das ist ein Fortschritt gegenüber den alten InstallSchema-Klassen, ändert aber nichts am grundlegenden Problem: setup:upgrade führt die Migrationen durch und danach ist die Datenbank im neuen Zustand, egal ob der alte Code noch läuft.

Im Zero-Downtime-Modell gibt es für setup:upgrade zwei Ansätze: Es läuft vor dem Symlink-Wechsel im neuen Release-Verzeichnis (während die alte Version noch aktiv ist), oder es läuft nach dem Symlink-Wechsel. Der erste Ansatz ist für rückwärtskompatible Migrationen korrekt – die Datenbank ist im neuen Zustand, der alte Code kann noch damit arbeiten. Der zweite Ansatz minimiert das Zeitfenster, in dem neuer Code mit alter Datenbank läuft, ist aber nur bei rückwärtskompatiblen Änderungen sicher. Für inkompatible Änderungen gibt es keinen sicheren Ansatz ohne Wartungsfenster – das ist die ehrliche Aussage, die Zero-Downtime-Versprechen oft nicht machen.

# Deployment pipeline with explicit migration step and compatibility check
deploy:production:
  stage: deploy
  environment: production
  script:
    - |
      ssh "$DEPLOY_USER@$DEPLOY_HOST" bash <<'REMOTE'
        set -euo pipefail
        RELEASE="$DEPLOY_PATH/releases/$(date +%Y%m%d-%H%M%S)"

        # Phase 1: Prepare new release directory
        mkdir -p "$RELEASE"
        rsync -az --delete /tmp/release/ "$RELEASE/"
        ln -sfn "$DEPLOY_PATH/shared/app/etc/env.php" "$RELEASE/app/etc/env.php"
        ln -sfn "$DEPLOY_PATH/shared/pub/media" "$RELEASE/pub/media"

        # Phase 2: Run migrations BEFORE switching symlink
        # Safe only for backward-compatible schema changes
        # If migration is breaking, enable maintenance mode here
        php "$RELEASE/bin/magento" setup:upgrade --no-interaction --keep-generated

        # Phase 3: Build static assets
        php "$RELEASE/bin/magento" setup:di:compile
        php "$RELEASE/bin/magento" setup:static-content:deploy de_DE -f

        # Phase 4: Atomic symlink switch
        ln -sfn "$RELEASE" "$DEPLOY_PATH/current"
        php "$DEPLOY_PATH/current/bin/magento" cache:flush

        echo "Migration and deployment complete"
      REMOTE

4. Rückwärtskompatible Schemaänderungen – was erlaubt ist

Rückwärtskompatibel bedeutet: Der alte Code kann nach der Migration noch korrekt lesen und schreiben. Das schließt folgende Operationen ein: Neue Spalte hinzufügen mit einem NULL-Default oder einem DEFAULT-Wert, den der alte Code nicht explizit befüllen muss. Neue Tabelle hinzufügen – der alte Code ignoriert sie. Index hinzufügen – verändert keine Datenstrukturen, nur die Abfrageperformance. Spalte nullable machen – erlaubt dem alten Code, NULL zu schreiben, wenn er die Spalte gar nicht kennt. Diese Operationen sind im Expand/Contract-Kontext die Expand-Phase.

Auch einige Änderungen, die nicht offensichtlich rückwärtskompatibel sind, können es trotzdem sein: Das Erhöhen der Länge einer VARCHAR-Spalte (z.B. von 100 auf 255) bricht den alten Code nicht, weil bereits gespeicherte Daten weiterhin lesbar sind und der alte Code keine Daten schreibt, die die neue Länge überschreiten würden. Das Hinzufügen einer neuen ENUM-Option zu einer bestehenden ENUM-Spalte ist kompatibel, wenn der alte Code diese Option niemals schreibt. Das Entfernen eines Index ist kompatibel (nur Performance-Auswirkung, keine Datenstruktur-Änderung). Wer diese Kategorien kennt und bewusst anwendet, kann die meisten Release-Migrationen Zero-Downtime-kompatibel gestalten.

5. Inkompatible Änderungen – wann ein Wartungsfenster nötig ist

Inkompatible Schemaänderungen sind solche, bei denen der alte Code nach der Migration scheitert. Die Kategorie "Spalte löschen" ist eindeutig: Wenn der alte Code auf eine nicht mehr existierende Spalte zugreift, gibt es einen Fehler. Die Kategorie "Spalte umbenennen" ist äquivalent – aus Datenbankperspektive ist eine Umbenennung ein Drop der alten und ein Add der neuen Spalte; der alte Code kennt den neuen Namen nicht. NOT NULL-Constraint ohne Default hinzufügen bricht jeden INSERT des alten Codes, der die Spalte nicht befüllt. Datentyp inkompatibel ändern (z.B. VARCHAR zu INT) kann gespeicherte Daten unlesbar oder konvertierungsfehlerhaft machen.

Für diese Änderungen gibt es keine Zero-Downtime-Lösung ohne Refactoring des Ansatzes: Das Expand/Contract-Pattern vertagt die inkompatible Phase auf ein separates Release, nachdem kein alter Code mehr läuft. Wer das nicht will oder kann – etwa weil das Release ein einziges großes Update enthält mit neuen Features und Breaking-Schemaänderungen – braucht ein Wartungsfenster. Das ist die ehrliche Antwort. Maintenance Mode in Magento (bin/magento maintenance:enable) sorgt dafür, dass keine Requests mehr verarbeitet werden, während die Migration läuft. Das ist kein Versagen, sondern ein kontrollierter Kompromiss, der besser ist als ein unkontrollierter Fehler in Production.

6. Online Schema Change: pt-online-schema-change und gh-ost

Für große Tabellen in Magento-Projekten ist das eigentliche Problem oft nicht die Kompatibilität, sondern die Laufzeit der Migration. Ein ALTER TABLE auf einer catalog_product_entity_varchar-Tabelle mit Millionen von Einträgen kann Minuten oder Stunden dauern – während der die Tabelle unter MySQL 8.0 mit instant DDL oder inplace DDL zwar lesbar bleibt, aber Writes blockiert werden können. pt-online-schema-change und gh-ost sind Werkzeuge, die Schema-Änderungen auf großen Tabellen als Online-Operation durchführen, ohne Tabellen zu sperren.

Diese Werkzeuge erstellen eine neue Tabelle mit dem gewünschten Schema, kopieren die Daten in Batches und schalten am Ende atomar um. Writes während der Migration werden über Trigger (pt-osc) oder Row-Based Replication (gh-ost) synchronisiert. Das ermöglicht Schema-Änderungen auf Millionen-Zeilen-Tabellen mit minimalem Impact auf den laufenden Betrieb. Der Nachteil: Diese Werkzeuge sind nicht in Magento's db_schema.xml-System integriert. Sie müssen separat im Deployment-Skript oder als Pre-Migration-Schritt ausgeführt werden, bevor setup:upgrade läuft. Wer sie einsetzt, muss auch sicherstellen, dass db_schema.xml den neuen Zustand korrekt beschreibt, damit Magento nicht versucht, die Migration erneut durchzuführen.

7. Migration im GitLab-Pipeline-Prozess verankern

Migrationen müssen im GitLab-Pipeline-Prozess an der richtigen Stelle verankert sein. Ein häufiger Fehler: setup:upgrade läuft am Ende des Deployments, nach dem Symlink-Wechsel – das bedeutet, der neue Code läuft kurz mit der alten Datenbankstruktur. Für die meisten rückwärtskompatiblen Änderungen ist das kein Problem, aber für Schemaänderungen, die der neue Code voraussetzt (neue Spalte, neuer Index), kann der neue Code scheitern, bis die Migration abgeschlossen ist.

Die sicherere Reihenfolge: setup:upgrade läuft im neuen Release-Verzeichnis vor dem Symlink-Wechsel. Die Datenbank ist dann im neuen Zustand, während noch der alte Code aktiv ist – das setzt Rückwärtskompatibilität voraus. Nach dem Symlink-Wechsel läuft der neue Code gegen die bereits migrierte Datenbank, ohne eine weitere Migrations-Phase. Diese Reihenfolge macht Migrationen explizit zu einem Gate im Deployment-Prozess: Schlägt setup:upgrade fehl, wechselt der Symlink nicht. Das ist das gewollte Verhalten – ein fehlgeschlagener Migrate ist kein Deployment.

8. Vergleich: kompatible vs. inkompatible Schemaänderungen

Die wichtigste Entscheidung bei jeder Schemaänderung ist: Ist diese Änderung mit der aktuell laufenden Code-Version kompatibel? Die Antwort bestimmt, ob Zero-Downtime möglich ist oder ein Wartungsfenster nötig wird.

Schemaänderung Kompatibel? Expand/Contract? Wartungsfenster?
Neue Spalte (nullable / Default) Ja Expand-Phase Nein
Neue Spalte (NOT NULL, kein Default) Nein Erst Expand (nullable), dann NOT NULL Oder Expand/Contract
Spalte löschen Nein Contract-Phase (nach Rollback-Verzicht) Oder Wartungsfenster
Spalte umbenennen Nein Expand (neue Spalte) → beide schreiben → Contract (alte löschen) Oder 2 Releases
Index hinzufügen Ja Nein (pt-osc bei großen Tabellen)

Die Tabelle zeigt, dass Zero-Downtime keine binäre Eigenschaft ist, sondern von jeder einzelnen Schemaänderung abhängt. Wer alle Änderungen im selben Release bündelt – Feature-Entwicklung, neue Spalten, gelöschte Spalten – hat fast garantiert eine inkompatible Migration dabei. Die Lösung: Migrations-Checkliste im Review-Prozess integrieren. Jede db_schema.xml-Änderung wird vor dem Merge auf Kompatibilität geprüft. Inkompatible Änderungen werden in separate Releases aufgeteilt oder mit explizitem Wartungsfenster markiert.

9. Typische Fehlerbilder bei Datenbank-Migrationen im Deployment

Das häufigste Fehlerbild ist der setup:upgrade-Timeout auf großen Tabellen: Die Migration läuft länger als der GitLab-Job-Timeout, der Job bricht ab, aber die Migration läuft auf dem Server weiter. Ergebnis: Das Deployment ist aus Pipeline-Sicht fehlgeschlagen, aber die Datenbank ist in einem Zwischenzustand. Diagnose: Im MySQL-Processlist nachschauen, ob ein ALTER TABLE noch läuft. Prävention: Job-Timeout großzügig setzen, pt-osc oder gh-ost für große Tabellen verwenden.

Das zweite Fehlerbild ist der Foreign-Key-Deadlock: Magento's setup:upgrade deaktiviert während Migrationen vorübergehend Foreign Key Checks (SET FOREIGN_KEY_CHECKS=0). Wenn gleichzeitig Queue Consumer oder Cron-Jobs schreiben, können Deadlocks entstehen, weil MySQL Lock-Queues mit dem Foreign-Key-Mechanismus interagieren. Prävention: Consumer und Cron vor setup:upgrade stoppen (siehe vorheriger Artikel). Das dritte Fehlerbild ist die inkonsistente Rollback-Situation: Der Rollback auf das alte Release funktioniert für den Code, aber nicht für die Datenbank – weil Migrationen nicht rückgängig gemacht werden. Wer bei inkompatiblen Migrationen rollt, muss die Datenbank separat auf den alten Stand bringen oder die Inkompatibilität als bekanntes Risiko akzeptieren.

# Pre-migration compatibility check job
validate:migration:
  stage: build
  image: php:8.4-cli
  needs: ["build:composer"]
  script:
    # Check for potentially breaking schema changes
    - |
      php -r "
        // Parse db_schema.xml changes from git diff
        \$diff = shell_exec('git diff HEAD~1 HEAD -- */db_schema.xml 2>/dev/null');
        if (empty(\$diff)) {
          echo 'No schema changes detected' . PHP_EOL;
          exit(0);
        }

        // Warn about potentially breaking patterns
        \$breakingPatterns = [
          'nullable=\"false\"' => 'NOT NULL column — check backward compatibility',
          'xsi:type=\"drop\"'   => 'DROP column/table — requires prior code removal',
        ];

        \$warnings = [];
        foreach (\$breakingPatterns as \$pattern => \$message) {
          if (str_contains(\$diff, \$pattern)) {
            \$warnings[] = 'WARNING: ' . \$message;
          }
        }

        if (!empty(\$warnings)) {
          echo implode(PHP_EOL, \$warnings) . PHP_EOL;
          echo 'Review these changes for Zero-Downtime compatibility.' . PHP_EOL;
          // Exit 0 — warning only, not blocking; team decides
          exit(0);
        }
        echo 'Schema changes appear backward compatible' . PHP_EOL;
      "
  allow_failure: true
  when: on_success

10. Zusammenfassung

Datenbank-Migrationen bei Zero-Downtime-Deployments erfordern bewusstes Schema-Design, nicht nur gutes Pipeline-Design. Das Expand/Contract-Pattern ist der strukturelle Ansatz: Erst Expand (backward-compatible Änderungen, die der alte Code toleriert), dann Contract (Bereinigung, wenn kein alter Code mehr läuft). Rückwärtskompatible Änderungen – neue nullable Spalten, neue Tabellen, neue Indizes – können ohne Wartungsfenster deployt werden. Inkompatible Änderungen – Spalten löschen, umbenennen, NOT NULL ohne Default – erfordern zwei separate Releases oder ein Wartungsfenster. Das ist keine Schwäche, sondern eine ehrliche Bestandsaufnahme.

setup:upgrade sollte vor dem Symlink-Wechsel laufen, damit der neue Code direkt gegen die migrierte Datenbank geht. Für große Tabellen ist pt-osc oder gh-ost der richtige Weg, um Migrations-Timeouts und Lock-Probleme zu vermeiden. Eine Migrations-Checkliste im Review-Prozess – jede db_schema.xml-Änderung auf Kompatibilität geprüft – verhindert, dass Breaking-Änderungen unbemerkt in einen Release rutschen, der als Zero-Downtime geplant war. Das ist der Standard, dem Magento-Teams, die wirklich Zero-Downtime anstreben, entsprechen müssen.

Datenbank-Migrationen bei Zero-Downtime — Das Wichtigste auf einen Blick

Expand/Contract

Jede Breaking-Änderung in zwei Releases aufteilen. Expand = backward compatible. Contract = erst wenn kein alter Code mehr läuft.

setup:upgrade Timing

Vor dem Symlink-Wechsel ausführen – bei rückwärtskompatiblen Migrationen. Schlägt es fehl, wechselt der Symlink nicht.

Große Tabellen

pt-online-schema-change oder gh-ost für Millionen-Zeilen-Tabellen. Verhindert Timeouts und Lock-Probleme bei DDL-Statements.

Wartungsfenster

Bei inkompatiblen Änderungen die ehrliche Antwort. maintenance:enable + migrate + maintenance:disable ist besser als ein stiller Fehler.

11. FAQ: Datenbank-Migrationen bei Zero-Downtime

1Was ist das Expand/Contract-Pattern?
Breaking-Schemaänderung in zwei Phasen: Expand (backward-compatible) und Contract (Bereinigung nach nächstem Release ohne alten Code).
2Rückwärtskompatible Schemaänderungen?
Neue nullable Spalten, neue Tabellen, neue Indizes, VARCHAR-Länge erhöhen. Bricht den alten Code nicht.
3Wann ist Wartungsfenster nötig?
Bei inkompatiblen Änderungen: Spalte löschen, umbenennen, NOT NULL ohne Default, Datentyp inkompatibel ändern.
4setup:upgrade Timing in der Pipeline?
Vor dem Symlink-Wechsel – bei rückwärtskompatiblen Migrationen. Schlägt es fehl, wechselt der Symlink nicht.
5Was ist pt-online-schema-change?
DDL auf großen Tabellen ohne Lock. Schattentabelle, Batch-Kopie, atomarer Wechsel. Verhindert Timeouts bei Millionen-Zeilen-Tabellen.
6Rollback nach Breaking-Migration?
Code-Rollback funktioniert, DB bleibt im neuen Zustand. Bei inkompatiblen Änderungen muss die DB separat zurückgesetzt werden.
7pt-osc vs. gh-ost?
pt-osc nutzt Trigger, gh-ost nutzt Row-Based Replication. Beide ermöglichen Online Schema Changes ohne Tabellen-Locks.
8Rückwärtskompatibilität prüfen?
Frage: Kann der alte Code nach der Migration noch korrekt lesen/schreiben? Neue nullable Spalte: ja. Spalte löschen: nein.
9Migrations-Checkliste in GitLab-Reviews?
Validate-Job im Build-Stage: db_schema.xml-Diffs auf Breaking-Patterns prüfen, Warnungen ausgeben. Teams entscheiden über Fortführung.
10Cron-Probleme bei setup:upgrade?
Ja – Cron-Jobs können während setup:upgrade Deadlocks verursachen. cron:remove vor setup:upgrade verhindert das.