Ir para o conteúdo principal
Engenharia

Armazenamento de objetos + WAL: Lakebase Postgres para a era dos agentes

Mude a maneira como os agentes trabalham com o Postgres tratando o WAL como uma fonte durável da verdade

por Cassie Murray e Carlota Soto

  • O Lakebase Postgres reinventa os bancos de dados OLTP tradicionais ao desacoplar computação e armazenamento, tratando o Write-Ahead Log (WAL) em armazenamento de objetos escalável (como o S3) como a fonte definitiva da verdade.
  • Ao substituir a pesada duplicação física de dados por ponteiros leves para Log Sequence Numbers (LSNs), a arquitetura permite ramificação instantânea de banco de dados, restauração para um ponto no tempo e consultas de viagem no tempo (time-travel) que são ideais para cargas de trabalho de agentes.
  • Armazenar um histórico de transações imutável em formatos colunares abertos permite que mecanismos transacionais e analíticos consultem diretamente um único conjunto de dados unificado, eliminando a necessidade de réplicas de leitura separadas ou pipelines complexos de sincronização de dados.

Agentes que interagem com um banco de dados OLTP tradicional frequentemente criam gargalos na camada de armazenamento. Novas implantações, cópias, restaurações e réplicas significam a movimentação de grandes volumes de dados, o que consome muito tempo e é caro.

O oposto é verdadeiro para o armazenamento de objetos. O Amazon S3, por exemplo, é barato, eficiente e quase invisível de operar. Ele cria uma camada de armazenamento escalável e econômica para a memória dos agentes.

O que nos leva à pergunta: o armazenamento de objetos pode ficar abaixo de um banco de dados transacional e facilitar o trabalho dos agentes?

Essa pergunta foi o que deu início ao Lakebase Postgres. A resposta não depende apenas da velocidade do seu armazenamento de objetos, mas sim de onde você coloca a fonte da verdade.

Dois modelos OLTP

O modelo mental usual para OLTP é centrado em dados. Os dados são organizados em tabelas com linhas e colunas, cada uma representando uma entidade. O armazenamento é o local onde reside o estado atual, e o trabalho do banco de dados é armazená-lo e recuperá-lo.

Mas existe um segundo modelo: centrado em transações. Aqui, o banco de dados é um diário de transações. Cada entrada é uma operação, e o armazenamento é uma linha do tempo dessas operações, em vez de um instantâneo do presente. O estado atual é algo que você pode derivar da linha do tempo.

Durante anos, o modelo centrado em dados foi o único que importava na prática, porque o que a equipe de operações exigia de um banco de dados eram leituras e gravações em relação ao presente. Nos últimos anos, isso mudou drasticamente. As operações que as cargas de trabalho dos agentes exigem são quase todas operações no histórico de transações:

  • Dê-me uma cópia isolada de produção para trabalhar
  • Coloque de volta como estava antes das minhas últimas três instruções
  • Mostre-me como era esta tabela antes da migração
  • Execute vinte destas de uma vez e exclua dezenove delas em uma hora

Todas essas são consultas sobre a linha do tempo. Um banco de dados que armazena apenas o presente fornece cópias e backups, que são lentos e caros.

No entanto, o Postgres já contém essa linha do tempo: ela é chamada de write-ahead log (WAL).

A escrita no WAL

O WAL do Postgres registra cada modificação antes que ela chegue aos arquivos de dados. Ele existia originalmente para que o Postgres pudesse se recuperar: se o servidor caísse entre a gravação do log e a gravação do arquivo de dados, uma reprodução do WAL preenchia a lacuna.

Mas o conteúdo do WAL é interessante muito além da recuperação. Considere uma tabela e uma inserção:

Antes que essa alteração chegue à tabela users no disco, o Postgres a anexa ao WAL. O log é binário, mas o pg_waldump irá renderizá-lo. Os registros para esta inserção são mais ou menos assim:

Estes são quatro registros e uma transação. Observe como cada um possui um número de sequência de log (LSN), um identificador monotonicamente crescente.

As linhas heap e btree também nomeiam a página exata de 8 KB que foi alterada. O log não diz "uma linha foi adicionada". Ele diz qual página, em qual relação, em qual ponto da linha do tempo.

Leia isso como um mecanismo de recuperação e será uma lista de tarefas a serem refeitas após uma falha. Mas se você ler isso como um diário de transações, será algo diferente: um relato completo, ordenado e em nível de byte de cada página que o banco de dados já alterou, com um nome exclusivo em cada entrada.

Esse nome, o LSN, é a parte que mais importa. Isso significa que a linha do tempo já é endereçável. Nada precisa ser adicionado ao Postgres para tornar "o banco de dados em um ponto no tempo" algo bem definido. Ele precisa apenas de uma camada de armazenamento que mantenha o log disponível e possa responder a perguntas com base nele.

O log se torna a fonte da verdade

Em uma implantação convencional do Postgres, o WAL é um meio para um fim. Os arquivos de dados são o banco de dados, o log os protege e o log é truncado assim que seus registros são aplicados com segurança. O armazenamento é simplesmente um disco conectado à máquina que executa o Postgres, e tudo sobre a identidade do banco de dados está vinculado a essa máquina.

Agora, vamos inverter isso. Faça do log o banco de dados, e dos arquivos de dados uma representação derivada e armazenada em cache dele. Assim, você pode manter a linha do tempo completa e não precisa mais mover dados para copiar ou retroceder o banco de dados. O histórico torna-se endereçável, de modo que uma "cópia" do banco de dados se torna um ponteiro em vez de um segundo conjunto de arquivos. Isso torna as implantações, restaurações e réplicas baratas o suficiente para serem tratadas como código.

Foi isso que fizemos no Lakebase Postgres. Concretamente, dividimos o sistema em duas camadas:

A camada de computação

A camada de computação executa o Postgres padrão. Ela analisa o SQL, planeja e executa consultas, impõe o MVCC, gerencia bloqueios e índices.

Nada no mecanismo de consulta é reescrito. O que muda é a responsabilidade do nó de computação: ele existe para executar o trabalho, não para preservar os dados. Ele possui RAM para buffers compartilhados e NVMe local como cache de página, e pode iniciar, parar, dimensionar ou falhar a qualquer momento sem colocar a durabilidade em risco.

A camada de armazenamento

A camada de armazenamento é responsável pela correção, durabilidade e histórico. Ela sobrevive a qualquer nó de computação individual e é construída a partir de três componentes com funções distintas:

  • Os safekeepers replicam o WAL. Quando o nó de computação gera registros do WAL, ele os transmite para vários safekeepers, e uma transação é confirmada assim que um quórum confirma o registro por meio de um protocolo baseado em Paxos. A durabilidade é uma propriedade de replicação e consenso, e não do fsync de uma única máquina.
  • O pageserver transforma o WAL em páginas. Ele combina páginas base com registros do WAL confirmados para materializar a versão de uma página que uma determinada consulta precisa, e persiste essas versões materializadas no armazenamento de objetos de forma assíncrona.
  • O armazenamento de objetos mantém o histórico imutável de longo prazo. As versões de página materializadas e os estados históricos são mantidos como um registro do tipo append-only (somente anexação), em vez de um sistema de arquivos mutável.

image2.png

O caminho de gravação

Como é o caminho de gravação? Um commit neste sistema segue estas etapas:

  1. O Postgres aplica as alterações na memória. Os buffers são atualizados, os índices são modificados e os registros do WAL são gerados exatamente como de costume.
  2. Em vez de descarregar o WAL em um sistema de arquivos local, o nó de computação o transmite pela rede para os safekeepers.
  3. A transação é confirmada assim que um quórum de safekeepers confirma o recebimento do registro. Esse é o ponto em que o cliente recebe a confirmação de sucesso.
  4. A materialização da página acontece depois, na camada de armazenamento, fora do caminho crítico da transação. Um commit nunca espera que as páginas sejam gravadas ou carregadas.

image3.png

Este design pode receber uma objeção óbvia: a de que a etapa 2 adiciona um salto de rede ao caminho de commit. Mas qualquer implantação do Postgres que leve a durabilidade a sério já executa replicação síncrona, que também é um salto de rede. Externalizar o WAL substitui uma viagem de ida e volta de rede por outra, em vez de adicionar uma nova.

O caminho de leitura

Cada solicitação de leitura de um nó de computação carrega um identificador de página e um LSN, e a camada de armazenamento retorna a página como ela existia naquele LSN. Este GetPage@LSN é uma operação central nesta arquitetura.

O atendimento a essa solicitação segue uma ordem de preferência:

  1. Primeiro vem a RAM para buffers compartilhados do Postgres, exatamente como em qualquer Postgres.
  2. Depois vem o NVMe local, que continua rápido e local. Se a página não estiver na memória, o nó de computação verificará seu cache de disco local
  3. Somente em caso de falha local a solicitação cruza a rede até o pageserver. O pageserver então verifica se já possui essa versão de página materializada. Caso contrário, ele encontra a imagem mais recente da página no LSN solicitado ou antes dele, coleta os registros do WAL sobre ela, os reproduz e retorna a página reconstruída.

A página retornada é então armazenada em cache na RAM e no NVMe, para que a próxima leitura dela seja local novamente.

image4.png

Um nó primário solicita a versão mais recente de cada página, de modo que, em estado estável, ele se comporta como qualquer Postgres lendo de um cache quente. Mas nada no protocolo exige o "mais recente". Solicite uma página em um LSN de quatro horas atrás e você obterá essa página de quatro horas atrás.

A consequência útil é que a distinção entre dados ativos e backups históricos desaparece. Existe apenas um sistema de armazenamento. As versões antigas das páginas não são um artefato separado mantido em outro lugar em um formato diferente; elas são os mesmos arquivos imutáveis, ainda endereçáveis.

Armazenamento sem sobrescrita

Em outras palavras, o pageserver nunca atualiza um arquivo no local. Os arquivos são criados, mesclados e excluídos, mas nunca modificados. Isso se ajusta perfeitamente ao armazenamento de objetos, que não oferece atualizações aleatórias, e torna o histórico barato o suficiente para ser mantido.

Os dados são organizados em dois tipos de arquivos de camada:

  • Uma camada de imagem contém um snapshot de cada chave em um intervalo de chaves em um LSN
  • Uma camada delta contém todas as alterações em um intervalo de chaves e LSN. As chaves que não foram modificadas não são armazenadas. O WAL recebido é gravado como camadas delta.

As camadas de imagem são produzidas em segundo plano por dois motivos: elas encurtam a cadeia de reprodução que uma leitura precisa percorrer e tornam os deltas antigos coletáveis. Sem elas, a reconstrução de uma página poderia exigir voltar arbitrariamente no tempo.

Assim, GetPage@LSN se torna uma busca: comece na chave e no LSN solicitados, desça pelas camadas coletando registros de WAL para essa página e pare na primeira imagem dela. Para manter essa busca curta, as camadas delta e de imagem são reorganizadas por compactação em segundo plano, e as camadas que ficam fora da janela de retenção passam pela coleta de lixo (garbage collection).

Como encontrar a camada certa rapidamente

A busca descrita acima parece simples, mas não é. Vale a pena dedicar algum tempo a isso, pois determina se todo o design é viável.

Uma leitura especifica uma chave e um LSN. O sistema de armazenamento precisa encontrar a camada mais próxima que cubra essa chave no LSN ou antes dele. Esse é um problema geométrico, e não é óbvio como resolvê-lo em dezenas de milhões de camadas. Uma varredura linear é lenta demais, e as estruturas espaciais óbvias não se encaixam: as árvores R (R-trees) respondem a consultas de contenção em vez de "a primeira camada abaixo deste ponto", e as árvores de segmentos (segment trees) escalam com o tamanho do espaço de coordenadas, e não com o número de camadas.

Existem várias abordagens para esse design, mas o que funcionou foi resolver o problema fácil primeiro e, depois, fazer com que a estrutura de dados se lembrasse do seu próprio passado.

Passo um: Resolva para um único LSN

Para um LSN fixo, determinamos qual camada responde a cada chave. Essa resposta muda apenas em alguns pontos do espaço de chaves, por isso registramos esses pontos e os armazenamos em uma árvore de busca binária. Essa árvore é a cobertura de camada para esse LSN e responde a qualquer leitura nesse LSN com uma única consulta.

Isso funciona, mas apenas para um LSN. A cobertura muda toda vez que uma camada é adicionada, e existem milhões de LSNs, por isso não podemos criar e manter uma árvore separada para cada um.

Passo dois: Torne a árvore persistent

Persistente no sentido de "manter as versões antigas disponíveis". Construímos a cobertura de forma incremental, inserindo camadas na ordem do LSN, de baixo para cima. A inserção de uma camada afeta apenas os nós ao longo de um único caminho, da raiz para baixo. Em vez de sobrescrever esses nós, o sistema os copia e deixa os originais intactos. As novas cópias apontam para as subárvores antigas e inalteradas de cada lado.

Duas consequências decorrem disso:

  • A inserção custa apenas alguns novos nós em vez de uma árvore inteiramente nova, porque tudo o que está fora do caminho é compartilhado
  • A raiz antiga ainda descreve a árvore exatamente como ela era antes da inserção, portanto, continua sendo uma cobertura válida para o LSN anterior

Fazemos isso para cada camada, em ordem, e acabamos com uma única estrutura que contém cada raiz intermediária, cada uma representando a cobertura em um LSN diferente. Obtemos todas essas árvores por quase o preço de uma.

Uma leitura histórica custa o mesmo que uma leitura atual: o sistema escolhe a raiz para o LSN desejado e faz a mesma consulta única.

Esse é o truque, em resumo:

  • As leituras apenas do estado mais recente exigem uma única consulta à árvore
  • As leituras históricas usam uma raiz mais antiga, portanto, custam o mesmo
  • A criação dessas raízes continua barata à medida que as camadas se acumulam, de modo que um histórico longo não torna as consultas mais lentas

Onde o armazenamento de objetos realmente se posiciona

É aqui que a discussão atual sobre o Postgres e o armazenamento de objetos costuma falhar, em ambas as direções.

O argumento clássico contra a construção de OLTP em armazenamento de objetos é o seguinte:

  • O Postgres processa muitas E/Ss (I/Os) pequenas e sensíveis à latência
  • O armazenamento de objetos foi projetado para solicitações maiores com maior latência, e uma leitura dele pode levar centenas de milissegundos
  • Se você colocar o S3 na frente da execução de consultas, o resultado será um banco de dados lento

Por si só, essa não é uma afirmação polêmica. O que o argumento erra é na suposição de que um banco de dados baseado em armazenamento de objetos deve estar lendo do armazenamento de objetos para responder às consultas.

Na arquitetura que propomos, isso nunca acontece:

  • As consultas não leem o armazenamento de objetos. O nó de computação lê a RAM, depois o NVMe local e, em seguida, o pageserver. O armazenamento de objetos é lido apenas dentro do pageserver, somente ao reconstruir uma versão de página que ele não possui, e nunca diretamente pelo Postgres.
  • Os commits não gravam no armazenamento de objetos. Um commit é confirmado quando um quórum de safekeepers tem o registro de WAL; a materialização das páginas e o upload delas ocorrem posteriormente.

Quando o Postgres é arquitetado dessa forma, ele se torna uma evolução dos sistemas OLTP tradicionais, projetado para lidar com cargas de trabalho de agentes. É por isso que criamos o Lakebase Postgres: um banco de dados OLTP onde a computação e o armazenamento são desacoplados, e a fonte durável da verdade é construída em armazenamento de objetos.

Por que usar o Lakebase Postgres em vez do Postgres padrão

Com o Lakebase Postgres, o histórico de transações é endereçável por LSN, e as cópias são referências em vez de dados. Isso torna possível criar recursos que dão ao Postgres o fluxo de trabalho leve que é um requisito absoluto para agentes.

Branching

Primeiro, o Postgres agora pode criar ramificações (branches). A criação de uma ramificação não copia páginas; ela cria um ponteiro para um LSN específico, e a ramificação começa a divergir a partir daí com semântica de cópia na gravação (copy-on-write).

As gravações na ramificação são armazenadas como deltas em relação ao pai, de modo que uma ramificação de um banco de dados de 2 TB é criada em segundos e não custa nada até que algo seja alterado. O pai não sofre carga adicional, e é por isso que é seguro fazer isso em ambiente de produção.

Isso é o que um agente precisa para trabalhar com segurança. Ele pode criar uma ramificação por tarefa, executar a migração que acabou de escrever com dados reais em volume real e inspecionar o resultado antes que qualquer coisa afete o pai. Vinte agentes podem fazer isso ao mesmo tempo, cada um isolado dos outros e da produção.

Com o Lakebase Postgres, nós até estendemos a ramificação para além do banco de dados. Buckets de armazenamento de objetos, Functions, estado do Managed Better Auth e configurações do AI Gateway são ramificados junto com o banco de dados, de modo que uma ramificação é uma cópia isolada do backend, e não apenas das tabelas do Postgres.

Restauração instantânea

A recuperação de ponto no tempo (Point-in-time recovery) é uma ramificação com uma intenção diferente. Restaurar significa apontar para um LSN anterior e retomar a partir daí, de modo que não envolve copiar os dados de volta para o lugar e seu custo não escala com o tamanho do banco de dados. O quão longe você pode voltar é uma configuração de retenção.

Isso é o que torna os erros de um agente baratos. Quando um agente executa a instrução errada, a solução não é uma janela de restauração e um plano de recuperação, mas sim apontar a ramificação de volta para o LSN de antes da execução. Desfazer a ação custa o mesmo em um banco de dados de 2 TB ou em um vazio, permitindo que o agente tente novamente em vez de encaminhar o problema para um humano.

Consultas de viagem no tempo

Como o pageserver pode reconstruir qualquer página em qualquer LSN dentro da janela de histórico, você pode consultar um estado anterior diretamente, em vez de restaurá-lo primeiro.

O uso prático é a comparação (diffing): como era essa tabela antes da migração e como ela está agora. Também é assim que você confirma se escolheu o timestamp correto antes de confirmar uma restauração.

Réplicas de leitura sem réplicas

Um nó de computação somente leitura não é uma cópia dos dados. Ele solicita páginas da mesma camada de armazenamento que o primário, portanto, adicionar um não significa provisionar um conjunto de dados e esperar que ele se atualize. Inicializar um deles é uma operação de metadados.

Escalar para zero

Como o estado durável reside fora da computação, um nó de computação ocioso pode ser totalmente desligado em vez de continuar em execução para proteger os dados. Os recursos de computação suspendem após 5 minutos de inatividade e reativam em poucas centenas de milissegundos na próxima consulta. Para uma frota de bancos de dados por sessão ou por ramificação, a maioria dos quais fica ociosa na maior parte do tempo, essa é a diferença entre um modelo de custo viável e um inviável. Observe que a cobrança da computação é interrompida enquanto ela está suspensa; o armazenamento continua sendo cobrado, pois o histórico ainda está lá.

Uma sessão de agente que funciona por quatro minutos e fica inativa deixa de gerar custos de computação cinco minutos depois, sem que ninguém precise desativá-la. É isso que torna um banco de dados por agente, por sessão ou por ramificação acessível o suficiente para ser o padrão.

Uma única cópia para transações e análises

Há outra consequência em colocar dados operacionais no armazenamento de objetos

Uma vez que o registro durável de um banco de dados transacional reside em um armazenamento de objetos comum, ele deixa de ficar bloqueado no formato privado de um único mecanismo em seus discos. Outros mecanismos podem lê-lo.

Essa é a base para o que chamamos de LTAP, para Lake Transactional/Analytical Processing: em vez de duas cópias dos dados em dois formatos mantidos em sincronia por um pipeline, há uma única cópia durável em formatos colunares abertos que tanto o lado transacional quanto o analítico leem.

O mecanismo segue o caminho de leitura já descrito. À medida que o pageserver materializa as páginas no armazenamento de objetos, ele as transcodifica do formato de linha do Postgres para o formato colunar, preservando a representação exata de cada valor no Postgres. Uma consulta analítica solicita ao Postgres o LSN atual, que é uma busca rápida de metadados, lê a grande maioria dos dados do armazenamento de objetos a partir desse LSN e busca apenas as alterações não materializadas mais recentes do pageserver. O Postgres não atende a nenhum tráfego de leitura analítica além de retornar esse único número, de modo que uma grande consulta analítica não compete com as transações pela mesma CPU.

A diferença em relação ao change data capture (CDC) e ao espelhamento é que não há nada a ser ativado. Não há lista de tabelas replicadas, porque não há replicação. Uma tabela já existe no lake, o que também significa que as duas visualizações não podem divergir.

Lakebase Postgres para agentes

Começamos este post com uma pergunta: o armazenamento de objetos poderia ficar abaixo do Postgres e facilitar o trabalho dos agentes?

A resposta é sim. O armazenamento de objetos pode ficar abaixo do Postgres e mudar a forma como você interage com ele, mas não apenas porque o S3 é rápido ou barato de executar. Como descrito neste post, isso exige mais engenharia do que isso. RAM e NVMe local ainda são necessários para atender às consultas com rapidez suficiente, e um commit ainda vai para o WAL replicado em vez de um bucket.

Essa parte do WAL é a chave. O armazenamento de objetos adiciona uma maneira barata e escalável de armazenar todo o histórico, mas tornar o WAL a fonte da verdade é o que torna esse histórico endereçável e muda a forma como os agentes interagem com o Postgres e os recursos que você pode criar com base nele.

Peça ao seu agente para implantar o Lakebase Postgres e faça o teste. Comece aqui.

O Lakebase Postgres pode ser usado como um banco de dados independente, e você também pode integrá-lo ao restante da plataforma Databricks Data + AI: governança do Unity Catalog, análises de lakehouse, notebooks e fluxos de trabalho de IA.

(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original

Receba os posts mais recentes na sua caixa de entrada

Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.