[!NOTE]
Última parte do Artigo 2 (Fundamentos e Virtual Memory). Nas partes anteriores cobrimos hierarquia de memória, paginação, TLB, page faults, COW e swapping. Agora fechamos com zones e NUMA, a conexão com os runtimes de Python, .NET e Go, diagnóstico e boas práticas.
Sumário
- Memory Zones
- NUMA: quando a memória tem geografia
- Conexão com Backend: Python (CPython)
- Conexão com Backend: .NET (CLR)
- Conexão com Backend: Go
- Técnicas de Diagnóstico
- Boas Práticas
- Estudos de Caso
- Referências Bibliográficas
Memory Zones
Até agora tratamos a RAM como um bloco uniforme de frames. Mas o kernel precisa lidar com restrições de hardware que tornam certas regiões da memória física especiais. Por isso ele divide a memória física em zones:
┌──────────────┬─────────────────────────────────────────────────────┐
│ ZONE_DMA │ Memória para dispositivos antigos com endereçamento │
│ │ limitado (ex.: primeiros 16 MB em x86) │
├──────────────┼─────────────────────────────────────────────────────┤
│ ZONE_DMA32 │ Memória endereçável por dispositivos de 32 bits │
│ │ (primeiros 4 GB) │
├──────────────┼─────────────────────────────────────────────────────┤
│ ZONE_NORMAL │ Memória "normal", diretamente mapeada pelo kernel. │
│ │ Onde vive a maioria das alocações. │
├──────────────┼─────────────────────────────────────────────────────┤
│ ZONE_HIGHMEM │ (Relíquia do x86 32-bit; irrelevante em 64 bits) │
└──────────────┴─────────────────────────────────────────────────────┘
Por que isso existe? Porque alguns dispositivos de hardware (especialmente antigos) só conseguem fazer DMA (acesso direto à memória) em faixas baixas de endereço. O kernel precisa garantir que buffers de DMA venham dessas zonas específicas. Em servidores modernos de 64 bits, a esmagadora maioria das alocações vem de ZONE_NORMAL, e você raramente pensa nas outras — mas elas aparecem quando você lê /proc/zoneinfo diagnosticando pressão de memória.
# Ver as zonas e suas watermarks (min/low/high — importantes no Artigo 3)
cat /proc/zoneinfo | grep -E 'Node|zone|free|min|low|high' | head -20
Cada zona tem suas próprias watermarks (min, low, high) que governam quando o kernel começa a recuperar memória — um mecanismo central que detalharemos no Artigo 3 (memory reclaim).
NUMA: quando a memória tem geografia
Em servidores com múltiplos sockets de CPU, a memória deixa de ser uniforme numa dimensão nova: a distância. Isso é o NUMA (Non-Uniform Memory Access).
Num sistema NUMA, cada socket de CPU (um nó NUMA) tem sua própria memória local, fisicamente próxima. Acessar a memória local é rápido; acessar a memória de outro nó (remota) passa pela interconexão entre sockets e é mais lento.
Nó 0 Nó 1
┌───────────────────┐ ┌───────────────────┐
│ CPUs 0-15 │ │ CPUs 16-31 │
│ │ │ │ │ │
│ ┌────┴────┐ │◀──────────────▶│ ┌────┴────┐ │
│ │ RAM │ │ interconexão │ │ RAM │ │
│ │ local │ │ (mais lenta) │ │ local │ │
│ └─────────┘ │ │ └─────────┘ │
└───────────────────┘ └───────────────────┘
Acesso local: ~100 ns Acesso remoto: ~150-200 ns
A diferença de latência entre acesso local e remoto pode ser de 1.5x a 2x. Para aplicações sensíveis a latência, isso importa muito.
O kernel expõe a topologia e tenta, por padrão, alocar memória no mesmo nó onde a thread está rodando (política first-touch: a página vai para o nó que a tocou primeiro, não o que a alocou).
# Ver a topologia NUMA da máquina
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 ... 15
# node 0 size: 64000 MB
# node 1 cpus: 16 17 ... 31
# node distances:
# node 0 1
# 0: 10 21 ← distância relativa (10 = local, 21 = ~2x mais lento)
# 1: 21 10
# Estatísticas NUMA por nó (hits locais vs faltas remotas)
numastat
A distance matrix (node distances) quantifica o custo relativo: 10 é o baseline local, e valores maiores indicam nós mais "distantes".
Automatic NUMA Balancing
Desde o Kernel 3.8, o Linux tem balanceamento automático de NUMA: ele monitora quais nós acessam quais páginas e migra páginas e threads para melhorar a localidade, dinamicamente. Isso usa uma técnica engenhosa: periodicamente desmapeia páginas (marcando PTEs como não-presentes) para forçar page faults "de amostragem" (NUMA hinting faults) que revelam o padrão de acesso.
# Verificar se está ativo (1 = ativo)
cat /proc/sys/kernel/numa_balancing
Para muitas aplicações isso é ótimo e transparente. Mas para workloads de altíssima performance e latência previsível (bancos de dados, trading, caches grandes), o balanceamento automático pode introduzir jitter — nesses casos, faz-se pinning explícito (fixar processo e memória a um nó via numactl), que veremos nas boas práticas.
Conexão com Backend: Python (CPython)
Agora conectamos tudo com o que roda de fato em produção. Começando pelo CPython.
O modelo de memória do CPython
O CPython tem seu próprio alocador em camadas sobre o malloc do sistema:
Objetos Python
│
▼
┌──────────────┐ pymalloc: alocador especializado para objetos pequenos (< 512 bytes)
│ pymalloc │ organiza memória em "arenas" (256 KB), "pools" (4 KB = 1 página!)
└──────┬───────┘ e "blocks". Reduz overhead de malloc para os milhões de objetinhos.
│ (objetos grandes)
▼
┌──────────────┐
│ malloc/mmap │ → brk/mmap → kernel → page faults → frames físicos
└──────────────┘
Repare que um pool do pymalloc tem exatamente 4 KB — o tamanho de uma página. Não é coincidência: alinhar com a página evita desperdício e melhora a localidade.
Reference counting e page faults
Como vimos na Parte III, o refcount do CPython é uma escrita a cada toque num objeto. Isso tem duas consequências de memória:
- Quebra de COW em workers pre-forked (discutido na Parte III) — o grande vilão do inchaço de memória em Gunicorn/uWSGI.
- Dirty pages constantes: mesmo objetos "só de leitura" ficam sujos (dirty bit ligado), o que afeta reclaim e swap.
Fragmentação em aplicações long-running
Um problema clássico do CPython: memória liberada nem sempre volta ao SO. O pymalloc segura arenas, e a fragmentação da heap pode fazer o RSS de um worker só crescer, nunca diminuir, mesmo após picos de carga terminarem. É por isso que muitas equipes configuram Gunicorn com max_requests (recicla o worker após N requisições, devolvendo memória ao SO).
Diagnóstico prático
import tracemalloc
tracemalloc.start()
# ... executa a carga de trabalho ...
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics('lineno')
for stat in top[:10]:
print(stat) # mostra as linhas que mais alocam memória
# Correlacionar com o kernel: page faults do processo Python
perf stat -e minor-faults,major-faults -p <PID_python>
Mitigando o inchaço por COW
import gc
# ANTES do fork (no master), congela os objetos já criados na geração
# permanente, evitando que o GC toque seus headers e quebre o COW.
gc.freeze()
# ... então o Gunicorn/uWSGI faz fork dos workers ...
Estratégia complementar: manter dados grandes e imutáveis fora do heap gerenciado do Python — em arrays NumPy (buffer contíguo, sem refcount por elemento), em memória mapeada (mmap), ou num serviço externo (Redis). Assim os workers realmente compartilham, sem cópia.
Conexão com Backend: .NET (CLR)
O CLR (CoreCLR) no Linux tem um modelo de memória sofisticado, dominado pelo Garbage Collector.
Managed heap vs unmanaged memory
┌─────────────────────────────────────────────┐
│ Managed Heap │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ SOH │ │ LOH (Large Object │ │
│ │ (Gen0/1/2) │ │ Heap, ≥ 85 KB) │ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────┘
│ o GC gerencia; sob o capô é tudo
▼ mmap/VirtualAlloc → páginas do kernel
- O Small Object Heap (SOH) é geracional (Gen0, Gen1, Gen2), apostando que a maioria dos objetos morre jovem.
- O Large Object Heap (LOH) guarda objetos ≥ 85 KB. Ele não é compactado por padrão (compactar objetos grandes é caro), então sofre de fragmentação externa — exatamente o conceito da Parte I reaparecendo. Buracos no LOH podem inflar o RSS.
GC e page faults
Um ciclo de GC "toca" muita memória (varrendo objetos, movendo-os na compactação do SOH). Isso pode disparar rajadas de page faults — inclusive major faults se parte da heap tiver ido para o swap. Um GC que precisa "puxar" a heap de volta do swap é uma fonte clássica de pausas longas em produção.
Workstation GC vs Server GC no Linux
- Workstation GC: uma thread de GC, menor footprint. Bom para apps pequenas/desktop.
- Server GC: uma heap e uma thread de GC por core, muito mais throughput — mas consome mais memória. Em containers, o Server GC pode alocar heaps por CPU do host, não do container, causando surpresas de consumo.
// runtimeconfig.json — atenção crítica em containers
{
"configProperties": {
"System.GC.Server": true,
// Faz o CLR respeitar os limites de cgroup do container:
"System.GC.HeapHardLimitPercent": 75
}
}
[!IMPORTANT]
Em Kubernetes/containers, garanta que o CLR "enxerga" o limite de memória do cgroup. Versões modernas do .NET detectam cgroups automaticamente, mas vale verificar — senão o Server GC dimensiona a heap pela RAM do host e você toma OOM Kill (Artigo 3).
Diagnóstico
# Contadores de GC e memória ao vivo
dotnet-counters monitor -p <PID> --counters System.Runtime
# Coletar um dump para analisar a heap
dotnet-dump collect -p <PID>
dotnet-dump analyze core_dump
# > dumpheap -stat (objetos por tipo/tamanho)
# > gcheapstat (fragmentação por geração/LOH)
Conexão com Backend: Go
O runtime do Go tem um alocador inspirado no TCMalloc e um GC concorrente, com decisões que impactam diretamente o padrão de páginas.
Stack vs heap e escape analysis
O compilador Go decide, em tempo de compilação, se um valor vive na stack (barato, liberado ao retornar da função) ou "escapa" para a heap (gerenciado pelo GC). Isso é a escape analysis:
// NÃO escapa: 'x' vive na stack, zero pressão sobre o GC/heap.
func soma(a, b int) int {
x := a + b
return x
}
// ESCAPA: o ponteiro sobrevive à função → vai para a heap → pressão de GC.
func criaUsuario(nome string) *Usuario {
u := Usuario{Nome: nome}
return &u // &u escapa
}
# Ver as decisões de escape analysis do compilador
go build -gcflags='-m' ./...
# ./main.go:12:9: &u escapes to heap
Menos alocações na heap = menos trabalho do GC = menos páginas sujas = menos pressão de memória no kernel. Otimizar escape analysis é otimizar a interação com o subsistema de memória do kernel.
O alocador e o kernel
O Go pede memória ao kernel em grandes blocos via mmap e os subdivide internamente em size classes (semelhante ao pymalloc e ao slab do kernel). O runtime tem seu próprio conceito de páginas (8 KB, dois frames de 4 KB) organizadas em spans.
Diagnóstico com pprof e GODEBUG
import _ "net/http/pprof" // expõe profiles em /debug/pprof/
# Profile de heap: quem está alocando
go tool pprof http://localhost:6060/debug/pprof/heap
# Rastreio de cada ciclo de GC (pausa, heap, etc.)
GODEBUG=gctrace=1 ./minha-app
# gc 1 @0.012s 2%: 0.018+1.2+0.003 ms clock, ... 4->5->2 MB, 5 MB goal
# Correlacionar com page faults do kernel
perf stat -e minor-faults,major-faults ./minha-app
O GOGC (padrão 100) controla o trade-off: valores maiores = GC menos frequente = mais RAM usada, menos CPU em GC. O GOMEMLIMIT (Go 1.19+) define um teto suave de memória — essencial em containers para evitar OOM Kill, funcionando como um "freio" antes de o cgroup estourar.
Técnicas de Diagnóstico
Um resumo do arsenal para investigar memória em produção, conectando com tudo que vimos:
# 1. Visão geral do processo: virtual vs residente
pmap -x <PID> # mapa detalhado com RSS por região
ps -o pid,vsz,rss,cmd -p <PID> # VSZ (virtual) vs RSS (físico)
# 2. Detalhamento por VMA: dirty pages, PSS (proporcional), swap
cat /proc/<PID>/smaps # rico, porém verboso
grep -E 'Rss|Pss|Swap' /proc/<PID>/smaps_rollup # resumo agregado
# 3. Page faults (minor = RAM, major = disco!)
perf stat -e minor-faults,major-faults -p <PID>
# 4. Pressão de memória do sistema
vmstat 1 # colunas si/so (swap in/out) → alerta!
free -h
# 5. Transparent Huge Pages em uso
grep -i AnonHugePages /proc/<PID>/smaps_rollup
# 6. NUMA: acessos locais vs remotos
numastat -p <PID>
PSS (Proportional Set Size) merece destaque: quando várias instâncias compartilham páginas (código, COW), o RSS conta a página inteira para cada processo, superestimando o total. O PSS divide páginas compartilhadas proporcionalmente, dando uma medida mais honesta de "quanto este processo realmente custa" de memória.
Boas Práticas
Dimensione heaps explicitamente em containers. Configure
GOMEMLIMIT(Go),HeapHardLimitPercent(.NET) e limites de worker (Python/Gunicornmax_requests). Nunca confie que o runtime vai adivinhar o limite do cgroup — verifique.Use huge pages para databases e caches grandes, mas com critério. Habilite HugeTLB explícito para o banco; desabilite THP (
madviseounever) se sua stack (Redis, Mongo, PostgreSQL) recomenda, para evitar jitter de latência.NUMA pinning para aplicações latency-sensitive. Fixe processo e memória ao mesmo nó:
numactl --cpunodebind=0 --membind=0 ./minha-app-critica
Preserve o COW em workers Python pre-forked:
gc.freeze()antes do fork, dados grandes fora do heap gerenciado.Configure swap conscientemente em containers. Em SSD/NVMe, um swap moderado pode ser um amortecedor útil (kernels 5.14+ o tornaram eficiente), mas ajuste
vm.swappinessconforme o workload (aprofundamos no Artigo 3).
Estudos de Caso
1. Python — Otimizando um cliente Redis com huge pages
Uma aplicação de cache intensivo com grandes estruturas em memória sofria com TLB misses (working set de vários GB, muitas páginas de 4 KB). Habilitar huge pages para a região de dados reduziu a pressão sobre a TLB, cortando a latência de cauda (p99) de acessos ao cache. Diagnóstico feito com perf stat -e dTLB-load-misses.
2. .NET — Tuning de GC para uma API de alta vazão
Uma API ASP.NET Core em Kubernetes tomava OOM Kill esporádico. Investigação com dotnet-counters mostrou o Server GC dimensionando a heap pela RAM do host (não do container). Correção: HeapHardLimitPercent no runtimeconfig + verificação da detecção de cgroup. RSS estabilizou abaixo do limite e o OOM sumiu.
3. Go — Reduzindo allocation pressure em um stream processor
Um processador de streams tinha ciclos de GC frequentes (visíveis em GODEBUG=gctrace=1) causando picos de CPU. go build -gcflags='-m' revelou allocations desnecessárias na heap (buffers escapando em hot path). Reuso via sync.Pool e ajuste de GOGC reduziram a frequência de GC e a quantidade de páginas sujas por segundo.
Conteúdo parcialmente gerado com auxílio de IA generativa (me ajudou a organizar tudo isso kkkk)
Top comments (0)