Requests: Fuite de mémoire possible

Créé le 17 oct. 2013  ·  53Commentaires  ·  Source: psf/requests

J'ai un programme très simple qui récupère périodiquement une image d'une caméra IP. J'ai remarqué que l'ensemble de travail de ce programme se développe de manière monotone. J'ai écrit un petit programme qui reproduit le problème.

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()

L'utilisation de la mémoire est imprimée à la fin de chaque itération. Voici l'exemple de sortie.
* Itération 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..."

* Itération 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..."

L'utilisation de la mémoire n'augmente pas à chaque itération, mais elle continue d'augmenter, requests.get étant le coupable qui augmente l'utilisation de la mémoire.

Par ** Itération 99 **, voici à quoi ressemble le profil de mémoire.

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..."

L'utilisation de la mémoire ne diminue pas tant que le programme n'est pas terminé.

Y a-t-il un bogue ou une erreur de l'utilisateur?

Bug

Commentaire le plus utile

Il n'y a eu aucune autre plainte à ce sujet et je pense que nous avons fait de notre mieux à cet égard. Je suis heureux de le rouvrir et de réexaminer si nécessaire

Tous les 53 commentaires

Merci d'avoir soulevé cette question et d'avoir fourni autant de détails!

Dites-moi, avez-vous déjà vu l'utilisation de la mémoire diminuer à un moment donné?

Je n'ai pas vu l'utilisation de la mémoire diminuer. Je me demandais si cela avait à voir avec le ramasse-miettes de Python et peut-être qu'il n'a tout simplement pas eu l'occasion de se lancer, alors j'ai ajouté un appel à gc.collect() après chaque téléchargement. Cela n'a fait aucune différence.

Puis-je demander pourquoi ce problème a été clos?

Mon entreprise a connu ce même problème, qui est devenu encore plus important lors de l'utilisation de pypy. Nous avons passé plusieurs jours à retracer l'origine de ce problème dans notre base de code aux requêtes python.

Pour souligner la gravité de ce problème, voici une capture d'écran de ce à quoi ressemblait l'un de nos processus serveur lors de l'exécution d'un profileur de mémoire:
http://cl.ly/image/3X3G2y3Y191h

Le problème est toujours présent avec cpython ordinaire, mais est moins perceptible. C'est peut-être pourquoi ce problème n'a pas été signalé, malgré les graves conséquences qu'il a pour ceux qui utilisent cette bibliothèque pour des processus de longue date.

À ce stade, nous sommes suffisamment désespérés pour envisager d'utiliser curl avec un sous-processus.

Veuillez me faire savoir ce que vous pensez et si cela sera jamais étudié en profondeur. Sinon, je suis d'avis que les requêtes python sont trop dangereuses pour être utilisées pour des applications critiques (par exemple: services liés aux soins de santé).

Merci,
-Mat

Il a été fermé pour cause d'inactivité. Si vous pensez pouvoir fournir des diagnostics utiles pour nous orienter dans la bonne direction, nous serons heureux de rouvrir.

Eh bien, permettez-moi de vous aider alors.

J'ai créé un petit dépôt git pour faciliter l'examen de ce problème.
https://github.com/mhjohnson/memory-profiling-requests

Voici une capture d'écran du graphique qu'il génère:
http://cl.ly/image/453h1y3a2p1r

J'espère que cela t'aides! Faites-moi savoir si j'ai fait quelque chose de mal.

-Mat

Merci Matt! Je vais commencer à étudier cela maintenant. Les premières fois que j'ai exécuté le script (et les variantes que j'ai essayées) ont montré que c'était facilement reproductible. Je vais devoir commencer à jouer avec ça maintenant.

Donc, cela augmente à environ 0,1 Mo / demande. J'ai essayé de coller le décorateur profile sur des méthodes de niveau inférieur, mais ils sont trop longs pour que la sortie soit utile à distance et l'utilisation d'un intervalle supérieur à 0,1 semble servir uniquement à suivre l'utilisation globale, pas la per- utilisation de la ligne. Existe-t-il de meilleurs outils que mprof?

J'ai donc décidé à la place de diriger sa sortie vers | ag '.*0\.[1-9]+ MiB.*' pour obtenir les lignes où la mémoire est ajoutée et j'ai déplacé le décorateur profile vers Session#send . Sans surprise, la majeure partie provient de l'appel à HTTPAdapter#send . Dans le terrier du lapin je vais

Et maintenant, tout vient de l'appel à conn.urlopen sur L355 et HTTPAdapter#get_connection . Si vous décorez get_connection , il y a 7 fois qu'il alloue de la mémoire lorsqu'il appelle PoolManager#connection_from_url . Maintenant que la majorité est déclenchée par les HTTPResponse retournés par urllib3, je vais voir s'il y a quelque chose que nous devrions faire avec eux et que nous ne sommes pas pour nous assurer que la mémoire est libérée après coup. Si je ne trouve pas un bon moyen de gérer cela, je vais commencer à creuser dans urllib3.

@ sigmavirus24 Wow. Bon travail! Il semble que vous ayez identifié le point chaud dans le code.
En ce qui concerne le traçage de quel objet est responsable de la fuite de mémoire, vous pouvez obtenir des conseils supplémentaires en utilisant objgraph comme ceci:

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

Laissez-moi savoir si je peux vous aider de quelque manière que ce soit.

-Mat

Ma première supposition quant au coupable serait les objets socket. Cela expliquerait pourquoi c'est pire sur PyPy ...

Je suis assis dans un aéroport en ce moment et je serai bientôt dans une plaine pendant plusieurs heures. Je ne pourrai probablement pas y arriver ce soir ou jusqu'à potentiellement plus tard cette semaine (sinon le week-end / la semaine prochaine). Jusqu'à présent, j'ai essayé d'utiliser release_conn sur le HTTPResponse nous recevons. J'ai vérifié avec gc.get_referents ce que contient l'objet Response qui ne parvient pas à être GC. Il a l'original httplib HTTPResponse (stocké sous la forme _original_response et qui (d'après ce que get_referents rapporté) n'a qu'un message électronique (pour les en-têtes) et tout le reste est soit une chaîne Si ce sont des sockets, je ne vois pas où elles ne seraient pas ramassées.

De plus, l'utilisation de Session#close (j'ai d'abord fait en sorte que le code utilise des sessions au lieu de l'API fonctionnelle) n'aide pas (et cela devrait effacer les PoolManagers qui effacent les pools de connexions). Donc, l'autre chose intéressante était que PoolManager#connection_from_url ajouterait ~ 0,8 Mo (donner ou prendre 0,1) les premières fois qu'il était appelé. Cela ajoute ~ 3 Mo mais le reste provient de conn.urlopen en HTTPAdapter#send . La chose bizarre est que gc.garbage a des éléments étranges si vous utilisez gc.set_debug(gc.DEBUG_LEAK) . Il a quelque chose comme [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] et comme vous vous en doutez gc.garbage[0] is gc.garbage[0][0] , cette information est donc absolument inutile. Je devrai expérimenter avec objgraph quand j'en aurai l'occasion.

J'ai donc creusé dans urllib3 et suivi le terrier du lapin plus tôt ce matin. J'ai profilé ConnectionPool#urlopen ce qui m'a conduit à ConnectionPool#_make_request . À ce stade, il y a beaucoup de mémoire allouée à partir des lignes 306 et 333 dans urllib3/connectionpool.py . L306 est self._validate_conn(conn) et L333 est conn.getresponse(buffering=True) . getresponse est la méthode httplib sur une HTTPConnection . Il ne sera pas facile de creuser davantage cela. Si nous regardons _validate_conn la ligne qui provoque cela est conn.connect() qui est une autre méthode HTTPConnection . connect est presque certainement l'endroit où le socket est créé. Si je désactive le profilage de la mémoire et que je colle un print(old_pool) dans HTTPConnectionPool#close il n'imprime jamais rien. Il semblerait que nous ne fermions pas réellement les pools car la session est détruite. Je suppose que c'est la cause de la fuite de mémoire.

J'adorerais aider à déboguer ceci, je serai dans / hors d'IRC aujourd'hui et demain.

Donc, pour aller plus loin, si vous ouvrez python avec _make_request toujours décoré (avec profile ), et que vous créez une session, faites des requêtes toutes les 10 ou 20 secondes (pour la même URL même), vous verrez que le conn a été considéré comme abandonné, donc le VerifiedHTTPSConnection est fermé puis réutilisé. Cela signifie que la classe connection est réutilisée, pas le socket sous-jacent. La méthode close est celle qui vit sur httplib.HTTPConnection (L798). Cela ferme l'objet socket, puis le définit sur None. Ensuite, il ferme (et définit sur Aucun) le plus récent httplib.HTTPResponse . Si vous profilez également VerifiedHTTPSConnection#connect , toute la mémoire créée / perdue se produit dans urllib3.util.ssl_.ssl_wrap_socket .

Donc, en regardant cela, ce que memory_profiler utilise pour rapporter l'utilisation de la mémoire est la taille de l'ensemble résident (rss) du processus. C'est la taille du processus en RAM (le vms, ou la taille de la mémoire virtuelle, a à voir avec les mallocs), donc je cherche à voir si nous perdons de la mémoire virtuelle, ou si nous avons juste des pages allouées pour mémoire que nous ne perdons pas.

Donc, étant donné que toutes les URL que nous utilisions jusqu'à présent utilisaient HTTPS vérifié, je suis passé à l'utilisation de http://google.com et bien qu'il y ait toujours une augmentation constante de la mémoire, il semble que cela consomme ~ 11-14MiB de moins dans l'ensemble. Tout revient toujours à la ligne conn.getresponse (et dans une moindre mesure maintenant, conn.request ).

La chose intéressante est que VMS ne semble pas grandir beaucoup lorsque je l'examine dans la réplique. Je n'ai pas encore modifié mprof pour renvoyer cette valeur au lieu de la valeur RSS. Un VMS en constante augmentation indiquera certainement une fuite de mémoire tandis que RSS pourrait simplement être un grand nombre de mallocs (ce qui est possible). La plupart des systèmes d'exploitation (si je comprends bien) ne récupèrent pas RSS avec empressement, donc jusqu'à ce qu'une autre page d'application soit défaillante et qu'il n'y ait pas d'autre endroit où l'attribuer, RSS ne diminuera jamais (même si cela pouvait être le cas). Cela dit, si nous augmentons constamment sans atteindre un état stable, je ne peux pas être certain s'il s'agit de requêtes / urllib3 ou simplement de l'interpréteur

Je vais également voir ce qui se passe lorsque nous utilisons directement urllib2 / httplib car je commence à penser que ce n'est pas notre problème. Autant que je sache, Session#close ferme correctement toutes les sockets et supprime les références à celles-ci pour leur permettre d'être GC. De plus, si un socket doit être remplacé par le pool de connexions, la même chose se produit. Même les SSLSockets semblent gérer correctement le ramassage des ordures.

Ainsi, urllib2 semble constamment stagner autour de 13,3 Mo. La différence est que je devais l'envelopper dans un essai / sauf parce qu'il plantait systématiquement avec une URLError après un court moment. Alors peut-être que ça ne fait rien après un moment.

@ sigmavirus24 Vous l'

Hmm ... Python ne libère que de la mémoire pour être réutilisée par lui-même à nouveau, et le système ne récupère pas la mémoire tant que le processus n'est pas terminé. Donc, je pense que la ligne plate que vous voyez à 13,3 Mo est probablement une indication qu'il n'y a pas de fuite de mémoire avec urllib2, contrairement à urllib3.

Ce serait bien de confirmer que le problème peut être isolé à urllib3. Pouvez-vous partager les scripts que vous utilisez pour tester avec urllib2?

Je commence donc à me demander si cela n'a rien à voir avec les objets HTTPConnection . Si tu fais

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)

Les trois premiers devraient afficher 1, le dernier 3. [1] J'ai déjà identifié qu'une HTTPConnection a _HTTPConnection__response qui est une référence à _original_response . Je m'attendais donc à ce que ce nombre soit 3. Ce que je ne peux pas comprendre, c'est ce qui contient la référence à la troisième copie.

Pour plus de divertissement, ajoutez ce qui suit

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

au début du script. Il y a 2 objets inaccessibles après avoir fait l'appel aux requêtes, ce qui est intéressant, mais rien n'était irrécupérable. Si vous ajoutez ceci au script @mhjohnson fourni et que vous filtrez la sortie pour les lignes inaccessibles, vous verrez qu'il y a beaucoup de fois où il y a bien plus de 300 objets inaccessibles. Je ne sais pas encore quelle est la signification des objets inaccessibles. Comme toujours, je vous tiendrai au courant.

@mhjohnson pour tester urllib3, remplacez simplement votre appel par requests.get par urllib2.urlopen (j'aurais probablement dû faire r.read() mais ce n'était pas le cas).

J'ai donc pris la suggestion précédente de @mhjohnson et utilisé objgraph pour déterminer où se trouvait l'autre référence, mais objgraph n'arrive pas à la trouver. J'ai ajouté:

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

Dans le script 2 commentaires ci-dessus et ont obtenu ce qui suit:
requests ce qui montre seulement qu'il y aurait 2 références. Je me demande s'il y a quelque chose avec le fonctionnement de sys.getrefcount qui n'est pas fiable.

C'est donc un hareng rouge. a urllib3.response.HTTPResponse a à la fois _original_response et _fp . Cela combiné avec _HTTPConection__response nous donne trois références.

Ainsi, urllib3.response.HTTPResponse a un attribut _pool qui est également référencé par PoolManager . De même, le HTTPAdapter utilisé pour faire la requête, a une référence sur les retours de requêtes Response . Peut-être que quelqu'un d'autre peut identifier quelque chose d'ici:

requests

Le code qui génère cela est: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus24
Ouais, je me suis un peu perdu avec ce dernier graphique. Probablement parce que je ne connais pas très bien la base de code, et que je ne suis pas non plus très expérimenté dans le débogage des fuites de mémoire.

Savez-vous quel objet je pointe avec la flèche rouge sur cette capture d'écran de votre graphique?
http://cl.ly/image/3l3g410p3r1C

J'ai pu obtenir le code pour afficher la même utilisation de la mémoire qui augmente lentement
sur python3 en remplaçant urllib3 / requests par urllib.request.urlopen.

Code modifié ici: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
téléphone: 925.271.7005 | twentymilliseconds.com

Le lundi 3 novembre 2014 à 21:28, Matthew Johnson [email protected]
a écrit:

@ sigmavirus24 https://github.com/sigmavirus24
Ouais, je me suis un peu perdu avec ce dernier graphique. Probablement parce que je ne
je connais très bien la base de code, et je ne suis pas non plus très expérimenté sur le débogage de la mémoire
fuites.

Savez-vous quel objet c'est que je pointe avec la flèche rouge
dans cette capture d'écran de votre graphique?
http://cl.ly/image/3l3g410p3r1C

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Autant que je sache, faire des demandes à un site Web qui renvoie un
Connexion: fermer l'en-tête (par exemple https://api.twilio.com/2010-04-01.json)
n'augmente pas considérablement l'utilisation de la mémoire. La mise en garde est
il y a plusieurs facteurs différents et je suppose juste que c'est une prise
problème connexe.

Kevin Burke
téléphone: 925.271.7005 | twentymilliseconds.com

Le lundi 3 novembre 2014 à 21h43 , Kevin Burke

J'ai pu obtenir le code pour afficher la même utilisation de la mémoire qui augmente lentement
sur python3 en remplaçant urllib3 / requests par urllib.request.urlopen.

Code modifié ici:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
téléphone: 925.271.7005 | twentymilliseconds.com

Le lundi 3 novembre 2014 à 21:28, Matthew Johnson [email protected]
a écrit:

@ sigmavirus24 https://github.com/sigmavirus24
Ouais, je me suis un peu perdu avec ce dernier graphique. Probablement parce que je
je ne connais pas très bien la base de code, ni je ne suis très expérimenté en débogage
la mémoire fuit.

Savez-vous quel objet c'est que je pointe avec la flèche rouge
dans cette capture d'écran de votre graphique?
http://cl.ly/image/3l3g410p3r1C

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson qui semble être le nombre de références au métatype type par object qui est de type type . En d'autres termes, je pense que ce sont toutes les références de object ou type , mais je ne suis pas tout à fait sûr. Quoi qu'il en soit, si j'essaye de les exclure, le graphique devient quelque chose comme 2 nœuds.

Je suis également très préoccupé par ce problème de fuite de mémoire car nous utilisons des requêtes dans notre système d'exploration Web dans lequel un processus s'exécute généralement pendant plusieurs jours. Y a-t-il des progrès sur cette question?

Après avoir passé du temps là-dessus avec @mhjohnson , je peux confirmer la théorie de @kevinburke liée à la façon dont GC traite les sockets sur PyPy.

Le commit 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e est intéressant. Plus précisément, la ligne https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

L'appel de self.raw.release_conn() avant de renvoyer du contenu a considérablement réduit la mémoire utilisée sur PyPy, même s'il reste encore de la place pour des améliorations.

De plus, je pense que ce serait mieux si nous documentions les appels .close() qui se rapportent aux classes de session et de réponse, comme également mentionné par @ sigmavirus24. Les utilisateurs des requêtes doivent être conscients de ces méthodes, car dans la plupart des cas, les méthodes ne sont pas appelées implicitement.

J'ai également une question et une suggestion concernant le QA de ce projet. Puis-je demander aux responsables de la maintenance pourquoi nous n'utilisons pas de CI pour garantir l'intégrité de nos tests? Avoir un CI nous permettrait également d'écrire des cas de test de référence où nous pouvons profiler et garder une trace de toutes les régressions de performances / mémoire.

Un bon exemple d'une telle approche peut être trouvé dans le projet pq:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

Merci à tous ceux qui ont sauté dessus et ont décidé d'aider!
Nous continuerons d'étudier d'autres théories à l'origine de cela.

@stas je veux aborder une chose:

Les utilisateurs des requêtes doivent être conscients de ces méthodes, car dans la plupart des cas, les méthodes ne sont pas appelées implicitement.

Laissant PyPy de côté pendant un moment, ces méthodes ne devraient pas être appelées explicitement. Si les objets socket deviennent inaccessibles dans CPython, ils obtiendront automatiquement gc'd, ce qui inclut la fermeture des descripteurs de fichiers. Ce n'est pas un argument pour ne pas documenter ces méthodes, mais c'est un avertissement pour ne pas trop se concentrer sur elles.

Nous sommes censés utiliser un CI, mais cela ne semble pas bien pour le moment, et seul @kennethreitz est en mesure de le réparer. Il y arrivera quand il aura le temps. Notez, cependant, que les tests de référence sont extrêmement difficiles à réaliser d'une manière qui ne les rend pas extrêmement bruyants.

Laissant PyPy de côté pendant un moment, ces méthodes ne devraient pas avoir besoin d'être appelées explicitement. Si les objets socket deviennent inaccessibles dans CPython, ils obtiendront automatiquement gc'd, ce qui inclut la fermeture des descripteurs de fichiers. Ce n'est pas un argument pour ne pas documenter ces méthodes, mais c'est un avertissement pour ne pas trop se concentrer sur elles.

Je suis en quelque sorte d'accord avec ce que vous dites, sauf pour la partie dont nous discutons de Python ici. Je ne veux pas commencer un argument, mais en lisant _Le Zen de Python_, la manière pythonique serait de suivre l'approche _Explicite est meilleure que l'approche implicite_. Je ne suis pas non plus familier avec cette philosophie de projet, alors veuillez ignorer mes pensées si cela ne s'applique pas à requests .

Je serais heureux de vous aider avec le CI ou les tests de référence chaque fois que l'occasion se présente! Merci d'avoir expliqué la situation actuelle.

Donc, je pense avoir trouvé la cause du problème lors de l'utilisation de l'API fonctionnelle. Si tu fais

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

La prise semble toujours ouverte. La raison pour laquelle je dis _apparaît_ est parce qu'il a toujours un attribut _sock parce que si vous le faites

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

Vous verrez None imprimé. Donc, ce qui se passe, c'est que urllib3 inclut sur chaque HTTPResponse un attribut qui pointe vers le pool de connexions dont il provient. Le pool de connexions a la connexion dans la file d'attente qui a le socket non fermé. Le problème, pour l'API fonctionnelle, serait résolu si dans requests/api.py nous faisons:

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

Alors r.raw._pool sera toujours le pool de connexion mais r.raw._pool.pool sera None .

La partie délicate devient ce qui se passe lorsque les gens utilisent des sessions. Les avoir close la session après chaque requête n'est pas sensé et va à l'encontre de l'objectif de la session. En réalité, si vous utilisez une session (sans threads) et faites 100 requêtes au même domaine (et au même schéma, par exemple, https ) en utilisant une session, la fuite de mémoire est beaucoup plus difficile à voir, sauf si vous attendez environ 30 secondes pour qu'un nouveau socket soit créé. Le problème est que comme nous l'avons déjà vu, r.raw._pool est un objet très mutable. Il s'agit d'une référence au pool de connexions qui est géré par le gestionnaire de pool dans les demandes. Ainsi, lorsque le socket est remplacé, il est remplacé par des références à celui-ci de chaque réponse qui est encore accessible (dans la portée). Ce que je dois faire davantage, c'est déterminer si quelque chose contient encore des références aux sockets après la fermeture des pools de connexions. Si je peux trouver quelque chose qui tient des références, je pense que nous trouverons la fuite de mémoire _real_.

Donc, une idée que j'avais était d'utiliser objgraph pour déterminer ce qui référence réellement un SSLSocket après un appel à requests.get et j'ai obtenu ceci:

socket

La chose intéressante est qu'il y a apparemment 7 références au SSLSocket mais seulement deux références arrières que objgraph pourrait trouver. Je pense que l'une des références est celle passée à objgraph et l'autre est la liaison que je crée dans le script qui génère cela, mais cela laisse toujours 3 ou 4 sans compte pour les références dont je ne sais pas d'où elles viennent.

Voici mon script pour générer ceci:

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)

En utilisant

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)

Le socket-after.png montre ceci:

socket-after

Nous éliminons donc une référence à la socket ssl. Cela dit, quand je regarde s._sock le sous-jacent socket.socket est fermé.

Après avoir exécuté un tas de tests de performance de longue durée, voici ce que nous avons trouvé:

  • appeler close() aide explicitement!
  • les utilisateurs exécutant plusieurs requêtes doivent utiliser Session et le fermer correctement après avoir terminé. Veuillez fusionner # 2326
  • Les utilisateurs de PyPy sont meilleurs sans JIT! Ou ils devraient appeler gc.collect() explicitement!

TL, DR; requests semble bon, vous trouverez ci-dessous quelques graphiques exécutant cet extrait:

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 sans 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 avec 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.'

Je crois que l'une des raisons pour lesquelles nous avons tous été confus au départ est que l'exécution des benchmarks nécessite un ensemble plus grand pour exclure le comportement de GC d'une implémentation à une autre.

L'exécution des requêtes dans un environnement thread nécessite également un plus grand nombre d'appels en raison de la façon dont les threads fonctionnent (nous n'avons vu aucune variation majeure dans l'utilisation de la mémoire après l'exécution de plusieurs pools de threads).

En ce qui concerne PyPy avec JIT, appeler gc.collect() pour le même nombre d'appels, économise ~ 30% de mémoire. C'est pourquoi je pense que les résultats du JIT devraient être exclus de cette discussion, car il s'agit de la façon dont tout le monde modifie la VM et optimise le code pour JIT.

D'accord, le problème semble donc être explicitement lié à la façon dont nous gérons la mémoire en interaction avec le PyPy JIT. Cela pourrait être une bonne idée de faire appel à un expert PyPy: @alex?

Je ne peux vraiment pas imaginer ce que font les demandes (et l'entreprise) qui pourraient causer quelque chose comme ça. Pouvez-vous exécuter votre test avec PYPYLOG=jit-summary:- dans l'environnement env et coller les résultats (cela affichera des informations à la fin du processus)

J'espère que cela t'aides:

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}

Je suis sur le 32 bits fidèle en utilisant le dernier PyPy de https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 chemins compilés n'expliquent pas plus de 200 Mo de RAM utilisés.

Pouvez-vous mettre quelque chose dans votre programme à exécuter
gc.dump_rpy_heap('filename.txt') tant qu'il est à une mémoire très élevée
usage? (Il suffit de l'exécuter une fois, cela générera un vidage de tous les
mémoire que le GC connaît).

Ensuite, avec une vérification de l'arborescence des sources PyPy, exécutez ./pypy/tool/gcdump.py filename.txt et montrez-nous les résultats.

Merci!

Le sam 08 novembre 2014 à 15:20:52 Stas Sușcov [email protected]
a écrit:

J'espère que cela t'aides:

Ligne # Mem usage Incrément Contenu de la ligne

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
Traçage: 41 0.290082
Backend: 30 0,029096
TOTAL: 1612,933400
opérations: 79116
opérations enregistrées: 23091
appels: 2567
gardes: 7081
ops optionnels: 5530
opt gardes: 1400
forçages: 198
abandonner: trace trop longue: 2
abandonner: compilation: 0
abandonner: vable escape: 9
abandonner: mauvaise boucle: 0
abandonner: force quasi-immut: 0
nvirtuels: 9318
trous nv: 1113
nvréutilisé: 6666
Nbre total de boucles: 23
Nombre total de ponts: 8
Nombre de boucles libérées: 0
Nombre de ponts libérés: 0
[2cbb7c242e8b] jit-summary}

Je suis sur 32 bits fidèle en utilisant le dernier PyPy de
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Journal:

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}

La décharge ici: https://gist.github.com/stas/ad597c87ccc4b563211a

Merci d'avoir pris votre temps pour vous aider!

Cela représente donc peut-être 100 Mo d'utilisation. Il y a deux endroits pour se reposer
de celui-ci peut être, dans la "mémoire de réserve", le GC conserve pour diverses choses, et
dans les allocations non-GC - cela signifie des choses comme le système interne d'OpenSSL
allocations. Je me demande s'il existe un bon moyen de voir si les structures OpenSSL
sont en cours de fuite, est-ce que la chose testée ici avec TLS, si oui, pouvez-vous
essayer avec un site non-TLS et voir s'il se reproduit?

Le sam 08 novembre 2014 à 17:38:04, Stas Sușcov [email protected]
a écrit:

Journal:

Ligne # Mem usage Incrément Contenu de la ligne

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
Traçage: 41 0.293192
Backend: 30 0,026873
TOTAL: 1615,665337
opérations: 79116
opérations enregistrées: 23091
appels: 2567
gardes: 7081
ops optionnels: 5530
opt gardes: 1400
forçages: 198
abandonner: trace trop longue: 2
abandonner: compilation: 0
abandonner: vable escape: 9
abandonner: mauvaise boucle: 0
abandonner: force quasi-immut: 0
nvirtuels: 9318
trous nv: 1113
nvréutilisé: 6637
Nbre total de boucles: 23
Nombre total de ponts: 8
Nombre de boucles libérées: 0
Nombre de ponts libérés: 0
[3fd756c29302] jit-summary}

La décharge ici: https://gist.github.com/stas/ad597c87ccc4b563211a

Merci d'avoir pris votre temps pour vous aider!

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@alex ,

Je crois que @stas avait utilisé une @stas et l'

Si cela aide, voici mes résultats à comparer (en utilisant vos instructions):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Laissez-moi savoir ce que vous pensez.

Merci,

-Mat

L'expression régulière de GitHub est trop lâche. Je rouvre cela parce que je ne pense pas que ce soit entièrement réglé.

Bonjour, je pourrais peut-être vous aider à signaler que le problème existe. J'ai un robot d'exploration qui utilise des requêtes et un processus utilisant le multitraitement. Il arrive que plusieurs instances reçoivent le même résultat. Peut-être qu'il y a une fuite sur le tampon de résultat ou sur le socket lui-même.

Dites-moi si je peux envoyer un échantillon du code ou comment générer l’arbre de référence pour identifier quelle partie de l’information est "partagée" (fuite)

Merci

@barroca, c'est un problème différent. Vous utilisez probablement une session sur plusieurs threads et utilisez stream=True . Si vous fermez une réponse avant d'avoir fini de la lire, le socket est replacé dans le pool de connexion avec ces données toujours dedans (si je me souviens bien). Si cela ne se produit pas, il est également plausible que vous récupériez la connexion la plus récente et que vous receviez une réponse mise en cache du serveur. Quoi qu'il en soit, ce n'est pas une indication d'une fuite de mémoire.

@ sigmavirus24 Merci Ian, c'était une mauvaise utilisation de la session à travers les threads comme vous l'avez mentionné. Merci pour l'explication et désolé d'avoir mis à jour le mauvais problème.

Pas de soucis @barroca :)

Il n'y a eu aucune autre plainte à ce sujet et je pense que nous avons fait de notre mieux à cet égard. Je suis heureux de le rouvrir et de réexaminer si nécessaire

alors, quelle est la solution de ce problème?

@Makecodeeasy je veux savoir ça aussi

Jusqu'à présent, mon problème autour de requests est qu'il n'est pas thread-safe,
mieux utiliser une session séparée pour différents threads,

mon travail en cours pour parcourir des millions d'url pour valider la réponse du cache m'amène ici

car je découvre que l'utilisation de la mémoire augmente au-delà du raisonnable lorsque requests interagit avec ThreadPoolExecutor ou threading ,
à la fin, j'utilise simplement multiprocessing.Process pour isoler le travailleur et avoir une session indépendante pour chaque travailleur

@AndCycle alors votre problème n'est pas là. Il y avait un PR fusionné pour corriger ce cas de fuite de mémoire particulier. Il n'a pas régressé car il y a des tests autour de lui. Et votre problème semble être complètement différent.

Cette page vous a été utile?
0 / 5 - 0 notes