Como o Linux 7.0 quebrou o PostgreSQL
(read.thecoder.cafe)- 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
pgbenchconduzidos 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
pgbenchem 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
perfmostrou que 55,60% do tempo de CPU foi gasto dentro da funçãos_lock- Caminho de chamada:
StartReadBuffer→GetVictimBuffer→StrategyGetBuffer→s_lock
- Caminho de chamada:
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
- PREEMPT_NONE: a thread quase não é interrompida até ceder a CPU voluntariamente (
- 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
- A função responsável por essa seleção de buffer é
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
StrategyGetBufferusa 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_buffersfoi 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
taté o scheduler devolver o controle- Esse tempo extra não custa apenas
t, mas é amplificado em número de backends girando no momento ×tem 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
- Esse tempo extra não custa apenas
A solução com Huge Pages
- Com
shared_buffersem 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
StrategyGetBufferdeixa 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) tryusa 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
- Há suporte para três valores:
- 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_buffersgrandes, 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
rseqpode 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
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??
Embora
rseqtenha 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.
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. :)
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..
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.)
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.Como é open source, provavelmente também deve ser um peso mexer nisso e testar... e também tem usuários demais.
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.
"Não quebre o user space" vs "não faça spinlock no user space"
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.
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?