2009-11-16 27 views
9

Tengo una aplicación Django que muestra un extraño comportamiento de recolección de basura. Hay una vista en particular que simplemente seguirá aumentando el tamaño de la VM de forma significativa cada vez que se invoca, hasta cierto límite, en cuyo punto el uso volverá a disminuir. El problema es que lleva un tiempo considerable hasta que se llega a ese punto, y de hecho, la máquina virtual que ejecuta mi aplicación no tiene suficiente memoria para que todos los procesos FCGI tomen tanta memoria como a veces lo hacen.Python: Comportamiento del recolector de basura

He pasado los últimos dos días investigando esto y aprendiendo sobre la recolección de basura de Python, y creo que entiendo lo que está sucediendo ahora, en su mayor parte. Al utilizar

gc.set_debug(gc.DEBUG_STATS) 

Luego de una única solicitud, veo el siguiente resultado:

>>> c = django.test.Client() 
>>> c.get('/the/view/') 
gc: collecting generation 0... 
gc: objects in each generation: 724 5748 147341 
gc: done. 
gc: collecting generation 0... 
gc: objects in each generation: 731 6460 147341 
gc: done. 
[...more of the same...]  
gc: collecting generation 1... 
gc: objects in each generation: 718 8577 147341 
gc: done. 
gc: collecting generation 0... 
gc: objects in each generation: 714 0 156614 
gc: done. 
[...more of the same...] 
gc: collecting generation 0... 
gc: objects in each generation: 715 5578 156612 
gc: done. 

Así que, esencialmente, una enorme cantidad de objetos se asignan, pero se mueven inicialmente para la generación 1, y cuando Gen 1 se barre durante la misma solicitud, se mueven a la generación 2. Si hago un manual gc.collect (2) después, se eliminan. Y, como mencioné, también se eliminó cuando ocurre el siguiente barrido gen 2 automático, que, si lo entiendo correctamente, sería en este caso algo así como cada 10 solicitudes (en este punto, la aplicación necesita alrededor de 150 MB).

Muy bien, así que inicialmente pensé que podría haber alguna referencia cíclica pasando dentro del procesamiento de una solicitud que impida que cualquiera de estos objetos se recopile dentro del manejo de esa solicitud. Sin embargo, he pasado horas tratando de encontrar uno usando pympler.muppy y objgraph, tanto después como mediante la depuración dentro del proceso de solicitud, y parece que no hay ninguno. Por el contrario, parece que los 14.000 objetos más o menos que se crean durante la solicitud están todos dentro de una cadena de referencia para algún objeto request-global, es decir, una vez que la solicitud desaparece, pueden liberarse.

Ese ha sido mi intento de explicarlo, de todos modos. Sin embargo, si eso es cierto y no hay dependencias cíclicas, no debería liberarse todo el árbol de objetos una vez que el objeto de solicitud que los hace desaparecer, sin que el recolector de basura esté involucrado, simplemente en virtud de los recuentos de referencia. cayendo a cero?

Con esa configuración, aquí están mis preguntas:

  • ¿El anterior exactamente esto, o tengo que buscar el problema en otro lugar? ¿Es simplemente un desafortunado accidente que se guarden datos significativos durante tanto tiempo en este caso de uso particular?

  • ¿Hay algo que pueda hacer para evitar el problema. Ya veo cierto potencial para optimizar la vista, pero parece ser una solución de alcance limitado, aunque tampoco estoy seguro de cuál sería el genérico; ¿Qué tan conveniente es, por ejemplo, llamar gc.collect() o gc.set_threshold() manualmente?

En términos de cómo funciona el recolector de basura en sí:

  • ¿Entiendo correctamente que un objeto siempre se mueve a la próxima generación si un barrido mira y determina que las referencias de TI tiene no cíclico, pero de hecho se puede remontar a un objeto raíz.

  • ¿Qué sucede si el gc hace, digamos, un barrido de generación 1 y encuentra un objeto al que hace referencia un objeto dentro de la generación 2; ¿Sigue esa relación dentro de la generación 2 o espera a que ocurra un barrido de generación 2 antes de analizar la situación?

  • Cuando uso gc.DEBUG_STATS, me preocupo principalmente por la información de "objetos en cada generación"; sin embargo, sigo recibiendo cientos de "gc: 0.0740s transcurrido", "gc: 1258233035.9370s transcurrido". mensajes; son totalmente inconvenientes, les toma un tiempo considerable imprimirlos, y hacen que las cosas interesantes sean mucho más difíciles de encontrar. ¿Hay alguna manera de deshacerse de ellos?

  • Supongo que no hay una forma de hacer un gc.get_objects() por generación, es decir, ¿solo recuperar los objetos de la generación 2, por ejemplo?

Respuesta

2

Creo que su análisis se ve bien. No soy un experto en el gc, así que cada vez que tengo un problema como este, simplemente agrego una llamada al gc.collect() en un lugar apropiado, que no sea crítico en el tiempo, y me olvido de ello.

Le sugiero que llame al gc.collect() en su (s) vista (s) y vea qué efecto tiene en su tiempo de respuesta y en el uso de su memoria.

Tenga en cuenta también this question que sugiere que la configuración DEBUG=True come memoria como si ya hubiera pasado su fecha de caducidad.

+0

+1 por mencionar la configuración DEBUG = False para que Django no registre todas sus consultas SQL. – Kekoa

+0

wow, qué imagen mental de Django comiendo mucho alimento –

3

¿Tiene sentido lo anterior, o tengo que buscar el problema en otro lado? ¿Es simplemente un desafortunado accidente que se guarden datos significativos durante tanto tiempo en este caso de uso particular?

Sí, tiene sentido. Y sí, hay otros asuntos que vale la pena considerar. Django usa threading.local como base para DatabaseWrapper (y algunas contribuciones lo usan para hacer que el objeto de solicitud sea accesible desde lugares donde no se pasa explícitamente). Estos objetos globales sobreviven a las solicitudes y pueden mantener referencias a objetos hasta que se maneje alguna otra vista en el hilo.

¿Hay algo que pueda hacer para evitar el problema. Ya veo cierto potencial para optimizar la vista, pero parece ser una solución de alcance limitado, aunque tampoco estoy seguro de cuál sería el genérico; ¿Qué tan conveniente es, por ejemplo, llamar gc.collect() o gc.set_threshold() manualmente?

Consejos generales (probablemente usted lo sepa, pero de todos modos): evite referencias circulares y globales (incluyendo threading.local). Intenta romper los ciclos y eliminar los globales cuando el diseño de django hace que sea difícil evitarlos. gc.get_referrers(obj) podría ayudarlo a encontrar lugares que requieran atención. Otra forma de desactivar el recolector de basura y llamarlo manualmente después de cada solicitud, cuando es el mejor lugar para hacerlo (esto evitará que los objetos se muevan a la próxima generación).

Supongo que no hay una forma de hacer un gc.get_objects() por generación, es decir, ¿solo recuperar los objetos de la generación 2, por ejemplo?

Desafortunadamente, esto no es posible con la interfaz gc. Pero hay varias maneras de hacerlo. Puede considerar el final de la lista devuelto por gc.get_objects() solamente, ya que los objetos en esta lista están ordenados por generación.Puede comparar la lista con una devuelta de una llamada anterior almacenando referencias débiles a ellas (por ejemplo, en WeakKeyDictionary) entre llamadas. Puede reescribir gc.get_objects() en su propio módulo C (¡es fácil, sobre todo copiar y pegar!) Ya que están almacenados por generación internamente, o incluso tienen acceso a las estructuras internas con ctypes (requiere bastante conocimiento de ctypes).

+0

get_objects() ordenado es suficiente, gracias por la pista. – miracle2k

Cuestiones relacionadas