Subtransaktionen in PostgreSQL: Wenn ein Overflow das Cluster ausbremst

Subtransaktionen in PostgreSQL: Wenn ein Overflow das Cluster ausbremst
Deine Reaktion:

Ein Datenbankadministrator starrt auf sein Monitoring-Dashboard. Die Kurve für Transaktionen pro Sekunde fällt abrupt von rund 7.200 auf 160 ab – ohne dass ein Deployment, ein Schema-Change oder ein Hardware-Problem vorliegt. Er prüft die Replikationsverzögerung, die CPU-Last, die I/O-Werte – alles normal. Erst nach einer Stunde normalisiert sich der Wert wieder. Was ist passiert? Ein einzelner Prozess hat eine Subtransaktion eröffnet, die mehr als 64 verschachtelte Blöcke mit EXCEPTION-Handling durchlief. Genau daran krankt PostgreSQL – und es kann noch schlimmer kommen.

Subtransaktionen sind in PostgreSQL ein mächtiges Werkzeug: Sie erlauben es, innerhalb einer größeren Transaktion Rücksprungpunkte zu setzen. Ein Anwendungsentwickler nutzt sie über SAVEPOINT-Anweisungen, und PL/pgSQL verwendet sie automatisch für jeden Block mit einer EXCEPTION-Klausel. Sinnvoll? Ja. Aber wenn eine einzelne Transaktion zu viele Subtransaktionen aufstapelt, verändert sich das Verhalten des gesamten Datenbanksystems. Zudem kann eine neue Read Replica dauerhaft blockiert werden, obwohl sie die WAL-Daten weiterhin repliziert.

Wie Subtransaktionen entstehen und was sie bewirken

Eine Subtransaktion ist eine Transaktion innerhalb einer Transaktion. Du startest sie implizit mit SAVEPOINT oder in PL/pgSQL, sobald ein Block eine EXCEPTION-Klausel enthält. Jede Subtransaktion erhält eine eigene Transaktions-ID (XID). Das System muss diese IDs verwalten, um atomares Verhalten zu garantieren. Dazu hält jeder Backend-Prozess eine Liste der aktiv zugewiesenen Subtransaktions-IDs im Arbeitsspeicher – den sogenannten Subtransaktions-Cache.

Dieser Cache hat eine feste Größe: PGPROC_MAX_CACHED_SUBXIDS, in der Regel 64. Wenn du also mehr als 64 Subtransaktionen in einer einzigen Top-Level-Transaktion anlegst – explizit oder über Schleifen mit EXCEPTION-Blöcken –, platzt der Cache. PostgreSQL markiert den Cache als überlaufen. Ab diesem Moment kann keine Snapshot-Berechnung mehr alle aktiven Subtransaktions-IDs vollständig erfassen. Das bleibt nicht lokal – die Auswirkung greift auf alle Backends und sogar auf Read Replicas über.

WAL-Replikation und die Rolle der RUNNING_XACTS-Einträge

Um zu verstehen, warum ein Subtransaktions-Overflow eine Replica blockieren kann, musst du kurz die Replikationsarchitektur von PostgreSQL kennen. Die primäre Instanz schreibt alle Änderungen sequenziell in das Write-Ahead Log (WAL). Dieses Log dient als Grundlage für Crash-Recovery, wird aber auch an Replikas gestreamt. Eine Read Replica empfängt den WAL-Stream, dekodiert die Records und wendet sie auf ihre eigene Datenbasis an. Das ist der physikalische Teil der Synchronisation.

Doch für eine konsistente Sicht auf die Daten muss die Replica zusätzlich wissen, welche Transaktionen auf dem Primary noch aktiv sind. Dafür gibt es spezielle WAL-Records: RUNNING_XACTS. Sie werden vom Primary in regelmäßigen Abständen in den WAL-Stream eingefügt. Sie enthalten eine Momentaufnahme der laufenden Transaktionen, inklusive der IDs der aktiven Top-Level-Transaktionen und ihrer Subtransaktionen. Erst wenn eine Replica einen solchen Record verarbeitet hat, kann sie lesenden Zugriff erlauben – sofern Hot Standby aktiviert ist.

Der Trick: Eine Replica startet aus einem Base-Backup und muss den WAL-Stream ab dem Checkpoint des Backups anwenden. Ohne ein RUNNING_XACTS-Record fehlt ihr der Kontext, welche Transaktionen schon vor dem Checkpoint liefen. Deshalb wird dieses Record gezielt injiziert. Solange die Replica einen sauberen, nicht überlaufenen RUNNING_XACTS-Record verarbeitet, kann sie den Zustand aller Transaktionen nachvollziehen und sicher lesen.

Der Subtransaktions-Cache und sein Overflow

Jeder Backend-Prozess verwaltet seinen eigenen Cache mit den XIDs der aktuell verschachtelten Subtransaktionen. Die vollständige Zuordnung von Subtransaktionen zu ihrer Top-Level-Transaktion liegt in der Systemtabelle pg_subtrans, die auf der Platte liegt und über einen SLRU-Cache abgefragt wird. Solange die Anzahl der Subtransaktionen unter der Cache-Grenze liegt, ist alles schnell. Überschreitet sie die Grenze, wird der Cache als „overflowed“ markiert.

Ein Überlauf hat eine kritische Eigenschaft: Er wirkt global. Wenn irgendein Backend auf dem Primary einen Overflow meldet, wird jede danach erzeugte RUNNING_XACTS-Record ebenfalls als overflowed markiert – selbst wenn andere Transaktionen völlig sauber sind. Das liegt daran, dass das Record nur eine Momentaufnahme aller aktiven Transaktionen liefert. Fehlt einem davon die vollständige Subtransaktions-ID-Liste, ist das gesamte Bild unvollständig. Dadurch wird jeder Snapshot, der auf einer solchen Basis aufbaut, ebenfalls als overflowed behandelt.

Cluster-weite Leistungsprobleme durch einen einzigen Overflow

Was passiert konkret mit der Performance? Wenn eine Query eine Zeile liest, muss sie prüfen, ob die Transaktions-ID der Zeile (xmin oder xmax) in der Menge der abgeschlossenen oder aktiven Transaktionen liegt. Dazu wird der Snapshot herangezogen, der normalerweise alle relevanten IDs in einem kompakten Format enthält. Bei einem overflowed Snapshot fehlen jedoch einige Subtransaktions-IDs. Das System muss dann bei jeder Abfrage der pg_subtrans-Tabelle nachschlagen, um die fehlende Zuordnung zu ermitteln – ein teurer Prozess, der SLRU-Locks benötigt und bei hoher Last sogar Disk-I/O auslöst.

Ein einzelner Overflow kann also dazu führen, dass alle Queries auf dem Cluster langsamer werden, selbst wenn sie gar keine Subtransaktionen nutzen. In Benchmarks wurde ein Einbruch der TPS von über 95 Prozent beobachtet. Der Overhead entsteht durch das ständige Nachschlagen in pg_subtrans und die damit verbundenen Lock-Konflikte. Der Effekt ist nicht nur messbar, sondern erstreckt sich über alle Datenbanken, weil SLRU-Locks global sind. Ein einzelner Savepoint-Stapel kann also die Produktivität des gesamten Clusters zerstören.

Warum neue Read Replicas keine Verbindungen annehmen

Die zweite Gefahr betrifft Read Replicas, die während eines Overflows hinzugefügt werden. Sie beziehen ihr Base-Backup vom Primary und beginnen, den WAL-Stream abzuspielen. Wenn sie dabei auf einen overflowed RUNNING_XACTS-Record stoßen, können sie sich kein konsistentes Bild der laufenden Transaktionen machen. Sie wissen nicht, welche Subtransaktionen unterhalb der Grenze existierten. Aus Sicherheitsgründen versetzt PostgreSQL die Replica in den Status STANDBY_SNAPSHOT_PENDING – sie setzt das WAL-Replay fort, erlaubt aber keine Leseabfragen.

Die Replica bleibt in diesem Zustand, bis einer von drei Fällen eintritt: Sie erhält ein vollständiges, nicht überlaufenes RUNNING_XACTS-Record, sie replayt ein Shutdown-Checkpoint, der garantiert, dass keine Transaktionen aktiv sind, oder sie wartet, bis alle Transaktionen, die zum Zeitpunkt des Overflow-Events aktiv waren, abgeschlossen sind. Das kann Minuten oder Stunden dauern. Für einen Administrator, der gerade eine Read Replica hochgefahren hat, um Last abzufangen, ist das ein fataler Schlag: Die Replica wird nicht lesbar, obwohl sie WAL fleißig anwendet.

Was du tun kannst, um Probleme zu vermeiden

Das erste und einfachste Gegenmittel ist, die Anzahl der Subtransaktionen pro Transaktion zu begrenzen. In PL/pgSQL vermeidest du durch Refactoring, dass Schleifen mit EXCEPTION-Klauseln zu viele Subtransaktionen erzeugen. Vermeide EXCEPTION-Blöcke in Schleifen oder fasse sie in separate Funktionen zusammen. Oft lässt sich Logik ohne verschachtelte SAVEPOINTs umsetzen.

Falls du keine Kontrolle über den Code hast, kannst du überwachen, wie viele Subtransaktionen ein Backend erzeugt. PostgreSQL liefert in den Statistiken des pg_stat_database die Zahl der Subtransaktionen, und pg_stat_statements gibt Hinweise auf problematische Queries. Behalte außerdem die Anzahl der gleichzeitig aktiven Transaktionen im Auge – ein einzelner Prozess kann den ganzen Cluster beeinflussen. Einen Parameter, um den Overflow zu verhindern, gibt es derzeit nicht; du kannst nur durch Code-Richtlinien oder Query-Rewriting gegensteuern.

Für das DBA-Team bedeutet das: Teste deine Anwendungen gezielt auf dieses Muster. Simuliere eine Transaktion mit mehr als 64 Subtransaktionen und beobachte die Wirkung auf dein Cluster. Nur so stellst du sicher, dass ein solcher Ausfall nicht in der Produktion passiert. Es ist ein versteckter Fallstrick – aber mit den beschriebenen Mechanismen kennst du die Risiken und kannst sie vermeiden.

Quelle: planetscale.com

Deine Reaktion:
Artikel teilen:
Krötzsch-Check0 — 100
Fakten 85
Relevanz 88
Hype 15
Einschätzung 80
Redaktion 50 Stand 50 · noch keine Stimmen
Ist das Hype?
Sebastian Krötzsch
Autor

Sebastian Krötzsch

Sebastian Krötzsch schreibt auf sebask.de über Künstliche Intelligenz, Automatisierung, digitale Systeme und die Frage, was davon im Alltag wirklich nützlich ist. Ohne Buzzword-Nebel, dafür mit klarem Blick auf Praxis, Tools und echte Wirkung.