Verändern Sie die Art und Weise, wie Agenten mit Postgres arbeiten, indem Sie WAL als dauerhafte Quelle der Wahrheit behandeln
von Cassie Murray und Carlota Soto
Agenten, die mit einer herkömmlichen OLTP-Datenbank interagieren, verursachen oft Engpässe auf der Speicherebene. Neue Bereitstellungen, Kopien, Wiederherstellungen und Replikate bedeuten immer, dass große Datenmengen verschoben werden müssen, was zeitaufwendig und teuer ist.
Das genaue Gegenteil gilt für den Objektspeicher. Amazon S3 zum Beispiel ist günstig, leistungsstark und im Betrieb fast unsichtbar. Es schafft eine skalierbare, kosteneffiziente Speicherebene für den Speicher von Agenten.
Was uns zu der Frage bringt: Kann ein Objektspeicher unter einer relationalen Transaktionsdatenbank liegen und Agenten die Arbeit damit erleichtern?
Diese Frage war der Ausgangspunkt für Lakebase Postgres. Die Antwort hängt nicht nur davon ab, wie schnell Ihr Objektspeicher ist, sondern vielmehr davon, wo Sie die Quelle der Wahrheit ansiedeln.
Das übliche mentale Modell für OLTP ist datenzentriert. Daten sind in Tabellen mit Zeilen und Spalten organisiert, die jeweils eine Entität darstellen. Der Speicher ist der Ort, an dem der aktuelle Zustand liegt, und die Aufgabe der Datenbank ist es, diesen zu speichern und abzurufen.
Es gibt aber noch ein zweites Modell: transaktionszentriert. Hier ist die Datenbank ein Journal von Transaktionen. Jeder Eintrag ist eine Operation, und der Speicher ist eine Timeline dieser Operationen und kein Schnappschuss der Gegenwart. Der aktuelle Zustand ist nur eine Sache, die man aus dieser Timeline ableiten kann.
Jahrelang war das datenzentrierte Modell in der Praxis das einzige, das zählte, weil das Betriebsteam von einer Datenbank nur Lese- und Schreibzugriffe auf die Gegenwart verlangte. In den letzten Jahren hat sich das drastisch geändert. Die Operationen, die von Agenten-Workloads angefordert werden, sind fast ausnahmslos Operationen auf der Transaktionshistorie:
Dies sind alles Abfragen zur Timeline. Eine Datenbank, die nur die Gegenwart speichert, liefert Kopien und Backups, die langsam und teuer sind.
Postgres enthält diese Timeline jedoch bereits: Sie wird als Write-Ahead-Log (WAL) bezeichnet.
Das WAL von Postgres zeichnet jede Änderung auf, bevor sie die Datendateien erreicht. Es existierte ursprünglich, damit Postgres sich wiederherstellen konnte: Wenn der Server zwischen dem Schreiben des Logs und dem Schreiben der Datendatei abstürzte, schloss ein WAL-Replay die Lücke.
Aber die WAL-Inhalte sind weit über die Wiederherstellung hinaus interessant. Nehmen wir eine Tabelle und ein Insert:
Bevor diese Änderung die Tabelle users auf der Festplatte erreicht, hängt Postgres sie an das WAL an. Das Log ist binär, aber pg_waldump stellt es dar. Die Datensätze für dieses Insert sehen ungefähr so aus:
Dies sind vier Datensätze und eine Transaktion. Beachten Sie, dass jeder eine Log Sequence Number (LSN) hat, einen monoton steigenden Identifikator.
Die Heap- und B-Tree-Zeilen nennen auch die genaue 8-KB-Seite, die sich geändert hat. Das Log sagt nicht „Eine Zeile wurde hinzugefügt“. Es sagt, welche Seite, in welcher Relation, an welchem Punkt der Timeline.
Wenn man das als Wiederherstellungsmechanismus liest, ist es eine Liste von Aufgaben, die nach einem Absturz erneut ausgeführt werden müssen. Wenn man es jedoch als Transaktionsjournal liest, ist es etwas anderes: Ein vollständiger, geordneter Bericht auf Byte-Ebene über jede Seite, die die Datenbank jemals geändert hat, mit einem eindeutigen Namen für jeden Eintrag.
Dieser Name, die LSN, ist der wichtigste Teil. Das bedeutet, dass die Timeline bereits adressierbar ist. Es muss nichts zu Postgres hinzugefügt werden, um „die Datenbank zu einem bestimmten Zeitpunkt“ zu einer klar definierten Sache zu machen. Es wird lediglich eine Speicherebene benötigt, die das Log aufbewahrt und Abfragen darauf beantworten kann.
In einer herkömmlichen Postgres-Bereitstellung ist das WAL ein Mittel zum Zweck. Die Datendateien sind die Datenbank, das Log schützt sie, und das Log wird gekürzt, sobald seine Datensätze sicher angewendet wurden. Der Speicher ist einfach eine Festplatte, die an den Rechner angeschlossen ist, auf dem Postgres läuft, und alles an der Identität der Datenbank ist an diesen Rechner gebunden.
Kehren wir das nun um. Machen wir das Log zur Datenbank und die Datendateien zu einer abgeleiteten, zwischengespeicherten Darstellung davon. Dann können Sie die gesamte Timeline behalten und müssen keine Daten mehr verschieben, um die Datenbank zu kopieren oder zurückzuspulen. Die Historie wird adressierbar, sodass eine „Kopie“ der Datenbank zu einem Zeiger statt zu einem zweiten Satz von Dateien wird. Dies macht Bereitstellungen, Wiederherstellungen und Replikate so günstig, dass man sie wie Code behandeln kann.
Genau das haben wir in Lakebase Postgres getan. Konkret haben wir das System in zwei Ebenen aufgeteilt:
Die Compute-Ebene führt Standard-Postgres aus. Sie parst SQL, plant und führt Abfragen aus, setzt MVCC durch, verwaltet Sperren und Indizes.
In der Query-Engine wird nichts neu geschrieben. Was sich ändert, ist die Verantwortung des Compute-Knotens: Er existiert, um Arbeit auszuführen, nicht um Daten zu bewahren. Er verfügt über RAM für Shared Buffers und lokales NVMe als Seitencache und kann jederzeit gestartet, gestoppt, skaliert oder beendet werden, ohne die Beständigkeit zu gefährden.
Die Speicherebene ist für Korrektheit, Beständigkeit und Historie verantwortlich. Sie überlebt jeden einzelnen Compute-Knoten und besteht aus drei Komponenten mit unterschiedlichen Aufgaben:
fsync einer einzelnen Maschine.
Wie sieht der Schreibpfad aus? Ein Commit in diesem System folgt diesen Schritten:

Gegen dieses Design könnte ein offensichtlicher Einwand erhoben werden: dass Schritt 2 dem Commit-Pfad einen Netzwerk-Hop hinzufügt. Aber jede Postgres-Bereitstellung, die Beständigkeit ernst nimmt, führt bereits eine synchrone Replikation aus, was ebenfalls ein Netzwerk-Hop ist. Die Auslagerung des WAL ersetzt einen Netzwerk-Roundtrip durch einen anderen, anstatt einen hinzuzufügen.
Jede Leseanforderung von einem Compute-Knoten enthält eine Seitenkennung und eine LSN, und die Speicherebene gibt die Seite so zurück, wie sie bei dieser LSN existierte. Dieses GetPage@LSN ist eine zentrale Operation in dieser Architektur.
Die Bereitstellung folgt einer Prioritätenreihenfolge:
Die zurückgegebene Seite wird dann im RAM und auf dem NVMe zwischengespeichert, sodass der nächste Lesevorgang wieder lokal erfolgt.

Ein primärer Node fordert die neueste Version jeder Seite an, sodass er sich im eingeschwungenen Zustand wie jedes Postgres verhält, das aus einem warmen Cache liest. Aber nichts im Protokoll erfordert „aktuellste“. Wenn Sie eine Seite mit einer LSN von vor vier Stunden anfordern, erhalten Sie genau diese Seite von vor vier Stunden.
Die nützliche Konsequenz daraus ist, dass der Unterschied zwischen Live-Daten und historischen Backups verschwindet. Es gibt nur ein einziges Speichersystem. Alte Seitenversionen sind kein separates Artefakt, das an einem anderen Ort in einem anderen Format aufbewahrt wird; es sind dieselben unveränderlichen Dateien, die weiterhin adressierbar sind.
Mit anderen Worten: Der Pageserver aktualisiert eine Datei niemals direkt vor Ort (in-place). Dateien werden erstellt, zusammengeführt und gelöscht, aber niemals geändert. Dies passt perfekt zu Object Storage, der keine zufälligen Updates bietet, und macht die Historie kostengünstig genug, um sie aufzubewahren.
Die Daten sind in zwei Arten von Layer-Dateien organisiert:
Image-Layer werden im Hintergrund erstellt, und zwar aus zwei Gründen: Sie verkürzen die Replay-Kette, die ein Lesevorgang durchlaufen muss, und sie machen alte Deltas bereinigbar. Ohne sie könnte die Rekonstruktion einer Seite erfordern, beliebig weit zurückzugehen.
So wird GetPage@LSN zu einer Suche: Beginnen Sie beim angeforderten Schlüssel und der LSN, gehen Sie die Layer durch, um WAL-Records für diese Seite zu sammeln, und stoppen Sie beim ersten Image davon. Um diese Suche kurz zu halten, werden Delta- und Image-Layer durch Hintergrund-Compaction neu geordnet, und Layer, die außerhalb des Aufbewahrungsfensters liegen, werden per Garbage Collection bereinigt.
Die oben beschriebene Suche klingt einfach, ist es aber nicht. Es lohnt sich, etwas Zeit darauf zu verwenden, da sie darüber entscheidet, ob das gesamte Design tragfähig ist.
Ein Lesevorgang nennt einen Schlüssel und eine LSN. Das Speichersystem muss den nächstgelegenen Layer finden, der diesen Schlüssel bei oder vor dieser LSN abdeckt. Das ist ein geometrisches Problem, und es ist nicht offensichtlich, wie man es über zig Millionen Layer hinweg löst. Ein linearer Scan ist viel zu langsam, und die naheliegenden räumlichen Strukturen passen nicht: R-Bäume beantworten eher Containment-Abfragen als „den ersten Layer unter diesem Punkt“, und Segmentbäume skalieren mit der Größe des Koordinatenraums statt mit der Anzahl der Layer.
Es gibt verschiedene Ansätze für dieses Design, aber was funktioniert hat, war, zuerst das einfache Problem zu lösen und dann die Datenstruktur dazu zu bringen, sich an ihre eigene Vergangenheit zu erinnern.
Für eine feste LSN ermitteln wir, welcher Layer auf jeden Schlüssel antwortet. Diese Antwort ändert sich nur an einer Handvoll Punkten im gesamten Schlüsselraum. Daher zeichnen wir diese Punkte auf und speichern sie in einem binären Suchbaum. Dieser Baum ist die Layer-Abdeckung für diese LSN, und er beantwortet jeden Lesevorgang bei dieser LSN mit einem einzigen Lookup.
Das funktioniert, aber nur für eine einzige LSN. Die Abdeckung ändert sich jedes Mal, wenn ein Layer hinzugefügt wird, und es gibt Millionen von LSNs, sodass wir nicht für jede einzelne einen eigenen Baum erstellen und vorhalten können.
Persistent im Sinne von „die alten Versionen verfügbar halten“. Wir bauen die Abdeckung inkrementell auf, indem wir Layer in LSN-Reihenfolge von unten nach oben einfügen. Das Einfügen eines Layers berührt nur die Knoten entlang eines einzigen Pfads von der Wurzel nach unten. Anstatt diese Knoten zu überschreiben, kopiert das System sie und lässt die Originale unberührt. Die neuen Kopien verweisen auf die alten, unveränderten Teilbäume auf beiden Seiten.
Daraus ergeben sich zwei Dinge:
Das machen wir für jeden Layer der Reihe nach und erhalten am Ende eine einzige Struktur, die jede Zwischenwurzel enthält, von denen jede die Abdeckung bei einer anderen LSN darstellt. Wir erhalten all diese Bäume fast zum Preis von einem.
Ein historischer Lesevorgang kostet dann genauso viel wie ein aktueller: Das System wählt die Wurzel für die gewünschte LSN aus und führt denselben einzelnen Lookup durch.
Das ist zusammenfassend der Trick:
An dieser Stelle geht die aktuelle Diskussion über Postgres and Object Storage meist in beide Richtungen am Ziel vorbei.
Das klassische Argument gegen den Aufbau von OLTP auf Object Storage sieht wie folgt aus:
An sich ist das keine umstrittene Behauptung. Was an dem Argument jedoch falsch ist, ist die Annahme, dass eine auf Object Storage aufbauende Datenbank aus dem Object Storage lesen muss, um Abfragen zu beantworten.
In der von uns vorgeschlagenen Architektur tut sie das nie:
Wenn Postgres auf diese Weise konzipiert ist, wird es zu einer Weiterentwicklung traditioneller OLTP-Systeme, die für die Bewältigung agentenbasierter Workloads ausgelegt sind. Aus diesem Grund haben wir Lakebase Postgres entwickelt: eine OLTP-Datenbank, bei der Compute und Storage entkoppelt sind und die dauerhafte Source of Truth auf Object Storage aufbaut.
Mit Lakebase Postgres ist die Transaktionshistorie über die LSN adressierbar, und Kopien sind Referenzen statt Daten. Dies ermöglicht die Entwicklung von Features, die Postgres den leichtgewichtigen Workflow verleihen, der für Agenten eine absolute Voraussetzung ist.
Erstens kann Postgres jetzt abzweigen (branchen). Das Erstellen eines Branches kopiert keine Seiten, sondern erstellt einen Pointer auf eine bestimmte LSN, und der Branch weicht ab diesem Punkt mit Copy-on-Write-Semantik ab.
Schreibvorgänge in den Branch werden als Deltas gegenüber dem Parent gespeichert, sodass ein Branch einer 2-TB-Datenbank in Sekundenschnelle erstellt wird und nichts kostet, bis sich etwas ändert. Der Parent erfährt keine zusätzliche Last, weshalb dies auch in der Produktionsumgebung sicher durchgeführt werden kann.
Das ist es, was ein Agent braucht, um sicher zu arbeiten. Er kann pro Aufgabe einen Branch erstellen, die soeben geschriebene Migration auf echten Daten mit realem Volumen ausführen und das Ergebnis prüfen, bevor irgendetwas den Parent berührt. Zwanzig Agenten können das gleichzeitig tun, jeder isoliert von den anderen und von der Produktion.
Mit Lakebase Postgres haben wir das Branching sogar über die Datenbank hinaus erweitert. Object-Storage-Buckets, Functions, der Zustand von Managed Better Auth und die Konfiguration des AI Gateways werden zusammen mit der Datenbank gebrancht, sodass ein Branch eine isolierte Kopie des Backends und nicht nur der Postgres-Tabellen ist.
Point-in-Time-Recovery ist Branching mit einer anderen Absicht. Wiederherstellen bedeutet, auf eine frühere LSN zu verweisen und von dort aus fortzufahren. Es müssen also keine Daten zurückkopiert werden, und die Kosten skalieren nicht mit der Datenbankgröße. Wie weit Sie zurückgehen können, ist eine Aufbewahrungseinstellung.
Das macht die Fehler eines Agenten kostengünstig. Wenn ein Agent die falsche Anweisung ausführt, ist die Lösung kein Wiederherstellungsfenster und kein Recovery-Plan, sondern das Zurückverweisen des Branches auf die LSN von vor der Ausführung. Das Rückgängigmachen kostet bei einer 2-TB-Datenbank genauso viel wie bei einer leeren, sodass ein Agent den Versuch einfach wiederholen kann, anstatt einen Menschen hinzuzuziehen.
Da der Pageserver jede Seite bei jeder beliebigen LSN innerhalb des Historienfensters rekonstruieren kann, können Sie einen vergangenen Zustand direkt abfragen, anstatt ihn zuerst wiederherzustellen.
Der praktische Nutzen liegt im Diffing: Wie sah diese Tabelle vor der Migration aus und wie sieht sie jetzt aus. Auf diese Weise können Sie auch bestätigen, dass Sie den richtigen Zeitstempel gewählt haben, bevor Sie eine Wiederherstellung durchführen.
Ein schreibgeschützter Compute-Node ist keine Kopie der Daten. Er fordert Seiten von demselben Storage-Layer wie der primäre Node an. Das Hinzufügen eines solchen Nodes bedeutet also nicht, dass ein Datensatz bereitgestellt und auf dessen Synchronisierung gewartet werden muss. Das Starten eines solchen Nodes ist eine reine Metadatenoperation.
Da der persistente Zustand außerhalb von Compute existiert, kann ein inaktiver Compute-Knoten vollständig heruntergefahren werden, anstatt ihn zum Schutz der Daten weiterlaufen zu lassen. Compute-Ressourcen werden nach 5 Minuten Inaktivität pausiert und bei der nächsten Abfrage innerhalb weniger Hundert Millisekunden wieder aktiviert. Für eine Flotte von Pro-Sitzung- oder Pro-Branch-Datenbanken, von denen die meisten die meiste Zeit inaktiv sind, ist dies der Unterschied zwischen einem tragfähigen und einem unrentablen Kostenmodell. Beachten Sie, dass für Compute im pausierten Zustand keine Kosten anfallen; der Speicher wird weiterhin abgerechnet, da der Verlauf erhalten bleibt.
Eine Agenten-Sitzung, die vier Minuten lang aktiv ist und dann inaktiv wird, verursacht fünf Minuten später keine Compute-Kosten mehr, ohne dass sie manuell beendet werden muss. Das macht eine Datenbank pro Agent, pro Sitzung oder pro Branch so kostengünstig, dass dies der Standard sein kann.
Die Auslagerung von operativen Daten in den Objektspeicher hat noch eine weitere Konsequenz.
Sobald die persistenten Daten einer transaktionalen Datenbank in einem Standard-Objektspeicher liegen, sind sie nicht mehr im proprietären Format einer einzelnen Engine auf deren Festplatten gefangen. Andere Engines können sie lesen.
Das ist die Grundlage für das, was wir LTAP (Lake Transactional/Analytical Processing) nennen: Anstelle von zwei Kopien der Daten in zwei Formaten, die durch eine Pipeline synchronisiert werden, gibt es eine einzige persistente Kopie in offenen Spaltenformaten, die sowohl von der transaktionalen als auch von der analytischen Seite gelesen wird.
Der Mechanismus ergibt sich aus dem bereits beschriebenen Lesepfad. Wenn der Pageserver Pages im Objektspeicher materialisiert, transkodiert er sie vom Postgres-Zeilenformat in eine Spaltenform, wobei die exakte Postgres-Darstellung jedes Werts erhalten bleibt. Eine analytische Abfrage fragt Postgres nach der aktuellen LSN (eine kostengünstige Metadatenabfrage), liest den Großteil der Daten ab dieser LSN aus dem Objektspeicher und ruft nur die neuesten, nicht materialisierten Änderungen vom Pageserver ab. Postgres übernimmt außer der Rückgabe dieser einen Zahl keinen Teil des analytischen Leseverkehrs, sodass eine große analytische Abfrage nicht mit Transaktionen um dieselbe CPU konkurriert.
Der Unterschied zu Change Data Capture (CDC) und Spiegelung besteht darin, dass man nichts extra aktivieren muss. Es gibt keine Liste replizierter Tabellen, da keine Replikation stattfindet. Eine Tabelle existiert bereits im Lake, was auch bedeutet, dass die beiden Ansichten nicht voneinander abweichen können.
Wir haben diesen Beitrag mit einer Frage begonnen: Könnte ein Objektspeicher unter Postgres liegen und Agenten die Arbeit damit erleichtern?
Die Antwort lautet: Ja. Ein Objektspeicher kann unter Postgres liegen und die Art und Weise verändern, wie Sie damit interagieren – aber nicht nur, weil S3 schnell oder günstig im Betrieb ist. Wie in diesem Beitrag beschrieben, erfordert dies mehr Entwicklungsarbeit. RAM und lokaler NVMe werden weiterhin benötigt, um Abfragen schnell genug zu bedienen, und ein Commit landet immer noch auf einem replizierten WAL statt in einem Bucket.
Dieser WAL-Teil ist der Schlüssel. Der Objektspeicher bietet eine kostengünstige und skalierbare Möglichkeit, den gesamten Verlauf zu speichern. Aber erst die Nutzung des WAL als Quelle der Wahrheit macht diesen Verlauf adressierbar und verändert, wie Agenten mit Postgres interagieren und welche Funktionen Sie darauf aufbauen können.
Lassen Sie Ihren Agenten Lakebase Postgres bereitstellen und testen Sie es selbst. Jetzt loslegen.
Lakebase Postgres kann als eigenständige Datenbank verwendet werden. Sie können sie auch in die restliche Databricks Data + AI-Plattform integrieren: Unity Catalog-Governance, Lakehouse-Analysen, Notebooks und AI-Workflows.
(Dieser Blogbeitrag wurde mit KI-gestützten Tools übersetzt.) Originalbeitrag
Abonnieren Sie unseren Blog und erhalten Sie die neuesten Beiträge direkt in Ihren Posteingang.