Requests: Posible pérdida de memoria

Creado en 17 oct. 2013  ·  53Comentarios  ·  Fuente: psf/requests

Tengo un programa muy simple que recupera periódicamente una imagen de una cámara IP. He notado que el conjunto de trabajo de este programa crece de forma monótona. Escribí un pequeño programa que reproduce el problema.

import requests
from memory_profiler import profile


<strong i="6">@profile</strong>
def lol():
    print "sending request"
    r = requests.get('http://cachefly.cachefly.net/10mb.test')
    print "reading.."
    with open("test.dat", "wb") as f:
        f.write(r.content)
    print "Finished..."

if __name__=="__main__":
    for i in xrange(100):
        print "Iteration", i
        lol()

El uso de memoria se imprime al final de cada iteración. Esta es la salida de muestra.
* Iteración 0 *

Iteration 0
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     12.5 MiB      0.0 MiB   <strong i="12">@profile</strong>
     6                             def lol():
     7     12.5 MiB      0.0 MiB       print "sending request"
     8     35.6 MiB     23.1 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     35.6 MiB      0.0 MiB       print "reading.."
    10     35.6 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     35.6 MiB      0.0 MiB        f.write(r.content)
    12     35.6 MiB      0.0 MiB       print "Finished..."

* Iteración 1 *

Iteration 1
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     35.6 MiB      0.0 MiB   <strong i="17">@profile</strong>
     6                             def lol():
     7     35.6 MiB      0.0 MiB       print "sending request"
     8     36.3 MiB      0.7 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     36.3 MiB      0.0 MiB       print "reading.."
    10     36.3 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     36.3 MiB      0.0 MiB        f.write(r.content)
    12     36.3 MiB      0.0 MiB       print "Finished..."

El uso de la memoria no crece con cada iteración, pero sigue aumentando, siendo requests.get el culpable que aumenta el uso de la memoria.

Por ** Iteración 99 **, así es como se ve el perfil de memoria.

Iteration 99
sending request
reading..
Finished...
Filename: test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     40.7 MiB      0.0 MiB   <strong i="23">@profile</strong>
     6                             def lol():
     7     40.7 MiB      0.0 MiB       print "sending request"
     8     40.7 MiB      0.0 MiB       r = requests.get('http://cachefly.cachefly.net/10mb.test')
     9     40.7 MiB      0.0 MiB       print "reading.."
    10     40.7 MiB      0.0 MiB       with open("test.dat", "wb") as f:
    11     40.7 MiB      0.0 MiB        f.write(r.content)
    12     40.7 MiB      0.0 MiB       print "Finished..."

El uso de la memoria no se reduce a menos que finalice el programa.

¿Hay algún error o es un error del usuario?

Bug

Comentario más útil

No ha habido más quejas por este hecho y creo que hemos hecho todo lo posible al respecto. Me complace volver a abrirlo y volver a investigar si es necesario

Todos 53 comentarios

¡Gracias por plantear esto y brindar tantos detalles!

Dígame, ¿alguna vez ha visto disminuir el uso de la memoria en algún momento?

No he visto disminuir el uso de la memoria. Me preguntaba si tenía que ver con el recolector de basura de Python y tal vez simplemente no ha tenido la oportunidad de activarse, así que agregué una llamada a gc.collect() después de cada descarga. Eso no hizo ninguna diferencia.

¿Puedo preguntar por qué se ha cerrado este problema?

Mi empresa ha experimentado este mismo problema, que se agravó aún más al usar pypy. Pasamos varios días rastreando el origen de este problema en nuestra base de código hasta las solicitudes de Python.

Solo para resaltar la gravedad de este problema, aquí hay una captura de pantalla de cómo se veía uno de nuestros procesos de servidor al ejecutar un generador de perfiles de memoria:
http://cl.ly/image/3X3G2y3Y191h

El problema sigue presente con cpython normal, pero es menos notorio. Quizás, esta es la razón por la que este problema no se ha informado, a pesar de las graves consecuencias que tiene para quienes utilizan esta biblioteca para procesos de larga duración.

En este punto, estamos lo suficientemente desesperados como para considerar usar curl con un subproceso.

Hágame saber lo que piensa y si esto se investigará a fondo. De lo contrario, tengo la opinión de que las solicitudes de python son demasiado peligrosas para su uso en aplicaciones de misión crítica (por ejemplo, servicios relacionados con la atención médica).

Gracias,
-Mate

Fue cerrado por inactividad. Si cree que puede proporcionarnos diagnósticos útiles para orientarnos en la dirección correcta, estaremos encantados de reabrir.

Bueno, entonces permíteme ayudarte.

Creé un pequeño repositorio de git para ayudar a facilitar el examen de este problema.
https://github.com/mhjohnson/memory-profiling-requests

Aquí hay una captura de pantalla del gráfico que genera:
http://cl.ly/image/453h1y3a2p1r

¡Espero que esto ayude! Avísame si hice algo incorrecto.

-Mate

¡Gracias Matt! Voy a empezar a investigar esto ahora. Las primeras veces que ejecuté el script (y las variaciones que probé) demostraron que se puede reproducir fácilmente. Voy a tener que empezar a jugar con esto ahora.

Entonces esto crece a aproximadamente 0.1 MB / solicitud. Intenté pegar el decorador profile en métodos de nivel inferior, pero son demasiado largos para que la salida sea remotamente útil y el uso de un intervalo superior a 0.1 parece solo servir para rastrear el uso general, no el per- uso de la línea. ¿Hay mejores herramientas que mprof?

Así que, en cambio, decidí canalizar su salida a | ag '.*0\.[1-9]+ MiB.*' para obtener las líneas donde se agrega la memoria y moví el decorador profile a Session#send . Como era de esperar, la mayor parte proviene de la llamada a HTTPAdapter#send . Por la madriguera del conejo voy

Y ahora todo viene de la llamada a conn.urlopen en L355 y HTTPAdapter#get_connection . Si decora get_connection , hay 7 veces que asigna memoria cuando llama PoolManager#connection_from_url . Ahora, dado que la mayoría son activados por HTTPResponse s devueltos desde urllib3, voy a ver si hay algo que _deberíamos_ hacer con ellos que no estemos para asegurarnos de que la memoria se libere después del hecho. Si no puedo encontrar una buena manera de manejar eso, comenzaré a buscar en urllib3.

@ sigmavirus24 Vaya. ¡Buen trabajo! Parece que puede haber identificado el punto de acceso en el código.
En cuanto a rastrear qué objeto es responsable de la pérdida de memoria, puede obtener algunas sugerencias adicionales al usar objgraph así:

import gc
import objgraph
# garbage collect first
gc.collect()  
# print most common python types
objgraph.show_most_common_types()

Déjame saber si puedo ayudar de alguna manera.

-Mate

Mi primera suposición sobre el culpable serían los objetos de socket. Eso explicaría por qué es peor en PyPy ...

Estoy sentado en un aeropuerto en este momento y pronto estaré en una llanura durante varias horas. Probablemente no pueda llegar a esto esta noche o hasta potencialmente más tarde esta semana (si no el próximo fin de semana / semana). Sin embargo, hasta ahora intenté usar release_conn en el HTTPResponse que recibimos. Verifiqué con gc.get_referents qué tiene el objeto Respuesta que puede estar fallando en ser GC. Tiene el HTTPResponse HTTPResponse original (almacenado como _original_response y eso (de lo que get_referents informado) solo tiene un mensaje de correo electrónico (para los encabezados) y todo lo demás es una cadena o un diccionario (o tal vez listas) .Si se trata de sockets, no veo dónde no se recogerían la basura.

Además, usar Session#close (primero hice que el código usara sesiones en lugar de la API funcional) no ayuda (y eso debería borrar los PoolManagers que borran los grupos de conexiones). Entonces, la otra cosa que fue interesante fue que PoolManager#connection_from_url agregaría ~ 0.8 MB (más o menos 0.1) las primeras veces que se llamó. Eso agrega ~ 3MB pero el resto proviene de conn.urlopen en HTTPAdapter#send . Lo extraño es que gc.garbage tiene algunos elementos extraños si usa gc.set_debug(gc.DEBUG_LEAK) . Tiene algo como [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] y como era de esperar gc.garbage[0] is gc.garbage[0][0] esa información es absolutamente inútil. Tendré que experimentar con objgraph cuando tenga la oportunidad.

Así que cavé en urllib3 y seguí la madriguera del conejo más temprano esta mañana. Hice un perfil de ConnectionPool#urlopen que me llevó a ConnectionPool#_make_request . En este punto, hay mucha memoria asignada de las líneas 306 y 333 en urllib3/connectionpool.py . L306 es self._validate_conn(conn) y L333 es conn.getresponse(buffering=True) . getresponse es el método httplib en una HTTPConnection . Profundizar más en eso no será fácil. Si miramos _validate_conn la línea que causa esto es conn.connect() que es otro método HTTPConnection . connect es casi con certeza donde se está creando el socket. Si desactivo la generación de perfiles de memoria y coloco un print(old_pool) en HTTPConnectionPool#close , nunca imprime nada. Parece que en realidad no estamos cerrando grupos cuando la sesión se destruye. Supongo que esta es la causa de la pérdida de memoria.

Me encantaría ayudar a depurar esto, estaré dentro / fuera de IRC hoy y mañana.

Por lo tanto, rastreando esto más allá, si abre python con _make_request todavía decorado (con profile ), y crea una sesión, haga solicitudes cada 10 o 20 segundos (para incluso la misma URL), verá que la conexión se ha considerado descartada, por lo que VerifiedHTTPSConnection se cierra y luego se reutiliza. Esto significa que se reutiliza la clase connection , no el socket subyacente. El método close es el que vive en httplib.HTTPConnection (L798). Esto cierra el objeto socket y luego lo establece en None. Luego cierra (y establece en Ninguno) el httplib.HTTPResponse más reciente. Si también perfila VerifiedHTTPSConnection#connect , toda la memoria creada / filtrada ocurre en urllib3.util.ssl_.ssl_wrap_socket .

Entonces, mirando esto, lo que memory_profiler está usando para informar el uso de la memoria es el tamaño del conjunto residente del proceso (rss). Este es el tamaño del proceso en la RAM (el vms, o tamaño de la memoria virtual, tiene que ver con mallocs), así que estoy buscando para ver si estamos perdiendo memoria virtual, o si solo tenemos páginas asignadas para memoria que no estamos perdiendo.

Entonces, dado que todas las URL que habíamos estado usando hasta ahora usaban HTTPS verificado, cambié a usar http://google.com y aunque todavía hay un aumento constante en la memoria, parece que consume ~ 11-14MiB menos en conjunto. Todo vuelve a la línea conn.getresponse (y en menor grado ahora, conn.request ).

Lo interesante es que VMS no parece crecer mucho cuando lo examino en la respuesta. Todavía tengo que modificar mprof para devolver ese valor en lugar del valor RSS. Un VMS en constante aumento seguramente señalará una pérdida de memoria, mientras que RSS podría ser simplemente una gran cantidad de mallocs (lo cual es posible). La mayoría de los sistemas operativos (si lo entiendo correctamente) no reclaman RSS con entusiasmo, por lo que hasta que otra página de la aplicación falle y no haya otro lugar para asignarlo, RSS nunca se reducirá (incluso si pudiera). Dicho esto, si aumentamos constantemente sin alcanzar un estado estable, no puedo estar seguro de si son solicitudes / urllib3 o solo el intérprete

También voy a ver qué sucede cuando usamos urllib2 / httplib directamente porque estoy empezando a pensar que este no es nuestro problema. Por lo que puedo decir, Session#close cierra correctamente todos los sockets y elimina las referencias a ellos para permitir que sean GC. Además, si un socket necesita ser reemplazado por Connection Pool, sucede lo mismo. Incluso SSLSockets parece manejar adecuadamente la recolección de basura.

Por lo tanto, urllib2 parece tener una línea plana constante alrededor de 13,3MiB. La diferencia es que tuve que envolverlo en un intento / excepto porque se bloqueaba constantemente con un URLError después de un corto tiempo. Entonces, quizás no esté haciendo nada después de un tiempo.

@ sigmavirus24 ¡ Lo estás aplastando! :)

Hmm ... Python solo libera memoria para ser reutilizada por sí mismo nuevamente, y el sistema no recupera la memoria hasta que el proceso termina. Entonces, creo que la línea plana que está viendo en 13.3MiB es probablemente una indicación de que no hay una fuga de memoria presente con urllib2, a diferencia de urllib3.

Sería bueno confirmar que el problema se puede aislar en urllib3. ¿Puedes compartir los scripts que estás usando para probar con urllib2?

Así que estoy empezando a preguntarme si esto no tiene algo que ver con los objetos HTTPConnection . Si lo haces

import sys
import requests

s = requests.Session()
r = s.get('https://httpbin.org/get')
print('Number of response refs: ', sys.getrefcount(r) - 1)
print('Number of session refs: ', sys.getrefcount(s) - 1)
print('Number of raw refs: ', sys.getrefcount(r.raw) - 1)
print('Number of original rsponse refs: ', sys.getrefcount(r.raw._original_response) - 1)

Los primeros tres deben imprimir 1, los últimos 3. [1] Ya identifiqué que una HTTPConnection tiene _HTTPConnection__response que es una referencia a _original_response . Así que esperaba que ese número fuera 3. Lo que no puedo averiguar es qué contiene la referencia a la tercera copia.

Para mayor entretenimiento, agregue lo siguiente

import gc
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_UNCOLLECTABLE)

al comienzo del guión. Hay 2 objetos inalcanzables después de realizar la llamada a las solicitudes, lo cual es interesante, pero nada fue incobrable. Si agrega esto al script que @mhjohnson proporcionó y filtra la salida de las líneas con inalcanzable, verá que hay muchas ocasiones en las que hay más de 300 objetos inalcanzables. Sin embargo, todavía no sé cuál es el significado de los objetos inalcanzables. Como siempre, los mantendré informados.

@mhjohnson para probar urllib3, simplemente reemplace su llamada a requests.get con urllib2.urlopen (también probablemente debería haber estado haciendo r.read() pero no lo estaba).

Así que tomé la sugerencia anterior de @mhjohnson y usé objgraph para averiguar dónde estaba la otra referencia, pero objgraph parece que no puede encontrarla. Yo añadí:

objgraph.show_backrefs([r.raw._original_response], filename='requests.png')

En el guión 2 comentarios arriba y obtuve lo siguiente:
requests lo que solo muestra que habría 2 referencias a él. Me pregunto si hay algo en el funcionamiento de sys.getrefcount que no sea confiable.

Así que eso es una pista falsa. a urllib3.response.HTTPResponse tiene tanto _original_response como _fp . Eso combinado con _HTTPConection__response nos da tres referencias.

Entonces, urllib3.response.HTTPResponse tiene un atributo _pool que también es referenciado por PoolManager . Asimismo, el HTTPAdapter usado para realizar la solicitud, tiene una referencia en las devoluciones de solicitudes Response . Quizás alguien más pueda identificar algo desde aquí:

requests

El código que genera eso es: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus24
Sí, me perdí un poco con ese último gráfico. Probablemente porque no conozco muy bien la base del código, ni tengo mucha experiencia en la depuración de fugas de memoria.

¿Sabes qué objeto es este al que estoy apuntando con la flecha roja en esta captura de pantalla de tu gráfico?
http://cl.ly/image/3l3g410p3r1C

Pude obtener el código para mostrar el mismo uso de memoria que aumenta lentamente
en python3 reemplazando urllib3 / request con urllib.request.urlopen.

Código modificado aquí: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
teléfono: 925.271.7005 | twentymilliseconds.com

El lunes 3 de noviembre de 2014 a las 9:28 p.m., Matthew Johnson [email protected]
escribió:

@ sigmavirus24 https://github.com/sigmavirus24
Sí, me perdí un poco con ese último gráfico. Probablemente porque yo no
conozco muy bien la base del código, ni tengo mucha experiencia en depurar memoria
fugas.

¿Sabes qué objeto es este al que estoy apuntando con la flecha roja?
en esta captura de pantalla de su gráfico?
http://cl.ly/image/3l3g410p3r1C

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Por lo que puedo decir, hacer solicitudes a un sitio web que devuelve un
Conexión: cerrar encabezado (por ejemplo https://api.twilio.com/2010-04-01.json)
no aumenta el uso de la memoria en una cantidad significativa. La advertencia es
hay varios factores diferentes y solo asumo que es un enchufe
asunto relacionado.

Kevin Burke
teléfono: 925.271.7005 | twentymilliseconds.com

El lunes 3 de noviembre de 2014 a las 9:43 p.m., Kevin Burke [email protected] escribió:

Pude obtener el código para mostrar el mismo uso de memoria que aumenta lentamente
en python3 reemplazando urllib3 / request con urllib.request.urlopen.

Código modificado aquí:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
teléfono: 925.271.7005 | twentymilliseconds.com

El lunes 3 de noviembre de 2014 a las 9:28 p.m., Matthew Johnson [email protected]
escribió:

@ sigmavirus24 https://github.com/sigmavirus24
Sí, me perdí un poco con ese último gráfico. Probablemente porque yo
no conozco muy bien la base del código, ni soy muy experimentado en depuración
pérdidas de memoria.

¿Sabes qué objeto es este al que estoy apuntando con la flecha roja?
en esta captura de pantalla de su gráfico?
http://cl.ly/image/3l3g410p3r1C

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson que parece ser el número de referencias al metatipo type por object que es del tipo type . En otras palabras, creo que esas son todas las referencias de object o type , pero no estoy muy seguro. De cualquier manera, si trato de excluirlos, el gráfico se convierte en algo así como 2 nodos.

También estoy muy preocupado por este problema de pérdida de memoria porque usamos solicitudes en nuestro sistema de rastreo web en el que un proceso generalmente se ejecuta durante varios días. ¿Hay algún avance en este tema?

Después de pasar un tiempo en esto junto con @mhjohnson , puedo confirmar la teoría de @kevinburke relacionada con la forma en que GC trata los sockets en PyPy.

La confirmación 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e es interesante. Específicamente la línea https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

Llamar a self.raw.release_conn() antes de devolver el contenido redujo significativamente la memoria utilizada en PyPy, aunque todavía hay espacio para mejoras.

Además, creo que sería mejor si documentamos las llamadas .close() que se relacionan con las clases de sesión y respuesta, como también lo menciona @ sigmavirus24. Los usuarios de las solicitudes deben conocer esos métodos, porque en la mayoría de los casos los métodos no se llaman implícitamente.

También tengo una pregunta y una sugerencia relacionada con el control de calidad de este proyecto. ¿Puedo preguntar a los encargados del mantenimiento por qué no usamos un CI para garantizar la integridad de nuestras pruebas? Tener un CI también nos permitiría escribir casos de prueba de referencia en los que podemos perfilar y realizar un seguimiento de cualquier regresión de rendimiento / memoria.

Un buen ejemplo de este enfoque se puede encontrar en el proyecto pq:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

¡Gracias a todos los que se lanzaron a esto y decidieron ayudar!
Seguiremos investigando otras teorías que causan esto.

@stas quiero abordar una cosa:

Los usuarios de las solicitudes deben conocer esos métodos, porque en la mayoría de los casos los métodos no se llaman implícitamente.

Dejando PyPy a un lado por un momento, esos métodos no deberían _necesitar_ ser llamados explícitamente. Si los objetos de socket se vuelven inalcanzables en CPython, obtendrán auto gc'd, lo que incluye cerrar los identificadores de archivo. Este no es un argumento para no documentar esos métodos, pero es una advertencia para no concentrarse demasiado en ellos.

Estamos destinados a usar un CI, pero parece que no está bien en este momento, y solo @kennethreitz está en condiciones de solucionarlo. Lo hará cuando tenga tiempo. Sin embargo, tenga en cuenta que las pruebas comparativas son extremadamente difíciles de realizar de una manera que no las haga extremadamente ruidosas.

Dejando PyPy a un lado por un momento, esos métodos no deberían necesitar ser llamados explícitamente. Si los objetos de socket se vuelven inalcanzables en CPython, obtendrán auto gc'd, lo que incluye cerrar los identificadores de archivo. Este no es un argumento para no documentar esos métodos, pero es una advertencia para no concentrarse demasiado en ellos.

Estoy un poco de acuerdo con lo que dices, excepto por la parte que discutimos sobre Python aquí. No quiero comenzar una discusión, pero leyendo _El Zen de Python_, la forma pitónica sería seguir el enfoque _Explicit is better than implícito_. Tampoco estoy familiarizado con la filosofía de este proyecto, así que ignore mis pensamientos si esto no se aplica a requests .

¡Estaré encantado de ayudar con las pruebas de CI o de referencia siempre que tenga la oportunidad! Gracias por explicarnos la situación actual.

Entonces, creo que encontré la causa del problema al usar la API funcional. Si lo haces

import requests
r = requests.get('https://httpbin.org/get')
print(r.raw._pool.pool.queue[-1].sock)

El enchufe parece estar todavía abierto. La razón por la que digo _aparece_ es porque todavía tiene un atributo _sock es porque si lo hace

r.raw._pool.queue[-1].close()
print(repr(r.raw._pool.queue[-1].sock))

Verá None impreso. Entonces, lo que está sucediendo es que urllib3 incluye en cada HTTPResponse un atributo que apunta al grupo de conexiones del que proviene. El grupo de conexiones tiene la conexión en la cola que tiene el socket abierto. El problema, para la API funcional, se solucionaría si en requests/api.py hacemos:

def request(...):
    """..."""
    s = Session()
    response = s.request(...)
    s.close()
    return s

Entonces r.raw._pool seguirá siendo el grupo de conexiones, pero r.raw._pool.pool será None .

La parte complicada es lo que sucede cuando la gente usa sesiones. Tenerlos close la sesión después de cada solicitud no tiene sentido y frustra el propósito de la sesión. En realidad, si usa una sesión (sin subprocesos) y hace 100 solicitudes al mismo dominio (y el mismo esquema, por ejemplo, https ) usando una sesión, la pérdida de memoria es mucho más difícil de ver, a menos que espere unos 30 segundos para que se cree un nuevo socket. El problema es que, como ya hemos visto, r.raw._pool es un objeto muy mutable. Es una referencia al grupo de conexiones administrado por el administrador del grupo en las solicitudes. Entonces, cuando se reemplaza el socket, se reemplaza con referencias a él de cada respuesta que aún es accesible (en el alcance). Lo que necesito hacer más es averiguar si algo todavía se aferra a las referencias a los sockets después de cerrar los grupos de conexiones. Si puedo encontrar algo que se aferre a referencias, creo que encontraremos la fuga de memoria _real_.

Entonces, una idea que tuve fue usar objgraph para descubrir qué hace referencia realmente a SSLSocket después de una llamada a requests.get y obtuve esto:

socket

Lo interesante es que aparentemente hay 7 referencias al SSLSocket pero solo dos referencias posteriores que objgraph pudo encontrar. Creo que 1 de las referencias es la que se pasó a objgraph y la otra es la vinculación que hago en el script que genera esto, pero que aún deja 3 o 4 referencias sin contar que no estoy seguro de dónde vienen.

Aquí está mi script para generar esto:

import objgraph
import requests

r = requests.get('https://httpbin.org/get')
s = r.raw._pool.pool.queue[-1].sock
objgraph.show_backrefs(s, filename='socket.png', max_depth=15, refcounts=True)

Utilizando

import objgraph
import requests

r = requests.get('https://httpbin.org/get')
s = r.raw._pool.pool.queue[-1].sock
objgraph.show_backrefs(s, filename='socket-before.png', max_depth=15,
                       refcounts=True)
r.raw._pool.close()
objgraph.show_backrefs(s, filename='socket-after.png', max_depth=15,
                       refcounts=True)

El socket-after.png muestra esto:

socket-after

Entonces eliminamos una referencia al socket ssl. Dicho esto, cuando miro s._sock el socket.socket subyacente está cerrado.

Después de ejecutar varios puntos de referencia de larga duración, esto es lo que hemos encontrado:

  • llamar a close() explícitamente ayuda!
  • los usuarios que ejecutan varias solicitudes deben usar Session y cerrarlo correctamente una vez que hayan terminado. Por favor fusiona # 2326
  • ¡Los usuarios de PyPy son mejores sin JIT! ¡O deberían llamar explícitamente a gc.collect() !

TL; DR; requests ve bien, a continuación encontrará un par de gráficos con este fragmento:

import requests
from memory_profiler import profile

<strong i="15">@profile</strong>
def get(session, i):
    return session.get('http://stas.nerd.ro/?{0}'.format(i))

<strong i="16">@profile</strong>
def multi_get(session, count):
    for x in xrange(count):
        resp = get(session, count+1)
        print resp, len(resp.content), x
        resp.close()

<strong i="17">@profile</strong>
def run():
    session = requests.Session()
    print 'Starting...'
    multi_get(session, 3000)
    print("Finished first round...")
    session.close()
    print 'Done.'

if __name__ == '__main__':
    run()

CPython:

Line #    Mem usage    Increment   Line Contents
================================================
    15      9.1 MiB      0.0 MiB   <strong i="23">@profile</strong>
    16                             def run():
    17      9.1 MiB      0.0 MiB       session = requests.Session()
    18      9.1 MiB      0.0 MiB       print 'Starting...'
    19      9.7 MiB      0.6 MiB       multi_get(session, 3000)
    20      9.7 MiB      0.0 MiB       print("Finished first round...")
    21      9.7 MiB      0.0 MiB       session.close()
    22      9.7 MiB      0.0 MiB       print 'Done.'

PyPy sin JIT:

Line #    Mem usage    Increment   Line Contents
================================================
    15     15.0 MiB      0.0 MiB   <strong i="29">@profile</strong>
    16                             def run():
    17     15.4 MiB      0.5 MiB       session = requests.Session()
    18     15.5 MiB      0.0 MiB       print 'Starting...'
    19     31.0 MiB     15.5 MiB       multi_get(session, 3000)
    20     31.0 MiB      0.0 MiB       print("Finished first round...")
    21     31.0 MiB      0.0 MiB       session.close()
    22     31.0 MiB      0.0 MiB       print 'Done.'

PyPy con JIT:

Line #    Mem usage    Increment   Line Contents
================================================
    15     22.0 MiB      0.0 MiB   <strong i="35">@profile</strong>
    16                             def run():
    17     22.5 MiB      0.5 MiB       session = requests.Session()
    18     22.5 MiB      0.0 MiB       print 'Starting...'
    19    219.0 MiB    196.5 MiB       multi_get(session, 3000)
    20    219.0 MiB      0.0 MiB       print("Finished first round...")
    21    219.0 MiB      0.0 MiB       session.close()
    22    219.0 MiB      0.0 MiB       print 'Done.'

Creo que una de las razones por las que todos nos confundimos inicialmente es porque ejecutar los puntos de referencia requiere un conjunto más grande para excluir la forma en que GC se comporta de una implementación a otra.

Además, ejecutar las solicitudes en un entorno con subprocesos requiere un conjunto mayor de llamadas debido a la forma en que funcionan los subprocesos (no vimos ninguna variación importante en el uso de la memoria después de ejecutar varios grupos de subprocesos).

Con respecto a PyPy con JIT, llamar a gc.collect() por la misma cantidad de llamadas, ahorró ~ 30% de la memoria. Es por eso que creo que los resultados de JIT deben excluirse de esta discusión, ya que es un tema de cómo todos ajustan la VM y optimizan el código para JIT.

Bien, entonces el problema explícitamente parece estar en la forma en que manejamos la memoria interactuando con PyPy JIT. Podría ser una buena idea convocar a un experto en PyPy: @alex?

Realmente no puedo imaginar qué solicitudes (y compañía) posiblemente están haciendo que causarían algo como esto. ¿Puede ejecutar su prueba con PYPYLOG=jit-summary:- en el entorno y pegar los resultados (que imprimirán algunas cosas cuando finalice el proceso)

Espero que esto ayude:

Line #    Mem usage    Increment   Line Contents
================================================
    15     23.7 MiB      0.0 MiB   <strong i="6">@profile</strong>
    16                             def run():
    17     24.1 MiB      0.4 MiB       session = requests.Session()
    18     24.1 MiB      0.0 MiB       print 'Starting...'
    19    215.1 MiB    191.0 MiB       multi_get(session, 3000)
    20    215.1 MiB      0.0 MiB       print("Finished first round...")
    21    215.1 MiB      0.0 MiB       session.close()
    22    215.1 MiB      0.0 MiB       print 'Done.'


[2cbb7c1bbbb8] {jit-summary
Tracing:        41  0.290082
Backend:        30  0.029096
TOTAL:              1612.933400
ops:                79116
recorded ops:       23091
  calls:            2567
guards:             7081
opt ops:            5530
opt guards:         1400
forcings:           198
abort: trace too long:  2
abort: compiling:   0
abort: vable escape:    9
abort: bad loop:    0
abort: force quasi-immut:   0
nvirtuals:          9318
nvholes:            1113
nvreused:           6666
Total # of loops:   23
Total # of bridges: 8
Freed # of loops:   0
Freed # of bridges: 0
[2cbb7c242e8b] jit-summary}

Estoy en el confiable 32bit usando el último PyPy de https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 rutas compiladas no explican más de 200 MB de RAM en uso.

¿Puedes poner algo en tu programa para ejecutar
gc.dump_rpy_heap('filename.txt') mientras está en una memoria muy alta
¿uso? (Solo necesita ejecutarlo una vez, esto generará un volcado de todos los
memoria que conoce el GC).

Luego, con una comprobación del árbol de fuentes de PyPy, ejecute ./pypy/tool/gcdump.py filename.txt y muéstrenos los resultados.

¡Gracias!

El sábado 08 de noviembre de 2014 a las 3:20:52 p.m. Stas Sușcov [email protected]
escribió:

Espero que esto ayude:

Línea # Uso de Mem Incrementar el contenido de la línea

15     23.7 MiB      0.0 MiB   <strong i="20">@profile</strong>
16                             def run():
17     24.1 MiB      0.4 MiB       session = requests.Session()
18     24.1 MiB      0.0 MiB       print 'Starting...'
19    215.1 MiB    191.0 MiB       multi_get(session, 3000)
20    215.1 MiB      0.0 MiB       print("Finished first round...")
21    215.1 MiB      0.0 MiB       session.close()
22    215.1 MiB      0.0 MiB       print 'Done.'

[2cbb7c1bbbb8] {jit-summary
Rastreo: 41 0.290082
Backend: 30 0.029096
TOTAL: 1612.933400
operaciones: 79116
operaciones grabadas: 23091
llamadas: 2567
guardias: 7081
opt ops: 5530
opt guardias: 1400
forzamientos: 198
abortar: seguimiento demasiado largo: 2
abortar: compilar: 0
abortar: vable escape: 9
abortar: bucle incorrecto: 0
abortar: forzar cuasi-immut: 0
nvirtuales: 9318
nvholes: 1113
nvreused: 6666
Número total de bucles: 23
# Total de puentes: 8
# De bucles liberados: 0
# De puentes liberados: 0
[2cbb7c242e8b] jit-summary}

Estoy en un confiable 32bit usando el último PyPy de
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Iniciar sesión:

Line #    Mem usage    Increment   Line Contents
================================================
    16     22.0 MiB      0.0 MiB   <strong i="6">@profile</strong>
    17                             def run():
    18     22.5 MiB      0.5 MiB       session = requests.Session()
    19     22.5 MiB      0.0 MiB       print 'Starting...'
    20    217.2 MiB    194.7 MiB       multi_get(session, 3000)
    21    217.2 MiB      0.0 MiB       print("Finished first round...")
    22    217.2 MiB      0.0 MiB       session.close()
    23    217.2 MiB      0.0 MiB       print 'Done.'
    24    221.0 MiB      3.8 MiB       gc.dump_rpy_heap('bench.txt')


[3fd7569b13c5] {jit-summary
Tracing:        41  0.293192
Backend:        30  0.026873
TOTAL:              1615.665337
ops:                79116
recorded ops:       23091
  calls:            2567
guards:             7081
opt ops:            5530
opt guards:         1400
forcings:           198
abort: trace too long:  2
abort: compiling:   0
abort: vable escape:    9
abort: bad loop:    0
abort: force quasi-immut:   0
nvirtuals:          9318
nvholes:            1113
nvreused:           6637
Total # of loops:   23
Total # of bridges: 8
Freed # of loops:   0
Freed # of bridges: 0
[3fd756c29302] jit-summary}

El volcado aquí: https://gist.github.com/stas/ad597c87ccc4b563211a

¡Gracias por tomarse su tiempo para ayudar con esto!

Esto representa tal vez 100 MB de uso. Hay dos lugares para descansar
de esto puede ser, en la "memoria libre" que el GC guarda para varias cosas, y
en las asignaciones que no son de GC: estos significan cosas como el código interno de OpenSSL
asignaciones. Me pregunto si hay una buena manera de ver si las estructuras OpenSSL
se están filtrando, es lo que se está probando aquí con TLS, si es así, ¿puede
intente con un sitio que no sea TLS y vea si se reproduce.

El sábado 08 de noviembre de 2014 a las 5:38:04 p.m. Stas Sușcov [email protected]
escribió:

Iniciar sesión:

Línea # Uso de Mem Incrementar el contenido de la línea

16     22.0 MiB      0.0 MiB   <strong i="18">@profile</strong>
17                             def run():
18     22.5 MiB      0.5 MiB       session = requests.Session()
19     22.5 MiB      0.0 MiB       print 'Starting...'
20    217.2 MiB    194.7 MiB       multi_get(session, 3000)
21    217.2 MiB      0.0 MiB       print("Finished first round...")
22    217.2 MiB      0.0 MiB       session.close()
23    217.2 MiB      0.0 MiB       print 'Done.'
24    221.0 MiB      3.8 MiB       gc.dump_rpy_heap('bench.txt')

[3fd7569b13c5] {jit-summary
Rastreo: 41 0.293192
Backend: 30 0.026873
TOTAL: 1615.665337
operaciones: 79116
operaciones grabadas: 23091
llamadas: 2567
guardias: 7081
opt ops: 5530
opt guardias: 1400
forzamientos: 198
abortar: seguimiento demasiado largo: 2
abortar: compilar: 0
abortar: vable escape: 9
abortar: bucle incorrecto: 0
abortar: forzar cuasi-immut: 0
nvirtuales: 9318
nvholes: 1113
nvreused: 6637
Número total de bucles: 23
# Total de puentes: 8
# De bucles liberados: 0
# De puentes liberados: 0
[3fd756c29302] jit-summary}

El volcado aquí: https://gist.github.com/stas/ad597c87ccc4b563211a

¡Gracias por tomarse su tiempo para ayudar con esto!

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@alex ,

Creo que @stas había usado una conexión http (no SSL / TLS) regular para este punto de referencia. Por si acaso, también utilicé el script de referencia de realicé en mi Mac (OSX 10.9.5 2.5 GHz i5 8 GB 1600 MHz DDR3) con una conexión http regular.

Si ayuda, aquí están mis resultados para comparar (siguiendo sus instrucciones):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Déjame saber lo que piensas.

Gracias,

-Mate

La expresión regular de GitHub es demasiado imprecisa. Estoy reabriendo esto porque no creo que esté completamente arreglado.

Hola, tal vez podría ayudar a señalar que el problema existe. Tengo un rastreador que usa solicitudes y tengo un proceso que usa multiprocesamiento. Está sucediendo que más de una instancia está recibiendo el mismo resultado. Tal vez haya alguna fuga en el búfer de resultado o en el propio socket.

Avíseme si puedo enviar alguna muestra del código o cómo puedo generar el árbol de referencia para identificar qué parte de la información se está "compartiendo" (filtrada)

Gracias

@barroca ese es un tema diferente. Es probable que esté usando una sesión en varios subprocesos y esté usando stream=True . Si está cerrando una respuesta antes de haber terminado de leerla, el socket se vuelve a colocar en el grupo de conexiones con esos datos todavía en él (si mal no recuerdo). Si eso no sucede, también es plausible que esté recogiendo la conexión más reciente y recibiendo una respuesta en caché del servidor. De cualquier manera, esto no es una indicación de pérdida de memoria.

@ sigmavirus24 Gracias, Ian. Como mencionaste, fue un error el uso de la sesión en los hilos. Gracias por la explicación y perdón por actualizar el problema incorrecto.

No worries @barroca :)

No ha habido más quejas por este hecho y creo que hemos hecho todo lo posible al respecto. Me complace volver a abrirlo y volver a investigar si es necesario

entonces, ¿cuál es la solución a este problema?

@Makecodeeasy Yo también quiero saber eso

hasta ahora mi problema alrededor de requests es que no es seguro para subprocesos,
mejor use una sesión separada para diferentes hilos,

mi trabajo en curso para recorrer millones de URL para validar la respuesta de caché me lleva aquí

a medida que descubro que el uso de la memoria crece más allá de lo razonable cuando requests interactúan con ThreadPoolExecutor o threading ,
al final, solo uso multiprocessing.Process para aislar al trabajador y tener una sesión independiente para cada trabajador

@AndCycle, entonces su problema no está aquí. Se fusionó un PR para solucionar este caso particular de pérdida de memoria. No ha retrocedido ya que hay pruebas a su alrededor. Y su problema parece ser completamente diferente.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

remram44 picture remram44  ·  4Comentarios

JimHokanson picture JimHokanson  ·  3Comentarios

8key picture 8key  ·  3Comentarios

ReimarBauer picture ReimarBauer  ·  4Comentarios

jakul picture jakul  ·  3Comentarios