1 pontos por GN⁺ 1 시간 전 | Ainda não há 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)

Ainda não há comentários.

Ainda não há comentários.