Vitor Bellini
Todos os posts

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-jobs ou --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_requests no 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:restart podemos 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.

1 curtida