DEV Community

Alex Volnei Galante
Alex Volnei Galante

Posted on

Kernel Linux para Desenvolvedores Backend - Gerenciamento de Memória Parte VII

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

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Páginas de page cache limpas (file-backed, não modificadas) — as mais baratas: basta descartar (Parte VI).
  2. Páginas de page cache sujas — precisam de write-back antes.
  3. Páginas anônimas — precisam ir para o swap (se houver).
  4. 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+)
Enter fullscreen mode Exit fullscreen mode

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
                 └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

[!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 com memory.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
Enter fullscreen mode Exit fullscreen mode
  • 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. full subindo é 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 configure GOMEMLIMIT (Go), HeapHardLimitPercent (.NET) ou reciclagem de workers (Python) abaixo do memory.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)

Referências Bibliográficas

Top comments (0)