Ir para o conteúdo principal
Lakebase

Carregue terabytes de dados em minutos no Lakebase Postgres

Cargas em lote mais rápidas e cargas de trabalho OLTP mais seguras com a arquitetura LTAP

por Yecheng Yang, Nolan Biscaro, Szu-Po Wang e Pranav Aurora

  • A arquitetura LTAP (Lake Transactional/Analytical Processing) descarrega operações em lote pesadas da computação principal do Lakebase Postgres para mecanismos distribuídos como o Spark.
  • Ao permitir que o Spark construa páginas e índices válidos do Postgres em paralelo e escreva diretamente no armazenamento, o Lakebase Postgres atinge carregamentos de dados até 147x mais rápidos sem consumir recursos da aplicação em execução.
  • O nó primário do Lakebase Postgres publica o manifesto final por meio de um registro WAL compacto, garantindo que as consultas OLTP em execução não sejam afetadas e que o alto desempenho seja mantido.

Bancos de dados operacionais como o Postgres são criados para executar com confiabilidade consultas de baixa latência e alta concorrência em subconjuntos de dados. O que muitas vezes afeta essa confiabilidade são as operações em lote, como o carregamento de terabytes de dados ou a execução de consultas analíticas que varrem uma tabela inteira. Essas operações competem pelos mesmos recursos que as cargas de trabalho da sua aplicação, correndo o risco de degradação de desempenho ou tempo de inatividade.

Nosso objetivo é tornar o Lakebase Postgres o lugar mais seguro e confiável para suas cargas de trabalho operacionais. Alcançamos isso descarregando operações pesadas em lote da computação principal para mecanismos distribuídos como o Spark, que são construídos sob medida para essas tarefas. Isso é possível graças à arquitetura LTAP (Lake Transactional/Analytical Processing), que permite que mecanismos transacionais e analíticos trabalhem exatamente nos mesmos dados no lake.

Sem esse isolamento, as equipes são frequentemente forçadas a ter um cuidado extremo com operações em lote. Elas sacrificam o frescor dos dados, executam carregamentos raramente fora do horário de pico e gerenciam manualmente preenchimentos (backfills) e pontos de verificação (checkpointing) complexos. Hoje, ao aproveitar a arquitetura LTAP, elas descarregam os pipelines de ingestão inteiramente no Spark, garantindo que os aplicativos recebam dados atualizados sem comprometer o desempenho do sistema em produção.

Mostre-me os números

Durante o nosso beta, um cliente usava o Synced Tables para carregar cerca de 1 bilhão de linhas no Lakebase diariamente. Ao aproveitar a arquitetura LTAP, aceleramos esses carregamentos drasticamente, mantendo suas cargas de trabalho operacionais completamente intocadas.

image1.png

Anteriormente, o carregamento em lote levava mais de 8 horas e saturava completamente a CPU e a memória. Mesmo com o escalamento automático do Lakebase, eles eram forçados a superdimensionar fortemente seus recursos de OLTP apenas para sobreviver à sincronização. Isso acontece porque as arquiteturas tradicionais do Postgres tornam o nó primário o único guardião do estado durável. Os carregamentos em lote são forçados através desse único gargalo, o que significa que cada linha importada gera páginas de heap, atualiza índices e grava registros de WAL na mesma instância que atende ao tráfego de produção do seu aplicativo.

A arquitetura LTAP aliviou totalmente a pressão na instância primária, protegendo o tráfego do aplicativo em produção. Você obtém todo o poder de um mecanismo distribuído, escalando o rendimento de carga de forma quase linear conforme seus dados crescem. Nossos testes internos de referência (benchmarks) mostram que carregar 1 TB agora leva menos de 5 minutos.

image6.png

Nota: Este benchmark mede o tempo de carregamento de dados (construção de páginas de heap). Também estamos trabalhando ativamente na paralelização das construções de índices para esses dados carregados.

image7.png

No restante desta publicação, vamos nos aprofundar nos desafios do carregamento em lote de dados em um banco de dados OLTP como o Postgres e explorar como aproveitamos a arquitetura LTAP para resolvê-los.

O problema com carregamentos em lote em escala no Postgres

O comando nativo COPY do Postgres é eficiente para ingestões padrão e menores.

Mas à medida que os clientes aproximam seus ecossistemas operacionais e analíticos, a escala muda. Uma carga de trabalho cada vez mais comum envolve servir tabelas gigantescas de nível gold do Lakehouse para aplicativos com padrões de consulta operacional. Enviar dados nesse volume extremo para um banco de dados em produção expõe dois limites fundamentais:

  1. O carregamento é inerentemente lento porque cada linha importada deve passar por um único gravador primário.
  2. A mesma computação usada para o carregamento também atende às consultas do seu aplicativo. Os carregamentos em lote exigem muito uso de CPU, I/O, conexões e largura de banda do WAL, competindo com o seu tráfego OLTP.

Isso permanece verdadeiro mesmo se você iniciar o carregamento como um trabalho distribuído do Spark. O Spark pode ler partições de origem em paralelo, mas cada linha ainda precisa passar por um único gravador do Postgres:

  1. Os workers enviam linhas para o Postgres por meio de COPY
  2. O nó primário converte essas linhas em páginas de heap e índice
  3. O nó primário registra as alterações no Write-Ahead Log (WAL)
  4. O WAL precisa ser gravado no armazenamento durável antes de confirmar o carregamento

Os carregamentos em lote tradicionais sofrem gargalos devido a um único gravador

O lado de origem pode escalar horizontalmente, mas o lado de destino não. Adicionar executores do Spark acelera a varredura, mas não remove o gargalo de gravador único. Além disso, esse mesmo nó primário do Postgres está atendendo às transações do seu aplicativo online. Uma grande importação compete com elas por CPU, memória, I/O, conexões e largura de banda de WAL. A latência aumenta, as equipes agendam carregamentos para janelas de pouco movimento e muitas vezes dimensionam o nó primário para a maior importação em vez do tráfego do dia a dia.

O que o LTAP muda

A arquitetura LTAP abre um caminho para contornar isso, porque o nó primário não é mais a única maneira de criar um estado durável do Postgres. A computação transacional é sem estado (stateless) no lakebase: o estado durável reside em uma camada de armazenamento distribuído, não no disco local do nó primário. De certa forma, o Postgres é um cliente do armazenamento – ele atende a consultas e transações, mas não precisa ser o processo que materializa cada nova página.

image2.png

Para o carregamento em lote, isso significa que o Spark pode construir o estado do Postgres e gravá-lo no armazenamento, enquanto o nó primário apenas publica o resultado.

As consequências operacionais são:

  • Os carregamentos em lote não competem com o OLTP no nó primário. Isso é executado fora do endpoint de computação em produção. As cargas de trabalho do aplicativo mantêm o uso de CPU, I/O, conexões e largura de banda de WAL de que precisam.
  • Grandes carregamentos no mesmo destino podem ser executados simultaneamente. Cada importação termina gravando um único registro de WAL no nó primário, portanto, os carregamentos não ficam mais na fila atrás dos fluxos de COPY uns dos outros.
  • O nó primário não precisa escalar de acordo com o tamanho da carga. Um nó primário de 1 CU pode continuar atendendo ao tráfego enquanto o Spark carrega bilhões de linhas em computação separada, e a computação do Spark é encerrada assim que o carregamento é concluído.

Construindo páginas válidas do Postgres de forma segura e paralela

Existem diferentes otimizações para construir arquivos do Postgres e construir o índice de chave primária.

Construindo o heap em paralelo

As páginas produzidas pelo Spark devem ser válidas para o banco de dados de destino, como se o nó primário as tivesse construído.

Cada executor do Spark inicia uma instância do Postgres em um ambiente isolado (sandbox) no modo de atualização binária, o mesmo mecanismo que o pg_upgrade usa para preservar OIDs de catálogo durante atualizações de versões principais. Nós o usamos para transplantar os OIDs de catálogo do destino para cada sandbox, garantindo que os OIDs incorporados nas páginas geradas correspondam aos do destino. O driver também atribui intervalos de OID sem sobreposição aos sandboxes, para que os objetos criados simultaneamente não colidam.

Dentro de cada sandbox, um COPY binário com FREEZE constrói a fatia daquele executor do heap. O congelamento (freezing) marca as tuplas importadas como já confirmadas, para que o Postgres possa tratá-las como visíveis sem consultar o histórico de transações do sandbox.

Cada worker calcula checksums para cada página gerada. Os pageservers do Lakebase validam esses checksums quando ingerem os arquivos, detectando corrupção antes que as páginas importadas se tornem autoritativas.

Juntos, o modo de atualização binária, tuplas congeladas, OIDs coordenados e validação de checksum garantem que o Spark produza páginas que o destino possa ler como páginas comuns do Postgres. Implementamos isso por meio de extensões do Postgres e de um método de acesso a tabelas, sem modificar o núcleo do Postgres.

Os sandboxes descartáveis também permitem otimizações de desempenho, como tabelas auxiliares UNLOGGED. Essas são otimizações, não mecanismos de segurança: elas evitam contenção desnecessária de WAL e de bloqueios, pois nenhum tráfego de aplicativo compartilha o sandbox.

Isso ainda é “apenas Postgres”?
Sim. Os arquivos que o Spark grava são páginas do Postgres, não um formato de importação que o nó primário traduz posteriormente. Adicionamos extensões e um método de acesso a tabelas nesse caminho, o que é um dos superpoderes do Postgres, permitindo adicionar novos recursos sem modificar o código-fonte.

Construindo o índice sem varrer o heap

Uma B-tree é uma estrutura ordenada sobre todo o espaço de chaves, e suas entradas folha contêm pares de chave e ID de tupla que apontam de volta para o heap.

Normalmente, o Postgres obtém esses pares varrendo o heap. Aqui, no entanto, o heap está distribuído em fatias enviadas, e baixar tudo isso para um worker de criação de índice anularia grande parte do benefício de construí-lo em paralelo.

O construtor de índice na verdade não precisa do conteúdo do heap. Ele precisa do fluxo de pares de (key, ctid) que uma varredura de heap produziria. Portanto, cada worker de heap da etapa anterior exporta suas colunas de chave e IDs de tupla para um arquivo de armazenamento de objetos separado enquanto constrói sua fatia.

Alimentamos esses registros em uma tabela auxiliar que contém apenas as colunas de chave exportadas e o ID de tupla, em vez das linhas originais. Durante o CREATE INDEX, um método de acesso a tabelas personalizado varre a tabela auxiliar de forma idêntica ao heapam, mas grava o ctid em cada entrada de índice em vez do próprio ID de tupla físico da linha auxiliar. À medida que os registros são carregados, cada ctid local da fatia é deslocado pelo tamanho cumulativo das fatias de heap anteriores, fazendo com que ele aponte para a localização final da tupla no heap concatenado. Resumindo, o construtor padrão de B-tree do Postgres pode varrer essa tabela auxiliar sem alterações e produzir um índice sobre um heap que ele nunca baixou. Isso substitui a movimentação de todo o heap pela movimentação da representação muito menor de chave e ID de tupla.

Entregando o resultado ao Postgres

Assim que as fatias de heap e índice estiverem no armazenamento de objetos, o driver do Spark grava um manifesto e invoca uma função SQL no destino. O primário registra a importação como um registro WAL compacto. Um COPY convencional enviaria todo o volume de dados pelo quórum do safekeeper. Nós enviamos apenas a descrição da importação.

Os pageservers reivindicam os arquivos enviados como páginas autoritativas e validam seus checksums. Eles também fazem o pré-aquecimento das páginas no SSD local para que as leituras iniciais não gerem buscas lentas no armazenamento de objetos. Por fim, o primário substitui atomicamente os dados preparados na tabela sincronizada visível ao usuário.

Até que essa transação seja confirmada, a tabela importada permanece isolada. Depois disso, o primário vê páginas comuns de heap e B-tree produzidas por meio de formatos e interfaces padrão do Postgres.

Comece hoje mesmo.

Usamos a arquitetura LTAP para impulsionar as Synced Tables, permitindo sincronizações muito mais rápidas ao servir conjuntos de dados gold a partir do seu Lakehouse. Se você está executando trabalhos manuais de ReverseETL a partir do Lakehouse, com o Lakebase, você deve considerar o uso do Lakebase.

Estamos entusiasmados em generalizar esse protocolo para lidar com grandes operações e manutenções, como a criação de índices ou até mesmo migrações. O Lakebase com a arquitetura LTAP é o melhor lugar para executar cargas de trabalho OLTP.

Se você ainda não testou o Lakebase e as Synced Tables, comece hoje mesmo.

(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.