Cambia il modo in cui gli agenti lavorano con Postgres trattando il WAL come una fonte di verità duratura
Gli agenti che interagiscono con un database OLTP tradizionale creano spesso colli di bottiglia a livello di storage. Nuovi deployment, copie, ripristini e repliche comportano lo spostamento di grandi volumi di dati, un'operazione che richiede tempo ed è costosa.
L'esatto contrario vale per l'object storage. Amazon S3, ad esempio, è economico, performante e quasi invisibile da gestire. Crea un livello di storage scalabile e conveniente per la memoria degli agenti.
Il che ci porta alla domanda: l'object storage può essere posizionato al di sotto di un database transazionale e facilitare il lavoro degli agenti?
Questa domanda è ciò che ha dato il via a Lakebase Postgres. La risposta non dipende solo dalla velocità dell'object store, ma piuttosto da dove si posiziona la fonte di verità.
Il modello mentale comune per l'OLTP è incentrato sui dati. I dati sono organizzati in tabelle con righe e colonne, ognuna delle quali rappresenta un'entità. Lo storage è il luogo in cui risiede lo stato corrente e il compito del database è memorizzarlo e recuperarlo.
But there is a second model: transaction-centric. Here the database is a journal of transactions. Each entry is an operation, and storage is a timeline of those operations rather than a snapshot of the present. The current state is one thing you can derive from the timeline.
Per anni, il modello incentrato sui dati è stato l'unico a contare nella pratica, perché ciò che il team delle operazioni richiedeva a un database erano letture e scritture rispetto al presente. Negli ultimi anni, la situazione è cambiata radicalmente. Le operazioni richieste dai carichi di lavoro degli agenti sono quasi tutte operazioni sulla cronologia delle transazioni:
Queste sono tutte query sulla cronologia. Un database che memorizza solo il presente fornisce copie e backup, che sono lenti e costosi.
Tuttavia, Postgres contiene già questa cronologia: si chiama write-ahead log (WAL).
Il WAL di Postgres registra ogni modifica prima che raggiunga i file di dati. Inizialmente esisteva per consentire il ripristino di Postgres: se il server si arrestava tra la scrittura del log e quella del file di dati, un replay del WAL colmava il divario.
Ma i contenuti del WAL sono interessanti ben oltre il ripristino. Prendiamo una tabella e un inserimento:
Prima che tale modifica raggiunga la tabella users su disco, Postgres la aggiunge al WAL. Il log è binario, ma pg_waldump lo renderizzerà. I record per questo inserimento si presentano all'incirca così:
Questi sono quattro record e una transazione. Si noti come ognuno abbia un log sequence number (LSN), un identificatore monotonicamente crescente.
Le righe heap e btree indicano anche l'esatta pagina da 8 KB che è cambiata. Il log non dice "è stata aggiunta una riga". Dice quale pagina, in quale relazione, in quale punto della cronologia.
Se lo si legge come un meccanismo di ripristino, si tratta di un elenco di operazioni da ripetere dopo un arresto anomalo. Ma se lo si legge come un registro delle transazioni, è qualcos'altro: un resoconto completo, ordinato e a livello di byte di ogni singola pagina che il database abbia mai modificato, con un nome univoco per ogni voce.
Quel nome, l'LSN, è la parte che conta di più. Significa che la cronologia è già indirizzabile. Non è necessario aggiungere nulla a Postgres per rendere "il database a un determinato istante nel tempo" un concetto ben definito. Richiede solo un livello di storage che conservi il log e possa rispondere alle query su di esso.
In un deployment Postgres convenzionale, the WAL è un mezzo per raggiungere un fine. I file di dati sono il database, il log li protegge e il log viene rimosso una volta che i suoi record sono stati applicati in modo sicuro. Lo storage è semplicemente un disco collegato alla macchina che esegue Postgres e tutto ciò che riguarda l'identità del database è legato a quella macchina.
Ora invertiamo il concetto. Rendiamo il log il database e i file di dati una sua rappresentazione derivata e memorizzata nella cache. In questo modo è possibile conservare l'intera cronologia e non è più necessario spostare i dati per copiare o riavvolgere il database. La cronologia diventa indirizzabile, quindi una "copia" del database diventa un puntatore invece di un secondo set di file. Questo rende i deployment, i ripristini e le repliche abbastanza economici da poter essere trattati come codice.
Questo è ciò che abbiamo fatto in Lakebase Postgres. In concreto, abbiamo suddiviso il sistema in due livelli:
Il livello compute esegue Postgres standard. Analizza l'SQL, pianifica ed esegue le query, applica l'MVCC, gestisce i blocchi e gli indici.
Nulla nel query engine viene riscritto. Ciò che cambia è la responsabilità del nodo compute: esiste per eseguire il lavoro, non per preservare i dati. Dispone di RAM per i buffer condivisi e di NVMe locale come cache di pagina, e può avviarsi, arrestarsi, scalare o interrompersi in qualsiasi momento senza mettere a rischio la durabilità.
Il livello di storage gestisce la correttezza, la durabilità e la cronologia. Sopravvive a qualsiasi singolo nodo compute ed è composto da tre componenti con compiti distinti:
fsync di una singola macchina.
Come si presenta il percorso di scrittura? Un commit in questo sistema segue questi passaggi:

Questo design potrebbe sollevare un'obiezione ovvia: che il passaggio 2 aggiunga un hop di rete al percorso di commit. Tuttavia, qualsiasi deployment Postgres che prenda sul serio la durabilità esegue già la replica sincrona, che è anch'essa un hop di rete. L'esternalizzazione del WAL sostituisce un round trip di rete con un altro, anziché aggiungerne uno.
Ogni richiesta di lettura da un nodo compute contiene un identificatore di pagina e un LSN, e il livello di storage restituisce la pagina così com'era a quell'LSN. Questo GetPage@LSN è un'operazione centrale in questa architettura.
La sua gestione segue un ordine di preferenza:
La pagina restituita viene quindi memorizzata nella cache in RAM e su NVMe, in modo che la lettura successiva sia nuovamente locale.

Un nodo primario richiede la versione più recente di ogni pagina, quindi a regime si comporta come qualsiasi Postgres che legge da una cache calda. Ma nulla nel protocollo richiede la versione "più recente". Richiedi una pagina a un LSN di quattro ore fa e otterrai quella pagina di quattro ore fa.
Il vantaggio pratico è che la distinzione tra dati attivi e backup storici scompare. Esiste un unico sistema di storage. Le vecchie versioni delle pagine non sono un artefatto separato conservato altrove in un formato diverso; sono gli stessi file immutabili, ancora indirizzabili.
In altre parole, il pageserver non aggiorna mai un file in-place. I file vengono creati, uniti ed eliminati, ma mai modificati. Questo si adatta perfettamente all'object storage, che non offre aggiornamenti casuali, e rende la conservazione della cronologia molto economica.
I dati sono organizzati in due tipi di file layer:
I layer d'immagine vengono generati in background per due motivi: accorciano la catena di replay che una lettura deve percorrere e rendono i vecchi delta recuperabili. Senza di essi, la ricostruzione di una pagina potrebbe richiedere di andare a ritroso all'infinito.
Quindi GetPage@LSN diventa una ricerca: inizia dalla chiave e dall'LSN richiesti, scendi attraverso i layer raccogliendo i record WAL per quella pagina e fermati alla sua prima immagine. Per mantenere breve questa ricerca, i layer delta e d'immagine vengono riorganizzati tramite compattazione in background, e i layer che non rientrano nella finestra di conservazione vengono eliminati tramite garbage collection.
La ricerca sopra descritta sembra semplice, ma non lo è. Vale la pena soffermarsi su questo aspetto, poiché determina la fattibilità dell'intero progetto.
Una lettura specifica una chiave e un LSN. Il sistema di storage deve trovare il layer più vicino che copre quella chiave in corrispondenza o prima di quell'LSN. Si tratta di un problema geometrico e non è ovvio come risolverlo su decine di milioni di layer. Una scansione lineare è decisamente troppo lenta e le strutture spaziali ovvie non sono adatte: gli R-tree rispondono a query di contenimento anziché a "il primo layer sotto questo punto", e i segment tree scalano con le dimensioni dello spazio delle coordinate anziché con il numero di layer.
Esistono diversi approcci a questo design, ma ciò che ha funzionato è stato risolvere prima il problema semplice, per poi fare in modo che la struttura dati ricordasse il proprio passato.
Per un LSN fisso, determiniamo quale layer risponde a ciascuna chiave. Tale risposta cambia solo in pochi punti dello spazio delle chiavi, quindi registriamo questi punti e li memorizziamo in un albero binario di ricerca. Questo albero rappresenta la copertura del layer per quell'LSN e risponde a qualsiasi lettura a quell'LSN con un singolo lookup.
Questo funziona, ma solo per un singolo LSN. La copertura cambia ogni volta che viene aggiunto un layer e ci sono milioni di LSN, quindi non possiamo creare e mantenere un albero separato per ognuno di essi.
Persistente nel senso di "mantenere disponibili le vecchie versioni". Costruiamo la copertura in modo incrementale, inserendo i layer in ordine di LSN dal basso verso l'alto. L'inserimento di un layer tocca solo i nodi lungo un singolo percorso dalla radice verso il basso. Invece di sovrascrivere questi nodi, il sistema li copia e lascia intatti gli originali. Le nuove copie puntano ai vecchi sottoalberi invariati su entrambi i lati.
Da ciò derivano due conseguenze:
Eseguiamo questa operazione per ogni layer, in ordine, e otteniamo un'unica struttura che contiene ogni radice intermedia, ciascuna corrispondente alla copertura a un LSN diverso. Otteniamo tutti questi alberi quasi al prezzo di uno.
Una lettura storica ha quindi lo stesso costo di una corrente: il sistema sceglie la radice per l'LSN desiderato ed esegue lo stesso singolo lookup.
In sintesi, questo è il trucco:
È qui che l'attuale dibattito su Postgres e l'object storage tende a sbagliare, in entrambe le direzioni.
La classica tesi contraria alla creazione di OLTP su object storage è la seguente:
Di per sé, questa non è un'affermazione controversa. Ciò che questa tesi sbaglia è l'assunto che un database basato su object storage debba necessariamente leggere dall'object storage per rispondere alle query.
Nell'architettura che proponiamo, questo non accade mai:
Quando Postgres è strutturato in questo modo, è un'evoluzione dei sistemi OLTP tradizionali progettata per gestire carichi di lavoro agentici. Ecco perché abbiamo creato Lakebase Postgres: un database OLTP in cui calcolo e storage sono disaccoppiati, e la fonte di verità durevole è basata su object storage.
Con Lakebase Postgres, la cronologia delle transazioni è indirizzabile tramite LSN e le copie sono riferimenti anziché dati. Ciò consente di creare funzionalità che offrono a Postgres quel workflow leggero che rappresenta un requisito fondamentale per gli agenti.
In primo luogo, ora Postgres supporta il branching. La creazione di un branch non copia le pagine, ma crea un puntatore a un LSN specifico, e il branch inizia a divergere da quel punto con semantica copy-on-write.
Le scritture sul branch vengono memorizzate come delta rispetto al parent, quindi un branch di un database da 2 TB viene creato in pochi secondi e non costa nulla finché non viene modificato qualcosa. Il parent non subisce alcun carico aggiuntivo, motivo per cui questa operazione è sicura da eseguire in produzione.
Questo è ciò di cui un agente ha bisogno per lavorare in sicurezza. Può creare un branch per ogni task, eseguire la migrazione appena scritta su dati reali con volumi reali e ispezionare il risultato prima che venga toccato il parent. Venti agenti possono farlo contemporaneamente, ciascuno isolato dagli altri e dalla produzione.
Con Lakebase Postgres abbiamo persino esteso il branching oltre il database. I bucket di Object Storage, le Functions, lo stato di Managed Better Auth e la configurazione di AI Gateway creano branch insieme al database, in modo che un branch sia una copia isolata del backend anziché solo delle tabelle Postgres.
Il point-in-time recovery è un branching con un intento diverso. Il ripristino consiste nel puntare a un LSN precedente e riprendere da lì, quindi non comporta la ricopiatura dei dati e il suo costo non scala con le dimensioni del database. La possibilità di andare a ritroso nel tempo dipende da un'apposita impostazione di conservazione.
Questo è ciò che rende economici gli errori di un agente. Quando un agente esegue un'istruzione errata, la soluzione non è una finestra di ripristino e un piano di recovery, ma consiste nel riportare il branch all'LSN precedente all'esecuzione. L'annullamento dell'operazione ha lo stesso costo su un database da 2 TB rispetto a uno vuoto, consentendo all'agente di riprovare invece di richiedere l'intervento umano.
Poiché il pageserver può ricostruire qualsiasi pagina a qualsiasi LSN all'interno della finestra temporale della cronologia, è possibile interrogare direttamente uno stato passato anziché ripristinarlo prima.
L'utilità pratica risiede nel confronto (diffing): che aspetto aveva questa tabella prima della migrazione e come si presenta ora. Inoltre, consente di verificare di aver scelto il timestamp corretto prima di procedere con il ripristino.
Un nodo di calcolo di sola lettura non è una copia dei dati. Richiede le pagine dallo stesso layer di storage del primario, quindi aggiungerne uno non significa dover eseguire il provisioning di un dataset e attendere che si allinei. L'avvio di un nuovo nodo è un'operazione sui metadati.
Poiché lo stato durevole risiede al di fuori del compute, un nodo di calcolo inattivo può essere spento completamente anziché essere lasciato in esecuzione per proteggere i dati. I nodi di calcolo si sospendono dopo 5 minuti di inattività e si riattivano entro poche centinaia di millisecondi alla query successiva. Per una flotta di database per sessione o per branch, la maggior parte dei quali è inattiva per la maggior parte del tempo, questa è la differenza tra un modello di costo sostenibile e uno insostenibile. Nota che la fatturazione del calcolo si interrompe durante la sospensione; lo storage continua a essere fatturato, perché la cronologia è ancora presente.
Una sessione di un agente che lavora per quattro minuti e poi si ferma smette di generare costi di calcolo cinque minuti dopo, senza che nessuno debba arrestarla. Questo è ciò che rende un database per agente, per sessione o per branch sufficientemente conveniente da poter essere l'opzione predefinita.
C'è un'altra conseguenza nel collocare i dati operativi nell'object storage
Una volta che il record durevole di un database transazionale risiede in un object storage standard, smette di essere bloccato all'interno del formato privato di un singolo motore sui suoi dischi. Altri motori possono leggerlo.
Questa è la base di ciò che chiamiamo LTAP, ovvero Lake Transactional/Analytical Processing: invece di avere due copie dei dati in due formati diversi mantenute sincronizzate da una pipeline, esiste un'unica copia durevole in formati colonnari aperti che viene letta sia dal lato transazionale che da quello analitico.
Il meccanismo deriva dal percorso di lettura già descritto. Mentre il pageserver materializza le pagine nell'object storage, le transcodifica dal formato riga di Postgres al formato colonnare, preservando l'esatta rappresentazione Postgres di ogni valore. Una query analitica richiede a Postgres l'LSN corrente, che è una ricerca di metadati economica, legge la stragrande maggioranza dei dati dall'object storage a partire da quell'LSN e recupera solo le modifiche non materializzate più recenti dal pageserver. Postgres non gestisce alcun traffico di lettura analitica oltre a restituire quel singolo numero, quindi una query analitica di grandi dimensioni non compete con le transazioni per la stessa CPU.
La differenza rispetto al change data capture (CDC) e al mirroring è che non c'è nulla da attivare. Non esiste un elenco di tabelle replicate, perché non c'è replica. Una tabella esiste già nel lake, il che significa anche che le due viste non possono divergere.
Abbiamo iniziato questo post con una domanda: l'object storage potrebbe trovarsi al di sotto di Postgres e facilitare il lavoro degli agenti?
La risposta è sì. L'object storage può trovarsi al di sotto di Postgres e cambiare il modo in cui interagisci con esso, ma non solo perché S3 è veloce o economico da gestire. Come descritto in questo post, richiede una progettazione più complessa. RAM e NVMe locali sono ancora necessari per servire le query in modo sufficientemente rapido, e un commit finisce comunque su un WAL replicato anziché in un bucket.
Quella parte di WAL è la chiave. L'object storage aggiunge un modo economico e scalabile per memorizzare tutta la cronologia, ma rendere il WAL la fonte di verità è ciò che rende tale cronologia indirizzabile e cambia il modo in cui gli agenti interagiscono con Postgres e le funzionalità che puoi creare su di esso.
Chiedi al tuo agente di distribuire Lakebase Postgres e mettilo alla prova. Inizia qui.
Lakebase Postgres può essere utilizzato come database autonomo e puoi anche integrarlo con il resto della piattaforma Databricks Data + AI: governance di Unity Catalog, analytics del lakehouse, notebook e workflow di AI.
(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.