Der Lake ist der Speicher der Datenbank, keine Replik davon. Deshalb bleibt Postgres schnell, Analysen bleiben aktuell und es wird keine CDC-Pipeline benötigt
von Sergey Melnik
Reynold Xins Beitrag „From monolith to Lakebase to LTAP“ erklärte, was LTAP ist und wie die Lakebase-Architektur dies ermöglicht: eine verwaltete Kopie operativer Daten, die sowohl Postgres als auch Analysen bedient, ganz ohne CDC-Pipeline und ohne zweite Datenbank.
Ein Großteil der Kommentare dazu interpretierte diese Behauptung durch das Prisma bekannter Systemparadigmen. Vier Sichtweisen wurden dabei besonders häufig genannt:
Jedes dieser Mythen beruht auf einer falschen Annahme darüber, wie Lakebase Daten speichert. Schauen wir uns diese der Reihe nach an.
Die Realität: LTAP behält genau eine persistente Kopie jeder Tabelle im Data Lake. Die davor liegenden Schichten halten einige derselben Bytes vor, um diese Tabelle langlebig und schnell zu machen, aber keine davon ist eine zweite Datenbank.
Von außen betrachtet kann LTAP wie eine vertraute Pipeline wirken. Postgres schreibt Zeilen, ein Prozess überwacht das Log, die Änderungen landen in einer Iceberg-Tabelle und die Analytik liest Iceberg. Diese Pipeline entspricht CDC. Sie entspricht zudem der Struktur der logischen Replikation, dem Spiegeln und den meisten Zero-ETL-Produkten. Daher neigt man schnell dazu, LTAP demselben Design zuzuordnen.
Um zu verstehen, warum das nicht zutrifft, hilft es, sich anzusehen, was genau als zweite Kopie zählt. Es gibt zwei verschiedene Wege, wie ein System am Ende dieselbe Data in mehr als einem Speicherort ablegen kann:
Der Test ist also denkbar einfach: Kann diese Schicht als eigenständige Datenbank abgefragt oder kann auf sie als Failover-Ziel umgeschaltet werden? Wenn ja, handelt es sich um ein Replikativ. Wenn nein, ist es ein Speichertier.
Gemessen an diesem Test war Postgres schon immer hierarchisch geschichtet. Der WAL, der Buffer Cache und die Indizes speichern alle Bytes, die sich mit dem Heap überschneiden. Niemand bezeichnet einen Index als Replik der Tabelle, da er weder einen eigenen Katalog noch einen eigenen Zeitstrahl besitzt.
LTAP behält genau diese Hierarchie bei. Es verlagert sie lediglich in den Object Storage und erlaubt einer Lakehouse-Engine, die untere, persistente Ebene zu lesen.
So sieht diese Hierarchie in Lakebase aus, wenn man einen Schreib- und anschließend einen Lesevorgang betrachtet:
Der Object Storage selbst funktioniert auf dieselbe Weise. Wenn Sie ein Objekt per PUT in S3 ablegen, wird es mehrfach gespeichert – oft über Availability Zones hinweg mittels Erasure Coding. Die API spricht nach wie vor ein einzelnes Objekt an, und es läuft kein Synchronisationsjob zwischen den Kopien. Lakebase wendet dieses Prinzip gebündelt vor dem Lakehouse an: Log, Caches, Pageserver, Object Storage. Die zusätzlichen Dateien sorgen dafür, dass eine Tabelle langlebig und performant ist.
CDC erzeugt hingegen tatsächlich eine zweite Datenbank. Es dekodiert einen logischen Änderungsstrom in ein fremdes Schema, meist über einen Primärschlüssel indiziert, und führt Upserts in einer Tabelle aus, die das Quellsystem niemals zurückliest. Diese zweite Tabelle kann eigenständig abgefragt werden und kann sich unbemerkt vom Original wegbewegen: Datentypen driften auseinander, DDL-Änderungen fehlen, ein Replikationsslot verliert Zeilen, und Tabellen, die niemand explizit ausgewählt hat, tauchen überhaupt nicht auf. Das Resultat sind zwei eigenständige Kataloge.
LTAP hat keine zweite Tabelle, die synchron gehalten werden müsste. Es gibt keine Liste von Tabellen, für die man sich extra anmelden muss. Eine Tabelle, die in Postgres existiert, existiert auch im Data Lake, und die vom Primärsystem bereitgestellten Seiten sind lediglich Caches dieses Speichers.
Die Realität: Zwei verlustfreie Kodierungen derselben Zeilen können unterschiedliche Zugriffspfade bedienen – Heap-Seiten für Point Lookups, Parquet für Scans. Jede Abfrage liest genau das Layout, das für sie am schnellsten ist.
Selbst wenn die zusätzlichen Dateien eine Hierarchie bilden, bleibt das Datenlayout eine separate Fragestellung. Punktsuchen (Point Lookups) arbeiten auf Zeilenebene effizient, während Scans auf Spaltenebene optimal sind. Gibt es auf der Festplatte nur eine einzige Kodierung, verursacht eines dieser Zugriffsmuster höhere I/O-Kosten. Die Tabelle als eine einzige logische Kopie zu bezeichnen, ändert daran erst einmal nichts.
Diese Überlegung ist zwar richtig, baut jedoch auf einer Annahme auf – nämlich, dass eine einzelne Kopie auch nur eine einzige Kodierung bedeutet. Das stimmt jedoch nicht, solange zwei Formate dieselben Zeilen verlustfrei abbilden können.
Die entscheidende Frage lautet somit, ob die beiden Formate von LTAP verlustfrei arbeiten:
Da nichts verloren geht, kann die spaltenorientierte Kopie als persistenter Speicher im Data Lake dienen, während die Heap-Seiten als Cache dafür fungieren. Jeder Zugriffspfad liest somit das Format, das am besten zu ihm passt. Suchabfragen treffen auf 8-KB-Seiten auf dem Compute Node, auf dem lokalen Datenträger sowie auf dem Pageserver. Scans greifen über eine OLAP-Engine auf Parquet zu. Indizes verbleiben in der Hot-Ebene und werden nicht nach Parquet transkodiert.
Postgres arbeitet bereits exakt auf diese Weise. Es hält zwei physische Strukturen für dieselben Zeilen vor, um unterschiedliche Zugriffspfade zu bedienen: einen Heap und einen Covering Index. Niemand käme auf die Idee, den Index als CDC-Replik von customer anzusehen. SELECT * FROM t WHERE id = 1 und SELECT count(*) FROM t stellen lediglich zwei Zugriffspfade über ein und dieselbe Tabelle dar.
Eine etwas spezifischere Beobachtung trifft allerdings zu. Die spaltenorientierten Dateien aus Postgres sind nach Heap sortiert: Zeilen liegen in der Reihenfolge ihrer Seitennummer vor, da die Seite bei jeder Komprimierung als Ganzes neu geschrieben wird. Dieses Layout ist effizient, um eine Seite zu rekonstruieren und lediglich geänderte (Dirty) Seiten neu zu schreiben. Es stellt jedoch einen suboptimalen Clustering-Schlüssel für WHERE order_date >= … dar. Benötigt eine Data-Warehouse-Abfrage ein entsprechendes Clustering, kann dies auf dieselbe Weise abgeleitet werden, wie Silber-Tabellen ohnehin bereits aus jeder operativen Quelle generiert werden. Was hingegen wegfällt, ist eine CDC-Pipeline, deren einziger Zweck darin bestand, die Kopie für den Data Lake bereitzustellen.
Die Realität: Jede analytische Abfrage liest einen konsistenten Snapshot. Commits, die den Lake noch nicht erreicht haben, werden direkt von den Pageservern gelesen, sodass kein Commit verloren geht, der vor dem Start der Abfrage erfolgt ist.
Iceberg und Delta bieten Snapshot-Isolierung auf Objektspeicher, und Objektspeicher hat eine hohe Latenz. Daher ist es naheliegend anzunehmen, dass die Analytik immer hinterherhinkt, dass viele kleine Tabellen nicht aktuell bleiben können und dass ein Join über Tabellen hinweg nicht konsistent sein kann. Diese Beschreibung passt zur CDC-Ingestion in einen Lake.
Das hängt von einer Annahme ab: dass eine Abfrage nur sehen kann, was bereits im Objektspeicher gelandet ist. Bei LTAP kann eine Abfrage auch Seiten sehen, die sich noch nicht im Objektspeicher befinden. Das ändert die Antwort, und das lässt sich am besten anhand einer Abfrage nachvollziehen.
Wenn eine analytische Abfrage startet,
Postgres übernimmt keinen großen Teil des Bulk-Read-Traffics – es gibt eine Nummer und maximal diese aktuellen Seiten zurück. Der Lake muss nicht den neuesten Commit enthalten, damit dieser Commit sichtbar ist. Er muss fast alles enthalten sowie eine Möglichkeit, Seiten abzurufen, die nach der letzten Materialisierung committet wurden. Das LSN ist die Grenze: Es teilt der Engine mit, wie weit die Zusammenführung gehen muss.
Dasselbe LSN sorgt auch dafür, dass Joins konsistent bleiben. CDC wird Tabelle für Tabelle angewendet, sodass sich eine Relation an einer späteren Slot-Position als eine andere befinden kann und ein Join zwei Zeitpunkte mischt. LTAP liest jede Tabelle bei einem einzigen LSN – derselben Art von Grenze, die Postgres für REPEATABLE READ verwendet –, anstatt bei dem Stand, den die jeweiligen Pipelines zuletzt geflasht haben. Dieses LSN ist ein gemeinsames Wasserzeichen. APIs zum Lesen neuer Änderungen seit einem Wasserzeichen werden sowohl nativ für Lakebase als auch als Erweiterung für Upstream-Postgres entwickelt.
Kleine Tabellen bedeuten Metadatenkosten, keine Einschränkung der Aktualität. Der Overhead von Iceberg besteht aus Dateien und Snapshots. Bei einer Relation mit nur wenigen Zeilen kostet dieser Verwaltungsaufwand mehr, als er einspart, und das Layout spielt für die Scan-Leistung keine Rolle. Diese Tabellen verbleiben im Page-Cache, nehmen am LSN-konsistenten Lesen teil und überspringen den Iceberg-Commit.
Bleiben zwei Aussagen übrig, die ähnlich klingen, es aber nicht sind:
Der materialisierte spaltenbasierte Snapshot kann dem Primärsystem um das jüngste Komprimierungsintervall hinterherhinken, aber die Abfrage selbst tut das nicht. Die Tatsache, dass Parquet-Dateien möglicherweise einige Minuten im Rückstand sind, bedeutet nicht, dass ein Leser keine committeten Daten sehen kann.
Die Realität: Logische Replikation ist ein teurer Weg, um den Lake mit der Datenbank synchron zu halten. Sie spielt jede Änderung Zeile für Zeile ab und erfordert eine Pipeline, die Schritt halten muss. Das parallele Dekodieren des WAL umgeht dies nicht, da Protokolldatensätze nicht unabhängig voneinander verarbeitet werden können.
Ein Standardweg zur Analyse von frischem Postgres ist die Aktivierung der logischen Replikation. Debezium, PeerDB, pgoutput und ähnliche Tools gehen von der Beobachtung aus, dass die Datenbank bereits einen Änderungswream ausgibt, und stellen die Frage, welches Speicherformat zusätzlich erforderlich ist.
Die Frage ist, ob ein Änderungsstrom ein guter Ansatz ist, um eine Tabelle im Lake aufzubauen. Zwei Eigenschaften des Streams sprechen dagegen: wie viel er lesen muss und wie viel Status er behalten muss.
Logische Replikation liest jede Änderung
Das WAL ist ein physisches Protokoll von Seitenänderungen, das geschrieben wird, damit Postgres abstürzen und wiederherstellen kann. Die logische Dekodierung wandelt es in Reihenereignisse in Commit-Reihenfolge um. Diese Konvertierung kann viele Aktualisierungen derselben Seite nicht zu dem aktuellen Abbild der Seite zusammenfassen; jede Änderung muss durchlaufen. Auf einer heißen Seite protokolliert Postgres dieselben 8 KB möglicherweise hundert Mal zwischen Checkpoints. Die logische Dekodierung gibt hundert Ereignisse aus, und ein nachgelagerter Job führt diese dann per Upsert nach Primärschlüssel in Parquet zusammen. Im Gegensatz dazu liest ein Page-Tiering-Pfad die Seite einmal. Bei den von uns gemessenen schreibintensiven Workloads liest CDC mindestens das WAL-Volumen, während die Komprimierung schmutziger Seiten nur einen Bruchteil davon lesen kann.
Logische Replikation führt Status mit sich
Ein Replikationsslot ist ein Cursor im WAL, den der Consumer immer weiter vorantreiben muss. Wenn der Consumer ins Hintertreffen gerät, verbleibt das WAL so lange auf der Festplatte, bis der Slot gelöscht wird oder die Festplatte voll ist. Die logische Dekodierung gibt kein DDL aus, weshalb ALTER TABLE, Typänderungen und Umbenennungen entweder Ereignisse inkonsistent mit dem aktuellen Katalog machen oder den Slot ungültig machen. Tabellen ohne Primärschlüssel erfordern REPLICA IDENTITY FULL. Da die Pipeline aufwendig ist, wird sie typischerweise auf eine Untergruppe von Tabellen angewendet. Das ist das Gegenteil davon, dass jede Postgres-Tabelle im Lake erscheint, nur weil sie existiert.
Eine verwandte Behauptung: Die logische Dekodierung kann übersprungen und das WAL parallel dekodiert werden
Pageserver konsumieren bereits WAL, daher kann ein paralleler Log-to-Parquet-Pfad wie ein kleiner zusätzlicher Schritt aussehen. Prinzipiell ist das möglich. Es wurde jedoch nicht als Produktions-Extraktor gebaut und ist auch nicht der Weg, den LTAP verwendet. WAL-Datensätze sind geordnet, da spätere Datensätze frühere voraussetzen. Sichtbarkeit liegt in pg_xact, dem Proc-Array und dem Katalog bei diesem LSN, nicht im Tupel. Unabhängige Worker können nicht jeweils einen disjunkten Byterereich dekodieren und korrekte aktuelle Zeilen ausgeben.
Was LTAP parallelisiert, sind Seiten, nicht Protokolldatensätze. Das System fragt ab, welche Seiten sich seit dem letzten Snapshot geändert haben (WAL-Zusammenfassungen auf Postgres 17+ oder die Log-Structured Merge-Trees des Pageservers), und jede veränderte Seite stellt eine unabhängige Arbeit dar. Diese Menge wird partitioniert, die Seiten werden transkodiert, und ein Iceberg-Snapshot wird committet. Das Protokoll zeigt, wie Schreibvorgänge dauerhaft gemacht wurden und ist eine Möglichkeit herauszufinden, welche Seiten sich geändert haben. Der Snapshot enthält die aktuellen Seiten und ist keine Wiederholung jedes Datensatzes, der sie verändert hat.
Nichts davon macht die Replikation zum falschen Werkzeug für ihre eigentlichen Aufgaben: Migrationen, Upgrades auf Hauptversionen und Umstellungen auf ein zweites Postgres. Aber die Verwendung der logischen Replikation als Speicher erzeugt eine zweite Quelle der Wahrheit.
Alle vier Mythen basieren auf demselben Missverständnis: Jede Materialisierung der Daten als zweite Datenbank zu behandeln.
LTAP behält eine logische Tabelle bei, und der Lake ist ihre dauerhafte Kopie. Heap-Seiten, Indizes und Caches sind die Mittel, mit denen Postgres diese Tabelle mit OLTP-Latenz bereitstellt, ähnlich wie ein abdeckender Index Abfragen über einen Heap bedient. Eine OLAP-Engine scannt das Parquet direkt und führt transaktionskonsistente Änderungen seit der letzten Materialisierung zusammen.
Keine dieser Ebenen fügt einen zweiten Katalog oder eine zweite Quelle der Wahrheit hinzu, die driften könnten.
Eine detaillierte Analyse, wie die Speicherarchitektur von Lakebase zu LTAP führt, finden Sie in diesem Beitrag.
(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.