Parte 1: Como grandes nós de computação do Postgres rodam até 2x mais rápido com menor latência
por David Wein, Sunil Kamath e Haoyu Huang
O modelo de armazenamento desagregado do Lakebase Postgres oferece uma plataforma rica em recursos, flexível e de baixo custo. O cache eficiente de dados é fundamental para fornecer alto throughput e baixa latência enquanto os dados são armazenados em um armazenamento de objetos como o S3.
Esse cache ocorre em duas camadas: no armazenamento distribuído, onde as páginas do Postgres são materializadas para alto throughput de gravação e atendimento de leitura; e na própria computação do Postgres para servir páginas acessadas com frequência a partir da DRAM para um acesso ultrarrápido.
Trabalhamos duro para fazer melhorias no cache do lado da computação e, neste blog, apresentaremos nossos planos de curto prazo e detalharemos o que já foi entregue aos clientes.
Primeiro, um pouco de contexto sobre como chegamos até aqui.
Os bancos de dados são famosos por consumir muita DRAM (memória). Eles usam essa memória principalmente como um cache de dados e esperam que o acesso às linhas no cache seja medido em nanossegundos — ordens de magnitude mais rápido do que as unidades NVMe mais rápidas.
O Postgres organiza os dados em linhas em páginas, e as páginas que estão sendo acessadas ativamente devem ser carregadas em uma área de memória conhecida como "shared buffers". Como o Postgres tradicionalmente armazena páginas usando o sistema de arquivos do sistema operacional, o kernel do OS também usará seu page cache flexível para fornecer cache entre os shared buffers do Postgres e o disco.
Esse esquema de shared buffers + page cache funciona razoavelmente bem, mas apresenta algumas desvantagens e desafios.

Agora que apresentamos um pouco de contexto, vamos falar sobre como estamos resolvendo isso na Databricks.
Nosso objetivo final é fazer o uso mais eficiente da DRAM na sua computação por meio de shared buffers dinâmicos do Postgres que fazem autoscaling com sua carga de trabalho e usam até 75% da memória disponível.
Precisamos ajustar nossa plataforma de computação para aproveitar os shared buffers com autoscaling, mas também queremos entregar melhorias incrementais sensatas aos nossos clientes à medida que estiverem disponíveis. Cada entrega incremental nos permite lançar com confiança uma ou mais partes do roadmap, oferecendo benefícios reais aos clientes. Portanto, mesmo que as computações com autoscaling sejam o objetivo, começamos com computações fixas, conforme abordado na próxima seção.
Aqui está o que implementamos.

Se você se lembra dos desafios técnicos acima, um sistema desagregado como o Lakebase não roteia suas leituras pelo sistema de arquivos padrão do OS e seu page cache. Lembre-se também de que os shared buffers do Postgres são estáticos e não podem fazer autoscaling.
Para resolver isso, criamos uma camada que chamamos de local file cache (LFC). O LFC funcionou como um substituto, criando um cache com autoscaling que funcionava em conjunto com os shared buffers e mantinha o máximo de dados possível em cache na computação. Essa foi uma solução inteligente e pragmática que permitiu ao Lakebase Postgres lançar o autoscaling e tem sido usada em todas as computações desde o lançamento.
Embora exposta aos usuários como um único cache de computação de alta velocidade, a arquitetura subjacente suporta até dois níveis:
Os shared buffers foram ajustados de forma conservadora para não consumirem muita memória ao serem executados na CU mínima configurada, com o tamanho máximo já configurado de 1 GB de shared buffers e o LFC consumindo o restante da capacidade total do cache de computação (até 75% da DRAM). Qualquer solicitação que resulte em uma falha (miss) em ambos os níveis é roteada do nó de computação para a camada de armazenamento distribuído.
Em conjuntos de trabalho maiores, limitar os shared buffers a 1 GB forçava a maioria dos acertos de cache (hits) a passar pelo nível mais lento do LFC. O LFC nos atendeu bem, mas nossa intenção é descontinuar sua forma atual à medida que avançamos em direção a shared buffers totalmente dinâmicos.
Nota: Computações fixas vieram primeiro Nossa primeira entrega de shared buffers maiores tem como alvo computações de tamanho fixo, já que os shared buffers ainda não são dinâmicos. Nelas, agora desativamos o LFC e definimos os shared buffers para 75% da DRAM. Isso já está ativo hoje para computações de tamanho fixo com CU >= 80. A eliminação do limite de buffer de ~1 GB mantém as páginas ativas (hot pages) na camada de memória mais rápida, em vez de cascatear para o armazenamento de arquivos local. Para ver se os shared buffers grandes estão habilitados para sua computação, execute |
Manter os dados ativos em shared buffers em vez do page cache do OS também resolve as desvantagens descritas anteriormente. Não há double buffering, portanto, 1 GB de dados em cache consome 1 GB de RAM em vez de 2 GB. E como o cache reside dentro do Postgres, e não no kernel, as decisões de remoção podem ser tomadas com base no estado do banco de dados — o que nos posiciona para buscar políticas de substituição mais inteligentes do que as que o OS pode oferecer.
Dimensionar os shared buffers em 75% da DRAM em computações de tamanho fixo não foi tão simples quanto fazer uma alteração de configuração. Isso se deve ao terceiro desafio técnico: a arquitetura de processo por backend.
A próxima seção descreve nossa solução.
O Postgres usa uma estrutura baseada em processos na qual cada backend mapeia os shared buffers em seu próprio espaço de endereço, exigindo suas próprias entradas de tabela de páginas (page table entries) — as estruturas mantidas pelo kernel que o hardware percorre para traduzir endereços virtuais em memória física. Por padrão, o Linux faz esse mapeamento em páginas de 4 KB.
Alguns números simples: cada 1 GB de shared buffers corresponde a 262.144 entradas de tabela de páginas por processo. Com 32 GB de shared buffers e 512 backends, isso representa cerca de 4,3 bilhões de entradas, ou cerca de 32 GB de tabelas de páginas para mapear 32 GB de cache.
Esse conjunto de trabalho também excede em muito a capacidade do Translation Lookaside Buffer (TLB), um cache na unidade de gerenciamento de memória da CPU que acelera a tradução de virtual para físico. Mesmo um acerto (hit) no shared buffer gera uma penalidade devido a falhas de TLB (TLB misses) e percursos na tabela de páginas (page table walks).
Para mitigar isso, a comunidade Postgres aconselha o uso de um mecanismo do OS chamado huge pages (de 2 MB cada) com shared buffers grandes. A mudança para huge pages reduz o tamanho das tabelas de páginas em um fator de 512 e diminui significativamente as taxas de falha de TLB.
Em nossos testes de benchmark, a configuração do Postgres com huge pages reduziu a latência de leitura de cauda (tail read latency) em até ~40% e diminuiu a utilização da CPU em até ~30%.
O Lakebase Postgres é executado em máquinas virtuais convidadas (guest VMs) leves em hosts bare-metal. A tradução de endereços de memória envolve duas camadas virtualizadas. Aproveitar as huge pages exige uma implementação consistente em toda a pilha: desde a reserva no nível do host, passando pelo hipervisor que dá suporte à memória da VM, até o kernel convidado. Uma falha em qualquer nível degrada os benefícios de desempenho resultantes.
Recentemente, introduzimos o suporte dedicado a huge pages em nossa infraestrutura de VM. Optamos por usar páginas HugeTLB explícitas de 2 MB em vez de depender de transparent huge pages de melhor esforço. Agora, as VMs alocadas para computações grandes de tamanho fixo são inicializadas com um volume predeterminado de huge pages suficiente para a inicialização do Postgres. Para otimizar os recursos do sistema, a inicialização da computação libera automaticamente qualquer excedente de huge pages além das exigidas pelo Postgres.
Dica: Para ver se as huge pages explícitas grandes estão habilitadas para sua computação, execute show huge_pages em uma conexão Postgres. Um endpoint do Lakebase de 80 CU deve exibir um valor de "on" |
A implementação começou região por região há algumas semanas. Os exemplos abaixo foram medidos em grandes endpoints de produção após a reinicialização que ativou a nova configuração.
Em um endpoint grande, a alteração tornou-se ativa por volta das 06:10 UTC em 11 de agosto. Os blocos do Postgres acessados por segundo dobraram, o que usamos aqui como um indicador para o throughput. O cliente relatou latência p50 e p99 menor em comparação com o dia, semana e mês anteriores.

Este endpoint configurou um grande cache de arquivos locais. Com buffers compartilhados maiores, o GetPage/s de armazenamento caiu de cerca de 8K por segundo para cerca de 1,5K.

Em outro endpoint grande, a alteração tornou-se ativa por volta das 01:30 UTC em 14 de agosto. O throughput aumentou cerca de 43%.

A taxa de acerto do cache de computação atingiu quase 100%, com as solicitações atendidas quase inteiramente a partir dos buffers compartilhados.

Nesta carga de trabalho, o uso de CPU caiu de 20 núcleos para 4 após a implementação de 15 de agosto. A taxa de acerto do cache de computação subiu para quase 100% e o throughput medido dobrou.


Atualmente, estamos trabalhando para trazer buffers compartilhados maiores para computações do Postgres com dimensionamento automático. O dimensionamento automático introduz complexidade adicional: devemos expandir dinamicamente os buffers compartilhados ao aumentar a escala e reduzi-los ao diminuir a escala — tudo isso enquanto alocamos o volume exato necessário de huge pages.
Para ir além das computações de tamanho fixo, desenvolvemos um protocolo para o dimensionamento automático de huge pages fornecidas ao guest. As huge pages são dimensionadas em conjunto com buffers compartilhados dinâmicos, garantindo que mantenhamos uma tradução de endereços eficiente mesmo com alta simultaneidade e tamanhos de memória elevados. Nossa próxima publicação (parte 2) abordará os detalhes técnicos desta implementação de buffers compartilhados dinâmicos, incluindo o estado atual do Postgres de código aberto e as áreas que escolhemos para aprimorar ainda mais o recurso e contribuir upstream.
Todas essas melhorias de desempenho decorrem da arquitetura do Lakebase Postgres. A camada de armazenamento atua como o sistema de registro autoritativo, um nó de computação é stateless e sua memória serve como uma camada de cache.
Implante o Lakebase Postgres e coloque o desempenho à prova. 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álise de lakehouse, notebooks e fluxos de trabalho de AI.
(Esta publicação no blog foi traduzida utilizando ferramentas baseadas em inteligência artificial) Publicação original
Assine nosso blog e receba os posts mais recentes diretamente na sua caixa de entrada.