Revenir au contenu principal
Ingénierie

Stockage d'objets + WAL : Lakebase Postgres pour l'ère agentique

Changez la façon dont les agents fonctionnent avec Postgres en traitant le WAL comme une source de vérité durable

par Cassie Murray et Carlota Soto

  • Lakebase Postgres reimagines traditional OLTP databases by decoupling compute and storage, treating the Write-Ahead Log (WAL) on scalable object storage (like S3) as the definitive source of truth.
  • By replacing heavy physical data duplication with lightweight pointers to Log Sequence Numbers (LSNs), the architecture enables instant database branching, point-in-time restores, and time-travel queries that are ideally suited for agentic workloads.
  • Storing an immutable transaction history in open columnar formats allows both transactional and analytical engines to query a single, unified dataset directly, eliminating the need for separate read replicas or complex data synchronization pipelines.

Les agents qui interagissent avec une base de données OLTP traditionnelle créent souvent des goulots d'étranglement au niveau de la couche de stockage. Les nouveaux déploiements, les copies, les restaurations et les réplicas impliquent tous le déplacement de volumes importants de données, ce qui est coûteux et prend du temps.

C'est tout le contraire pour le stockage d'objets. Amazon S3, par exemple, est économique, performant et presque invisible à gérer. Il crée une couche de stockage évolutive et rentable pour la mémoire des agents.

Ce qui nous amène à la question suivante : le stockage d'objets peut-il se situer sous une base de données transactionnelle et faciliter le travail des agents ?

C'est cette question qui a donné naissance à Lakebase Postgres. La réponse ne dépend pas seulement de la rapidité de votre stockage d'objets, mais plutôt de l'endroit où vous placez la source de vérité.

Deux modèles OLTP

Le modèle mental habituel pour l'OLTP est centré sur les données. Les données sont organisées en tables avec des lignes et des colonnes, chacune représentant une entité. Le stockage est l'endroit où réside l'état actuel, et le rôle de la base de données est de le stocker et de le récupérer.

Mais il existe un second modèle : centré sur les transactions. Ici, la base de données est un journal de transactions. Chaque entrée est une opération, et le stockage est une chronologie de ces opérations plutôt qu'un instantané du présent. L'état actuel est un élément que vous pouvez déduire de cette chronologie.

Pendant des années, le modèle centré sur les données était le seul qui importait en pratique, car ce que l'équipe des opérations demandait à une base de données, c'était des lectures et des écritures sur le présent. Au cours des dernières années, cela a radicalement changé. Les opérations demandées par les charges de travail des agents sont presque toutes des opérations sur l'historique des transactions :

  • Donnez-moi une copie isolée de la production pour y travailler
  • Remettez-la dans l'état où elle se trouvait avant mes trois dernières instructions
  • Montrez-moi à quoi ressemblait cette table avant la migration
  • Exécutez-en vingt à la fois, et supprimez-en dix-neuf dans une heure

Ce sont toutes des requêtes sur la chronologie. Une base de données qui ne stocke que le présent fournit des copies et des sauvegardes, qui sont lentes et coûteuses.

Cependant, Postgres contient déjà cette chronologie : on l'appelle le journal d'écriture anticipée (WAL).

L'écriture dans le WAL

Le WAL de Postgres enregistre chaque modification avant qu'elle n'atteigne les fichiers de données. À l'origine, il existait pour permettre à Postgres de récupérer : si le serveur s'arrêtait entre l'écriture du journal et l'écriture du fichier de données, une relecture du WAL comblait l'écart.

Mais le contenu du WAL est intéressant bien au-delà de la récupération. Prenons une table et une insertion :

Avant que ce changement n'atteigne la table users sur le disque, Postgres l'ajoute au WAL. Le journal est binaire, mais pg_waldump va le restituer. Les enregistrements pour cette insertion ressemblent à peu près à ceci :

Il s'agit de quatre enregistrements et d'une transaction. Notez que chacun possède un numéro de séquence de journal (LSN), un identifiant à croissance monotone.

Les lignes heap et btree nomment également la page exacte de 8 Ko qui a changé. Le journal ne dit pas « une ligne a été ajoutée ». Il indique quelle page, dans quelle relation, à quel moment de la chronologie.

Lisez cela comme un mécanisme de récupération et c'est une liste de tâches à refaire après un plantage. Mais si vous le lisez comme un journal de transactions, c'est autre chose : un compte rendu complet, ordonné et au niveau de l'octet de chaque page que la base de données a modifiée, avec un nom unique pour chaque entrée.

Ce nom, le LSN, est la partie qui importe le plus. Cela signifie que la chronologie est déjà adressable. Rien n'a besoin d'être ajouté à Postgres pour faire de « la base de données à un instant donné » une chose bien définie. Elle a seulement besoin d'une couche de stockage qui conserve le journal et peut répondre aux requêtes le concernant.

Le journal devient la source de vérité

Dans un déploiement Postgres conventionnel, the WAL est un moyen de parvenir à une fin. Les fichiers de données sont la base de données, le journal les protège, et le journal est tronqué une fois ses enregistrements appliqués en toute sécurité. Le stockage est simplement un disque connecté à la machine qui exécute Postgres, et tout ce qui concerne l'identité de la base de données est lié à cette machine.

Maintenant, inversons la tendance. Faites du journal la base de données, et des fichiers de données une représentation dérivée et mise en cache de celui-ci. Vous pouvez alors conserver l'intégralité de la chronologie, et vous n'avez plus besoin de déplacer des données pour copier ou rembobiner la base de données. L'historique devient adressable, de sorte qu'une « copie » de base de données devient un pointeur au lieu d'un deuxième ensemble de fichiers. Cela rend les déploiements, les restaurations et les réplicas suffisamment économiques pour être traités comme du code.

C'est ce que nous avons fait dans Lakebase Postgres. Concrètement, nous avons divisé le système en deux couches :

La couche de calcul

La couche de calcul exécute Postgres standard. Elle analyse le SQL, planifie et exécute les requêtes, applique le MVCC, gère les verrous et les index.

Rien dans le moteur de requête n'est réécrit. Ce qui change, c'est la responsabilité du nœud de calcul : il existe pour exécuter le travail, pas pour préserver les données. Il dispose de RAM pour les tampons partagés et d'un NVMe local comme cache de page, et il peut démarrer, s'arrêter, évoluer ou s'éteindre à tout moment sans compromettre la durabilité.

La couche de stockage

La couche de stockage est garante de l'exactitude, de la durabilité et de l'historique. Elle survit à tout nœud de calcul individuel et est construite à partir de trois composants ayant des rôles distincts :

  • Les safekeepers répliquent le WAL. Lorsque le nœud de calcul génère des enregistrements WAL, il les diffuse en continu vers plusieurs safekeepers, et une transaction est validée une fois qu'un quorum a accusé réception de l'enregistrement via un protocole basé sur Paxos. La durabilité est une propriété de la réplication et du consensus plutôt que d'un fsync d'une seule machine.
  • Le pageserver transforme le WAL en pages. Il combine les pages de base avec les enregistrements WAL validés pour matérialiser la version d'une page dont une requête donnée a besoin, et il persiste ces versions matérialisées dans le stockage d'objets de manière asynchrone.
  • Le stockage d'objets conserve l'historique immuable à long terme. Les versions de pages matérialisées et les états historiques sont conservés sous forme d'enregistrement en ajout uniquement plutôt que sous forme de système de fichiers modifiable.

image2.png

Le chemin d'écriture

À quoi ressemble le chemin d'écriture ? Une validation (commit) dans ce système suit ces étapes :

  1. Postgres applique les modifications en mémoire. Les tampons sont mis à jour, les index sont modifiés, les enregistrements WAL sont générés exactement comme d'habitude.
  2. Au lieu de vider le WAL sur un système de fichiers local, le nœud de calcul le diffuse en continu sur le réseau vers les safekeepers.
  3. La transaction est validée une fois qu'un quorum de safekeepers a accusé réception de l'enregistrement. C'est à ce moment-là que le client reçoit la confirmation de réussite.
  4. La matérialisation des pages se produit ensuite, dans la couche de stockage, en dehors du chemin critique de la transaction. Une validation n'attend jamais que les pages soient écrites ou téléchargées.

image3.png

Cette conception pourrait susciter une objection évidente : l'étape 2 ajoute un saut réseau au chemin de validation. Mais tout déploiement Postgres qui prend la durabilité au sérieux exécute déjà une réplication synchrone, ce qui constitue également un saut réseau. L'externalisation du WAL remplace un aller-retour réseau par un autre plutôt que d'en ajouter un.

Le chemin de lecture

Chaque requête de lecture provenant d'un nœud de calcul comporte un identifiant de page et un LSN, et la couche de stockage renvoie la page telle qu'elle existait à ce LSN. Ce GetPage@LSN est une opération centrale dans cette architecture.

Le traitement de cette requête suit un ordre de préférence :

  1. En premier lieu vient la RAM pour les tampons partagés de Postgres, exactement comme dans n'importe quel Postgres.
  2. Vient ensuite le NVMe local, qui est toujours rapide et local. Si la page n'est pas en mémoire, le nœud de calcul vérifie son cache disque local.
  3. Ce n'est qu'en cas d'échec local (miss) que la requête traverse le réseau vers le pageserver. Le pageserver vérifie alors s'il dispose déjà de cette version de page matérialisée. Si ce n'est pas le cas, il trouve l'image la plus récente de la page à ou avant le LSN demandé, collecte les enregistrements WAL correspondants, les rejoue et renvoie la page reconstruite.

La page renvoyée est ensuite mise en cache dans la RAM et sur le NVMe, de sorte que sa prochaine lecture soit à nouveau locale.

image4.png

Un nœud principal demande la dernière version de chaque page, de sorte qu'en régime permanent, il se comporte comme n'importe quel Postgres lisant à partir d'un cache chaud. Mais rien dans le protocole n'exige la "dernière" version. Demandez une page à un LSN d'il y a quatre heures et vous obtiendrez cette page d'il y a quatre heures.

La conséquence utile est que la distinction entre données actives et sauvegardes historiques disparaît. Il n'y a qu'un seul système de stockage. Les anciennes versions de pages ne sont pas un artefact distinct conservé ailleurs dans un format différent ; ce sont les mêmes fichiers immuables, toujours adressables.

Stockage sans écrasement

En d'autres termes, le pageserver ne met jamais à jour un fichier en place. Les fichiers sont créés, fusionnés et supprimés, mais jamais modifiés. Cela convient parfaitement au stockage d'objets, qui ne propose pas de mises à jour aléatoires, et rend la conservation de l'historique suffisamment économique.

Les données sont organisées en deux types de fichiers de couche :

  • Une couche d'image contient un instantané de chaque clé dans une plage de clés à un LSN donné
  • Une couche delta contient toutes les modifications dans une plage de clés et de LSN. Les clés qui n'ont pas été modifiées ne sont pas stockées. Le WAL entrant est écrit sous forme de couches delta.

Les couches d'image sont générées en arrière-plan, pour deux raisons : elles raccourcissent la chaîne de rejeu qu'une lecture doit parcourir, et elles permettent de récupérer les anciens deltas. Sans elles, la reconstruction d'une page pourrait nécessiter de remonter arbitrairement loin dans le temps.

Ainsi, GetPage@LSN devient une recherche : commencer à la clé et au LSN demandés, parcourir les couches vers le bas en collectant les enregistrements WAL pour cette page, et s'arrêter à sa première image. Pour que cette recherche reste rapide, les couches delta et d'image sont réorganisées par une compaction en arrière-plan, et les couches qui dépassent la fenêtre de rétention sont récupérées.

Comment trouver rapidement la bonne couche

La recherche décrite ci-dessus semble simple, mais elle ne l'est pas. Il convient d'y consacrer un peu de temps, car elle détermine la viabilité de l'ensemble de l'architecture.

Une lecture spécifie une clé et un LSN. Le système de stockage doit trouver la couche la plus proche qui couvre cette clé à ce LSN ou avant. C'est un problème géométrique, et la solution n'est pas évidente sur des dizaines de millions de couches. Un balayage linéaire est bien trop lent, et les structures spatiales évidentes ne conviennent pas : les R-trees répondent aux requêtes d'inclusion plutôt qu'à "la première couche sous ce point", et les arbres de segments évoluent en fonction de la taille de l'espace de coordonnées plutôt que du nombre de couches.

Il existe plusieurs approches pour cette architecture, mais ce qui a fonctionné a été de résoudre d'abord le problème simple, puis de faire en sorte que la structure de données se souvienne de son propre passé.

Étape 1 : Résoudre le problème pour un seul LSN

Pour un LSN fixe, nous déterminons quelle couche répond à chaque clé. Cette réponse ne change qu'en quelques points de l'espace des clés, nous enregistrons donc ces points et les stockons dans un arbre binaire de recherche. Cet arbre constitue la couverture de couche pour ce LSN, et il répond à toute lecture à ce LSN par une seule recherche.

Cela fonctionne, mais seulement pour un seul LSN. La couverture change à chaque fois qu'une couche est ajoutée, et il existe des millions de LSN, nous ne pouvons donc pas construire et conserver un arbre distinct pour chacun d'eux.

Étape 2 : Rendre l'arbre persistant

Persistant au sens de "garder les anciennes versions disponibles". Nous construisons la couverture de manière incrémentielle, en insérant les couches dans l'ordre des LSN, de bas en haut. L'insertion d'une couche ne touche qu'aux nœuds situés le long d'un seul chemin, de la racine vers le bas. Au lieu d'écraser ces nœuds, le système les copie et laisse les originaux intacts. Les nouvelles copies pointent vers les anciens sous-arbres inchangés de chaque côté.

Deux conséquences en découlent :

  • L'insertion ne coûte que quelques nouveaux nœuds plutôt qu'un arbre entièrement nouveau, car tout ce qui se trouve en dehors du chemin est partagé
  • L'ancienne racine décrit toujours l'arbre exactement tel qu'il était avant l'insertion, elle reste donc une couverture valide pour le LSN antérieur

Nous faisons cela pour chaque couche, dans l'ordre, et nous nous retrouvons avec une structure unique qui contient chaque racine intermédiaire, chacune représentant la couverture à un LSN différent. Nous obtenons tous ces arbres pour presque le prix d'un seul.

Une lecture historique coûte alors la même chose qu'une lecture actuelle : le système choisit la racine correspondant au LSN souhaité et effectue la même recherche unique.

Voici l'astuce, en résumé :

  • Les lectures de la dernière version uniquement ne nécessitent qu'une seule recherche dans l'arbre
  • Les lectures historiques utilisent une racine plus ancienne, elles coûtent donc la même chose
  • La construction de ces racines reste économique à mesure que les couches s'accumulent, de sorte qu'un long historique ne ralentit pas les recherches

Où se situe réellement le stockage d'objets

C'est là que le débat actuel sur Postgres et le stockage d'objets a tendance à faire fausse route, dans les deux sens.

L'argument classique contre la construction d'un système OLTP sur du stockage d'objets ressemble à ceci :

  • Postgres traite de nombreuses petites opérations d'I/O sensibles à la latence
  • Le stockage d'objets est conçu pour des requêtes plus volumineuses avec une latence plus élevée, et une lecture peut prendre des centaines de millisecondes
  • Si vous placez S3 devant l'exécution des requêtes, vous obtenez une base de données lente

En soi, cette affirmation n'est pas controversée. Là où l'argument fait fausse route, c'est en supposant qu'une base de données construite sur un stockage d'objets doit nécessairement lire à partir du stockage d'objets pour répondre aux requêtes.

Dans l'architecture que nous proposons, ce n'est jamais le cas :

  • Les requêtes ne lisent pas le stockage d'objets. Le nœud de calcul lit la RAM, puis le NVMe local, puis le pageserver. Le stockage d'objets n'est lu qu'à l'intérieur du pageserver, uniquement lors de la reconstruction d'une version de page qu'il ne possède pas, et jamais directement par Postgres.
  • Les commits n'écrivent pas dans le stockage d'objets. Un commit est validé lorsqu'un quorum de safekeepers dispose de l'enregistrement WAL ; la matérialisation des pages et leur chargement se produisent ensuite.

Lorsque Postgres est conçu de cette manière, il devient une évolution des systèmes OLTP traditionnels, conçu pour gérer des charges de travail agentiques. C'est pourquoi nous avons créé Lakebase Postgres : une base de données OLTP où le calcul et le stockage sont découplés, et où la source de vérité durable repose sur le stockage d'objets.

Pourquoi utiliser Lakebase Postgres plutôt que Postgres standard

Avec Lakebase Postgres, l'historique des transactions est adressable par LSN, et les copies sont des références plutôt que des données. Cela permet de créer des fonctionnalités qui offrent à Postgres le flux de travail léger qui est une exigence absolue pour les agents.

Création de branches

Tout d'abord, Postgres peut désormais créer des branches. La création d'une branche ne copie pas les pages, elle crée un pointeur vers un LSN spécifique, et la branche commence à diverger à partir de là avec une sémantique de copie sur écriture.

Les écritures sur la branche sont stockées sous forme de deltas par rapport au parent, de sorte qu'une branche d'une base de données de 2 TB est créée en quelques secondes et ne coûte rien tant qu'elle ne modifie rien. Le parent ne subit aucune charge supplémentaire, c'est pourquoi cette opération peut être effectuée en toute sécurité en production.

C'est ce dont un agent a besoin pour travailler en toute sécurité. Il peut créer une branche par tâche, exécuter la migration qu'il vient d'écrire sur des données réelles à un volume réel, et inspecter le résultat avant que quoi que ce soit ne touche le parent. Vingt agents peuvent faire cela en même temps, chacun étant isolé des autres et de la production.

Avec Lakebase Postgres, nous avons même étendu la création de branches au-delà de la base de données. Les buckets de stockage d'objets, les fonctions, l'état de Managed Better Auth et la configuration d'AI Gateway se ramifient aux côtés de la base de données, de sorte qu'une branche est une copie isolée du backend plutôt que de simples tables Postgres.

Restauration instantanée

La restauration à un instant précis est une création de branche avec une intention différente. Restaurer signifie pointer vers un LSN antérieur et reprendre à partir de là, ce qui n'implique pas de recopier les données et dont le coût ne dépend pas de la taille de la base de données. La limite de retour en arrière est définie par un paramètre de rétention.

C'est ce qui rend les erreurs d'un agent peu coûteuses. Lorsqu'un agent exécute une mauvaise instruction, la solution n'est pas une fenêtre de restauration et un plan de reprise, mais simplement de pointer à nouveau la branche vers le LSN antérieur à l'exécution. L'annulation coûte la même chose sur une base de données de 2 TB que sur une base vide, de sorte qu'un agent peut réessayer au lieu de faire appel à un humain.

Requêtes de voyage dans le temps

Puisque le pageserver peut reconstruire n'importe quelle page à n'importe quel LSN dans la fenêtre d'historique, vous pouvez interroger directement un état passé au lieu de devoir d'abord le restaurer.

L'utilité pratique est la comparaison : à quoi ressemblait cette table avant la migration, et à quoi ressemble-t-elle maintenant. C'est également ainsi que vous confirmez que vous avez choisi le bon horodatage avant de lancer une restauration.

Réplicas en lecture sans réplicas

Un nœud de calcul en lecture seule n'est pas une copie des données. Il demande des pages à la même couche de stockage que le nœud principal, donc en ajouter un ne signifie pas provisionner un jeu de données et attendre qu'il se mette à jour. En démarrer un est une simple opération de métadonnées.

Mise à l'échelle à zéro

Puisque l'état durable réside en dehors du calcul, un nœud de calcul inactif peut être entièrement arrêté plutôt que de rester actif pour protéger les données. Les calculs se suspendent après 5 minutes d'inactivité et se réactivent en quelques centaines de millisecondes lors de la requête suivante. Pour un parc de bases de données par session ou par branche, dont la plupart sont inactives la majeure partie du temps, c'est ce qui fait la différence entre un modèle de coût viable et un modèle non viable. Notez que la facturation du calcul s'arrête pendant la suspension ; le stockage continue d'être facturé, car l'historique est toujours présent.

Une session d'agent qui fonctionne pendant quatre minutes puis devient inactive cesse de générer des coûts de calcul cinq minutes plus tard, sans que personne n'ait à la supprimer. C'est ce qui rend une base de données par agent, par session ou par branche suffisamment abordable pour être l'option par défaut.

Une seule copie pour les transactions et les analyses

Placer les données opérationnelles dans un stockage d'objets a une autre conséquence.

Une partie de l'enregistrement durable d'une base de données transactionnelle réside dans un stockage d'objets standard, elle n'est plus verrouillée dans le format privé d'un moteur sur les disques de ce dernier. D'autres moteurs peuvent la lire.

C'est la base de ce que nous appelons LTAP, pour Lake Transactional/Analytical Processing : au lieu d'avoir deux copies des données dans deux formats maintenus synchronisés par un pipeline, il existe une seule copie durable dans des formats colonnaires ouverts que les composants transactionnels et analytiques lisent tous deux.

Le mécanisme découle du chemin de lecture déjà décrit. À mesure que le pageserver matérialise les pages dans le stockage d'objets, il les transcode du format de ligne Postgres vers un format colonnaire, préservant ainsi la représentation Postgres exacte de chaque valeur. Une requête analytique demande à Postgres le LSN actuel, ce qui correspond à une recherche de métadonnées peu coûteuse, lit la grande majorité des données du stockage d'objets à partir de ce LSN, et récupère uniquement les modifications non matérialisées les plus récentes auprès du pageserver. Postgres ne gère aucun trafic de lecture analytique au-delà du retour de ce seul nombre, de sorte qu'une requête analytique volumineuse n'entre pas en concurrence avec les transactions pour le même CPU.

La différence avec la capture de données modifiées (CDC) et le mirroring est qu'il n'y a rien à activer. Il n'y a pas de liste de tables répliquées, car il n'y a pas de réplication. Une table existe déjà dans le lake, ce qui signifie également que les deux vues ne peuvent pas diverger.

Lakebase Postgres pour les agents

Nous avons commencé cet article par une question : le stockage d'objets pourrait-il se situer sous Postgres et faciliter le travail des agents ?

La réponse est oui. Le stockage d'objets peut se situer sous Postgres et changer votre façon d'interagir avec lui, mais pas seulement parce que S3 est rapide ou économique à exécuter. Comme décrit dans cet article, cela nécessite plus d'ingénierie que cela. La RAM et le NVMe local sont toujours nécessaires pour répondre aux requêtes assez rapidement, et un commit atterrit toujours sur un WAL répliqué plutôt que dans un bucket.

Cet élément WAL est la clé. Le stockage d'objets ajoute un moyen économique et évolutif de stocker tout l'historique, mais faire du WAL la source unique de vérité est ce qui rend cet historique adressable et change la façon dont les agents interagissent avec Postgres ainsi que les fonctionnalités que vous pouvez créer par-dessus.

Demandez à votre agent de déployer Lakebase Postgres et mettez-le à l'épreuve. Commencez ici.

Lakebase Postgres peut être utilisé comme une base de données autonome, et vous pouvez également l'intégrer au reste de la plateforme Databricks Data + AI : la gouvernance Unity Catalog, les analyses lakehouse, les notebooks et les workflows d'IA.

(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original

Recevez les derniers articles dans votre boîte mail

Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.