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.