Requests: Demande une fuite de mémoire

CrĂ©Ă© le 20 avr. 2018  Â·  22Commentaires  Â·  Source: psf/requests

Sommaire.

résultat attendu

Programme en cours d'exécution normalement

RĂ©sultat actuel

Programme consommant toute la RAM jusqu'Ă  ce qu'il cesse de fonctionner

Étapes de reproduction

Pseudocode :

def function():
    proxies = {
        'https': proxy
    }
    session = requests.Session()
    session.headers.update({'User-Agent': 'user - agent'})
    try:                                           #
        login = session.get(url, proxies=proxies)  # HERE IS WHERE MEMORY LEAKS
    except:                                        #
        return -1                                  #
    return 0

Informations systĂšme

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": ""
  },
  "idna": {
    "version": "2.6"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.6.3"
  },
  "platform": {
    "release": "10",
    "system": "Windows"
  },
  "pyOpenSSL": {
    "openssl_version": "",
    "version": null
  },
  "requests": {
    "version": "2.18.4"
  },
  "system_ssl": {
    "version": "100020bf"
  },
  "urllib3": {
    "version": "1.22"
  },
  "using_pyopenssl": false
}
Needs Info Propose Close

Commentaire le plus utile

ProblĂšme similaire. Les requĂȘtes consomment de la mĂ©moire lors de l'exĂ©cution dans le thread. Code Ă  reproduire ici :

import gc
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from memory_profiler import profile

def run_thread_request(sess, run):
    response = sess.get('https://www.google.com')
    return

<strong i="6">@profile</strong>
def main():
    sess = requests.session()
    with ThreadPoolExecutor(max_workers=1) as executor:
        print('Starting!')
        tasks = {executor.submit(run_thread_request, sess, run):
                    run for run in range(50)}
        for _ in as_completed(tasks):
            pass
    print('Done!')
    return

<strong i="7">@profile</strong>
def calling():
    main()
    gc.collect()
    return

if __name__ == '__main__':
    calling()

Dans le code donné ci-dessus, je passe un objet de session, mais si je le remplace par l'exécution de requests.get rien ne change.

La sortie est :

➜  thread-test pipenv run python run.py
Starting!
Done!
Filename: run.py

Line #    Mem usage    Increment   Line Contents
================================================
    10     23.2 MiB     23.2 MiB   <strong i="13">@profile</strong>
    11                             def main():
    12     23.2 MiB      0.0 MiB       sess = requests.session()
    13     23.2 MiB      0.0 MiB       with ThreadPoolExecutor(max_workers=1) as executor:
    14     23.2 MiB      0.0 MiB           print('Starting!')
    15     23.4 MiB      0.0 MiB           tasks = {executor.submit(run_thread_request, sess, run):
    16     23.4 MiB      0.0 MiB                       run for run in range(50)}
    17     25.8 MiB      2.4 MiB           for _ in as_completed(tasks):
    18     25.8 MiB      0.0 MiB               pass
    19     25.8 MiB      0.0 MiB       print('Done!')
    20     25.8 MiB      0.0 MiB       return


Filename: run.py

Line #    Mem usage    Increment   Line Contents
================================================
    22     23.2 MiB     23.2 MiB   <strong i="14">@profile</strong>
    23                             def calling():
    24     25.8 MiB      2.6 MiB       main()
    25     25.8 MiB      0.0 MiB       gc.collect()
    26     25.8 MiB      0.0 MiB       return

Et Pipfile ressemble Ă  ceci :

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true

[requires]
python_version = "3.6"

[packages]
requests = "==2.21.0"
memory-profiler = "==0.55.0"

Tous les 22 commentaires

Veuillez nous fournir la sortie de

python -m requests.help

Si cela n'est pas disponible sur votre version de Requests, veuillez fournir des informations de base sur votre systĂšme (version Python, systĂšme d'exploitation, etc.).

@sigmavirus24 Terminé

Salut @munroc , quelques questions rapides sur votre implémentation de threading car elle n'est pas incluse dans le pseudo-code.

  • CrĂ©ez-vous une nouvelle session pour chaque thread et quelle est la taille du pool de threads que vous utilisez ?

  • Quel outil utilisez-vous pour dĂ©terminer d'oĂč vient la fuite ? Cela vous dĂ©rangerait-il de partager les rĂ©sultats ?

Nous avons des indices de fuites de mémoire autour des sessions depuis un certain temps maintenant, mais je ne suis pas sûr que nous ayons trouvé une arme fumante ou un impact vraiment confirmé.

@nateprewitt Bonjour, oui je crée une nouvelle session pour chaque thread. Le pool de threads est de 30. J'ai essayé avec 2 à 200 threads et des fuites de mémoire de toute façon. Je n'utilise pas d'outil, je viens d'apporter ces modifications à la fonction :
mettre return 0 avant login = session.get et pas de fuite de mémoire. si je mets le retour 0 aprÚs la connexion = la mémoire session.get commence à fuir. Si vous voulez, je peux vous envoyer mon code source n'est pas trop volumineux.

@Munroc si nous avons le code complet, alors je pense qu'il serait plus facile d'isoler la cause réelle. Mais sur la base de l'essentiel du code qui a été fourni, je pense qu'il est trÚs difficile de conclure qu'il y a une fuite de mémoire.

Comme vous l'avez mentionné, si vous return juste avant d'appeler session.get , alors seuls les objets proxies et session existeront dans la mémoire (simplifié.. mais je j'espÚre que vous avez l'idée :sourire:). Cependant, une fois que vous appelez session.get(url, proxies=proxies) , le code HTML du url sera récupéré et enregistré localement dans la variable login . Ce qui signifie que chaque appel de session.get "ressemblera" à une fuite de mémoire, mais ils se comportent en fait normalement en (mémoire) augmentant linéairement de la taille du résultat de url .

Cependant, disons que vous utilisiez des threads et que vous les .join() immédiatement aprÚs. Dans ce cas, je pense que nous devons examiner comment vos threads ont été gérés - et s'ils ont été fermés/nettoyés correctement.

@LeoSZN Je pense que dans votre exemple spécifique, vous fermez uniquement le dernier objet Process aprÚs avoir généré plusieurs Process par urls éléments.

Pourriez-vous essayer de les démoniser en utilisant p.daemon = True et de les exécuter (de sorte qu'une fois le thread principal terminé, tous les processus enfants générés meurent également) ? Sinon, stockez les processus générés dans un tableau séparé et assurez-vous de les fermer tous à l'aide d'une boucle.

@initbar

Dois-je exécuter p.daemon = True dans la boucle ou en dehors de la boucle avant p.join() ? Au fait, ai-je encore besoin de p.join() aprÚs avoir appliqué p.daemon = True ?

_Ok, j'ai été expulsé du nouveau sujet vers celui-ci, alors laissez-moi rejoindre le vÎtre.
Peut-ĂȘtre que ce problĂšme fournira plus d'informations et accĂ©lĂ©rera la rĂ©solution du problĂšme..._

J'exĂ©cute le bot Telegram et j'ai remarquĂ© la dĂ©gradation de la mĂ©moire libre lors de l'exĂ©cution du bot pendant une longue pĂ©riode. PremiĂšrement, je suspecte mon code ; puis je soupçonne bot et enfin j'en suis venu aux requĂȘtes. :)
J'ai utilisé len(gc.get_objects()) pour identifier que le problÚme existe. J'ai localisé les routines de communication, puis j'ai effacé tout le code du bot et j'en viens à l'exemple qui augmente le nombre d'objets gc à chaque itération.

résultat attendu

len(gc.get_objects()) devrait donner le mĂȘme rĂ©sultat Ă  chaque itĂ©ration de boucle

RĂ©sultat actuel

La valeur de len(gc.get_objects()) augmente à chaque itération de boucle.

Test N2
GetObjects len: 27959
Test N3
GetObjects len: 27960
Test N4
GetObjects len: 27961
Test N5
GetObjects len: 27962
Test N6
GetObjects len: 27963
Test N7
GetObjects len: 27964

Étapes de reproduction

token = "XXX:XXX"
chat_id = '111'
proxy = {'https':'socks5h://ZZZ'} #You may need proxy to run this in Russia

from time import sleep
import gc, requests

def garbage_info():
    res = ""
    res += "\nGetObjects len: " + str(len(gc.get_objects()))
    return res

def tester():
    count = 0
    while(True):
        sleep(1)
        count += 1
        msg = "\nTest N{0}".format(count) + garbage_info()
        print(msg)

        method_url = r'sendMessage'
        payload = {'chat_id': str(chat_id), 'text': msg}

        request_url = "https://api.telegram.org/bot{0}/{1}".format(token, method_url)
        method_name = 'get'

        session = requests.session()
        req = requests.Request(
            method=method_name.upper(),
            url=request_url,
            params=payload
        )
        prep = session.prepare_request(req)

        settings = session.merge_environment_settings(
            prep.url, None, None, None, None)
#            prep.url, proxy, None, None, None)  #Change the line to enable proxy
        send_kwargs = {
            'timeout': None,
            'allow_redirects': None,
        }
        send_kwargs.update(settings)
        resp = session.send(prep, **send_kwargs)

        # For more clean output
        gc.collect()

tester()

Informations systĂšme

{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": "2.3.1"
  },
  "idna": {
    "version": "2.7"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.6.6"
  },
  "platform": {
    "release": "4.15.0-36-generic",
    "system": "Linux"
  },
  "pyOpenSSL": {
    "openssl_version": "1010009f",
    "version": "17.5.0"
  },
  "requests": {
    "version": "2.19.1"
  },
  "system_ssl": {
    "version": "1010007f"
  },
  "urllib3": {
    "version": "1.23"
  },
  "using_pyopenssl": true
}

_Le mĂȘme comportement que j'avais sur Python 3.5.3 sur Windows10._

@LeoSZN

@initbar

Dois-je exécuter p.daemon = True dans la boucle ou en dehors de la boucle avant p.join() ? Au fait, ai-je encore besoin de p.join() aprÚs avoir appliqué p.daemon = True ?

# ..
     for i in urls:
        p = Process(target=main, args=(i,))
        p.daemon = True  # before `.start`
        p.start()
# ..

Comme note mineure, vous pouvez toujours .join processus dĂ©mons - mais ils sont presque garantis d'ĂȘtre tuĂ©s lorsque leur processus parent se termine (Ă  moins qu'ils ne deviennent orphelins involontairement ; dans ce cas, veuillez me le faire savoir ! Je J'adore en savoir plus).

Sinon, vous pouvez stocker les objets Process séparément sous forme de tableau et les joindre à la fin :

# ..
processes = [ 
  Process(target=main, args=(i,))
  for i in urls
]
# start the process activity.

résultat attendu

len(gc.get_objects()) devrait donner le mĂȘme rĂ©sultat Ă  chaque itĂ©ration de boucle

La raison de ce comportement a été trouvée dans le mécanisme de cache "demandes".

Cela fonctionne incorrectement (soupçonné): il ajoute un enregistrement de cache à chaque appel à l'URL de l'API Telegram (au lieu de le mettre en cache une fois). Mais cela n'entraßne pas de fuite de mémoire, car la taille du cache est limitée à 20 et le cache est réinitialisé aprÚs avoir atteint cette limite et le nombre croissant d'objets sera ramené à sa valeur initiale.

ProblĂšme similaire. Les requĂȘtes consomment de la mĂ©moire lors de l'exĂ©cution dans le thread. Code Ă  reproduire ici :

import gc
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from memory_profiler import profile

def run_thread_request(sess, run):
    response = sess.get('https://www.google.com')
    return

<strong i="6">@profile</strong>
def main():
    sess = requests.session()
    with ThreadPoolExecutor(max_workers=1) as executor:
        print('Starting!')
        tasks = {executor.submit(run_thread_request, sess, run):
                    run for run in range(50)}
        for _ in as_completed(tasks):
            pass
    print('Done!')
    return

<strong i="7">@profile</strong>
def calling():
    main()
    gc.collect()
    return

if __name__ == '__main__':
    calling()

Dans le code donné ci-dessus, je passe un objet de session, mais si je le remplace par l'exécution de requests.get rien ne change.

La sortie est :

➜  thread-test pipenv run python run.py
Starting!
Done!
Filename: run.py

Line #    Mem usage    Increment   Line Contents
================================================
    10     23.2 MiB     23.2 MiB   <strong i="13">@profile</strong>
    11                             def main():
    12     23.2 MiB      0.0 MiB       sess = requests.session()
    13     23.2 MiB      0.0 MiB       with ThreadPoolExecutor(max_workers=1) as executor:
    14     23.2 MiB      0.0 MiB           print('Starting!')
    15     23.4 MiB      0.0 MiB           tasks = {executor.submit(run_thread_request, sess, run):
    16     23.4 MiB      0.0 MiB                       run for run in range(50)}
    17     25.8 MiB      2.4 MiB           for _ in as_completed(tasks):
    18     25.8 MiB      0.0 MiB               pass
    19     25.8 MiB      0.0 MiB       print('Done!')
    20     25.8 MiB      0.0 MiB       return


Filename: run.py

Line #    Mem usage    Increment   Line Contents
================================================
    22     23.2 MiB     23.2 MiB   <strong i="14">@profile</strong>
    23                             def calling():
    24     25.8 MiB      2.6 MiB       main()
    25     25.8 MiB      0.0 MiB       gc.collect()
    26     25.8 MiB      0.0 MiB       return

Et Pipfile ressemble Ă  ceci :

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true

[requires]
python_version = "3.6"

[packages]
requests = "==2.21.0"
memory-profiler = "==0.55.0"

FWIW, je rencontre également une fuite de mémoire similaire à celle de @jotunskij, voici plus d'informations

https://github.com/nicolargo/glances/issues/1447

J'ai Ă©galement le mĂȘme problĂšme oĂč l'utilisation de request.get avec le threading consomme en fait de la mĂ©moire d'environ 0,1 Ă  0,9 par requĂȘte et elle ne s'efface pas aprĂšs les requĂȘtes mais l'enregistre.

Idem ici, un travail autour?

Éditer
Mon problĂšme semble ĂȘtre dĂ» Ă  l'utilisation de verify=False dans les requĂȘtes, j'ai signalĂ© un bogue sous #5215


Avoir le mĂȘme problĂšme. J'ai un script simple qui gĂ©nĂšre un thread, ce thread appelle une fonction qui exĂ©cute une boucle while, cette boucle interroge une API pour vĂ©rifier une valeur d'Ă©tat, puis dort pendant 10 secondes, puis la boucle s'exĂ©cutera Ă  nouveau jusqu'Ă  ce que le script soit arrĂȘtĂ©.

Lorsque vous utilisez la fonction requests.get , je peux voir l'utilisation de la mémoire augmenter lentement via le gestionnaire de tùches en observant le processus généré.

Mais si je supprime l'appel requests.get de la boucle ou que j'utilise urllib3 directement pour faire la requĂȘte get, il y a trĂšs peu ou pas de fluage de l'utilisation de la mĂ©moire.

J'ai regardé cela sur une période de deux heures dans les deux cas et lors de l'utilisation de requests.get l'utilisation urllib3 l'utilisation

Python 3.7.4 et requĂȘtes 2.22.0

Il semble que Requests soit encore en phase bĂȘta avec des fuites de mĂ©moire comme celle-lĂ . Allez, les gars, arrangez ça ! ??

Une mise Ă  jour pour ceci? Une simple requĂȘte POST avec un tĂ©lĂ©chargement de fichier crĂ©e Ă©galement le mĂȘme problĂšme de fuite de mĂ©moire.

Idem pour moi... la fuite lors de l'exécution de threadpool est également sur Windows python38.
demandes 2.22.0

Pareil pour moi

Voici mon problÚme de fuite de mémoire, quelqu'un peut-il m'aider ? https://stackoverflow.com/questions/59746125/memory-keep-growing-when-using-mutil-thread-download-file

Appelez Session.close() et Response.close() peut éviter la fuite de mémoire.
Et SSL consommera plus de mémoire, donc la fuite de mémoire sera plus remarquable lors de la demande d'URL https.

Je fais d'abord 4 cas de test :

  1. requĂȘtes + ssl (https://)
  2. requĂȘtes + non-ssl (http://)
  3. aiohttp + ssl (https://)
  4. aiohttp + non-ssl (http://)

Pseudo-code :

def run(url):
    session = requests.session()
    response = session.get(url)

while True:
    for url in urls:  # about 5k urls of public websites
        # execute in thread pool, size=10
        thread_pool.submit(run, url)

# in another thread, record memory usage every seconds

Graphique d'utilisation de la mĂ©moire (axe des y : Mo, axe des x : temps), les requĂȘtes utilisent beaucoup de mĂ©moire et la mĂ©moire augmente trĂšs rapidement, tandis que l'utilisation de la mĂ©moire aiohttp est stable :

requests-non-ssl
requests-ssl

aiohttp-non-ssl
aiohttp-ssl

Ensuite, j'ajoute Session.close() et teste à nouveau :

def run(url):
    session = requests.session()
    response = session.get(url)
    session.close()  # close session !!

L'utilisation de la mémoire a considérablement diminué, mais l'utilisation de la mémoire continue d'augmenter avec le temps :

requests-non-ssl-close-session
requests-ssl-close-session

Enfin, j'ajoute Response.close() et teste à nouveau :

def run(url):
    session = requests.session()
    response = session.get(url)
    session.close()  # close session !!
    response.close()  # close response !!

L'utilisation de la mémoire a de nouveau diminué et n'a pas augmenté avec le temps :

requests-non-ssl-close-all
requests-ssl-close-all

Comparez aiohttp et les requĂȘtes montrent que la fuite de mĂ©moire n'est pas causĂ©e par SSL, elle est causĂ©e par des ressources de connexion non fermĂ©es.

Scripts utiles :

class MemoryReporter:
    def __init__(self, name):
        self.name = name
        self.file = open(f'memoryleak/memory_{name}.txt', 'w')
        self.thread = None

    def _get_memory(self):
        return psutil.Process().memory_info().rss

    def main(self):
        while True:
            t = time.time()
            v = self._get_memory()
            self.file.write(f'{t},{v}\n')
            self.file.flush()
            time.sleep(1)

    def start(self):
        self.thread = Thread(target=self.main, name=self.name, daemon=True)
        self.thread.start()


def plot_memory(name):
    filepath = 'memoryleak/memory_{}.txt'.format(name)
    df_mem = pd.read_csv(filepath, index_col=0, names=['t', 'v'])
    df_mem.index = pd.to_datetime(df_mem.index, unit='s')
    df_mem.v = df_mem.v / 1024 / 1024
    df_mem.plot(figsize=(16, 8))

Informations systÚme :

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": ""
  },
  "idna": {
    "version": "2.8"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.7.4"
  },
  "platform": {
    "release": "18.0.0",
    "system": "Darwin"
  },
  "pyOpenSSL": {
    "openssl_version": "",
    "version": null
  },
  "requests": {
    "version": "2.22.0"
  },
  "system_ssl": {
    "version": "1010104f"
  },
  "urllib3": {
    "version": "1.25.6"
  },
  "using_pyopenssl": false
}

Le problÚme de fuite SSL est empaqueté OpenSSL <= 3.7.4 sous Windows et OSX, il ne libÚre pas correctement la mémoire du contexte

https://github.com/VeNoMouS/cloudscraper/issues/143#issuecomment -613092377

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