Se voce chegou ate aqui meus parabens, gerenciamento de memoria e kernel linux nao sao assuntos triviais. Nesta parte vamos finalmente falar de reclaim e OOM — como o kernel decide quando e como recuperar memoria, e o que acontece quando ele nao consegue.
Sumário
- Watermarks: os níveis de alerta
- kswapd: o reclaim em background
- Direct reclaim: quando não dá para esperar
- As LRU lists do Linux
- vm.swappiness
- PSI: Pressure Stall Information
- O OOM Killer
- Referências Bibliográficas
Watermarks: os níveis de alerta
Na Parte IV mencionamos que cada zona de memória tem watermarks (marcas d'água). Agora elas entram em ação. O kernel monitora a memória livre de cada zona contra três limiares:
Memória livre na zona
alta │────────────────────── WATERMARK_HIGH ← "tudo ok, pare de recuperar"
│
│────────────────────── WATERMARK_LOW ← "acorde o kswapd!"
│
baixa │────────────────────── WATERMARK_MIN ← "emergência: direct reclaim"
│
0 └────────────────────── abaixo do min → risco de OOM
- Acima de high: memória confortável, nada a fazer.
- Cruzou low (descendo): o kernel acorda o kswapd para recuperar memória em background.
- Cruzou min: situação crítica — a alocação força direct reclaim (a própria thread que pede memória precisa recuperá-la antes de prosseguir).
# Ver as watermarks de cada zona (em páginas)
cat /proc/zoneinfo | grep -E 'Node|zone|min|low|high|free'
# vm.min_free_kbytes controla o WATERMARK_MIN; aumentá-lo dá mais "folga"
# ao kernel (útil em máquinas com rajadas de alocação), ao custo de RAM reservada.
sysctl vm.min_free_kbytes
kswapd: o reclaim em background
O kswapd é uma thread de kernel (uma por nó NUMA) cuja função é recuperar memória proativamente, antes que falte. Quando a memória livre cai abaixo da watermark low, o kswapd acorda e começa a liberar páginas até voltar acima da watermark high.
O que ele recupera, em ordem de preferência:
- Páginas de page cache limpas (file-backed, não modificadas) — as mais baratas: basta descartar (Parte VI).
- Páginas de page cache sujas — precisam de write-back antes.
- Páginas anônimas — precisam ir para o swap (se houver).
- Slab reclaimable — via shrinkers (Parte VI).
Como o kswapd trabalha em background, idealmente ele mantém a memória saudável sem que as aplicações percebam. Um kswapd trabalhando muito (visível como consumo de CPU dele em top) é sinal de pressão de memória sustentada.
Direct reclaim: quando não dá para esperar
Se a alocação de memória é tão rápida que o kswapd não consegue acompanhar, e a memória livre bate na watermark min, o kernel recorre ao direct reclaim: a própria thread da aplicação que pediu memória é forçada a parar e recuperar páginas ela mesma, ali, sincronamente, antes de a alocação retornar.
Isso é péssimo para latência. Do ponto de vista da sua aplicação, uma alocação trivial (malloc, criar um objeto) de repente trava por milissegundos ou mais, enquanto a thread faz o trabalho sujo de liberar memória — possivelmente esperando I/O de swap ou write-back.
Alocação normal: app pede memória → kernel entrega → segue (µs)
Direct reclaim: app pede memória → SEM frames livres →
app é sequestrada para liberar páginas →
espera write-back/swap → só então recebe (ms+)
Picos de latência de cauda (p99) inexplicáveis em produção são, com frequência, direct reclaim acontecendo. A métrica PSI (adiante) foi criada justamente para tornar isso visível.
As LRU lists do Linux
Como o kernel decide quais páginas recuperar? Aqui a teoria da Parte V vira prática. O Linux mantém, por nó/zona (e por cgroup), um conjunto de listas LRU:
┌─────────────────┐
file-backed: │ active_file │ páginas de arquivo "quentes"
├─────────────────┤
│ inactive_file │ candidatas a despejo (frias)
└─────────────────┘
┌─────────────────┐
anônimas: │ active_anon │ heap/stack "quentes"
├─────────────────┤
│ inactive_anon │ candidatas a swap
└─────────────────┘
Páginas nascem na lista inactive. Se forem referenciadas de novo (o bit Accessed, Parte II), são promovidas para active. Sob pressão, o kernel recupera da ponta da lista inactive — as páginas frias — mantendo as active. É o algoritmo do relógio / LRU aproximado da Parte V, refinado (e agora, com MGLRU, ainda mais preciso — Parte VI).
A separação file vs anon é deliberada: recuperar uma página de arquivo limpa é barato (só descartar); recuperar uma anônima exige swap. O vm.swappiness controla o equilíbrio entre as duas.
Refault distance e proteção do working set
Um problema clássico do LRU simples: uma varredura única de muitos arquivos (um grep gigante, um backup) pode inundar o inactive_file e expulsar o working set real do cache. O Linux moderno rastreia a refault distance — se uma página descartada é logo pedida de volta (refault), o kernel aprende que ela era importante e protege o working set de ser expulso por acessos transitórios. É a formalização da Parte V ("proteger o working set") implementada de forma adaptativa.
vm.swappiness
O parâmetro mais famoso (e mais mal-entendido) do tuning de memória. O vm.swappiness (0 a 200; padrão geralmente 60) define o quanto o kernel prefere recuperar memória fazendo swap de páginas anônimas versus descartar page cache:
- swappiness baixo (ex.: 10): "evite swap, prefira descartar cache". Bom quando o page cache é descartável e você quer manter a heap na RAM.
- swappiness alto (ex.: 100): "não hesite em fazer swap de páginas anônimas frias". Pode liberar RAM para mais cache.
sysctl vm.swappiness
# Ajuste temporário:
sudo sysctl -w vm.swappiness=10
[!NOTE]
Não existe valor "correto" universal. Em bancos de dados que gerenciam o próprio cache, swappiness baixo costuma ajudar. Em servidores de arquivos, cache é rei. Em containers commemory.max, o comportamento muda (Parte VIII). E, com SSD/NVMe (Parte III), o swap não é mais o vilão que era nos tempos de HDD — swappiness moderado pode ser saudável.
PSI: Pressure Stall Information
Antes do PSI, medir pressão de memória era arte adivinhatória: free engana (Parte VI), e alta atividade de swap pode ser normal ou catastrófica. O PSI (Pressure Stall Information), introduzido no Kernel 4.20, mudou o jogo.
O PSI mede quanto tempo as tarefas ficaram paradas (stalled) esperando por um recurso — exatamente o que dói. Para memória:
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.15 avg300=0.05 total=12345678
# full avg10=0.00 avg60=0.08 avg300=0.02 total=6789012
- some: % do tempo em que pelo menos uma tarefa ficou parada esperando memória (reclaim, swap, refault).
-
full: % do tempo em que todas as tarefas não-ociosas ficaram paradas simultaneamente — o sistema inteiro travado por memória.
fullsubindo é o sinal mais claro e precoce de thrashing (Parte V).
Diferente de olhar swap in/out cru, o PSI mede o impacto real na execução. É a métrica moderna para alertas: em vez de "swap > X", monitore "memory pressure full avg60 > Y%". Ferramentas como o systemd-oomd e o oomd do Facebook usam PSI para agir antes do OOM Killer clássico — matando algo de forma controlada quando a pressão passa de um limiar, evitando o congelamento total.
# Pressão de memória por cgroup (essencial em containers — Parte VIII)
cat /sys/fs/cgroup/<meu-servico>/memory.pressure
O famoso OOMKiller
Chegamos ao mais temido. Quando todo o reclaim falha — não há mais page cache para descartar, nem swap para páginas anônimas, e a memória continua insuficiente — o kernel toma a decisão nuclear: invocar o OOM Killer (Out-Of-Memory Killer) para matar um processo e liberar memória, evitando o congelamento total do sistema.
Lembra do overcommit (Parte III)? O kernel prometeu mais memória do que tem, apostando (via zero-fill on demand e COW) que nem todos escreveriam tudo. O OOM Killer é o que acontece quando essa aposta dá errado e todos cobram ao mesmo tempo.
Como a vítima é escolhida
O OOM Killer não escolhe ao acaso — ele tenta matar o processo que libera mais memória causando o menor dano. Cada processo tem um oom_score, derivado principalmente do seu consumo de memória (RSS + swap + page tables), ajustado por oom_score_adj:
# Score atual de um processo (0 a 1000; maior = mais provável de ser morto)
cat /proc/<PID>/oom_score
# O ajuste que você controla (-1000 a +1000)
cat /proc/<PID>/oom_score_adj
Grosso modo: quanto mais memória um processo usa, maior seu score, mais provável ser a vítima. Isso costuma acertar (o maior consumidor geralmente é o culpado), mas nem sempre — às vezes mata justamente seu processo principal, o maior da máquina.
Ajustando o oom_score_adj
Você pode influenciar a decisão. O oom_score_adj vai de -1000 (nunca mate, imune) a +1000 (mate primeiro):
# Proteger um processo crítico (ex.: o banco de dados) de ser morto
echo -900 > /proc/<PID_do_banco>/oom_score_adj
# Marcar um processo "sacrificável" (ex.: um cache reconstruível) para morrer primeiro
echo 800 > /proc/<PID_do_cache>/oom_score_adj
Isso é útil para proteger o processo essencial e direcionar o OOM Killer a matar algo reconstruível. Quando o OOM dispara, o kernel registra tudo no log:
dmesg | grep -i 'killed process'
# Out of memory: Killed process 12345 (python) total-vm:8000000kB,
# anon-rss:7500000kB, file-rss:0kB, ... score 850
journalctl -k | grep -i oom
OOM em cgroups
Em containers, o OOM mais comum não é o global — é o OOM do cgroup, disparado quando o grupo estoura seu memory.max (Parte VIII). O kernel mata um processo dentro daquele cgroup, sem afetar o resto do host. É o clássico "meu pod reiniciou com OOMKilled no Kubernetes":
# Contadores de eventos de OOM do cgroup
cat /sys/fs/cgroup/<meu-pod>/memory.events
# low 0
# high 152 ← quantas vezes bateu memory.high (throttling)
# max 8 ← quantas vezes bateu memory.max
# oom 2 ← quantas vezes o OOM do cgroup disparou
# oom_kill 2 ← quantos processos foram mortos
Kernels 5.x melhoraram o OOM Killer para evitar thrashing prolongado: em vez de deixar o sistema agonizar minutos perto do limite (tudo em reclaim, nada progredindo), ele age mais decisivamente. E, com PSI + systemd-oomd/oomd, é possível agir em user space antes do OOM do kernel, matando de forma mais graciosa e previsível.
[!TIP]
No Kubernetes, defina requests e limits de memória conscientemente e configureGOMEMLIMIT(Go),HeapHardLimitPercent(.NET) ou reciclagem de workers (Python) abaixo domemory.max. Assim seu runtime faz GC/limpeza antes de o cgroup estourar, transformando um OOMKill abrupto numa degradação controlada. Detalhamos isso na Parte VIII.
Continua na Parte VIII — a última do Artigo 3 —, onde amarramos toda a teoria de controle de memória com os cgroups v2 (memory.max/high/low, accounting, memory.pressure) e mergulhamos na conexão com Python, .NET e Go: page cache em ETL, MemoryMappedFile, mmap em Go, tuning de containers, diagnóstico e estudos de caso reais.
:D
Conteúdo parcialmente gerado com auxílio de IA generativa (me ajudou a organizar tudo isso kkkk)
Top comments (0)