16 pontos por GN⁺ 2026-05-04 | 11 comentários | Compartilhar no WhatsApp
  • Com a remoção do modo de preempção PREEMPT_NONE, que era o padrão tradicional de servidores, no Linux 7.0, surgiu uma regressão grave de desempenho: no mesmo hardware, a vazão do PostgreSQL caiu pela metade
  • Em testes de pgbench conduzidos por um engenheiro da AWS em uma máquina Graviton4 com 96 vCPUs, o Linux 7.0 caiu de 98.565 para 50.751 transações por segundo em comparação com o Linux 6.x, com 55% da CPU consumida por uma única função de spinlock
  • O spinlock que protege o acesso ao pool de buffers compartilhados (shared buffer pool) do PostgreSQL, combinado com minor page faults em páginas de memória de 4 KB, faz com que, quando ocorre preempção do scheduler enquanto o lock está retido, todos os backends em espera desperdicem CPU girando
  • Ao ativar Huge Pages (2 MB ou 1 GB), o número de page faults potenciais cai de 31 milhões para dezenas de milhares ou centenas, eliminando a regressão
  • Do lado do kernel, foi proposta a adoção de Restartable Sequences (rseq), mas a comunidade do PostgreSQL argumenta que uma queda de desempenho causada por upgrade do kernel viola o princípio de "não quebrar o userspace"

O problema

  • O engenheiro da AWS Salvatore Dipietro executou pgbench em um processador Graviton4 com 96 vCPUs, em um teste de carga altamente paralelo com scale factor 8.470 (tabela com cerca de 847 milhões de linhas), 1.024 clientes e 96 threads
  • A vazão caiu de 98.565 TPS no Linux 6.x para 50.751 TPS no Linux 7.0, praticamente metade
  • O profiling com perf mostrou que 55,60% do tempo de CPU foi gasto dentro da função s_lock
    • Caminho de chamada: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

O que é preempção

  • Preempção é a decisão do scheduler do sistema operacional de interromper uma thread em execução e entregar a CPU para outra thread
  • Antes do Linux 7.0, existiam três opções
    • PREEMPT_NONE: a thread quase não é interrompida até ceder a CPU voluntariamente (syscall, bloqueio de I/O, sleep). Era o padrão tradicional em servidores, com menos trocas de contexto e maior vazão
    • PREEMPT_FULL: a thread em execução pode ser interrompida em praticamente qualquer ponto seguro. Reduz a latência, mas aumenta o overhead de troca de contexto. Era o padrão tradicional em desktops
    • PREEMPT_LAZY: compromisso introduzido no Linux 6.12, que espera fronteiras naturais, mas permite preempção quando necessário. Foi projetado para se aproximar das características de vazão do PREEMPT_NONE
  • No Linux 7.0, o PREEMPT_NONE foi removido em arquiteturas de CPU modernas, restando apenas PREEMPT_FULL e PREEMPT_LAZY
    • Embora o PREEMPT_LAZY funcione como substituto para a maior parte dos softwares de servidor, no PostgreSQL surge uma diferença crítica

Gerenciamento de memória no PostgreSQL

  • O PostgreSQL usa páginas de dados de tamanho fixo (8 KB por padrão) como unidade básica de armazenamento, e linhas de tabelas, nós de índice B-tree e metadados ficam todos armazenados nessas páginas
  • Para reduzir leituras em disco, ele armazena em cache páginas de dados lidas recentemente em uma grande área de memória compartilhada chamada pool de buffers compartilhados (shared buffer pool)
  • Quando um cliente se conecta, é criado um processo backend dedicado; se a página não estiver no pool de buffers, ela precisa ser lida do disco e então é necessário encontrar um buffer vazio ou um buffer que possa ser expulso
    • A função responsável por essa seleção de buffer é StrategyGetBuffer

Os spinlocks do PostgreSQL

  • Um spinlock é um mecanismo de lock em que, em vez de dormir esperando, a thread fica em loop verificando continuamente se o lock foi liberado
    • Em seções críticas muito curtas, girar pode ser mais eficiente do que o custo de colocar a thread para dormir e acordá-la depois
  • A premissa central é: a thread que segura o lock vai liberá-lo muito rapidamente
  • O StrategyGetBuffer usa um único spinlock global para proteger a seleção de buffers
    • Em um ambiente com 96 vCPUs e 1.024 clientes, todos os backends disputam o mesmo lock

Memória virtual e TLB

  • Todo processo usa endereços de memória virtual, e o hardware os traduz para endereços físicos por meio de tabelas de páginas (uma estrutura em árvore multinível)
  • Como percorrer as tabelas de páginas a cada acesso seria lento, a CPU mantém um TLB (Translation Lookaside Buffer), que armazena em cache traduções recentes
    • Com acerto no TLB, o acesso é rápido; com TLB miss, é necessário fazer page-table walk, o que consome tempo
  • O Linux usa o princípio de alocação preguiçosa (lazy allocation): ao reservar memória virtual, a página física real só é mapeada no primeiro acesso
    • No primeiro acesso ocorre um minor page fault: o kernel aloca a página física, grava o mapeamento e isso é mais lento do que uma leitura/escrita comum, na ordem de microssegundos

O problema das páginas de 4 KB

  • No benchmark, shared_buffers foi configurado em 120 GB; com páginas de memória de 4 KB, isso significa cerca de 31 milhões de páginas de memória e, portanto, 31 milhões de page faults potenciais no primeiro acesso
  • Em um benchmark de longa duração usando um pool de buffers compartilhados de 120 GB, novas regiões de memória continuam entrando no working set, então os page faults não acontecem só no início, mas de forma contínua
  • Se, dentro de StrategyGetBuffer, houver acesso à memória compartilhada enquanto o spinlock está retido e aquela região ainda não estiver mapeada, ocorre um minor page fault
  • Com PREEMPT_NONE (antes do Linux 7.0): mesmo que o backend A entre no handler de page fault, ele tende a evitar pontos voluntários de reescalonamento, então a chance de sair da CPU antes de resolver o fault é baixa. O tempo de espera fica maior do que o esperado, mas o dano é limitado
  • Com PREEMPT_LAZY (a partir do Linux 7.0): o scheduler pode preemptar o backend A dentro do próprio handler de page fault e escalar outro processo. Mesmo após o page fault ser resolvido, surge um tempo extra de espera t até o scheduler devolver o controle
    • Esse tempo extra não custa apenas t, mas é amplificado em número de backends girando no momento × t em desperdício de CPU
    • Em um ambiente com 96 vCPUs e centenas de backends, esse efeito multiplicador se torna crítico, e no fim 56% da CPU é consumida em s_lock

A solução com Huge Pages

  • Com shared_buffers em 120 GB, mudar o tamanho das páginas de memória reduz drasticamente o número de page faults potenciais
    • Páginas de 4 KB: ~31.000.000 page faults potenciais
    • Huge Pages de 2 MB: ~61.440
    • Huge Pages de 1 GB: ~120
  • O aumento do tamanho das páginas não reduz apenas os page faults, mas também alivia a pressão sobre o TLB: a mesma quantidade de memória pode ser coberta com muito menos entradas no TLB, reduzindo TLB misses e page-table walks
  • Como o StrategyGetBuffer deixa de gerar faults enquanto segura o lock, quem detém o lock termina rapidamente, e os demais backends esperam microssegundos em vez de milissegundos. A regressão desaparece
  • No PostgreSQL, a configuração de huge pages é controlada pelo parâmetro huge_pages
    • Há suporte para três valores: off, on, try (padrão)
    • try usa huge pages quando possível e, caso contrário, faz fallback silencioso para 4 KB, o que traz o risco de não perceber uma configuração incorreta
    • Ao definir on, se não for possível usar huge pages, o PostgreSQL falha na inicialização e o problema pode ser percebido imediatamente
  • Trade-off: huge pages usam pré-alocação e reserva, então, mesmo que o PostgreSQL não utilize toda a memória, essa parte fica indisponível para o restante do sistema. Se apenas parte da página for usada, o restante é desperdiçado. Em ambientes de produção com shared_buffers grandes, em geral vale a pena aceitar esse trade-off

Próximos desdobramentos

  • Peter Zijlstra, engenheiro de kernel da Intel que projetou a mudança de preempção, sugeriu que o PostgreSQL adotasse Restartable Sequences (rseq)
    • rseq é um recurso do kernel Linux que permite ao código em userspace detectar preempção ou migração durante seções críticas e reiniciar esse trecho
    • Se aplicado ao caminho de spinlock do PostgreSQL, o rseq pode evitar o cenário em que um detentor de lock preemptado atrasa todos os backends em espera
  • A reação da comunidade PostgreSQL foi negativa
    • É difícil aceitar a necessidade de adotar um recurso extra do kernel apenas para recuperar um desempenho que antes do Linux 7.0 vinha “de graça”
    • A visão é que isso viola o princípio histórico do kernel de "não quebrar o userspace" (software que funcionava normalmente antes do upgrade do kernel deve continuar funcionando normalmente depois)

11 comentários

 
y15un 2026-05-04

Pensando bem, esse título está claramente errado.

https://pt.news.hada.io/topic?id=28241#cid54772
Como o mantenedor principal do kernel já vinha recomendando isso ao PostgreSQL há muito tempo, o mais correto seria dizer "Por que o Postgres fica mais lento no Linux 7.0", e não que o 7.0 "quebrou" o Postgres.

Mesmo que o kernel não siga semver de forma rigorosa, ainda é um aumento de versão major; vão mesmo enquadrar um problema causado por eles próprios desse jeito??

 
domuji6 2026-05-05

Embora rseq tenha sido apresentado como alternativa, por exigir a introdução de código específico para Linux, parece uma proposta difícil de aceitar facilmente para um projeto open source que precisa considerar compatibilidade multiplataforma.

É compreensível que haja mudanças de comportamento em uma atualização de versão principal, mas, no fim das contas, se isso resultou em uma queda de desempenho de 50%, quem opera infraestrutura provavelmente não terá como deixar de olhar com cautela para a própria atualização do kernel.

 
y15un 2026-05-06

Uau, comentei durante uma viagem de trabalho e, quando cheguei ao hotel, vi que três pessoas já tinham deixado suas opiniões. Obrigado.

Eu entendo bem os pontos de vista que vocês mencionaram, mas ainda vejo isso como uma dívida técnica do lado do postgres, então considero que, no fim das contas, o próprio postgres precisa resolver isso. (Acho que a experiência de pagar caro por usar hacks etc. em nome de desempenho imediato já foi mais do que suficiente com o Spectre...)
No fim, parece que vamos ter que observar esse caso por mais algum tempo.

Tenham um bom dia. :)

 
ilsubyeega 2026-05-06

Concordo. Costumo ver notícias sobre engenheiros de Linux da Intel se aposentando, e se continuarem deixando os comportamentos existentes largados assim, um dia isso vai acabar ficando igual ao Windows o,o..

 
xenoside 2026-05-05

Como essa implementação já é uma parte cheia de código assembly dedicado para cada plataforma visando desempenho ideal,
afirmar que não dá para fazer só porque seria adicionado código específico para determinada plataforma não me parece ser um motivo válido.
(Resumi após perguntar ao Gemini.)

 
nanashi222 2026-05-06

Eu vi o código, e não é uma função a esse ponto cheia de código assembly; além disso, o fato de estar cheia de assembly não significa, na minha opinião, que adicionar código específico para uma determinada plataforma por si só seria um problema. Acho que quando falam em código assembly, devem estar se referindo às funções de operações atômicas (as funções builtin __atomic__ do GCC), mas olhando apenas para a função em si, não dá para adicionar código extra especialmente só para Linux.

 
jjw9512151 2026-05-11

Como é open source, provavelmente também deve ser um peso mexer nisso e testar... e também tem usuários demais.

 
hiseob 2026-05-05

Com certeza, como o Linus deus já deu uma bronca no passado com um “WE DO NOT BREAK USERSPACE!”, até dá a impressão de que talvez tivesse que haver uma opção.
Mas, por outro lado, também não parece fazer muito sentido insistir em usar spinlock no userspace.
É mais ou menos essa a sensação.

 
cafedead 2026-05-04

"Não quebre o user space" vs "não faça spinlock no user space"

 
savvykang 2026-05-05

Acho meio difícil entender que, ao passar 30 anos reimplementando algumas funcionalidades do sistema operacional, ninguém tenha pensado em documentar os limites e os motivos disso. Trinta anos atrás, certamente havia razões plausíveis para terem criado sincronização própria, gerenciamento de memória próprio e até um modelo de processo próprio.

 
hungryman 2026-05-05

Estamos na era da IA, então o fato de existir esse tipo de reação negativa significa que, do ponto de vista arquitetural, aquilo lá ficou complicado e todo embolado?