Der PostgreSQL-Fehler „cannot execute CREATE TABLE in a read-only transaction“ führt derzeit bei vielen Teams zu Problemen bei Migrationen, wodurch Schemata nur teilweise angewendet werden und Migration-History-Tabellen gesperrt bleiben. Der Fehler tritt normalerweise auf, wenn ein Schreibvorgang auf einer Replica statt auf dem Primärknoten landet, und blockiert jedes Tool, das auf Schemaänderungen angewiesen ist – Flyway, Liquibase, Django ORM und ähnliche.

Warum der Fehler auftritt

PostgreSQL deaktiviert alle Schreibvorgänge, Schemaänderungen und Sequenz-Updates, wenn eine Transaktion als „read-only“ (schreibgeschützt) markiert ist. Die häufigsten Gründe, warum eine Migration in diesen Zustand gerät, sind:

  • Connection Pooler (z. B. PgBouncer), die die Migrationsverbindung versehentlich an eine Read-Replica senden.
  • Rollen, bei denen der Parameter default_transaction_read_only standardmäßig auf on gesetzt ist.
  • Cloud-Endpunkte, die separate Reader- und Writer-URLs bereitstellen; die Verwendung der Reader-URL (wie es bei AWS RDS oder Aurora üblich ist) leitet Schreibvorgänge an eine Replica weiter.

Wenn eine dieser Bedingungen zutrifft, kann der Migrationsprozess zwar Tabellen erstellen, Spalten hinzufügen oder Sequenzen aktualisieren, stößt dann aber an eine Grenze, was die Datenbank in einem teilweise migrierten Zustand zurücklässt.

Sofortige Lösung innerhalb der Transaktion

Wenn der Fehler bereits aufgetreten ist, können Sie das Read-only-Flag für die aktuelle Transaktion überschreiben, ohne den Rest der Sitzung zu beeinflussen – das ist entscheidend, wenn ein Connection Pool dieselbe Sitzung für andere Aufgaben wiederverwendet.

BEGIN;
SET LOCAL default_transaction_read_only = off;
SET TRANSACTION READ WRITE;
CREATE TABLE orders (id SERIAL PRIMARY KEY, total NUMERIC);
COMMIT;

SET LOCAL ändert die Einstellung nur für die Dauer der Transaktion. Die Verwendung von einfachem SET würde die Änderung für die gesamte Sitzung beibehalten, was andere Operationen unterbrechen kann, die legitim einen Read-only-Standard benötigen.

Präventivmaßnahmen

1. Überprüfen Sie, ob Sie sich auf dem Primärknoten befinden

Fügen Sie eine kurze Prüfung hinzu, bevor eine Migration ausgeführt wird:

SELECT CASE WHEN pg_is_in_recovery() THEN 'REPLICA' ELSE 'PRIMARY' END;

Wenn das Ergebnis REPLICA lautet, brechen Sie die Migration ab. Die Funktion pg_is_in_recovery() gibt auf einem Standby-Server true zurück, was garantiert, dass Sie nicht versuchen, in eine schreibgeschützte Kopie zu schreiben.

2. Verwenden Sie dedizierte Migrationsrollen

Erstellen Sie eine Rolle, deren Standard-Transaktionsmodus schreibberechtigt ist, und gewähren Sie ihr nur die erforderlichen Berechtigungen:

  • CREATE auf der Ziel-Datenbank.
  • CONNECT, um dem Migrations-Tool zu ermöglichen, eine Sitzung zu öffnen.

Vermeiden Sie es, dieser Rolle das Attribut default_transaction_read_only = on zuzuweisen, das manchmal für allgemeine Benutzer gesetzt wird.

3. Infrastruktur auf den Writer-Endpunkt ausrichten

Konfigurieren Sie in IaC-Skripten (Terraform, CloudFormation usw.) die Migration-Runner so, dass sie den Writer-Endpunkt des Clusters verwenden, nicht den Reader-Endpunkt. Der Writer-Endpunkt löst auf den Primärknoten auf, während der Reader-Endpunkt auf eine Replica verweist, die Schreibvorgänge ablehnen wird.

4. CI/CD-Gate hinzufügen

Fügen Sie einen Shell-Schritt in Ihre Pipeline (GitHub Actions, GitLab CI usw.) ein, der die pg_is_in_recovery()-Abfrage ausführt. Wenn diese true zurückgibt, beenden Sie den Job mit einem Status ungleich Null, um das Deployment vorzeitig zu stoppen.

if psql $DATABASE_URL -c "SELECT pg_is_in_recovery()" | grep -q t; then
  echo "Connected to replica – aborting migration"
  exit 1
fi

5. Retries des Migrations-Tools optimieren

Tools wie Flyway versuchen oft automatisch, Verbindungen wiederherzustellen. Stellen Sie für Flyway 9+ flyway.connectRetries=0 ein. Dies verhindert, dass das Tool wiederholt eine Replica ansteuert, was ansonsten den Replikations-Lag erhöhen und Ressourcen verschwenden kann.

Worauf Sie als Nächstes achten sollten

  • Replikations-Lag-Metriken: Ein wachsender Lag kann darauf hindeuten, dass eine Migration versehentlich eine Replica anvisiert hat, wodurch Schreibversuche in eine Warteschlange gestellt werden.
  • Routing-Regeln des Connection Poolers: Stellen Sie sicher, dass die Pooler-Konfigurationen den Migrationsverkehr explizit an den Primärhost routen.
  • Rollen-Standardwerte nach Upgrades: Datenbank-Upgrades setzen Rollenparameter manchmal zurück; führen Sie nach größeren Versionsänderungen ein Re-Audit von default_transaction_read_only durch.

Das Wichtigste in Kürze: Ein Read-only-Transaktionsfehler ist selten ein PostgreSQL-Bug; er ist ein Symptom dafür, dass der Datenverkehr an den falschen Knoten gesendet wird oder eine Rolle falsch konfiguriert ist. Indem Sie die Knotenrolle prüfen, dedizierte Migrationskonten verwenden und Ihre CI/CD-Pipeline absichern, können Sie sicherstellen, dass Migrationen reibungslos ablaufen und Sie halb angewendete Schemata vermeiden, die die nachgelagerte Entwicklung lähmen.