Diagnóstico de pérdidas de memoria: tamaño de memoria permitido de # bytes agotados

98

Me he encontrado con el temido mensaje de error, posiblemente a través de un esfuerzo minucioso, PHP se ha quedado sin memoria:

Tamaño de memoria permitido de #### bytes agotados (intentó asignar #### bytes) en file.php en la línea 123

Aumentando el límite

Si sabe lo que está haciendo y desea aumentar el límite, consulte memory_limit :

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

¡Tener cuidado! ¡Es posible que solo esté resolviendo el síntoma y no el problema!

Diagnosticando la fuga:

El mensaje de error apunta a una línea dentro de un bucle que creo que está perdiendo o acumulando memoria innecesariamente. Imprimí memory_get_usage()declaraciones al final de cada iteración y puedo ver que el número crece lentamente hasta que alcanza el límite:

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 los propósitos de esta pregunta, supongamos que el peor código espagueti imaginable se esconde en algún lugar de alcance global en $usero Task.

¿Qué herramientas, trucos de PHP o depuración de vudú pueden ayudarme a encontrar y solucionar el problema?

Mike B
fuente
PD: Recientemente tuve un problema con este tipo de cosas. Desafortunadamente, también encontré que php tiene un problema de destrucción de objetos secundarios. Si desarma un objeto principal, sus objetos secundarios no se liberan. Tener que asegurarme de usar un desarmado modificado que incluye una llamada recursiva a todos los objetos secundarios __destruct y así sucesivamente. Detalles aquí: paul-m-jones.com/archives/262 :: Estoy haciendo algo como: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ elemento -> __ destruct (); } unset ($ artículo); }
Josh

Respuestas:

48

PHP no tiene un recolector de basura. Utiliza el recuento de referencias para administrar la memoria. Por tanto, la fuente más común de pérdidas de memoria son las referencias cíclicas y las variables globales. Si usa un marco, tendrá mucho código para rastrear para encontrarlo, me temo. El instrumento más simple es realizar llamadas selectivamente memory_get_usagey limitarlas a donde se filtra el código. También puede usar xdebug para crear un rastro del código. Ejecute el código con seguimientos de ejecución y show_mem_delta.

troelskn
fuente
3
Pero cuidado ... los archivos de seguimiento generados serán ENORMES. La primera vez que ejecuté un rastreo de xdebug en una aplicación Zend Framework, tardó muchísimo en ejecutarse y generó un archivo de varios GB (no kb o MB ... GB). Solo ten en cuenta esto.
rg88
1
Sí, es bastante pesado ... GB suena un poco demasiado, a menos que tengas un guión grande. Tal vez intente procesar solo un par de filas (debería ser suficiente para identificar la fuga). Además, no instale la extensión xdebug en el servidor de producción.
troelskn
31
Desde 5.3 PHP en realidad tiene un recolector de basura. Por otro lado, la función de creación de perfiles de memoria se ha eliminado de xdebug :(
wdev
3
¡+1 encontró la fuga! ¡Una clase que tenía referencias cíclicas! Una vez que estas referencias no se establecieron (), los objetos se recolectaron como basura como se esperaba. ¡Gracias! :)
rinogo
@rinogo, ¿cómo se enteró de la filtración? ¿Puedes compartir los pasos que tomaste?
JohnnyQ
11

Aquí hay un truco que hemos usado para identificar qué scripts están usando más memoria en nuestro servidor.

Guarde el siguiente fragmento en un archivo en, por ejemplo /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');

Úselo agregando lo siguiente a httpd.conf:

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

Luego analice el archivo de registro en /var/log/httpd/php_memory_log

Puede que necesite hacerlo touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logantes de que su usuario web pueda escribir en el archivo de registro.

Quinn acusado
fuente
8

Una vez me di cuenta de que en un antiguo script PHP mantendría la variable "as" como en el alcance incluso después de mi bucle foreach. Por ejemplo,

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

No estoy seguro de si las futuras versiones de PHP solucionaron esto o no desde que lo vi. Si este es el caso, puede unset($user)después de la doSomething()línea borrarlo de la memoria. YMMV.

patcoll
fuente
13
PHP no incluye bucles / condicionales como C / Java / etc. Cualquier cosa declarada dentro de un bucle / condicional sigue estando en el alcance incluso después de salir del bucle / condicional (por diseño [?]). Los métodos / funciones, por otro lado, tienen el alcance que cabría esperar: todo se libera una vez que finaliza la ejecución de la función.
Frank Farmer
Supuse que es por diseño. Una de sus ventajas es que, después de un ciclo, puede trabajar con el último elemento que encontró, por ejemplo, que satisfaga un criterio particular.
joachim
Podría unset()hacerlo, pero tenga en cuenta que para los objetos, todo lo que está haciendo es cambiar el lugar al que apunta su variable, en realidad no lo ha eliminado de la memoria. PHP liberará automáticamente la memoria una vez que esté fuera de alcance de todos modos, por lo que la mejor solución (en términos de esta respuesta, no la pregunta del OP) es usar funciones cortas para que no se aferren a esa variable del ciclo demasiado largo.
Rich Court
@patcoll Esto no tiene nada que ver con pérdidas de memoria. Esto es simplemente el cambio de puntero de matriz. Eche un vistazo aquí: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html en la versión 3a.
Harm Smits
7

Hay varios puntos posibles de pérdida de memoria en php:

  • php en sí
  • extensión php
  • biblioteca php que usa
  • tu código php

Es bastante difícil encontrar y arreglar los primeros 3 sin un profundo conocimiento de la ingeniería inversa o del código fuente php. Para el último, puede usar la búsqueda binaria para el código con fugas de memoria con memory_get_usage

kingoleg
fuente
91
Su respuesta es tan general como podría haber sido
TravisO
2
Es una pena que incluso php 7.2 no puedan reparar las fugas de memoria php del núcleo. No puede ejecutar procesos de larga duración en él.
Aftab Naveed
6

Recientemente me encontré con este problema en una aplicación, bajo circunstancias similares. Un script que se ejecuta en el cli de PHP que recorre muchas iteraciones. Mi script depende de varias bibliotecas subyacentes. Sospecho que una biblioteca en particular es la causa y pasé varias horas en vano tratando de agregar métodos de destrucción apropiados a sus clases en vano. Enfrentado con un largo proceso de conversión a una biblioteca diferente (que podría resultar tener los mismos problemas), se me ocurrió una solución burda para el problema en mi caso.

En mi situación, en un linux cli, estaba recorriendo un montón de registros de usuario y para cada uno de ellos creando una nueva instancia de varias clases que creé. Decidí intentar crear las nuevas instancias de las clases usando el método exec de PHP para que esos procesos se ejecutaran en un "nuevo hilo". Aquí hay una muestra realmente básica de lo que me refiero:

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, este enfoque tiene limitaciones, y uno debe ser consciente de los peligros de esto, ya que sería fácil crear un trabajo de conejo, sin embargo, en algunos casos raros, podría ayudar a superar un punto difícil, hasta que se pueda encontrar una mejor solución. , como en mi caso.

Nate Flink
fuente
6

Me encontré con el mismo problema y mi solución fue reemplazar foreach con un for. No estoy seguro de los detalles, pero parece que foreach crea una copia (o de alguna manera una nueva referencia) al objeto. Usando un bucle for regular, accede al elemento directamente.

Gunnar Lium
fuente
5

Le sugiero que consulte el manual de php o agregue la gc_enable()función para recolectar la basura ... Es decir, las pérdidas de memoria no afectan la forma en que se ejecuta su código.

PD: php tiene un recolector de basura gc_enable()que no acepta argumentos.

Kosgei
fuente
3

Recientemente me di cuenta de que las funciones lambda de PHP 5.3 dejan memoria adicional utilizada cuando se eliminan.

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

No estoy seguro de por qué, pero parece que se necesitan 250 bytes adicionales cada lambda incluso después de que se elimina la función.

Xeoncross
fuente
2
Yo iba a decir lo mismo. Esto se ha corregido a partir del 5.3.10 ( # 60139 )
Kristopher Ives
@KristopherIves, ¡gracias por la actualización! Tienes razón, esto ya no es un problema, así que no debería tener miedo de usarlos como loco ahora.
Xeoncross
2

Si lo que dice acerca de que PHP solo hace GC después de una función es verdadero, podría envolver el contenido del ciclo dentro de una función como una solución / experimento.

Bart van Heukelom
fuente
1
@DavidKullmann En realidad, creo que mi respuesta es incorrecta. Después de todo, lo run()que se llama también es una función, al final de la cual debería ocurrir la GC.
Bart van Heukelom
2

Un gran problema que tuve fue al usar create_function . Como en las funciones lambda, deja el nombre temporal generado en la memoria.

Otra causa de pérdidas de memoria (en el caso de Zend Framework) es Zend_Db_Profiler. Asegúrese de que esté deshabilitado si ejecuta scripts en Zend Framework. Por ejemplo, tenía en mi application.ini lo siguiente:

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

Ejecutar aproximadamente 25.000 consultas + cargas de procesamiento antes de eso, llevó la memoria a un agradable 128Mb (Mi límite máximo de memoria).

Simplemente configurando:

resources.db.profiler.enabled    = false

fue suficiente para mantenerlo por debajo de 20 Mb

Y este script se estaba ejecutando en CLI, pero estaba instanciando Zend_Application y ejecutando Bootstrap, por lo que usó la configuración de "desarrollo".

Realmente ayudó a ejecutar el script con la creación de perfiles xDebug

Andy
fuente
2

No vi que se mencionara explícitamente, pero xdebug hace un gran trabajo al perfilar el tiempo y la memoria (a partir de 2.6 ). Puede tomar la información que genera y pasarla a una interfaz gráfica de usuario de su elección: webgrind (solo tiempo), kcachegrind , qcachegrind u otros y genera árboles de llamadas y gráficos muy útiles para que pueda encontrar las fuentes de sus diversos problemas. .

Ejemplo (de qcachegrind): ingrese la descripción de la imagen aquí

SeanDowney
fuente
1

Llego un poco tarde a esta conversación, pero compartiré algo pertinente a Zend Framework.

Tuve un problema de pérdida de memoria después de instalar php 5.3.8 (usando phpfarm) para trabajar con una aplicación ZF que fue desarrollada con php 5.2.9. Descubrí que la pérdida de memoria se estaba activando en el archivo httpd.conf de Apache, en mi definición de host virtual, donde dice SetEnv APPLICATION_ENV "development". Después de comentar esta línea, las pérdidas de memoria se detuvieron. Estoy tratando de encontrar una solución alternativa en línea en mi script php (principalmente definiéndolo manualmente en el archivo index.php principal).

fronzee
fuente
1
La pregunta dice que se está ejecutando en CLI. Eso significa que Apache no está involucrado en absoluto en el proceso.
Maxime
1
@Maxime Buen punto, no pude captar eso, gracias. Bueno, espero que algún Googler al azar se beneficie de la nota que dejé aquí de todos modos, ya que esta página se me apareció mientras intentaba resolver mi problema.
fronzee
Comprueba mi respuesta a esta pregunta, quizás ese también fue tu caso.
Andy
Su aplicación debe tener diferentes configuraciones según el entorno. El "development"entorno suele tener un montón de registros y perfiles que otros entornos podrían no tener. Al comentar la salida de línea, su aplicación solo usó el entorno predeterminado, que generalmente es "production"o "prod". La pérdida de memoria todavía existe; el código que lo contiene simplemente no se llama en ese entorno.
Marco Roy
0

No vi que se mencionara aquí, pero una cosa que podría ser útil es usar xdebug y xdebug_debug_zval ('variableName') para ver el refcount.

También puedo dar un ejemplo de una extensión php que se interpone en el camino: Z-Ray de Zend Server. Si la recopilación de datos está habilitada, el uso de la memoria aumentará en cada iteración como si la recolección de basura estuviera desactivada.

HappyDude
fuente