Diagnostic des fuites de mémoire - Taille de mémoire autorisée de # octets épuisée


98

J'ai rencontré le message d'erreur redouté, probablement grâce à un effort minutieux, PHP a manqué de mémoire:

Taille de mémoire autorisée de #### octets épuisés (tentative d'allocation de #### octets) dans file.php à la ligne 123

Augmenter la limite

Si vous savez ce que vous faites et que vous souhaitez augmenter la limite, consultez memory_limit :

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

Il faut se méfier! Vous ne résolvez peut-être que le symptôme et non le problème!

Diagnostiquer la fuite:

Le message d'erreur pointe vers une ligne avec une boucle que je pense être une fuite ou une accumulation inutile de mémoire. J'ai imprimé des memory_get_usage()déclarations à la fin de chaque itération et je peux voir le nombre augmenter lentement jusqu'à ce qu'il atteigne la 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
}

Pour les besoins de cette question, supposons que le pire code spaghetti imaginable se cache dans une portée globale quelque part dans $userou Task.

Quels outils, astuces PHP ou débogage vaudou peuvent m'aider à trouver et à résoudre le problème?


PS - J'ai récemment rencontré un problème avec ce type de chose exact. Malheureusement, j'ai également constaté que php avait un problème de destruction d'objet enfant. Si vous annulez la définition d'un objet parent, ses objets enfants ne sont pas libérés. Devoir m'assurer que j'utilise un unset modifié qui inclut un appel récursif à tous les objets enfants __destruct et ainsi de suite. Détails ici: paul-m-jones.com/archives/262 :: Je fais quelque chose comme: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ élément -> __ destruct (); } unset ($ item); }
Josh

Réponses:


48

PHP n'a pas de ramasse-miettes. Il utilise le comptage de références pour gérer la mémoire. Ainsi, la source la plus courante de fuites de mémoire sont les références cycliques et les variables globales. Si vous utilisez un framework, vous aurez beaucoup de code à parcourir pour le trouver, j'en ai peur. L'instrument le plus simple consiste à effectuer des appels de manière sélective memory_get_usageet à les réduire à l'endroit où le code fuit. Vous pouvez également utiliser xdebug pour créer une trace du code. Exécutez le code avec les traces d'exécution et show_mem_delta.


3
Mais attention ... les fichiers de trace générés seront ÉNORMES. La première fois que j'ai exécuté une trace xdebug sur une application Zend Framework, il a fallu beaucoup de temps pour s'exécuter et générer un fichier de taille de plusieurs Go (pas de Ko ou Mo ... Go). Soyez juste conscient de cela.
rg88

1
Ouais, c'est assez lourd ... GB sonne un peu beaucoup cependant - sauf si vous avez un gros script. Essayez peut-être de traiter quelques lignes (cela devrait suffire pour identifier la fuite). N'installez pas non plus l'extension xdebug sur le serveur de production.
troelskn

31
Depuis 5.3, PHP a en fait un ramasse-miettes. D'autre part, la fonction de profilage de la mémoire a été supprimée de xdebug :(
wdev

3
+1 a trouvé la fuite! Une classe qui avait des références cycliques! Une fois ces références non définies (), les objets ont été récupérés comme prévu! Merci! :)
rinogo

@rinogo alors comment avez-vous découvert la fuite? Pouvez-vous partager les mesures que vous avez prises?
JohnnyQ

11

Voici une astuce que nous avons utilisée pour identifier les scripts qui utilisent le plus de mémoire sur notre serveur.

Enregistrez l'extrait suivant dans un fichier, par exemple /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');

Utilisez-le en ajoutant ce qui suit à httpd.conf:

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

Ensuite, analysez le fichier journal à /var/log/httpd/php_memory_log

Vous devrez peut-être le faire touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logavant que votre utilisateur Web puisse écrire dans le fichier journal.


8

J'ai remarqué une fois dans un ancien script que PHP maintiendrait la variable "as" comme dans la portée même après ma boucle foreach. Par exemple,

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

Je ne sais pas si les futures versions de PHP ont corrigé cela ou non depuis que je l'ai vu. Si tel est le cas, vous pouvez unset($user)après la doSomething()ligne pour l'effacer de la mémoire. YMMV.


13
PHP ne couvre pas les boucles / conditions comme C / Java / etc. Tout ce qui est déclaré à l'intérieur d'une boucle / conditionnelle est toujours dans la portée même après avoir quitté la boucle / conditionnel (par conception [?]). Les méthodes / fonctions, en revanche, ont une portée comme vous vous en doutez - tout est libéré une fois que l'exécution de la fonction se termine.
Frank Farmer

J'ai supposé que c'était par conception. L'un des avantages est qu'après une boucle, vous pouvez travailler avec le dernier élément que vous avez trouvé, par exemple qui répond à des critères particuliers.
joachim

Vous pourriez le unset()faire, mais gardez à l'esprit que pour les objets, tout ce que vous faites est de changer l'endroit où votre variable pointe - vous ne l'avez pas réellement supprimée de la mémoire. PHP libérera automatiquement la mémoire une fois qu'elle est hors de portée de toute façon, donc la meilleure solution (en termes de cette réponse, pas de la question de l'OP) est d'utiliser des fonctions courtes afin qu'elles ne s'accrochent pas à cette variable de la boucle aussi longue.
Rich Court

@patcoll Cela n'a rien à voir avec les fuites de mémoire. C'est simplement le changement du pointeur de tableau. Jetez un œil ici: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html à la version 3a.
Harm Smits

7

Il y a plusieurs points possibles de fuite de mémoire en php:

  • php lui-même
  • extension php
  • bibliothèque php que vous utilisez
  • votre code php

Il est assez difficile de trouver et de corriger les 3 premiers sans rétro-ingénierie approfondie ou connaissance du code source php. Pour le dernier, vous pouvez utiliser la recherche binaire pour le code de fuite de mémoire avec memory_get_usage


91
Votre réponse est à peu près aussi générale qu'elle aurait pu être
TravisO

2
C'est dommage que même php 7.2, ils ne soient pas capables de réparer les fuites de mémoire php. Vous ne pouvez pas y exécuter de processus de longue durée.
Aftab Naveed

6

J'ai récemment rencontré ce problème sur une application, dans ce que je crois être des circonstances similaires. Un script qui s'exécute dans le cli de PHP qui boucle sur de nombreuses itérations. Mon script dépend de plusieurs bibliothèques sous-jacentes. Je soupçonne qu'une bibliothèque particulière en est la cause et j'ai passé plusieurs heures en vain à essayer d'ajouter des méthodes de destruction appropriées à ses classes en vain. Confronté à un long processus de conversion vers une bibliothèque différente (qui pourrait se révéler avoir les mêmes problèmes), j'ai trouvé un moyen de contourner le problème dans mon cas.

Dans ma situation, sur un cli linux, je bouclais sur un tas d'enregistrements d'utilisateurs et pour chacun d'entre eux, je créais une nouvelle instance de plusieurs classes que j'avais créées. J'ai décidé d'essayer de créer les nouvelles instances des classes en utilisant la méthode exec de PHP afin que ces processus s'exécutent dans un "nouveau thread". Voici un échantillon vraiment basique de ce à quoi je fais référence:

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
}

De toute évidence, cette approche a des limites, et il faut être conscient des dangers de cela, car il serait facile de créer un travail de lapin, mais dans de rares cas, cela pourrait aider à surmonter une situation difficile, jusqu'à ce qu'une meilleure solution puisse être trouvée. , comme dans mon cas.


6

J'ai rencontré le même problème et ma solution a été de remplacer foreach par un for régulier. Je ne suis pas sûr des détails, mais il semble que foreach crée une copie (ou en quelque sorte une nouvelle référence) à l'objet. En utilisant une boucle for régulière, vous accédez directement à l'élément.


5

Je vous suggère de vérifier le manuel php ou d'ajouter la gc_enable()fonction pour collecter les déchets ... C'est-à-dire que les fuites de mémoire n'affectent pas la façon dont votre code fonctionne.

PS: php a un garbage collector gc_enable()qui ne prend aucun argument.


3

J'ai récemment remarqué que les fonctions lambda de PHP 5.3 laissent de la mémoire supplémentaire utilisée lorsqu'elles sont supprimées.

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

Je ne sais pas pourquoi, mais cela semble prendre 250 octets supplémentaires chaque lambda même après la suppression de la fonction.


2
J'allais dire la même chose. Cela a été corrigé à partir du 5.3.10 ( # 60139 )
Kristopher Ives

@KristopherIves, merci pour la mise à jour! Vous avez raison, ce n'est plus un problème donc je ne devrais pas avoir peur de les utiliser comme un fou maintenant.
Xeoncross

2

Si ce que vous dites à propos de PHP ne faisant GC qu'après une fonction est vrai, vous pouvez envelopper le contenu de la boucle dans une fonction comme solution de contournement / expérience.


1
@DavidKullmann En fait, je pense que ma réponse est fausse. Après tout, le run()qui est appelé est aussi une fonction, à la fin de laquelle le GC devrait se produire.
Bart van Heukelom

2

Un énorme problème que j'ai eu était d'utiliser create_function . Comme dans les fonctions lambda, il laisse le nom temporaire généré en mémoire.

Une autre cause de fuites de mémoire (dans le cas de Zend Framework) est le Zend_Db_Profiler. Assurez-vous que cela est désactivé si vous exécutez des scripts sous Zend Framework. Par exemple, j'ai eu dans mon application.ini le suivant:

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

Exécuter environ 25 000 requêtes + des charges de traitement avant cela, a amené la mémoire à 128 Mo (ma limite de mémoire maximale).

En réglant simplement:

resources.db.profiler.enabled    = false

il suffisait de le garder sous 20 Mo

Et ce script fonctionnait dans CLI, mais il instanciait la Zend_Application et exécutait le Bootstrap, donc il utilisait la configuration de "développement".

Cela a vraiment aidé à exécuter le script avec le profilage xDebug


2

Je ne l'ai pas vu explicitement mentionné, mais xdebug fait un excellent travail de profilage du temps et de la mémoire (à partir de 2.6 ). Vous pouvez prendre les informations qu'il génère et les transmettre à une interface graphique de votre choix: webgrind (time only), kcachegrind , qcachegrind ou autres et il génère des arbres d'appels et des graphiques très utiles pour vous permettre de trouver les sources de vos différents problèmes .

Exemple (de qcachegrind): entrez la description de l'image ici


1

Je suis un peu en retard pour cette conversation mais je vais partager quelque chose de pertinent pour Zend Framework.

J'ai eu un problème de fuite de mémoire après avoir installé php 5.3.8 (en utilisant phpfarm) pour travailler avec une application ZF qui a été développée avec php 5.2.9. J'ai découvert que la fuite de mémoire était déclenchée dans le fichier httpd.conf d'Apache, dans ma définition d'hôte virtuel, où il est dit SetEnv APPLICATION_ENV "development". Après avoir commenté cette ligne, les fuites de mémoire se sont arrêtées. J'essaie de trouver une solution de contournement en ligne dans mon script php (principalement en le définissant manuellement dans le fichier principal index.php).


1
La question dit qu'il s'exécute dans CLI. Cela signifie qu'Apache n'est pas du tout impliqué dans le processus.
Maxime

1
@Maxime Bon point, je n'ai pas réussi à comprendre, merci. Eh bien, j'espère qu'un Googler aléatoire bénéficiera de toute façon de la note que j'ai laissée ici, puisque cette page est venue pour moi en essayant de résoudre mon problème.
fronzee

Vérifiez ma réponse à cette question, peut-être que c'était votre cas aussi.
Andy

Votre application doit avoir des configurations différentes selon l'environnement. L' "development"environnement a généralement un tas de journalisation et de profilage que d'autres environnements peuvent ne pas avoir. Le commentaire de la ligne a simplement fait que votre application utilise à la place l'environnement par défaut, qui est généralement "production"ou "prod". La fuite de mémoire existe toujours; le code qui le contient n'est tout simplement pas appelé dans cet environnement.
Marco Roy

0

Je ne l'ai pas vu mentionné ici, mais une chose qui pourrait être utile est d'utiliser xdebug et xdebug_debug_zval ('variableName') pour voir le refcount.

Je peux également fournir un exemple d'une extension php gênant: le Z-Ray de Zend Server. Si la collecte de données est activée, l'utilisation de la mémoire apparaîtra à chaque itération, comme si la collecte des déchets était désactivée.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.