Diagnosticando Vazamentos de Memória - Tamanho de memória permitido de # bytes esgotados

98

Encontrei a temida mensagem de erro, possivelmente devido a um esforço meticuloso, o PHP ficou sem memória:

Tamanho de memória permitido de #### bytes exauridos (tentou alocar #### bytes) em file.php na linha 123

Aumentando o limite

Se você sabe o que está fazendo e deseja aumentar o limite, consulte memory_limit :

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

Cuidado! Você pode estar resolvendo apenas o sintoma e não o problema!

Diagnosticando o vazamento:

A mensagem de erro aponta para uma linha dentro de um loop que acredito estar vazando, ou acumulando desnecessariamente, memória. Imprimi memory_get_usage()declarações no final de cada iteração e posso ver o número crescer lentamente até atingir o limite:

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

Para o propósito desta pergunta, vamos supor que o pior código espaguete imaginável está escondido no escopo global em algum lugar em $userou Task.

Quais ferramentas, truques de PHP ou vodu de depuração podem me ajudar a encontrar e corrigir o problema?

Mike B
fonte
PS - Recentemente tive um problema com esse tipo exato de coisa. Infelizmente, também descobri que o php tem um problema de destruição de objeto filho. Se você remover a definição de um objeto pai, seus objetos filho não serão liberados. Ter que ter certeza de usar um unset modificado que inclui uma chamada recursiva para todos os objetos filho __destruct e assim por diante. Detalhes aqui: paul-m-jones.com/archives/262 :: Estou fazendo algo como: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ item -> __ destruct (); } não definido ($ item); }
Josh

Respostas:

48

PHP não tem um coletor de lixo. Ele usa a contagem de referência para gerenciar a memória. Portanto, a fonte mais comum de vazamentos de memória são referências cíclicas e variáveis ​​globais. Se você usar uma estrutura, terá muito código para pesquisar para encontrá-la, infelizmente. O instrumento mais simples é colocar chamadas seletivamente para memory_get_usagee reduzi-lo para onde os vazamentos de código. Você também pode usar o xdebug para criar um rastreamento do código. Execute o código com rastreios de execução e show_mem_delta.

Troelskn
fonte
3
Mas cuidado ... os arquivos de rastreamento gerados serão ENORMES. A primeira vez que executei um rastreamento xdebug em um aplicativo Zend Framework, demorou muuuuito tempo para ser executado e gerou um arquivo de tamanho de vários GB (não kb ou MB ... GB). Esteja ciente disso.
rg88
1
Sim, é muito pesado .. GB soa um pouco demais - a menos que você tenha um script grande. Tente processar apenas algumas linhas (deve ser o suficiente para identificar o vazamento). Além disso, não instale a extensão xdebug no servidor de produção.
troelskn
31
Desde 5.3 o PHP na verdade tem um coletor de lixo. Por outro lado, a função de criação de perfil de memória foi removida de xdebug :(
wdev
3
1 encontrou o vazamento! Uma aula que tinha referências cíclicas! Uma vez que essas referências foram desativadas (), os objetos foram coletados como lixo como esperado! Obrigado! :)
rinogo
@rinogo então como você soube do vazamento? Você pode compartilhar quais etapas você deu?
JohnnyQ
11

Aqui está um truque que usamos para identificar quais scripts estão usando mais memória em nosso servidor.

Salve o seguinte snippet em um arquivo em, por exemplo /usr/local/lib/php/strangecode_log_memory_usage.inc.php:

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

Use-o adicionando o seguinte ao httpd.conf:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

Em seguida, analise o arquivo de log em /var/log/httpd/php_memory_log

Pode ser necessário touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logantes que o usuário da web possa gravar no arquivo de log.

Quinn Comendant
fonte
8

Percebi uma vez em um script antigo que o PHP mantinha a variável "as" como no escopo, mesmo após meu loop foreach. Por exemplo,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

Não tenho certeza se as versões futuras do PHP corrigiram isso ou não desde que eu vi. Se for esse o caso, você pode unset($user)depois da doSomething()linha para apagá-la da memória. YMMV.

patcoll
fonte
13
PHP não faz escopo de loops / condicionais como C / Java / etc. Qualquer coisa declarada dentro de um loop / condicional ainda está no escopo, mesmo depois de sair do loop / condicional (por design [?]). Métodos / funções, por outro lado, têm o escopo definido como você esperaria - tudo é liberado assim que a execução da função termina.
Frank Farmer
Presumi que fosse intencional. Uma vantagem disso é que, após um loop, você pode trabalhar com o último item encontrado, por exemplo, que satisfaça critérios específicos.
joachim de
Você poderia fazer unset()isso, mas tenha em mente que, para objetos, tudo o que você está fazendo é mudar para onde sua variável está apontando - você não a removeu realmente da memória. O PHP irá liberar automaticamente a memória assim que estiver fora do escopo de qualquer maneira, então a melhor solução (em termos desta resposta, não a pergunta do OP) é ​​usar funções curtas para que eles não fiquem presos a essa variável do loop também longo.
Rich Court de
@patcoll Isso não tem nada a ver com vazamentos de memória. Isso é simplesmente a mudança do ponteiro do array. Dê uma olhada aqui: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html na versão 3a.
Harm Smits
7

Existem vários pontos possíveis de vazamento de memória no php:

  • o próprio php
  • extensão php
  • biblioteca php que você usa
  • seu código php

É muito difícil encontrar e corrigir os 3 primeiros sem um profundo conhecimento de engenharia reversa ou código-fonte php. Para o último, você pode usar a pesquisa binária para código de vazamento de memória com memory_get_usage

Kingoleg
fonte
91
Sua resposta é tão geral quanto poderia ter sido
TravisO
2
É uma pena que mesmo o php 7.2 eles não consigam consertar vazamentos de memória do núcleo do php. Você não pode executar processos de longa duração nele.
Aftab Naveed
6

Recentemente, encontrei esse problema em um aplicativo, em circunstâncias que considero semelhantes. Um script executado no cli do PHP que executa um loop em muitas iterações. Meu script depende de várias bibliotecas subjacentes. Suspeito que uma determinada biblioteca seja a causa e passei várias horas em vão tentando adicionar métodos de destruição apropriados às suas classes, mas sem sucesso. Diante de um longo processo de conversão para uma biblioteca diferente (que poderia ter os mesmos problemas), propus uma solução bruta para o problema no meu caso.

Na minha situação, em um Linux CLI, eu estava fazendo um loop em um monte de registros de usuário e para cada um deles criando uma nova instância de várias classes que criei. Decidi tentar criar as novas instâncias das classes usando o método exec do PHP para que esse processo fosse executado em um "novo thread". Aqui está um exemplo realmente básico do que estou me referindo:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

Obviamente, essa abordagem tem limitações, e é preciso estar ciente dos perigos disso, pois seria fácil criar um trabalho de coelho, no entanto, em alguns casos raros, pode ajudar a superar um ponto difícil, até que uma solução melhor pudesse ser encontrada , como no meu caso.

Nate Flink
fonte
6

Eu me deparei com o mesmo problema e minha solução foi substituir foreach por um for regular. Não tenho certeza sobre os detalhes, mas parece que foreach cria uma cópia (ou de alguma forma uma nova referência) para o objeto. Usando um loop for regular, você acessa o item diretamente.

Gunnar Lium
fonte
5

Eu sugiro que você verifique o manual do php ou adicione a gc_enable()função para coletar o lixo ... Ou seja, os vazamentos de memória não afetam a forma como o código é executado.

PS: php tem um coletor de lixo gc_enable()que não aceita argumentos.

Kosgei
fonte
3

Recentemente, notei que as funções lambda do PHP 5.3 deixam memória extra usada quando são removidas.

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

Não sei por que, mas parece que cada lambda ocupa 250 bytes extras, mesmo depois que a função é removida.

Xeoncross
fonte
2
Eu ia dizer o mesmo. Isso foi corrigido a partir de 5.3.10 ( # 60139 )
Kristopher Ives
@KristopherIves, obrigado pela atualização! Você está certo, isso não é mais um problema, então eu não deveria ter medo de usá-los como um louco agora.
Xeoncross
2

Se o que você diz sobre o PHP fazer apenas GC após uma função for verdadeiro, você pode envolver o conteúdo do loop dentro de uma função como uma solução alternativa / experimento.

Bart van Heukelom
fonte
1
@DavidKullmann Na verdade, acho que minha resposta está errada. Afinal, o run()que é chamado também é uma função, ao final da qual deve acontecer o CG.
Bart van Heukelom
2

Um grande problema que tive foi usando create_function . Como nas funções lambda, ele deixa o nome temporário gerado na memória.

Outra causa de vazamentos de memória (no caso do Zend Framework) é o Zend_Db_Profiler. Certifique-se de que está desabilitado se você executar scripts no Zend Framework. Por exemplo, eu tinha em meu application.ini o seguinte:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

Rodar aproximadamente 25.000 consultas + cargas de processamento antes disso, trouxe a memória para bons 128Mb (Meu limite máximo de memória).

Definindo apenas:

resources.db.profiler.enabled    = false

foi o suficiente para mantê-lo abaixo de 20 Mb

E este script estava rodando em CLI, mas estava instanciando o Zend_Application e rodando o Bootstrap, então ele usou a configuração "development".

Realmente ajudou a executar o script com a criação de perfil xDebug

Andy
fonte
2

Eu não vi isso explicitamente mencionado, mas xdebug faz um ótimo trabalho de perfil de tempo e memória (a partir de 2.6 ). Você pode pegar as informações que ele gera e passá-las para um front end gui de sua escolha: webgrind (somente tempo), kcachegrind , qcachegrind ou outros e gera árvores de chamadas e gráficos muito úteis para permitir que você encontre as fontes de seus vários problemas .

Exemplo (de qcachegrind): insira a descrição da imagem aqui

SeanDowney
fonte
1

Estou um pouco atrasado para esta conversa, mas compartilharei algo pertinente ao Zend Framework.

Tive um problema de vazamento de memória após instalar o php 5.3.8 (usando o phpfarm) para trabalhar com um aplicativo ZF que foi desenvolvido com o php 5.2.9. Descobri que o vazamento de memória estava sendo acionado no arquivo httpd.conf do Apache, na minha definição de host virtual, onde diz SetEnv APPLICATION_ENV "development". Depois de comentar esta linha, os vazamentos de memória pararam. Estou tentando criar uma solução alternativa embutida no meu script php (principalmente definindo-o manualmente no arquivo index.php principal).

Fronzee
fonte
1
A pergunta diz que ele está executando em CLI. Isso significa que o Apache não está envolvido no processo.
Máx.
1
@Maxime Bom argumento, não consegui perceber, obrigado. Bem, espero que algum Googler aleatório se beneficie da observação que deixei aqui de qualquer maneira, já que esta página surgiu para mim enquanto tentava resolver meu problema.
fronzee
Verifique minha resposta a esta pergunta, talvez esse seja o seu caso também.
Andy
Seu aplicativo deve ter configurações diferentes dependendo do ambiente. O "development"ambiente geralmente tem um monte de registros e perfis que outros ambientes podem não ter. Comentar a linha apenas faz seu aplicativo usar o ambiente padrão, que geralmente é "production"ou "prod". O vazamento de memória ainda existe; o código que o contém simplesmente não está sendo chamado nesse ambiente.
Marco Roy
0

Não vi isso mencionado aqui, mas uma coisa que pode ser útil é usar xdebug e xdebug_debug_zval ('variableName') para ver o refcount.

Também posso fornecer um exemplo de extensão php que atrapalha: Zend Server's Z-Ray. Se a coleta de dados estiver habilitada, o uso da memória aumentará em cada iteração, como se a coleta de lixo estivesse desativada.

HappyDude
fonte