Cambia la forma en que los agentes trabajan con Postgres al tratar el WAL como una fuente de verdad duradera
por Cassie Murray y Carlota Soto
Los agentes que interactúan con una base de datos OLTP tradicional suelen generar cuellos de botella en la capa de almacenamiento. Las nuevas implementaciones, copias, restauraciones y réplicas implican mover grandes volúmenes de datos, lo que resulta costoso y requiere mucho tiempo.
El caso del almacenamiento de objetos es totalmente opuesto. Amazon S3, por ejemplo, es económico, eficiente y casi invisible de operar. Crea una capa de almacenamiento escalable y rentable para la memoria de los agentes.
Lo que nos lleva a la siguiente pregunta: ¿puede el almacenamiento de objetos ubicarse debajo de una base de datos transaccional y facilitar el trabajo de los agentes?
Esta pregunta es la que dio origen a Lakebase Postgres. La respuesta no depende solo de la velocidad de su almacenamiento de objetos, sino de dónde se ubique la fuente de verdad.
El modelo mental habitual para OLTP está centrado en los datos. Los datos se organizan en tablas con filas y columnas, donde cada una representa una entidad. El almacenamiento es el lugar donde reside el estado actual, y el trabajo de la base de datos es almacenarlo y recuperarlo.
Pero existe un segundo modelo: centrado en las transacciones. Aquí, la base de datos es un registro de transacciones. Cada entrada es una operación y el almacenamiento es una línea de tiempo de esas operaciones, en lugar de una instantánea del presente. El estado actual es algo que se puede derivar de esa línea de tiempo.
Durante años, el modelo centrado en los datos fue el único que importaba en la práctica, porque lo que el equipo de operaciones le pedía a una base de datos eran lecturas y escrituras del presente. En los últimos años, esto ha cambiado drásticamente. Las operaciones que solicitan las cargas de trabajo de los agentes son casi todas operaciones sobre el historial de transacciones:
Todas estas son consultas sobre la línea de tiempo. Una base de datos que solo almacena el presente ofrece copias y respaldos, que son lentos y costosos.
Sin embargo, Postgres ya contiene esta línea de tiempo: se llama registro de escritura anticipada (WAL).
El WAL de Postgres registra cada modificación antes de que llegue a los archivos de datos. Originalmente existía para que Postgres pudiera recuperarse: si el servidor fallaba entre la escritura del registro y la del archivo de datos, una reproducción del WAL cerraba la brecha.
Pero el contenido del WAL es interesante mucho más allá de la recuperación. Tomemos una tabla y una inserción:
Antes de que ese cambio llegue a la tabla users en el disco, Postgres lo agrega al WAL. El registro es binario, pero pg_waldump lo representará. Los registros de esta inserción se ven aproximadamente así:
Estos son cuatro registros y una transacción. Observe cómo cada uno tiene un número de secuencia de registro (LSN), un identificador que aumenta de forma monótona.
Las líneas heap y btree también nombran la página exacta de 8 KB que cambió. El registro no dice "se agregó una fila". Dice qué página, en qué relación y en qué punto de la línea de tiempo.
Si se lee como un mecanismo de recuperación, es una lista de tareas que se deben rehacer después de una falla. Pero si se lee como un registro de transacciones, es algo más: un informe completo, ordenado y a nivel de bytes de cada página que la base de datos ha cambiado, con un nombre único para cada entrada.
Ese nombre, el LSN, es la parte que más importa. Significa que la línea de tiempo ya es direccionable. No es necesario agregar nada a Postgres para que "la base de datos en un momento dado" sea algo bien definido. Solo necesita una capa de almacenamiento que conserve el registro y pueda responder preguntas sobre él.
En una implementación convencional de Postgres, el WAL es un medio para un fin. Los archivos de datos son la base de datos, el registro los protege y este se recorta una vez que sus registros se aplican de forma segura. El almacenamiento es simplemente un disco conectado a la máquina que ejecuta Postgres, y todo lo relacionado con la identidad de la base de datos está vinculado a esa máquina.
Ahora, invirtámoslo. Hagamos que el registro sea la base de datos y los archivos de datos una representación derivada y almacenada en caché de este. De este modo, se puede conservar la línea de tiempo completa y ya no es necesario mover datos para copiar o rebobinar la base de datos. El historial se vuelve direccionable, por lo que una "copia" de la base de datos se convierte en un puntero en lugar de un segundo conjunto de archivos. Esto hace que las implementaciones, restauraciones y réplicas sean lo suficientemente económicas como para tratarlas como código.
Eso es lo que hicimos en Lakebase Postgres. En concreto, dividimos el sistema en dos capas:
La capa de cómputo ejecuta Postgres estándar. Analiza SQL, planifica y ejecuta consultas, aplica MVCC, y gestiona bloqueos e índices.
No se reescribe nada en el motor de consultas. Lo que cambia es la responsabilidad del nodo de cómputo: existe para ejecutar el trabajo, no para conservar los datos. Tiene RAM para búferes compartidos y NVMe local como caché de páginas, y puede iniciarse, detenerse, escalarse o fallar en cualquier momento sin poner en riesgo la durabilidad.
La capa de almacenamiento es responsable de la corrección, la durabilidad y el historial. Sobrevive a cualquier nodo de cómputo individual y está compuesta por tres componentes con tareas distintas:
fsync.
¿Cómo es la ruta de escritura? Una confirmación en este sistema sigue estos pasos:

Este diseño podría recibir una objeción obvia: que el paso 2 agrega un salto de red a la ruta de confirmación. Pero cualquier implementación de Postgres que se tome en serio la durabilidad ya ejecuta una réplica síncrona, que también es un salto de red. Externalizar el WAL reemplaza un viaje de ida y vuelta de red por otro, en lugar de agregar uno nuevo.
Cada solicitud de lectura de un nodo de cómputo lleva un identificador de página y un LSN, y la capa de almacenamiento devuelve la página tal como existía en ese LSN. Esta GetPage@LSN es una operación central en esta arquitectura.
Para atenderla, se sigue un orden de preferencia:
La página devuelta se almacena en caché en la RAM y en el NVMe, por lo que la siguiente lectura de esta vuelve a ser local.

Un nodo primario solicita la última versión de cada página, por lo que en estado estable se comporta como cualquier Postgres que lee de una caché caliente. Pero nada en el protocolo requiere la "última". Solicite una página en un LSN de hace cuatro horas y obtendrá esa página de hace cuatro horas.
La consecuencia útil es que la distinción entre datos activos y copias de seguridad históricas desaparece. Hay un solo sistema de almacenamiento. Las versiones de páginas antiguas no son un artefacto separado guardado en otro lugar en un formato diferente; son los mismos archivos inmutables, aún direccionables.
En otras palabras, el pageserver nunca actualiza un archivo en el mismo lugar. Los archivos se crean, se fusionan y se eliminan, pero nunca se modifican. Esto se adapta perfectamente al almacenamiento de objetos, que no ofrece actualizaciones aleatorias, y eso hace que mantener el historial sea lo suficientemente económico.
Los datos se organizan en dos tipos de archivos de capa:
Las capas de imagen se producen en segundo plano por dos razones: acortan la cadena de reproducción que debe recorrer una lectura y permiten recolectar los deltas antiguos. Sin ellas, reconstruir una página podría requerir retroceder arbitrariamente hacia atrás.
Así que GetPage@LSN se convierte en una búsqueda: comenzar en la clave y el LSN solicitados, descender a través de las capas recopilando registros WAL para esa página y detenerse en la primera imagen de ella. Para mantener esa búsqueda corta, las capas delta y de imagen se reorganizan mediante una compactación en segundo plano, y las capas que quedan fuera de la ventana de retención se eliminan mediante la recolección de basura.
La búsqueda descrita anteriormente parece sencilla, pero no lo es. Vale la pena dedicarle algún tiempo, ya que determina si todo el diseño es viable.
Una lectura nombra una clave y un LSN. El sistema de almacenamiento tiene que encontrar la capa más cercana que cubra esa clave en ese LSN o antes. Ese es un problema geométrico y no es obvio cómo resolverlo en decenas de millones de capas. Un escaneo lineal es demasiado lento y las estructuras espaciales obvias no se adaptan: los árboles R responden a consultas de contención en lugar de "la primera capa debajo de este punto", y los árboles de segmentos se escalan con el tamaño del espacio de coordenadas en lugar de con el número de capas.
Existen varios enfoques para este diseño, pero lo que funcionó fue resolver primero el problema fácil y luego hacer que la estructura de datos recuerde su propio pasado.
Para un LSN fijo, determinamos qué capa responde a cada clave. Esa respuesta solo cambia en un puñado de puntos en el espacio de claves, por lo que registramos esos puntos y los almacenamos en un árbol de búsqueda binaria. Ese árbol es la cobertura de capas para ese LSN y responde a cualquier lectura en ese LSN con una sola búsqueda.
Esto funciona, pero solo para un LSN. La cobertura cambia cada vez que se agrega una capa, y hay millones de LSN, por lo que no podemos construir y mantener un árbol separado para cada uno.
Persistente en el sentido de “mantener disponibles las versiones antiguas”. Construimos la cobertura de forma incremental, insertando capas en orden de LSN de abajo hacia arriba. Insertar una capa solo afecta a los nodos a lo largo de una única ruta desde la raíz hacia abajo. En lugar de sobrescribir esos nodos, el sistema los copia y deja los originales intactos. Las nuevas copias apuntan a los subárboles antiguos y sin cambios a cada lado.
De ello se deducen dos cosas:
Hacemos eso para cada capa, en orden, y terminamos con una sola estructura que contiene cada raíz intermedia, cada una con la cobertura en un LSN diferente. Obtenemos todos esos árboles por casi el precio de uno.
Una lectura histórica cuesta entonces lo mismo que una actual: el sistema elige la raíz para el LSN que desea y realiza la misma búsqueda única.
Ese es el truco, en resumen:
Aquí es donde el argumento actual sobre Postgres y el almacenamiento de objetos suele fallar, en ambas direcciones.
El argumento clásico en contra de construir OLTP en el almacenamiento de objetos es el siguiente:
En sí misma, esa no es una afirmación controvertida. Lo que el argumento interpreta mal es la suposición de que una base de datos construida sobre almacenamiento de objetos debe estar leyendo del almacenamiento de objetos para responder a las consultas.
En la arquitectura que proponemos, nunca lo hace:
Cuando Postgres se diseña de esta manera, se convierte en una evolución de los sistemas OLTP tradicionales que está diseñada para manejar cargas de trabajo agénticas. Por eso creamos Lakebase Postgres: una base de datos OLTP donde el cómputo y el almacenamiento están desacoplados, y la fuente de verdad duradera se basa en el almacenamiento de objetos.
Con Lakebase Postgres, el historial de transacciones es direccionable por LSN, y las copias son referencias en lugar de datos. Eso hace posible crear características que le dan a Postgres el flujo de trabajo ligero que es un requisito absoluto para los agentes.
En primer lugar, Postgres ahora permite crear ramas. Crear una rama no copia páginas, sino que crea un puntero a un LSN específico, y la rama comienza a divergir a partir de ahí con semántica de copia en escritura (copy-on-write).
Las escrituras en la rama se almacenan como deltas con respecto a la rama padre, por lo que una rama de una base de datos de 2 TB se crea en segundos y no cuesta nada hasta que cambia algo. La rama padre no experimenta ninguna carga adicional, por lo que es seguro hacerlo en producción.
Esto es lo que un agente necesita para trabajar de forma segura. Puede tomar una rama por tarea, ejecutar la migración que acaba de escribir con datos reales a un volumen real e inspeccionar el resultado antes de que algo afecte a la rama padre. Veinte agentes pueden hacer eso a la vez, cada uno aislado de los demás y de producción.
Con Lakebase Postgres, incluso hemos extendido la ramificación más allá de la base de datos. Los buckets de almacenamiento de objetos, las funciones, el estado de Managed Better Auth y la configuración de AI Gateway se ramifican junto con la base de datos, por lo que una rama es una copia aislada del backend en lugar de solo las tablas de Postgres.
La recuperación a un punto en el tiempo (point-in-time recovery) es una ramificación con una intención diferente. Restaurar significa apuntar a un LSN anterior y reanudar desde allí, por lo que no implica volver a copiar los datos a su lugar y su costo no escala con el tamaño de la base de datos. Qué tan atrás puede ir es una configuración de retención.
Esto es lo que hace que los errores de un agente sean económicos. Cuando un agente ejecuta la instrucción incorrecta, la solución no es una ventana de restauración y un plan de recuperación, sino volver a apuntar la rama al LSN anterior a la ejecución. Deshacer cuesta lo mismo en una base de datos de 2 TB que en una vacía, por lo que un agente puede volver a intentarlo en lugar de escalar el problema a un humano.
Debido a que el pageserver puede reconstruir cualquier página en cualquier LSN dentro de la ventana de historial, puede consultar un estado pasado directamente en lugar de restaurarlo primero.
El uso práctico es la comparación (diffing): cómo se veía esta tabla antes de la migración y cómo se ve ahora. También es la forma de confirmar que eligió la marca de tiempo correcta antes de comprometerse con una restauración.
Un nodo de cómputo de solo lectura no es una copia de los datos. Solicita páginas de la misma capa de almacenamiento que el primario, por lo que agregar uno no significa aprovisionar un conjunto de datos y esperar a que se actualice. Poner uno en funcionamiento es una operación de metadatos.
Dado que el estado duradero reside fuera del cómputo, un nodo de cómputo inactivo se puede apagar por completo en lugar de dejarlo en funcionamiento para proteger los datos. El cómputo se suspende después de 5 minutos de inactividad y se reactiva en unos pocos cientos de milisegundos en la siguiente consulta. Para una flota de bases de datos por sesión o por rama, la mayoría de las cuales están inactivas la mayor parte del tiempo, esta es la diferencia entre un modelo de costos viable y uno inviable. Ten en cuenta que el cómputo deja de facturarse mientras está suspendido; el almacenamiento se sigue facturando porque el historial sigue ahí.
Una sesión de agente que trabaja durante cuatro minutos y luego se silencia deja de generar costos de cómputo cinco minutos después, sin que nadie tenga que desmantelarla. Eso es lo que hace que una base de datos por agente, por sesión o por rama sea lo suficientemente económica como para ser la opción predeterminada.
Hay otra consecuencia de colocar los datos operativos en el almacenamiento de objetos
Una vez que el registro duradero de una base de datos transaccional reside en un almacenamiento de objetos estándar, deja de estar bloqueado dentro del formato privado de un motor en los discos de ese motor. Otros motores pueden leerlo.
Esa es la base de lo que llamamos LTAP, o procesamiento transaccional/analítico en el lago (Lake Transactional/Analytical Processing): en lugar de dos copias de los datos en dos formatos mantenidas sincronizadas por un pipeline, hay una sola copia duradera en formatos columnares abiertos que leen tanto el lado transaccional como el analítico.
El mecanismo se deriva de la ruta de lectura ya descrita. A medida que el pageserver materializa las páginas en el almacenamiento de objetos, las transcodifica del formato de filas de Postgres al formato columnar, preservando la representación exacta de Postgres de cada valor. Una consulta analítica le pide a Postgres el LSN actual, lo cual es una búsqueda de metadatos económica, lee la gran mayoría de los datos del almacenamiento de objetos a partir de ese LSN y recupera solo los cambios no materializados más recientes del pageserver. Postgres no atiende nada del tráfico de lectura analítica más allá de devolver ese único número, por lo que una consulta analítica grande no compite con las transacciones por la misma CPU.
La diferencia con la captura de datos modificados (CDC) y el mirroring es que no hay nada que activar. No hay una lista de tablas replicadas, porque no hay replicación. Una tabla ya existe en el lago, lo que también significa que las dos vistas no pueden desincronizarse.
Comenzamos esta publicación con una pregunta: ¿podría el almacenamiento de objetos estar debajo de Postgres y facilitar el trabajo de los agentes?
La respuesta es sí. El almacenamiento de objetos puede estar debajo de Postgres y cambiar la forma en que interactúas con él, pero no solo porque S3 sea rápido o económico de ejecutar. Como se describe en esta publicación, requiere más ingeniería que eso. Aún se necesitan RAM y NVMe local para servir las consultas lo suficientemente rápido, y un commit aún se registra en el WAL replicado en lugar de en un bucket.
Esa pieza de WAL es la clave. El almacenamiento de objetos añade una forma económica y escalable de almacenar todo el historial, pero hacer que el WAL sea la fuente de verdad es lo que hace que ese historial sea direccionable y cambia la forma en que los agentes interactúan con Postgres y las funciones que puedes crear sobre él.
Pídele a tu agente que implemente Lakebase Postgres y ponlo a prueba. Comienza aquí.
Lakebase Postgres se puede utilizar como una base de datos independiente, y también puedes integrarla con el resto de Databricks Data + AI Platform: gobernanza de Unity Catalog, analítica de lakehouse, notebooks y flujos de trabajo de IA.
(Esta entrada del blog ha sido traducida utilizando herramientas basadas en inteligencia artificial) Publicación original
Suscríbete a nuestro blog y recibe las últimas publicaciones directamente en tu bandeja de entrada.