Diagnose von Speicherlecks - Zulässige Speichergröße von # Bytes erschöpft

97

Ich bin auf die gefürchtete Fehlermeldung gestoßen, möglicherweise durch mühsame Bemühungen, PHP hat keinen Speicher mehr:

Zulässige Speichergröße von #### Bytes erschöpft (versucht, #### Bytes zuzuweisen) in file.php in Zeile 123

Das Limit erhöhen

Wenn Sie wissen, was Sie tun und das Limit erhöhen möchten, lesen Sie memory_limit :

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

In acht nehmen! Möglicherweise lösen Sie nur das Symptom und nicht das Problem!

Diagnose des Lecks:

Die Fehlermeldung zeigt auf eine Zeile mit einer Schleife, von der ich glaube, dass sie Speicher verliert oder sich unnötig ansammelt. Ich habe memory_get_usage()am Ende jeder Iteration Anweisungen gedruckt und kann sehen, dass die Anzahl langsam wächst, bis sie das Limit erreicht:

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
}

Für die Zwecke dieser Frage nehmen wir an, dass sich der schlimmste vorstellbare Spaghetti-Code irgendwo in $useroder im globalen Bereich versteckt Task.

Welche Tools, PHP-Tricks oder das Debuggen von Voodoo können mir helfen, das Problem zu finden und zu beheben?

Mike B.
quelle
PS: Ich bin kürzlich auf ein Problem mit genau dieser Art von Dingen gestoßen. Leider habe ich auch festgestellt, dass PHP ein Problem mit der Zerstörung von untergeordneten Objekten hat. Wenn Sie ein übergeordnetes Objekt deaktivieren, werden seine untergeordneten Objekte nicht freigegeben. Ich muss sicherstellen, dass ich ein modifiziertes Unset verwende, das einen rekursiven Aufruf aller untergeordneten Objekte enthält. __Destruct und so weiter. Details hier: paul-m-jones.com/archives/262 :: Ich mache so etwas wie: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ item -> __ destruct (); } unset ($ item); }
Josh

Antworten:

48

PHP hat keinen Garbage Collector. Es verwendet die Referenzzählung, um den Speicher zu verwalten. Daher sind zyklische Referenzen und globale Variablen die häufigste Ursache für Speicherverluste. Wenn Sie ein Framework verwenden, müssen Sie leider viel Code durchsuchen, um es zu finden. Das einfachste Instrument besteht darin, selektiv Anrufe zu tätigen memory_get_usageund auf die Stelle einzugrenzen , an der der Code leckt. Sie können auch xdebug verwenden , um eine Ablaufverfolgung des Codes zu erstellen. Führen Sie den Code mit Ausführungsspuren und aus show_mem_delta.

troelskn
quelle
3
Aber aufgepasst ... die generierten Trace-Dateien werden ENORMOUS sein. Als ich zum ersten Mal einen xdebug-Trace in einer Zend Framework-App ausführte, dauerte die Ausführung sehr lange und es wurde eine Datei mit mehreren GB (nicht kb oder MB ... GB) generiert. Sei dir dessen einfach bewusst.
rg88
1
Ja, es ist ziemlich schwer. GBs klingen allerdings ein bisschen viel - es sei denn, Sie hatten ein großes Skript. Versuchen Sie vielleicht, nur ein paar Zeilen zu verarbeiten (sollte ausreichen, um das Leck zu identifizieren). Installieren Sie die xdebug-Erweiterung auch nicht auf dem Produktionsserver.
troelskn
30
Seit 5.3 hat PHP tatsächlich einen Garbage Collector. Andererseits wurde die Speicherprofilierungsfunktion für xdebug entfernt :(
wdev
3
+1 hat das Leck gefunden! Eine Klasse mit zyklischen Referenzen! Sobald diese Referenzen nicht gesetzt waren (), wurden die Objekte wie erwartet mit Müll gesammelt! Vielen Dank! :)
Rinogo
@rinogo also wie hast du von dem Leck erfahren? Können Sie uns mitteilen, welche Schritte Sie unternommen haben?
JohnnyQ
11

Hier ist ein Trick, mit dem wir ermittelt haben, welche Skripte den meisten Speicher auf unserem Server belegen.

Speichern Sie den folgenden Code - Schnipsel in einer Datei an, zB /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');

Setzen Sie es ein, indem Sie Folgendes zu httpd.conf hinzufügen:

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

Analysieren Sie dann die Protokolldatei unter /var/log/httpd/php_memory_log

Möglicherweise müssen Sie dies tun, touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logbevor Ihr Webbenutzer in die Protokolldatei schreiben kann.

Quinn Comendant
quelle
8

Ich habe einmal in einem alten Skript bemerkt, dass PHP die Variable "as" auch nach meiner foreach-Schleife wie im Gültigkeitsbereich beibehalten würde. Beispielsweise,

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

Ich bin mir nicht sicher, ob zukünftige PHP-Versionen dies behoben haben oder nicht, seit ich es gesehen habe. Wenn dies der Fall ist, können Sie unset($user)nach der doSomething()Zeile aus dem Speicher löschen. YMMV.

Patcoll
quelle
13
PHP umfasst keine Schleifen / Bedingungen wie C / Java / etc. Alles, was in einer Schleife / Bedingung deklariert ist, ist auch nach dem Verlassen der Schleife / Bedingung (beabsichtigt [?]) Noch im Geltungsbereich. Methoden / Funktionen hingegen haben den erwarteten Umfang - alles wird freigegeben, sobald die Funktionsausführung endet.
Frank Farmer
Ich habe angenommen, dass dies beabsichtigt ist. Ein Vorteil davon ist, dass Sie nach einer Schleife mit dem zuletzt gefundenen Element arbeiten können, z. B. das bestimmte Kriterien erfüllt.
Joachim
Sie könnten unset()es tun, aber denken Sie daran, dass Sie für Objekte nur ändern, wohin Ihre Variable zeigt - Sie haben sie nicht tatsächlich aus dem Speicher entfernt. PHP gibt den Speicher automatisch frei, sobald er ohnehin außerhalb des Gültigkeitsbereichs liegt. Die bessere Lösung (in Bezug auf diese Antwort, nicht die Frage des OP) besteht darin, kurze Funktionen zu verwenden, damit sie nicht auch an dieser Variablen aus der Schleife hängen lange.
Rich Court
@patcoll Dies hat nichts mit Speicherlecks zu tun. Dies ist einfach der Array-Zeiger, der sich ändert. Schauen Sie hier: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html in Version 3a.
Harm Smits
7

Es gibt mehrere mögliche Speicherpunkte in PHP:

  • PHP selbst
  • PHP-Erweiterung
  • PHP-Bibliothek, die Sie verwenden
  • Ihr PHP-Code

Es ist ziemlich schwierig, die ersten 3 ohne tiefes Reverse Engineering oder PHP-Quellcode-Kenntnisse zu finden und zu reparieren. Für den letzten können Sie die binäre Suche nach Speicherleckcode mit memory_get_usage verwenden

Königoleg
quelle
89
Ihre Antwort ist ungefähr so ​​allgemein wie es hätte sein können
TravisO
2
Es ist eine Schande, dass selbst PHP 7.2 nicht in der Lage ist, Kern-PHP-Speicherlecks zu beheben. Sie können darin keine lang laufenden Prozesse ausführen.
Aftab Naveed
6

Ich bin kürzlich in einer Anwendung auf dieses Problem gestoßen, unter ähnlichen Umständen. Ein Skript, das in PHPs CLI ausgeführt wird und viele Iterationen durchläuft. Mein Skript hängt von mehreren zugrunde liegenden Bibliotheken ab. Ich vermute, dass eine bestimmte Bibliothek die Ursache ist, und ich habe mehrere Stunden vergeblich versucht, ihren Klassen geeignete Zerstörungsmethoden hinzuzufügen, ohne Erfolg. Angesichts eines langwierigen Konvertierungsprozesses in eine andere Bibliothek (der sich als dieselben Probleme herausstellen könnte) habe ich eine grobe Lösung für das Problem in meinem Fall gefunden.

In meiner Situation habe ich unter einer Linux-Cli eine Reihe von Benutzerdatensätzen durchlaufen und für jeden von ihnen eine neue Instanz mehrerer von mir erstellter Klassen erstellt. Ich beschloss, die neuen Instanzen der Klassen mit der exec-Methode von PHP zu erstellen, damit diese Prozesse in einem "neuen Thread" ausgeführt werden. Hier ist ein wirklich grundlegendes Beispiel dessen, worauf ich mich beziehe:

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
}

Offensichtlich weist dieser Ansatz Einschränkungen auf, und man muss sich der Gefahren bewusst sein, da es einfach wäre, einen Kaninchenjob zu schaffen. In einigen seltenen Fällen kann es jedoch hilfreich sein, eine schwierige Situation zu überwinden, bis eine bessere Lösung gefunden werden kann wie in meinem Fall.

Nate Flink
quelle
6

Ich bin auf das gleiche Problem gestoßen, und meine Lösung bestand darin, foreach durch ein reguläres für zu ersetzen. Ich bin mir über die Einzelheiten nicht sicher, aber es scheint, als würde foreach eine Kopie (oder irgendwie einen neuen Verweis) auf das Objekt erstellen. Mit einer regulären for-Schleife greifen Sie direkt auf das Element zu.

Gunnar Lium
quelle
5

Ich würde vorschlagen, dass Sie das PHP-Handbuch überprüfen oder die gc_enable()Funktion zum Sammeln des Mülls hinzufügen ... Das heißt, die Speicherlecks haben keinen Einfluss darauf, wie Ihr Code ausgeführt wird.

PS: PHP hat einen Garbage Collector gc_enable(), der keine Argumente akzeptiert.

Kosgei
quelle
3

Ich habe kürzlich festgestellt, dass PHP 5.3 Lambda-Funktionen zusätzlichen Speicherplatz belassen, wenn sie entfernt werden.

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

Ich bin mir nicht sicher warum, aber es scheint zusätzliche 250 Bytes pro Lambda zu benötigen, selbst nachdem die Funktion entfernt wurde.

Xeoncross
quelle
2
Ich würde dasselbe sagen. Dies wurde behoben am 5.3.10 ( # 60139 )
Kristopher Ives
@KristopherIves, danke für das Update! Du hast recht, das ist kein Problem mehr, also sollte ich keine Angst haben, sie jetzt wie verrückt zu benutzen.
Xeoncross
2

Wenn das, was Sie über PHP sagen, das GC erst nach einer Funktion ausführt, wahr ist, können Sie den Inhalt der Schleife als Problemumgehung / Experiment in eine Funktion einschließen.

Bart van Heukelom
quelle
1
@ DavidKullmann Eigentlich denke ich, meine Antwort ist falsch. Schließlich ist das run(), was aufgerufen wird, auch eine Funktion, an deren Ende der GC stattfinden soll.
Bart van Heukelom
2

Ein großes Problem war die Verwendung von create_function . Wie bei Lambda-Funktionen bleibt der generierte temporäre Name im Speicher.

Eine weitere Ursache für Speicherverluste (im Fall von Zend Framework) ist der Zend_Db_Profiler. Stellen Sie sicher, dass dies deaktiviert ist, wenn Sie Skripts unter Zend Framework ausführen. Zum Beispiel hatte ich in meiner application.ini Folgendes:

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

Das Ausführen von ungefähr 25.000 Abfragen + einer Menge Verarbeitung zuvor brachte den Speicher auf nette 128 MB (mein maximales Speicherlimit).

Durch einfaches Einstellen:

resources.db.profiler.enabled    = false

es war genug, um es unter 20 Mb zu halten

Dieses Skript wurde in der CLI ausgeführt, aber es instanziierte die Zend_Application und führte den Bootstrap aus, sodass die Konfiguration "Entwicklung" verwendet wurde.

Es hat wirklich geholfen, das Skript mit xDebug-Profiling auszuführen

Andy
quelle
2

Ich habe es nicht explizit erwähnt gesehen, aber xdebug leistet hervorragende Arbeit bei der Profilerstellung von Zeit und Speicher (ab 2.6 ). Sie können die generierten Informationen an ein GUI-Frontend Ihrer Wahl weitergeben: Webgrind (nur Zeit), Kcachegrind , Qcachegrind oder andere. sehr nützliche Ihrer verschiedenen Probleme finden können .

Beispiel (von qcachegrind): Geben Sie hier die Bildbeschreibung ein

SeanDowney
quelle
1

Ich bin etwas spät dran, aber ich werde etwas mitteilen, das für Zend Framework relevant ist.

Ich hatte ein Speicherverlustproblem nach der Installation von PHP 5.3.8 (mit Phpfarm), um mit einer ZF-App zu arbeiten, die mit PHP 5.2.9 entwickelt wurde. Ich habe festgestellt, dass der Speicherverlust in der Datei httpd.conf von Apache in meiner virtuellen Hostdefinition ausgelöst wurde SetEnv APPLICATION_ENV "development". Nach dem Kommentieren dieser Zeile wurden die Speicherlecks gestoppt. Ich versuche, eine Inline-Problemumgehung in meinem PHP-Skript zu finden (hauptsächlich durch manuelles Definieren in der Hauptdatei index.php).

Fronzee
quelle
1
Die Frage besagt, dass er in CLI ausgeführt wird. Das bedeutet, dass Apache überhaupt nicht in den Prozess involviert ist.
Maxime
1
@ Maxime Guter Punkt, ich habe das nicht verstanden, danke. Na ja, hoffentlich profitiert ein zufälliger Googler von der Notiz, die ich hier hinterlassen habe, da diese Seite für mich aufgetaucht ist, als ich versucht habe, mein Problem zu lösen.
Fronzee
Überprüfen Sie meine Antwort auf diese Frage, vielleicht war das auch Ihr Fall.
Andy
Ihre Anwendung sollte je nach Umgebung unterschiedliche Konfigurationen haben. Die "development"Umgebung verfügt normalerweise über eine Reihe von Protokollierungen und Profilen, die andere Umgebungen möglicherweise nicht haben. Durch das Kommentieren des Line-Outs hat Ihre Anwendung stattdessen die Standardumgebung verwendet, die normalerweise "production"oder ist "prod". Das Speicherleck besteht noch; Der Code, der ihn enthält, wird in dieser Umgebung einfach nicht aufgerufen.
Marco Roy
0

Ich habe es hier nicht erwähnt gesehen, aber eine Sache, die hilfreich sein könnte, ist die Verwendung von xdebug und xdebug_debug_zval ('variableName'), um den Refcount zu sehen.

Ich kann auch ein Beispiel für eine PHP-Erweiterung geben, die im Weg steht: Zend Ray von Zend Server. Wenn die Datenerfassung aktiviert ist, wird die Speichernutzung bei jeder Iteration erhöht, als ob die Speicherbereinigung deaktiviert wäre.

HappyDude
quelle