← Voltar para o Blog

Seu processo morreu sem log: entendendo o OOM killer

O serviço sumiu, o log da aplicação não registra nada e o systemd diz que ele terminou. Quando a memória acaba, quem mata o processo é o kernel — e ele deixa rastro em outro lugar.

Equipe EasyOps Cloud · · 7 min de leitura

O serviço estava no ar. Agora não está. O log da aplicação termina no meio de uma requisição comum, sem exceção, sem stack trace, sem despedida. O systemctl status informa que o processo terminou e o journalctl da unidade não explica nada.

Quando a aplicação some sem deixar recado, a hipótese mais provável é que ela não tenha decidido morrer: alguém a matou. Em servidor Linux, esse alguém quase sempre é o kernel — e ele registra o que fez em um lugar diferente do que você está olhando.

Confirmar antes de teorizar

O OOM killer escreve no buffer do kernel, não no log da sua unidade:

sudo dmesg -T | grep -i -E 'killed process|out of memory|oom'
sudo journalctl -k --since '2 hours ago' | grep -i oom

Uma ocorrência real tem esta cara:

Out of memory: Killed process 21847 (node) total-vm:4210332kB,
anon-rss:3894112kB, file-rss:0kB, shmem-rss:0kB, UID:1001
pgtables:8192kB oom_score_adj:0

Três números explicam o episódio. O anon-rss é a memória real que o processo segurava no momento da morte — 3,8 GB no exemplo. O total-vm é o que ele havia reservado, quase sempre bem maior, e não deve assustar: reservar não é usar. O oom_score_adj é o ajuste de prioridade, e voltaremos a ele.

Se não houver nenhuma linha de OOM, a causa é outra e vale seguir pelo diagnóstico de serviço que não sobe — exit code e sinal contam histórias diferentes.

Por que o kernel mata em vez de recusar

O Linux entrega memória de forma otimista. Quando um processo pede 2 GB, o kernel concede mesmo que não existam 2 GB livres, apostando que boa parte nunca será tocada de fato. Essa aposta se chama *overcommit* e costuma dar certo: programas reservam muito mais do que usam.

O problema aparece quando a aposta falha. Se os processos resolvem usar de verdade o que reservaram, não há como recusar retroativamente uma memória já concedida. O kernel então escolhe uma vítima e a elimina para salvar o resto do sistema.

A escolha não é aleatória. Cada processo tem um oom_score, calculado principalmente pelo quanto de memória ele consome. O maior consumidor costuma ser o escolhido — o que é razoável para o sistema e irritante para você, porque o maior consumidor geralmente é justamente a sua aplicação principal, não o processo que causou o pico.

# Quem está mais perto de ser escolhido agora
for p in /proc/[0-9]*; do
  printf '%s %s %s\n' "$(cat $p/oom_score 2>/dev/null)" \
    "${p#/proc/}" "$(cat $p/comm 2>/dev/null)"
done | sort -rn | head -10

Proteger o processo certo

Dá para influenciar a escolha. O oom_score_adj vai de -1000 (praticamente imune) a 1000 (candidato preferencial):

# /etc/systemd/system/meuapp.service
[Service]
OOMScoreAdjust=-500

Use com parcimônia. Proteger o banco de dados e deixar o worker de fila como candidato natural é uma decisão sensata — perder um job que será reprocessado custa menos que perder o Postgres no meio de uma transação. Proteger tudo, por outro lado, apenas transfere a morte para o processo errado, e no limite trava a máquina inteira em vez de sacrificar uma parte.

O caminho mais previsível é declarar o teto por serviço, com cgroup v2:

[Service]
MemoryMax=2G
MemoryHigh=1700M

A diferença entre os dois é importante. MemoryHigh é um freio: ao ultrapassar, o kernel pressiona o processo a devolver memória, com perda de desempenho mas sem matar. MemoryMax é a parede: ao encostar, o processo é morto — só que dentro do próprio cgroup, sem colocar em risco o restante da máquina.

Com isso, um vazamento na sua aplicação derruba a sua aplicação, e não o banco que por acaso estava usando mais memória.

Swap: o mal-entendido mais comum

Circula a ideia de que servidor não deve ter swap, porque swap é lento. A premissa é certa e a conclusão é errada.

Sem swap, o kernel só tem uma saída ao ficar sem memória: matar. Com swap, ele consegue mover para o disco páginas que ninguém acessa há horas — código de inicialização, biblioteca carregada e nunca mais usada — e recuperar RAM de graça, em termos práticos.

O que degrada desempenho não é ter swap, é usar swap ativamente. Distinga os dois:

free -h                          # quanto de swap está ocupado
vmstat 1 5                       # colunas si/so: troca acontecendo agora

Swap com 800 MB ocupados e si/so zerados é o cenário saudável: são páginas frias estacionadas, e a RAM que elas liberaram está servindo ao cache de disco. Swap com si/so diferentes de zero de forma contínua é *thrashing* — aí sim o sistema está sofrendo, e a resposta é mais RAM, não menos swap.

# 2 GB de swap em arquivo, sem reparticionar
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

E o swappiness, que controla a agressividade da troca:

sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf

O padrão 60 é pensado para desktop. Em servidor, 10 mantém o swap disponível como rede de segurança sem que o kernel o use por preferência.

Ler a memória sem se enganar

O free -h engana quem lê a coluna errada. Cache de disco aparece como memória usada, mas é liberado na hora em que alguém precisar:

free -h
ColunaO que éPreocupa?
usedMemória de processo, de fatoSim, se perto do total
buff/cacheCache de disco, liberável na horaNão — é RAM trabalhando
availableO que uma aplicação nova conseguiriaEsta é a coluna que importa

available baixo com buff/cache alto é normal e saudável. available baixo com buff/cache também baixo é a antessala do OOM.

Para acompanhar ao longo do tempo em vez de olhar no susto, o gráfico de memória do painel mostra a curva — e vazamento tem assinatura inconfundível: sobe em linha reta e só volta ao zero quando o processo reinicia.

O indicador que avisa antes de matar

O OOM é o fim da linha. Antes dele, o sistema passa um bom tempo sob pressão — e existe uma métrica feita exatamente para enxergar isso, disponível em qualquer kernel recente:

cat /proc/pressure/memory
some avg10=12.43 avg60=8.91 avg300=3.02 total=98234123
full avg10=2.11 avg60=1.03 avg300=0.44 total=12093442

Os números são a porcentagem de tempo em que houve espera por memória. A linha some indica que ao menos um processo ficou parado esperando; full indica que todos ficaram — o sistema inteiro travado, ainda que por milissegundos.

A leitura prática é direta: some avg60 acima de 10% significa que o servidor já está gastando tempo real remanejando memória em vez de trabalhar. full acima de zero de forma sustentada é o aviso de que o OOM está próximo.

Isso é bem mais útil que olhar free -h, porque captura o esforço e não só o saldo. Uma máquina pode mostrar memória disponível e ainda assim estar sofrendo, se estiver descartando e recarregando cache o tempo todo.

O mesmo vale para CPU e disco:

cat /proc/pressure/cpu /proc/pressure/io

Vale registrar esses valores junto com as métricas que você já acompanha. Quando a pergunta for "a máquina está pequena ou o código está ruim?", a série histórica de pressão responde com muito mais precisão do que a média de uso de RAM.

Quando o problema é a máquina, não o código

Nem todo OOM é bug. Às vezes a aplicação simplesmente cresceu, e três sinais apontam para isso: o consumo é estável entre reinícios, sobe junto com o tráfego, e o pico coincide com o horário de maior uso.

Antes de aumentar a máquina, vale colher o ganho fácil, que costuma estar em configuração e não em hardware:

  • Banco com cache mal dimensionado. Um Postgres com shared_buffers no padrão usa pouco; com valor exagerado, disputa RAM com a aplicação. Um quarto da memória total é o ponto de partida usual.
  • Worker demais. workers = 2 × núcleos + 1 é regra de bolso para processo leve. Aplicação que carrega modelo ou cache grande em memória multiplica esse peso por worker.
  • Ausência de limite por serviço. Sem MemoryMax, um vazamento consome tudo que existir, independentemente do tamanho da máquina.

Se depois disso o consumo legítimo ainda encosta no teto, o caminho é ajustar o tamanho da VPS. O upgrade de CPU e RAM é aplicado em janela curta de manutenção, e o guia de dimensionamento ajuda a escolher o próximo degrau com base no perfil de uso — em vez de dobrar a máquina no chute e descobrir no fim do mês que o problema era shared_buffers.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta