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 oomUma 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:0Trê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 -10Proteger 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=-500Use 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=1700MA 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 agoraSwap 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/fstabE o swappiness, que controla a agressividade da troca:
sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.confO 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| Coluna | O que é | Preocupa? |
|---|---|---|
| used | Memória de processo, de fato | Sim, se perto do total |
| buff/cache | Cache de disco, liberável na hora | Não — é RAM trabalhando |
| available | O que uma aplicação nova conseguiria | Esta é 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/memorysome avg10=12.43 avg60=8.91 avg300=3.02 total=98234123
full avg10=2.11 avg60=1.03 avg300=0.44 total=12093442Os 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/ioVale 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_buffersno 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.