Memory Leak no PHP-FPM e Jobs
4 min de leitura #laravel
Trazido do LK: https://lnkd.in/p/dPfZpAQn
Quando trabalhamos com Laravel Queues em produção é bem comum usar o Supervisor para manter os workers sempre ativos. Para evitar o custo de bootstrap a cada job executado, principalmente quando o servidor é cobrado por processamento, acabamos configurando os workers para rodar de forma contínua. O problema é que isso cria um cenário onde o memory leak pode aparecer.
O modo daemon faz os workers ficarem em cache, evitando o custo de inicialização a cada execução. Só que o PHP não foi feito para rodar para sempre, ele foi pensado para funcionar de forma cíclica. Dá pra perceber isso no comportamento do garbage collector, que não consegue limpar algumas referências que ficam abertas no cache. É aí que a memória começa a acumular e nasce o memory leak.
Contexto Técnico
- PHP-FPM: Gerencia processos PHP.
- Laravel Queues: No modo
queue:work --daemon. - SO: Ubuntu.
Como o Memory Leak Acontece
- Container Laravel permanece vivo. Providers, singletons e caches continuam carregados em memória.
- Jobs carregam dados pesados. Processamentos de collections, Eloquent, PDF, imagens ou grandes arrays não são totalmente liberados após o
->handle(). - Acúmulo invisível. O garbage collector do PHP não é agressivo o suficiente devido as referências de memória permanecem abertas no cache. Isso faz com que a memória do processo não seja liberada de volta para o sistema operacional.
Resultado: crescimento linear ou até exponencial no uso de RAM até causar swap no sistema, morte de processos pelo OOM-Killer e lentidão geral no servidor.
Sinais de problema:
- Crescimento de memória estável e contínuo mesmo sem jobs em fila.
- Quedas do worker após algumas horas/dias de execução.
- Jobs mais lentos após muitas horas.
Ferramentas úteis:
- htop para monitorar consumo de RAM por processo. Podemos acompanhar a VRAM alocada também.
- Logs do Supervisor para ver se há reinicializações inesperadas.
- Logs do Nginx de slow-log, útil para identificar requests afetadas pela lentidão do sistema.
- Datadog / NewRelic / Grafana para gráficos históricos.
Boas Práticas para Mitigação
- Configurar
--max-jobsou--max-time. Laravel permite matar o worker após X jobs processados ou Y segundos em execução:
php artisan queue:work --max-jobs=500 --max-time=3600
- Ajustar
pm.max_requestsno PHP-FPM. Faz o FPM matar e recriar os processos após um número de requisições, forçando liberação de memória. - Reinícios periódicos, com o
queue:restartpodemos solicitar ao supervisor o encerramento dos jobs após a execução da reserva atual, garantindo que não perderemos execuções.
0 */3 * * * cd /www/html/laravel-project && /usr/bin/php<version> artisan queue:restart >> /home/forge/.forge/queue-restart.log 2>&1
- Monitoramento ativo. Configure alertas para quando um worker ultrapassar determinado uso de memória.
- Profiling de Jobs. Medir e salvar em log o tempo e memória das execuções.
- Evite guardar objetos pesados em singletons ou caches desnecessários.
Ciclo
sequenceDiagram
autonumber
participant S as Supervisor
participant W as Worker (queue:work --daemon)
participant P as PHP-FPM/Extensões
participant O as SO
S->>W: start
W->>W: Bootstrap do Laravel (container residente)
loop Processando fila
W->>W: Executa Job (carga alta de memória)
W->>P: Usa extensões (PDO/Redis/Imagick...)
P-->>W: Objetos/handles mantidos
W->>W: handle() finaliza
W->>O: GC tenta liberar
O-->>W: Memória do processo não retorna completamente
W->>W: Memória acumulada (+Δ)
end
W->>S: RAM alta / lentidão
alt Mitigação automática
S->>W: restart por --max-jobs/--max-time
P->>P: pm.max_requests recicla processos
else Manual/Agendada
W->>W: cron: artisan queue:restart (a cada 3h), encerra após o job atual
end
S->>W: start (memória "limpa")
Conclusão
O uso de queue:work –daemon com Supervisor é poderoso, mas precisa de um bom plano de ciclos de vida dos workers. Sem reinicializações programadas, você vai acumular memória e sofrer impactos de performance ou downtime.