Ir para o conteúdo principal
Data Engineering

Aprimorando o cache de computação do Lakebase Postgres

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 cache padrão do Postgres vs. cache do Lakebase Postgres
  • Como criamos um cache com escalonamento automático que funciona em conjunto com buffers compartilhados e mantém o máximo de dados possível na computação
  • Resultados em produção, incluindo throughput 2x maior, menos leituras da camada de armazenamento e menor latência

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.

O cache padrão do Postgres

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.

Desvantagens

  1. Double buffering (buffer duplo), que reduz a quantidade de dados que você pode armazenar em cache de forma eficaz na computação. Considere uma computação com 4 GB de RAM usando 1 GB para shared buffers. Conforme você lê páginas do disco para preencher o 1 GB de shared buffers, as leituras passam pelo page cache do OS, que também armazena esses dados. Agora você está consumindo 2 GB de RAM para armazenar 1 GB de dados em cache.
  2. O page cache do OS não sabe nada sobre os shared buffers ou os componentes internos do Postgres, portanto, não pode tomar decisões inteligentes sobre quais páginas substituir.

Desafios técnicos

  1. Em um sistema de armazenamento desagregado como o Lakebase Postgres, os dados lidos do armazenamento não passam pelo sistema de arquivos ou pelo page cache do OS.
  2. O shared buffers é um parâmetro estático, o que significa que é definido antes de iniciar o Postgres e não pode ser alterado sem reiniciar o banco de dados. Esse é um desafio significativo para um sistema de autoscaling serverless como o Lakebase.
  3. O Postgres usa um processo do sistema operacional separado para cada conexão ativa, portanto, quanto maiores os shared buffers — ou seja, quanto mais memória você der ao Postgres —, mais gerenciamento de memória o OS precisará fazer para cada conexão, o que, por sua vez, consome memória.

O caminho do cache do Lakebase

image3.png

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.

Shared buffers maiores

image1.png

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:

  • Shared buffers: o buffer compartilhado na memória do Postgres, representando o caminho de acesso de menor latência.
  • Local file cache: um cache secundário expandido que reside no NVMe local do nó de computação, oferecendo maior capacidade do que a memória, mas exigindo I/O de disco para acessar uma página.

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 show shared_buffers em uma conexão Postgres. Um endpoint do Lakebase na Databricks de 80 CU deve exibir um valor de 15278640.

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.

Como lidar com a sobrecarga de memória e tradução com huge pages

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

Suporte a huge pages em ambientes virtualizados

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"

Resultados de produção

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.

Exemplo 1: ~2× mais throughput, 5× menos leituras do armazenamento

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.

image2.png

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.

image4.png

Exemplo 2: 1,3× mais throughput

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

image7.png

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

image8.png

Exemplo 3: uso de CPU 5× menor, throughput 2× maior

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.

image5.png

image6.png

Parte 2: dimensionamento automático

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.

Experimente

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

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.