Pare de deixar o escalonador cobrar imposto do seu banco de dados: fixação de CPU ciente de cache para Postgres e Valkey

Por Diogo Hudson Dias
Site reliability engineer reviewing CPU topology and cache performance graphs on dual monitors in a São Paulo office.

Seu banco de dados não é lento. É a topologia de CPU que está cobrando um imposto que você nunca aprovou. Em servidores modernos — especialmente AMD EPYC com múltiplos complexos de núcleos — deixar o Linux escalonar Postgres ou Valkey pela máquina inteira costuma matar a localidade do cache L3 e a latência p95. A solução não é uma instância maior. É alinhar seus processos ao silício pelo qual você já paga.

Benchmarks públicos recentes e relatos da comunidade mostram ganhos de dois dígitos ao manter os workers do banco de dados dentro de um único domínio de cache (CCD/CCX em AMD, limites de nó NUMA em geral). Vimos vitórias semelhantes em campo: 15–35% mais throughput e p95 mais apertado apenas por sermos intencionais sobre o posicionamento de CPU e memória. Sem mudanças de código. Sem hardware novo. Apenas tratar cache como um recurso de primeira classe.

Por que isso importa agora

Duas realidades de 2026 colidem aqui:

  • A contagem de cores ultrapassou a consciência das aplicações. Uma máquina de 64–128 vCPUs parece eficiente na fatura da nuvem, mas esconde múltiplos caches de último nível e controladores de memória. Espalhar threads quentes às cegas entre eles é auto-sabotagem.
  • Bancos de dados são sensíveis a latência e amantes de cache. Postgres e Valkey reutilizam páginas quentes e metadados como loucos. Se você lhes tira localidade, multiplica os misses em todos os caminhos de chamada.

Se você opera em regiões mais caras (por exemplo, AWS sa-east-1 no Brazil frequentemente tem um ágio de dois dígitos sobre us-east-1), extrair 20% a mais por nó não é micro-otimização — é controle de orçamento.

Topologia de CPU moderna em 90 segundos

  • AMD EPYC (Zen 3/4/5): os cores são agrupados em Core Complex Dies (CCDs). Cada CCD tem seu próprio cache L3. Cruzar limites de CCD significa maior latência e mais misses de L3. Muitas instâncias com muitas vCPUs são múltiplos CCDs costurados.
  • Intel Xeon (Skylake e posteriores): um interconector em malha com LLC compartilhado porém particionado. A localidade ainda importa; mover dados pela malha custa ciclos.
  • Arm (AWS Graviton): tipicamente um único domínio NUMA com L2/L3 generosos, o que reduz mas não elimina preocupações de localidade, especialmente com alta contagem de threads.

Ideia-chave: mantenha threads que cooperam (as que tocam os mesmos dados) próximas ao mesmo cache de último nível e ao controlador de memória que o abastece. Isso significa menos misses de cache, menos viagens entre dies, melhor Instruções por Ciclo (IPC) e uma cauda de latência mais feliz.

Por que Postgres e Valkey são especialmente sensíveis

Postgres

  • Processo por conexão: cada backend movimenta suas próprias pilhas e também o buffer compartilhado que mapeia as mesmas tabelas e índices.
  • Caminhos quentes: buscas em btree, verificações de visibility map, acesso ao catálogo — tudo isso adora cache. Espalhe pelos CCDs e essas páginas quentes continuarão sendo expulsas.
  • Processos auxiliares: walwriter, checkpointer, background writer e autovacuum disputam CPU e banda de memória. Colocar backends e memória compartilhada no mesmo nó NUMA melhora a coerência.

Valkey (Redis)

  • QPS alto, objetos pequenos: a carga é “tiny-hot”, exatamente o que o L3 existe para acelerar.
  • I/O com threads: versões modernas usam I/O threads; posicionamento ruim causa bouncing entre cores e thrash de sockets.
  • Shards/instâncias: executar múltiplos shards em um host grande é comum. Se esses shards não respeitam os domínios de cache, você elimina o benefício do sharding.

Deveria fazer isso? Um framework rápido de decisão

Diga sim a um projeto ciente de cache se pelo menos dois forem verdadeiros:

  • A CPU do seu db frequentemente >50% enquanto o p95 dispara sob carga.
  • perf stat mostra taxas de cache-miss altas (>10–15%) e IPC caindo (<1) no pico.
  • O working set cabe em RAM e storage não é o gargalo (a latência p95 de disco está ok).
  • Você está em instâncias com 32+ vCPUs ou layouts multi-CCD/NUMA conhecidos (comum em c6a/c7a, c3d, Dav5/Dv5, etc.).

Se storage é o gargalo, corrija isso primeiro. Se você está majoritariamente ocioso, não notará os ganhos. Isso compensa quando você realmente aperta a máquina.

Caminho rápido: um plano de rollout em 10 dias

Dia 1–2: Auditoria de topologia

  • Mapeie a CPU e o cache: rode lscpu -e e numactl -H. Em AMD, procure pistas de CCD/CCX; ferramentas como hwloc desenham isso muito bem.
  • Capture o baseline de perf: perf stat -e cycles,instructions,cache-misses durante o pico; registre p95/99 do Postgres e latência/QPS do Valkey.
  • Confirme que storage não é o vilão: use iostat e ferramentas eBPF (por exemplo, perf-tools cachestat, biolatency) para verificar.

Dia 3–4: Benchmarks de baseline

  • Postgres: pgbench com escala realista (pense em fator de escala igual às conexões ativas, não defaults de brinquedo). Meça TPS e p95.
  • Valkey: redis-benchmark ou valkey-benchmark com seus tamanhos de chave e profundidade de pipeline.

Dia 5–6: Protótipo de fixação em staging

  • Crie um cpuset para o banco que mapeie para um domínio de cache (por exemplo, cores 0–15 que compartilham L3). Mantenha um conjunto de “housekeeping” separado para kernel, interrupções da NIC e tarefas de fundo.
  • Vincule a memória local: inicie o serviço com numactl --cpunodebind=X --membind=X para que a memória compartilhada favoreça o mesmo nó.
  • Desabilite o irqbalance para o teste e defina manualmente as afinidades de IRQ da NIC para os cores de housekeeping; defina rps_cpus de forma semelhante.

Dia 7: Canário em produção

  • Mova uma fração do tráfego para as instâncias fixadas. Compare throughput e p95/p99. Busque 15–35% de ganho em TPS ou p95 significativamente mais apertado.
  • Observe regressões: atraso de autovacuum, picos de checkpoint ou perda de pacotes por interrupções mal fixadas.

Dia 8–9: Operacionalize

  • systemd: adicione um drop-in para Postgres com AllowedCPUs= e MemoryNUMAPolicy=local. Torne persistentes as máscaras de IRQ em scripts de boot.
  • Kubernetes: habilite a política estática do CPU Manager, rode o pod do DB como Guaranteed (requests=limits) e solicite hugepages se aplicável. Use o Topology Manager para alinhar CPU e memória. Mantenha as “reserved CPUs” do SO separadas.

Dia 10: Documente e faça o dashboard

  • Registre no runbook o mapa de topologia, as máscaras de cpuset e as atribuições de IRQ.
  • Adicione painéis no Grafana para cache misses, IPC, latência de run queue e p95 do DB. Se não está no gráfico, não aconteceu.

Como fazer na prática: bare metal ou VM

1) Identifique o domínio de cache que você vai usar

  • Use lscpu -e para listar as colunas CPU, socket, node e L3 ID. Escolha um conjunto contíguo que compartilhe o mesmo L3 ID.
  • Confirme com hwloc ou inspecionando /sys/devices/system/cpu/cpuN/cache/index3/id.

2) Construa os cpusets

  • Crie dois cgroups: db e housekeeping. Atribua o serviço do banco aos CPUs locais ao L3 e todo o resto (irq, systemd-oomd, journald) ao conjunto de housekeeping.
  • No systemd, use AllowedCPUs= para o serviço e CPUAffinity= para serviços do SO que você quer longe dos cores quentes.

3) Vincule memória e habilite huge pages

  • Postgres: defina huge_pages = on (garanta que vm.nr_hugepages esteja provisionado). Inicie sob numactl --membind para manter o buffer pool local.
  • Valkey: mantenha os processos de shard alinhados ao seu cpuset e use SO_REUSEPORT com um listener por shard se você usar um único VIP na frente.

4) IRQ e rede

  • Desabilite ou restrinja o irqbalance. Defina manualmente os IRQs da NIC para os cores de housekeeping (verifique /proc/interrupts, defina /proc/irq/*/smp_affinity_list).
  • Para taxas altas de pacotes, defina rps_cpus e xps_cpus das filas da NIC para corresponder ao conjunto de housekeeping e evitar poluidores nos cores do DB.

5) Ajustes do Postgres que se beneficiam

  • shared_buffers: 25–40% da RAM é uma faixa sensata para muitos workloads OLTP. Maior nem sempre é melhor; a localidade importa mais do que o tamanho bruto.
  • max_parallel_workers e max_worker_processes: mantenha o total de workers ativos dentro da sua contagem de cores fixados. Ultrapassar o orçamento de cores locais ao L3 destrói o ganho.
  • checkpoint_timeout, max_wal_size, autovacuum_work_mem: espalhe trabalhos pesados para evitar pausas sincronizadas que punem o cache.

Variação em Kubernetes (o que a maioria de vocês realmente roda)

  • Use Guaranteed QoS: requests iguais a limits para CPU e memória. Caso contrário, o kubelet pode fatiar seu tempo de CPU entre vários cores e derrotar a localidade.
  • Habilite o CPU Manager com política estática: isso dá CPUs exclusivos para pods Guaranteed.
  • Topology Manager: defina como restricted ou best-effort para que alocações de CPU e memória caiam no mesmo nó NUMA.
  • Reserve CPUs para o SO e daemons do sistema: defina --reserved-cpus no kubelet para que a tubulação do nó não acampe no domínio de cache do seu DB.
  • Huge pages: solicite hugepages-2Mi no spec do Pod; configure no nível do nó primeiro.
  • Anti-affinity e node selectors: mantenha pods de DB longe de vizinhos barulhentos e garanta que eles agendem no tipo/topologia de instância pretendidos.

Se você usa um operador de Postgres (Crunchy, Zalando), ainda pode usar essas primitivas: defina requests=limits de recursos, adicione node selectors/taints e garanta que o operador não reagende você para hardware incompatível.

Notas sobre instâncias de nuvem que merecem sua atenção

  • AWS AMD (c6a/c7a/m7a/r7a): muitos tamanhos costuram múltiplos CCDs. Prefira menos máquinas grandes somente se você se comprometer com pinning; caso contrário, instâncias médias podem vencer devido à topologia mais simples.
  • AWS Graviton (c7g/m7g): os benefícios de localidade são menores, mas ainda reais sob multithreading pesado. Os ganhos tendem a aparecer mais na estabilidade do p95 do que no TPS de pico.
  • GCP C3D (AMD) / C3 (Intel): mesma orientação: mapeie os domínios de cache antes de escalar uma frota que briga consigo mesma.
  • Azure Dav5/Dv5: similar às famílias AMD/Intel da AWS; o mapa NUMA é seu amigo. Não agende às cegas.

Que tipo de ganho é realista?

Relatos públicos de profissionais mostram:

  • Postgres: 15–25% de melhoria em TPS em OLTP estilo pgbench ao fixar a um conjunto de cores equivalente a um único CCD com memória local, versus deixar os backends vagarem por dois ou mais CCDs. O p95 frequentemente aperta por um fator similar devido a menos viagens entre dies.
  • Valkey: 20–35% de ganhos em QPS ao rodar múltiplos shards, cada um fixado ao seu próprio domínio de cache, com I/O threads alinhados. Pipelines com payloads pequenos se beneficiam mais.

Seu resultado pode variar, mas se você não estiver vendo deltas de dois dígitos, re-cheque seu mapeamento de CPU e o posicionamento de IRQ; uma única interrupção perdida em um core quente pode apagar o ganho.

Trade-offs e pegadinhas (leia duas vezes)

  • Teto vs. eficiência: confinar a um único domínio de cache pode limitar o throughput máximo absoluto se seu workload realmente escala linearmente com mais cores. Muitos OLTPs não escalam além de certo ponto; eles perdem sob tempestades de coerência — a fixação ajuda aqui.
  • Complexidade operacional: agora você está no negócio de pinning e gerenciamento de IRQ. Modele isso em Ansible/Terraform ou na sua camada de cluster para sobreviver a reboots e rotações de nó.
  • Orquestração de contêineres reage: sem CPU Manager estático e Guaranteed QoS, o K8s vai fatiar seu tempo de CPU rumo à mediocridade. Não implemente pela metade.
  • Ruído do hipervisor: em hosts compartilhados, você não controla totalmente o posicionamento. Prefira sabores de host dedicado se você persegue cada microssegundo.
  • Temperatura e boost: cores quentes fixados podem sustentar temperaturas mais altas em regime; observe throttling. Não presuma que clocks de boost vão salvar você.

Verificação: como saber que você realmente ganhou

  • Micro-métricas: IPC deve subir; a taxa de cache-miss deve cair. Compare perf stat sob carga oferecida igual.
  • SLOs de app: latências p95/p99 devem apertar e derivar menos com surtos de tráfego.
  • Ruído do sistema: a latência da fila de execução nos cores quentes deve achatar; menos context switches involuntários; IRQs da NIC não devem mais aparecer nos cores do DB em /proc/interrupts.
  • Custo por TPS: acompanhe TPS por dólar-hora. Se você puder reduzir uma classe de instância ou consolidar nós após o tuning, há dinheiro na mão.

Torne isso repetível

  • Topologia como código: armazene as máscaras de cpuset e os node selectors junto do seu código de infra. Em K8s, bloqueie deploys com base em rótulos de nó que anunciem a topologia certa (Node Feature Discovery pode ajudar).
  • Golden images: incorpore configs de IRQ, ajustes de hugepages e drop-ins do systemd em AMIs/imagens. Não dependa de scripts de shell ad hoc.
  • Observabilidade embutida: dashboards para métricas de cache fazem parte da entrega. Alerta para explosões de cache-miss e runqlat crescente nos cores fixados.

Onde o nearshore entra

Isto é trabalho perfeito para nearshore: de duração definida, pesado em infraestrutura e mensurável. Um pod com forte domínio de Linux e bancos de dados consegue entregar isso em duas semanas com 6–8 horas de overlap para rodar canários e iterar. Para equipes operando na AWS sa-east-1 no Brazil, um lift de 20% em throughput pode compensar imediatamente prêmios regionais de custo; para equipes nos EUA, é um hedge contra pressão de escala enquanto você adia um projeto caro de sharding ou re-arquitetura.

Quando não vale a pena

  • Seu workload é claramente limitado por I/O (p95 de storage está feio) ou por rede (pacotes minúsculos saturam a NIC antes da CPU).
  • Você já roda em VMs pequenas de único CCD/NUMA — não há topologia contra a qual lutar.
  • Você está agendado em um hipervisor barulhento e compartilhado e não consegue controlar IRQs ou exclusividade de CPU — corrija a alocação/tenancy primeiro.

Em resumo

O escalonador padrão do Linux é um milagre de engenharia de uso geral. Seu banco de dados não é de uso geral. Trate cache e localidade NUMA como uma linha do orçamento e você vai parar de pagar um imposto oculto toda vez que o tráfego dispara. Isso não é feitiçaria; é disciplina: medir, fixar, vincular, verificar. Faça isso e você vai “comprar” 15–35% de folga por centavos.

Principais aprendizados

  • Seu banco pode estar pagando um imposto oculto de cache/NUMA — espere ganhos de 15–35% com localidade.
  • Direcione um único domínio de cache (CCD/nó NUMA) para os workers de Postgres/Valkey; vincule CPU e memória juntos.
  • Use cpusets/systemd em VMs; CPU Manager estático e Guaranteed QoS em Kubernetes.
  • Acerte o posicionamento de IRQ e de housekeeping; uma interrupção perdida em um core quente pode apagar seus ganhos.
  • Verifique com métricas de IPC e cache-miss mais p95/p99; incorpore as configurações no código de infra.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation