Como diagnosticar e corrigir vazamento de memória no PHP-FPM em servidores Linux

Por · Publicado em · Atualizado em · 8 min de leitura

Carrinho com monitor e teclado em frente a fileira de racks de servidores em data center
Console com monitor e teclado junto a racks, ilustrando infraestrutura de servidores. Foto: Brett Sayles / Pexels

Se seus workers do PHP-FPM crescem sem parar até o OOM Killer agir, você pode estar diante de vazamento de memória no PHP-FPM ou fragmentação/opcache. Veja como confirmar a causa em produção, reproduzir em staging com segurança e aplicar tuning e mitigações que estabilizam o ambiente enquanto a correção definitiva é implementada.

O problema: OOM Killer, travamentos e 502/503

Vazamentos de memória no PHP-FPM geralmente aparecem como aumento contínuo de RSS por worker, swap alta, lentidão e 502/503 sob carga. Antes de tudo, identifique o padrão do incidente e isole se é por excesso de concorrência (dimensionamento) ou retenção/fragmentação de memória por processo.

Use os sinais abaixo como pista inicial para decidir o próximo passo de diagnóstico.

Sintoma Hipótese provável Próximo passo
OOM Killer mata php-fpm pm.max_children alto ou leak por worker Medir RSS por worker; ajustar pm e memory_limit; ativar rotação (pm.max_requests)
Crescimento contínuo de RSS dos workers Fragmentação/opcache ou objetos não liberados/extensão C Inspecionar /proc/PID/smaps_rollup e pmap -x; observar Private_Dirty
Swap alta e latência Falta de RAM efetiva ou dimensionamento do pool Rever max_children; considerar reduzir concorrência/otimizar endpoints críticos
Erros 502/503 sob pico Fila do FPM esgota/timeout por saturação Ativar status_path; medir fila; correlacionar com rotas mais pesadas

Causas prováveis de vazamento de memória no PHP-FPM

Em PHP “puro”, o garbage collector limpa a maior parte dos objetos ao fim de cada requisição. Crescimento anormal de memória por worker tende a vir de:

Nem todo crescimento é vazamento: requisições que processam arquivos grandes, imagens ou grandes arrays podem elevar picos de memória sem retenção. Diferencie pico (VmHWM alto) de retenção (VmRSS sobe e não volta).

  • Extensões em C (imagem, XML, criptografia) que não liberam buffers sob certos caminhos de código.
  • Fragmentação de heap e do OpCache: muitas classes/arquivos geram fragmentação e retenção, mesmo sem “vazar” logicamente.
  • Caches estáticos/singletons que crescem com o catálogo/rotas acessadas.
  • Ciclos de referência e objetos ligados a resources externos que o GC não coleta bem.
  • Workers muito longevos sem reciclagem (pm.max_requests muito alto ou 0).

Diagnóstico rápido em produção com ps, top e /proc

Objetivo: confirmar se o problema está em poucos workers ou no pool inteiro, e se há retenção entre requisições. Foque em métricas baratas e sem downtime. Evite mudanças invasivas durante o expediente.

Correlacione picos com rotas/tenants usando logs estruturados. Se ainda não tem, veja o guia de logs estruturados e correlação.

# Visão rápida de consumo de memória
free -h && swapon --summary

# Quais processos consomem mais RSS (memória residente)
ps -o pid,ppid,cmd,%mem,rss --sort=-rss -C php-fpm | head

# Inspecione um worker específico ($PID)
PID=<um_pid_de_worker>
cat /proc/$PID/status | egrep 'VmRSS|VmHWM|Threads'

# Resumo de mapas de memória do processo
cat /proc/$PID/smaps_rollup | egrep 'Rss|Pss|Private'

Mapeando alocações: pmap e análise de smaps

Se um worker cresce após cada requisição, verifique onde está a memória (anônima vs mapeada, privada vs compartilhada). Procure grandes áreas Private_Dirty (heap) e crescimento contínuo de anon RSS.

Combine pmap (sumário) com smaps_rollup (total por tipo). Faça isso em um único worker sob baixa carga.

PID=<um_pid_de_worker>

# Top 20 regiões por RSS no processo
pmap -x $PID | sort -k3 -n | tail -n 20

# Resumo consolidado (verifique Private_* e Anonymous)
cat /proc/$PID/smaps_rollup | egrep 'Anonymous|Shared|Private|Rss|Pss'

Observando syscalls de memória com strace (mmap/brk)

Quando o RSS cresce, mas você quer confirmar atividade de alocação, observe syscalls de memória. strace adiciona overhead — use em um único worker e por tempo curto, preferencialmente fora do horário de pico.

PID=<um_pid_de_worker>

# Observar syscalls de memória (mmap/brk/munmap/mremap)
strace -p $PID -e trace=brk,mmap,munmap,mremap -f -tt -o /tmp/trace.$PID.log &
# Gere algumas requisições representativas... depois
sleep 10; kill %+

# Consulte o log em busca de alocações que não recebem munmap correspondente
awk '/mmap\(/ {m++} /munmap\(/ {u++} END {print "mmap:",m, "munmap:",u}' /tmp/trace.$PID.log

Reprodução em staging com Valgrind e Xdebug

Idealmente, reproduza o caso fora de produção com um script que invoca o endpoint problemático muitas vezes. Valgrind Massif identifica picos e crescimento do heap. Xdebug em modo trace mostra picos por função. Não rode Valgrind nos seus workers de produção.

# 1) Script reprodutor simples (CLI)
cat > repro.php <<'PHP'
<?php
function hit() {
  // Simule chamada ao código/endpoint alvo
  $data = file_get_contents('http://localhost/rota-pesada');
  return strlen($data);
}
for ($i = 0; $i < 500; $i++) {
  hit();
  if ($i % 10 === 0) {
    fprintf(STDERR, "it=%d rss=%d peak=%d\n", $i, memory_get_usage(), memory_get_peak_usage());
  }
}
PHP

# 2) Heap profiling com Valgrind Massif
valgrind --tool=massif --time-unit=B --massif-out-file=massif.out \
  php -d detect_leaks=1 repro.php
ms_print massif.out | head -n 100

# 3) Trace de memória por função com Xdebug (em CLI)
php -d xdebug.mode=trace -d xdebug.start_with_request=yes \
    -d xdebug.output_dir=. repro.php
ls -1 cachegrind* trace*

Tuning e mitigação imediata no PHP-FPM e PHP

Mitigue o risco de OOM enquanto investiga: recicle workers, limite picos e ajuste o tamanho do pool. Documente as mudanças e meça impacto.

Dimensionamento prático: max_children ≈ (RAM disponível ao FPM) / (RSS médio por worker sob carga). Meça RSS após aquecimento e tráfego representativo.

Revise também o OpCache: fragmentação pode amplificar uso de memória. Veja parâmetros comuns abaixo e a documentação do PHP-FPM, do memory_limit e do OpCache.

  • Ative reciclagem: pm.max_requests entre algumas centenas e poucos milhares, conforme carga e latência aceitável.
  • Aperte memory_limit no pool (php_admin_value) para conter outliers; ajuste por rota/job pesado.
  • Se o backlog do FPM cresce, reduza max_children e aumente capacidade por request (otimize código) em vez de só aumentar concorrência.
  • Monitore fila e latência após cada mudança; se houver regressão, rever configuração.
; /etc/php-fpm.d/www.conf (exemplos)
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 1000
pm.process_idle_timeout = 10s
request_terminate_timeout = 60s
php_admin_value[memory_limit] = 256M

; php.ini / opcache
opcache.enable=1
opcache.validate_timestamps=1
opcache.memory_consumption=256
opcache.max_accelerated_files=100000
opcache.interned_strings_buffer=16
opcache.jit_buffer_size=0  ; se JIT causar pressão de memória

Boas práticas e monitoramento contínuo

- Registre memory_get_peak_usage por rota e correle com request-id. Veja como em logs estruturados.

- Acompanhe RSS/Private_Dirty por worker e crie alertas antes do OOM.

- Adote APM para detectar endpoints que elevam alocações; compare opções em observabilidade para PHP.

- Adicione testes de regressão de memória no pipeline (CLI que roda o caso por N iterações). Base para isso em CI/CD com PHP.

- Em legados, isole “hot paths” com testes; veja ideias em PHP legado: testes.

Plano de ação sugerido em 7 passos

1) Confirme impacto: RSS por worker, swap e fila do FPM. 2) Identifique se o crescimento é global ou por rota. 3) Colete smaps_rollup/pmap de um worker-alvo. 4) Ative pm.max_requests para estabilizar. 5) Reproduza em staging e rode Valgrind/Xdebug. 6) Aplique correções no código/extensão/config. 7) Monitore pós-mudança por alguns ciclos de tráfego.

Perguntas frequentes

Como diferenciar vazamento real de pico legítimo de memória?

Vazamento real aparece como crescimento do VmRSS que não retorna ao patamar após a requisição, ciclo após ciclo. Picos legítimos elevam o VmHWM (máximo histórico), mas o VmRSS cai ao fim. Compare smaps_rollup, colete métricas entre requisições e observe se Private_Dirty cresce de forma monotônica.

pm.max_requests resolve vazamento?

Não resolve a causa, mas reciclar workers limita o tempo de vida de um processo “vazando” ou com fragmentação, reduzindo risco de OOM. Escolha um valor que balanceie estabilidade e latência de cold start. Monitore após ajustar para evitar flutuações no tempo de resposta.

OpCache pode causar aumento de memória?

Sim. Fragmentação e crescimento do cache de scripts e strings internadas elevam RSS, principalmente em aplicações com muitos arquivos/classes. Ajuste opcache.memory_consumption e max_accelerated_files, e recicle workers periodicamente. Analise também se deploys frequentes sem reinício do FPM mantêm restos no cache.

É seguro usar strace e valgrind em produção?

strace adiciona overhead; use apenas em um worker e por tempo curto, fora de picos. Valgrind não é indicado em produção: o overhead é grande e pode derrubar o serviço. Reproduza em staging com tráfego sintético para obter perfis do heap e traces de função.

Como estimar pm.max_children com pouca folga de RAM?

Meça RSS médio por worker sob carga representativa (após aquecimento). Subtraia do total a RAM de sistema e de outros serviços. Divida a RAM restante pelo RSS médio e aplique margem de segurança. Se o valor ficar baixo, priorize otimizações por requisição em vez de elevar concorrência.

Quer estabilizar o PHP-FPM e evitar OOM?

Converse sobre um plano de diagnóstico e mitigação seguro para seu ambiente: identificar a causa raiz, ajustar FPM e monitorar sem interromper o serviço.

Falar sobre diagnóstico PHP-FPM

Veja também