antonio leandro

dados e armazenamento

Umbra: A Disk-Based System with In-Memory Performance

paper · Thomas Neumann, Michael Freitag · · ~32 min de leitura do original · densa

a tese

o buffer manager voltou: com páginas de tamanho variável e uma faixa de memória virtual reservada por size class, um banco em ssd entrega desempenho de banco em memória no working set quente e degrada com elegância fora dele

o que fica

  1. DRAM parou de acompanhar: 2 TB de memória de servidor custavam cerca de US$ 20.000 contra US$ 500 de um SSD M.2 de 2 TB que lê a 3,5 GB/s — um fator de 40 a favor do disco.
  2. Página de tamanho fixo simplifica o buffer manager e complica todo o resto do banco; o Umbra aceita um buffer manager mais complexo para guardar strings longas e dicionários de compressão de forma contígua.
  3. A fragmentação externa do pool desaparece quando se reserva, por size class, uma faixa de memória virtual do tamanho do buffer pool inteiro e se devolve a memória física com madvise(MADV_DONTNEED).
  4. Pointer swizzling cobra um preço estrutural: cada página precisa ter exatamente um swip dono, então as folhas do B+-tree do Umbra não apontam para as irmãs e o scan navega segurando um latch otimista no pai.
  5. Uma página despejada no meio de uma leitura otimista não derruba o processo: a região virtual continua válida e devolve zeros, e o contador de versão do latch invalida o resultado depois.
  6. O buffer manager custa menos de 6% em média no JOB e no TPCH; o gargalo real é a banda do SSD, e a resposta dos autores é colocar mais SSDs na mesma máquina.

o problema

Por uma década a aposta foi óbvia: a RAM cresce, então guarde tudo nela. Foi assim que nasceram Hekaton, HANA e o HyPer, do próprio grupo de Munique. Esses sistemas jogaram fora o buffer manager inteiro, e com ele o overhead de traduzir identificador de página para endereço a cada acesso. O ganho de desempenho foi grande. O problema é que eles falham ou degradam feio quando os dados não cabem.

E a premissa parou de valer. Dez anos atrás dava para comprar um servidor commodity com 1 TB de memória; hoje o número razoável é 2 TB, e acima disso o preço dispara. Enquanto isso um SSD M.2 de 2 TB custa US$ 500 e lê a 3,5 GB/s, contra US$ 20.000 pelos mesmos 2 TB de DRAM de servidor. Lomet já tinha argumentado que banco puramente in-memory é antieconômico, e os autores concordam. Só que voltar ao buffer manager tradicional significa voltar ao gargalo conhecido: tabela hash global, latches disputados, todo o custo que a literatura documentou. O LeanStore já tinha resolvido boa parte disso — mas com páginas de tamanho fixo, o que empurra a complexidade para fora: uma string grande ou um dicionário de compressão não cabem numa página, e o resto do sistema precisa remontar o objeto ou pagar uma lógica de lookup cara em todo acesso.

a ideia

Inverter a troca. Aceitar um buffer manager mais complicado para deixar o resto do banco simples. Se o dicionário está contíguo na memória, descomprimir no Umbra é tão barato quanto num sistema in-memory — não existe caso especial.

O obstáculo clássico das páginas de tamanho variável é a fragmentação externa do pool. A saída é lembrar que endereço virtual é abundante e memória física é escassa, e que o kernel já sabe separar as duas coisas. O Umbra reserva endereço virtual à vontade e deixa o sistema operacional decidir quando existe página física por trás.

como funciona

As páginas são organizadas em size classes: a menor tem 64 KiB e cada classe seguinte dobra de tamanho. Para cada classe, o buffer manager faz um mmap privado e anônimo reservando uma região do tamanho do buffer pool inteiro — endereço virtual apenas, sem memória física. Essa região é fatiada em pedaços do tamanho da classe, e cada buffer frame guarda um ponteiro fixo para um deles, estável por toda a vida do processo. Carregar uma página é um pread naquele endereço, e é aí que o kernel cria o mapeamento físico. Despejar é pwrite seguido de madvise com MADV_DONTNEED, que devolve a física na hora. Como o mapeamento não tem arquivo por trás, o madvise é praticamente de graça. Dentro de uma classe não há fragmentação virtual; a física pode fragmentar, e isso é problema do kernel. Por padrão o pool ocupa metade da memória, e a outra metade fica como scratch para execução.

Referência a página é um swip de 64 bits. Se a página está em memória, o swip é o ponteiro — o bit mais baixo é zero pelo alinhamento de 8 bytes. Se está em disco, o bit vale um e os outros guardam 6 bits de size class e 57 bits de identificador. Um único branch a mais no caminho quente, e nenhuma tabela global.

A sincronização usa versioned latch: 64 bits por frame, 5 de estado e 59 de versão, com modo exclusivo, compartilhado e otimista. O modo otimista só anota a versão na entrada e valida na saída. O Umbra acrescenta o modo compartilhado ao esquema do LeanStore porque seus operadores são deliberadamente ignorantes de que a relação é paginada — sem isso, cada operador de pipeline precisaria carregar lógica de revalidação.

As relações vivem em B+-trees com identificador sintético de 8 bytes, estritamente crescente, o que dispensa split de folha: nó novo só quando a tupla não cabe. Nó interno tem 64 KiB e fanout de 8.192, e a folha usa layout PAX. Strings têm header de 16 bytes: 4 de comprimento, e até 12 caracteres cabem inline; acima disso vão ponteiro ou offset mais um prefixo de 4 caracteres que curto-circuita comparação. Três classes de duração — persistent, transient, temporary — codificadas em 2 bits dizem quando a referência precisa ser copiada porque a página pode sumir.

o que isso custou

O reuso de espaço em disco fica restrito a páginas do mesmo tamanho. Se duas páginas de 64 KiB ocupassem o espaço de uma de 128 KiB apagada, a recuperação poderia ler lixo da página antiga e interpretá-lo como log sequence number.

O PAX na folha carrega todos os atributos mesmo quando a consulta lê poucos — os autores admitem que isso não é o layout certo para dados em disco e apontam DataBlocks como trabalho futuro. O acesso ao SSD é essencialmente aleatório, porque a execução paralela não impõe ordem entre threads: 1,13 GiB/s com buffer manager contra 1,15 GiB/s ignorando ele, ambos perto do teto de acesso aleatório do disco usado. A resposta para isso não é software, é comprar mais SSDs.

Os números de comparação também pedem leitura cuidadosa. O speedup médio geométrico de 3,0x sobre o HyPer no JOB e 1,8x no TPCH vem em boa parte da compilação adaptativa, não do armazenamento: o HyPer chega a gastar 29x mais tempo compilando do que executando consultas baratas. Isolando só o tempo de execução, as diferenças ficam em torno de 30% no JOB e 10% no TPCH, e alguns outliers se explicam por planos lógicos diferentes — em uma consulta o HyPer venceu porque cometeu dois erros de estimativa que se cancelaram. Tudo isso com cache quente, numa máquina de 64 GiB e um SSD.

onde isso aparece hoje

A peça do Umbra com maior alcance não foi o buffer manager, foi o header de string de 16 bytes: prefixo inline, curto embutido, longo por referência. O desenho virou padrão de fato em engines colunares — o StringView do Apache Arrow é o exemplo mais visível — e ficou conhecido pelo apelido de German string. O sistema em si continuou sendo desenvolvido pelo grupo e foi comercializado depois como CedarDB.

O que o paper reutiliza sem cerimônia também diz algo sobre maturidade: a recuperação é ARIES puro, e a estimativa de cardinalidade por coluna sai de sketches atualizáveis de HyperLogLog combinados com amostragem por reservatório online. A contribuição está em como esse maquinário conhecido convive com páginas de tamanho variável, não em substituí-lo.

lido na íntegra por pipeline de llm, revisado por antonio leandro antes de publicar ·