Requests: Möglicher Speicherverlust

Erstellt am 17. Okt. 2013  ·  53Kommentare  ·  Quelle: psf/requests

Ich habe ein sehr einfaches Programm, das regelmäßig ein Bild von einer IP-Kamera abruft. Ich habe festgestellt, dass der Arbeitssatz dieses Programms monoton wächst. Ich habe ein kleines Programm geschrieben, das das Problem reproduziert.

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

Die Speichernutzung wird am Ende jeder Iteration gedruckt. Dies ist die Beispielausgabe.
* Iteration 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..."

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

Die Speichernutzung wächst nicht mit jeder Iteration, aber sie steigt weiter an, wobei requests.get der Schuldige ist, der die Speichernutzung erhöht.

Mit ** Iteration 99 ** sieht das Speicherprofil so aus.

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

Die Speichernutzung sinkt erst, wenn das Programm beendet wird.

Gibt es einen Fehler oder ist es ein Benutzerfehler?

Bug

Hilfreichster Kommentar

Es gab keine weiteren Beschwerden darüber und ich denke, wir haben unser Bestes gegeben. Gerne öffne ich es wieder und untersuche es bei Bedarf erneut

Alle 53 Kommentare

Vielen Dank, dass Sie dies angesprochen und so viele Details bereitgestellt haben!

Sagen Sie mir, sehen Sie jemals, dass die Speichernutzung irgendwann abnimmt?

Ich habe nicht gesehen, dass die Speichernutzung gesunken ist. Ich habe mich gefragt, ob es mit Pythons Garbage Collector zu tun hat, und vielleicht hatte es einfach keine Gelegenheit, sich einzuschalten, also habe ich gc.collect() nach jedem Download einen Aufruf hinzugefügt. Das machte keinen Unterschied.

Darf ich fragen, warum dieses Problem geschlossen wurde?

In meinem Unternehmen ist das gleiche Problem aufgetreten, das durch die Verwendung von Pypy noch verstärkt wurde. Wir haben mehrere Tage damit verbracht, den Ursprung dieses Problems in unserer Codebasis auf Python-Anfragen zurückzuführen.

Um die Schwere dieses Problems hervorzuheben, sehen Sie hier einen Screenshot davon, wie einer unserer Serverprozesse beim Ausführen eines Speicherprofilers aussah:
http://cl.ly/image/3X3G2y3Y191h

Das Problem tritt bei normalem cpython immer noch auf, ist jedoch weniger auffällig. Vielleicht wurde dieses Problem deshalb nicht gemeldet, trotz der schwerwiegenden Konsequenzen für diejenigen, die diese Bibliothek für langjährige Prozesse nutzen.

An diesem Punkt sind wir verzweifelt genug, um die Verwendung von Curl mit einem Unterprozess in Betracht zu ziehen.

Bitte lassen Sie mich wissen, was Sie denken und ob dies jemals gründlich untersucht wird. Ansonsten bin ich der Meinung, dass Python-Anfragen zu gefährlich sind, um sie für geschäftskritische Anwendungen zu verwenden (z. B. Gesundheitsdienste).

Vielen Dank,
-Matt

Es wurde wegen Inaktivität geschlossen. Wenn Sie der Meinung sind, dass Sie nützliche Diagnosen bereitstellen können, um uns in die richtige Richtung zu weisen, öffnen wir diese gerne wieder.

Erlauben Sie mir dann zu helfen.

Ich habe ein kleines Git-Repo erstellt, um die Prüfung dieses Problems zu erleichtern.
https://github.com/mhjohnson/memory-profiling-requests

Hier ist ein Screenshot des Diagramms, das es generiert:
http://cl.ly/image/453h1y3a2p1r

Hoffe das hilft! Lassen Sie mich wissen, wenn ich etwas falsch gemacht habe.

-Matt

Danke Matt! Ich werde mich jetzt damit befassen. Die ersten Male, in denen ich das Skript ausgeführt habe (und Variationen, die ich ausprobiert habe), haben gezeigt, dass dies leicht reproduzierbar ist. Ich werde jetzt damit anfangen müssen zu spielen.

Dies wächst also bei etwa 0,1 MB / Anfrage. Ich habe versucht, den Dekorator profile auf Methoden niedrigerer Ebene zu beschränken, aber sie sind alle zu lang, als dass die Ausgabe aus der Ferne nützlich wäre. Die Verwendung eines höheren Intervalls als 0,1 scheint nur dazu zu dienen, die Gesamtnutzung zu verfolgen, nicht die Leistung Leitungsnutzung. Gibt es bessere Tools als mprof?

Also habe ich mich stattdessen entschieden, die Ausgabe an | ag '.*0\.[1-9]+ MiB.*' , um die Zeilen zu erhalten, in denen Speicher hinzugefügt wird, und den Dekorator profile auf Session#send verschoben. Es ist nicht überraschend, dass das meiste davon aus dem Aufruf von HTTPAdapter#send . Ich gehe das Kaninchenloch hinunter

Und jetzt kommt alles vom Anruf zu conn.urlopen auf L355 und HTTPAdapter#get_connection . Wenn Sie get_connection dekorieren, wird 7 Mal Speicher zugewiesen, wenn PoolManager#connection_from_url . Angesichts der Tatsache, dass die Mehrheit durch HTTPResponse ausgelöst wird, die von urllib3 zurückgegeben werden, werde ich sehen, ob wir etwas mit ihnen tun sollten, das wir nicht sicherstellen sollen, dass der Speicher nachträglich freigegeben wird. Wenn ich keinen guten Weg finde, damit umzugehen, werde ich mich mit urllib3 befassen.

@ sigmavirus24 Wow. Gute Arbeit! Es sieht so aus, als hätten Sie den Hot Spot im Code genau bestimmt.
Wenn Sie nachverfolgen möchten, welches Objekt für den Speicherverlust verantwortlich ist, erhalten Sie möglicherweise einige zusätzliche Hinweise, wenn Sie das folgende Objekt verwenden:

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

Ich weiß, ob ich irgendwie helfen kann.

-Matt

Meine erste Vermutung bezüglich des Täters wären Socket-Objekte. Das würde erklären, warum es bei PyPy schlimmer ist ...

Ich sitze gerade auf einem Flughafen und werde bald einige Stunden in einer Ebene sein. Ich werde wahrscheinlich nicht in der Lage sein, dies heute Abend oder möglicherweise später in dieser Woche (wenn nicht nächstes Wochenende / Woche) zu erreichen. Bisher habe ich jedoch versucht, release_conn für die HTTPResponse wir zurückerhalten. Ich habe mit gc.get_referents überprüft, was das Antwortobjekt hat, das möglicherweise nicht GC-fähig ist. Es hat die ursprüngliche httplib HTTPResponse (gespeichert als _original_response und die (von dem, was get_referents gemeldet wurde) nur eine E-Mail-Nachricht (für die Header) und alles andere ist entweder eine Zeichenfolge oder ein Wörterbuch (oder vielleicht Listen). Wenn es Steckdosen sind, sehe ich nicht, wo sie nicht Müll gesammelt würden.

Außerdem hilft es nicht, Session#close (ich habe den Code zuerst dazu gebracht, Sitzungen anstelle der funktionalen API zu verwenden) (und das sollte die PoolManager löschen, die die Verbindungspools löschen). Das andere interessante war, dass PoolManager#connection_from_url beim ersten Aufruf ~ 0,8 MB (Geben oder Nehmen 0,1) hinzufügen würde. Das fügt also ~ 3 MB hinzu, aber der Rest kommt von conn.urlopen in HTTPAdapter#send . Das Bizarre ist, dass gc.garbage einige seltsame Elemente hat, wenn Sie gc.set_debug(gc.DEBUG_LEAK) . Es hat so etwas wie [[[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None], [[...], [...], [...], None]] und wie zu erwarten gc.garbage[0] is gc.garbage[0][0] so dass Informationen absolut nutzlos sind. Ich muss mit Objekt experimentieren, wenn ich die Chance dazu bekomme.

Also habe ich mich in urllib3 vertieft und bin heute Morgen dem Kaninchenbau weiter gefolgt. Ich habe ConnectionPool#urlopen profiliert, was mich zu ConnectionPool#_make_request . Zu diesem Zeitpunkt ist in den Zeilen 306 und 333 in urllib3/connectionpool.py viel Speicher zugewiesen. L306 ist self._validate_conn(conn) und L333 ist conn.getresponse(buffering=True) . getresponse ist die httplib -Methode für eine HTTPConnection . Eine weitere Profilierung wird nicht einfach sein. Wenn wir uns _validate_conn ansehen, ist die Zeile dort, die dies verursacht, conn.connect() , eine weitere HTTPConnection- Methode. connect ist mit ziemlicher Sicherheit der Ort, an dem der Socket erstellt wird. Wenn ich die Speicherprofilerstellung deaktiviere und ein print(old_pool) in HTTPConnectionPool#close stecke, wird nie etwas gedruckt. Es scheint, dass wir keine Pools schließen, da die Sitzung zerstört wird. Ich vermute, dies ist die Ursache für den Speicherverlust.

Würde gerne beim Debuggen helfen, ich werde heute und morgen im / aus dem IRC sein.

Wenn Sie also python mit _make_request öffnen, das noch dekoriert ist (mit profile ), und eine Sitzung erstellen, stellen Sie alle 10 oder 20 Sekunden (bis) Anfragen sogar dieselbe URL), Sie werden sehen, dass der Conn als gelöscht betrachtet wurde, sodass VerifiedHTTPSConnection geschlossen und dann wiederverwendet wird. Dies bedeutet, dass die Klasse connection wiederverwendet wird, nicht der zugrunde liegende Socket. Die close -Methode lebt von httplib.HTTPConnection (L798). Dadurch wird das Socket-Objekt geschlossen und auf "Keine" gesetzt. Dann wird das letzte httplib.HTTPResponse geschlossen (und auf None gesetzt). Wenn Sie auch VerifiedHTTPSConnection#connect profilieren, geschieht der gesamte erstellte / durchgesickerte Speicher in urllib3.util.ssl_.ssl_wrap_socket .

Wenn Sie sich das ansehen, wird memory_profiler verwendet, um die Speichernutzung zu melden, und zwar die residente Satzgröße (rss) des Prozesses. Dies ist die Größe des Prozesses im RAM (die VMs oder die Größe des virtuellen Speichers hat mit Mallocs zu tun). Ich möchte also prüfen, ob der virtuelle Speicher verloren geht oder ob nur Seiten zugewiesen sind Erinnerung, die wir nicht verlieren.

Angesichts der Tatsache, dass alle URLs, die wir bisher verwendet hatten, verifiziertes HTTPS verwendeten, wechselte ich zu http://google.com und obwohl der Speicher immer noch stetig zunimmt, scheint er ~ 11-14 MB weniger zu verbrauchen im Großen und Ganzen. Es kommt immer noch alles auf die Linie conn.getresponse zurück (und in geringerem Maße jetzt auf conn.request ).

Das Interessante ist, dass VMS nicht viel zu wachsen scheint, wenn ich es in der Antwort untersuche. Ich muss mprof noch ändern, um diesen Wert anstelle des RSS-Werts zurückzugeben. Ein stetig wachsendes VMS wird sicherlich auf einen Speicherverlust hinweisen, während RSS einfach eine große Anzahl von Mallocs sein könnte (was möglich ist). Die meisten Betriebssysteme (wenn ich das richtig verstehe) fordern RSS nicht eifrig zurück. Bis eine andere Anwendungsseite fehlerhaft ist und es keinen anderen Ort gibt, an dem sie zugewiesen werden kann, wird RSS niemals verkleinert (selbst wenn dies möglich wäre). Das heißt, wenn wir stetig zunehmen, ohne einen stabilen Zustand zu erreichen, kann ich nicht sicher sein, ob dies Anfragen / urllib3 oder nur der Interpreter sind

Ich werde auch sehen, was passiert, wenn wir urllib2 / httplib direkt verwenden, weil ich anfange zu denken, dass dies nicht unser Problem ist. Soweit ich das beurteilen kann, schließt Session#close alle Sockets ordnungsgemäß und entfernt Verweise auf sie, damit sie GC-fähig sind. Wenn ein Socket durch den Verbindungspool ersetzt werden muss, geschieht dasselbe. Sogar SSLSockets scheinen richtig damit umzugehen, dass Müll gesammelt wird.

Urllib2 scheint also konstant bei 13,3 MB zu liegen. Der Unterschied ist, dass ich es in einen Versuch einwickeln musste / außer weil es nach kurzer Zeit durchgehend mit einem URLError abstürzen würde. Vielleicht macht es nach einer Weile nichts mehr.

@ sigmavirus24 Du zerquetschst es! :) :)

Hmm ... Python gibt nur Speicher frei, der von selbst wieder verwendet werden kann, und das System erhält den Speicher erst zurück, wenn der Prozess beendet ist. Ich würde also denken, dass die Flatline, die Sie bei 13,3 MB sehen, wahrscheinlich ein Hinweis darauf ist, dass bei urllib2 im Gegensatz zu urllib3 kein Speicherverlust vorliegt.

Es wäre schön zu bestätigen, dass das Problem auf urllib3 isoliert werden kann. Können Sie die Skripte, die Sie zum Testen verwenden, mit urllib2 teilen?

Ich frage mich also, ob dies nichts mit den HTTPConnection -Objekten zu tun hat. Wenn Sie tun

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)

Die ersten drei sollten 1 und die letzten 3 drucken. [1] Ich habe bereits festgestellt, dass eine HTTPConnection _HTTPConnection__response was auf _original_response verweist. Ich hatte also erwartet, dass diese Zahl 3 sein würde. Was ich nicht herausfinden kann, ist, was den Verweis auf die 3. Kopie enthält.

Fügen Sie zur weiteren Unterhaltung Folgendes hinzu

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

zum Anfang des Skripts. Es gibt 2 nicht erreichbare Objekte nach dem Aufruf von Anfragen, was interessant ist, aber nichts war uneinbringlich. Wenn Sie dies zu dem bereitgestellten Skript @mhjohnson hinzufügen und die Ausgabe nach den Zeilen filtern, in denen sie nicht erreichbar sind, werden Sie

@mhjohnson , um requests.get durch urllib2.urlopen (auch ich hätte wahrscheinlich r.read() aber ich war es nicht).

Also nahm ich den vorherigen Vorschlag von objgraph um herauszufinden, wo sich die andere Referenz befand, aber objgraph scheint sie nicht zu finden. Ich fügte hinzu:

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

Im Skript 2 Kommentare oben und bekam die folgenden:
requests was nur zeigt, dass es 2 Verweise darauf geben würde. Ich frage mich, ob etwas mit der Funktionsweise von sys.getrefcount unzuverlässig ist.

Das ist also ein roter Hering. Ein urllib3.response.HTTPResponse hat sowohl _original_response als auch _fp . Das kombiniert mit _HTTPConection__response gibt uns drei Refs.

urllib3.response.HTTPResponse hat also ein _pool -Attribut, auf das auch durch PoolManager verwiesen wird. Ebenso enthält das HTTPAdapter , mit dem die Anfrage gestellt wurde, einen Verweis auf die Rückgabe der Response -Anfragen. Vielleicht kann jemand anderes von hier aus etwas identifizieren:

requests

Der Code, der das generiert, lautet: https://gist.github.com/sigmavirus24/bc0e1fdc5f248ba1201d

@ sigmavirus24
Ja, ich habe mich mit dieser letzten Grafik ein wenig verlaufen. Wahrscheinlich, weil ich die Codebasis nicht sehr gut kenne und auch nicht sehr erfahren darin bin, Speicherlecks zu debuggen.

Wissen Sie, auf welches Objekt ich mit dem roten Pfeil in diesem Screenshot Ihrer Grafik zeige?
http://cl.ly/image/3l3g410p3r1C

Ich konnte den Code dazu bringen, die gleiche langsam zunehmende Speichernutzung anzuzeigen
auf Python3 durch Ersetzen von urllib3 / request durch urllib.request.urlopen.

Geänderter Code hier: https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
Telefon: 925.271.7005 | twentymilliseconds.com

Am Montag, 3. November 2014, um 21.28 Uhr, Matthew Johnson [email protected]
schrieb:

@ sigmavirus24 https://github.com/sigmavirus24
Ja, ich habe mich mit dieser letzten Grafik ein wenig verlaufen. Wahrscheinlich, weil ich es nicht tue
Ich kenne die Codebasis sehr gut und bin auch nicht sehr erfahren im Debuggen von Speicher
Leckagen.

Wissen Sie, auf welches Objekt ich mit dem roten Pfeil zeige?
in diesem Screenshot Ihrer Grafik?
http://cl.ly/image/3l3g410p3r1C

- -
Antworte direkt auf diese E-Mail oder sieh sie dir auf GitHub an
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

Soweit ich das beurteilen kann, stellen Sie Anfragen an eine Website, die a zurückgibt
Verbindung: Header schließen (zum Beispiel https://api.twilio.com/2010-04-01.json)
erhöht die Speichernutzung nicht wesentlich. Die Einschränkung ist
Es gibt mehrere verschiedene Faktoren und ich gehe nur davon aus, dass es sich um eine Steckdose handelt
verwandtes Problem.

Kevin Burke
Telefon: 925.271.7005 | twentymilliseconds.com

Am Montag, den 3. November 2014 um 21:43 Uhr schrieb Kevin Burke [email protected] :

Ich konnte den Code dazu bringen, die gleiche langsam zunehmende Speichernutzung anzuzeigen
auf Python3 durch Ersetzen von urllib3 / request durch urllib.request.urlopen.

Geänderter Code hier:
https://gist.github.com/kevinburke/f99053641fab0e2259f0

Kevin Burke
Telefon: 925.271.7005 | twentymilliseconds.com

Am Montag, 3. November 2014, um 21.28 Uhr, Matthew Johnson [email protected]
schrieb:

@ sigmavirus24 https://github.com/sigmavirus24
Ja, ich habe mich mit dieser letzten Grafik ein wenig verlaufen. Wahrscheinlich weil ich
Ich kenne die Codebasis nicht sehr gut und bin auch nicht sehr erfahren im Debuggen
Speicherlecks.

Wissen Sie, auf welches Objekt ich mit dem roten Pfeil zeige?
in diesem Screenshot Ihrer Grafik?
http://cl.ly/image/3l3g410p3r1C

- -
Antworte direkt auf diese E-Mail oder sieh sie dir auf GitHub an
https://github.com/kennethreitz/requests/issues/1685#issuecomment -61595362
.

@mhjohnson , das scheint die Anzahl der Verweise auf den Metatyp type von object der vom Typ type . Mit anderen Worten, ich denke, das sind alle Referenzen von entweder object oder type , aber ich bin mir nicht ganz sicher. In beiden Fällen wird der Graph zu zwei Knoten, wenn ich versuche, diese auszuschließen.

Ich bin auch sehr besorgt über dieses Problem mit Speicherverlusten, da wir Anforderungen in unserem Webcrawling-System verwenden, in dem ein Prozess normalerweise mehrere Tage lang ausgeführt wird. Gibt es Fortschritte in diesem Bereich?

Nachdem ich einige Zeit zusammen mit @mhjohnson damit verbracht habe , kann ich die @ kevinburke- Theorie bestätigen, die sich auf die Art und Weise bezieht, wie GC die Sockets auf PyPy behandelt.

Das Commit 3c0b94047c1ccfca4ac4f2fe32afef0ae314094e ist interessant. Insbesondere die Zeile https://github.com/kennethreitz/requests/blob/master/requests/models.py#L736

Das Aufrufen von self.raw.release_conn() vor der Rückgabe von Inhalten hat den verwendeten Speicher auf PyPy erheblich reduziert, obwohl noch Verbesserungspotenzial besteht.

Ich denke auch, dass es besser wäre, wenn wir die .close() -Aufrufe dokumentieren, die sich auf die Sitzungs- und Antwortklassen beziehen, wie auch von @ sigmavirus24 erwähnt. Benutzer sollten diese Methoden kennen, da die Methoden in den meisten Fällen nicht implizit aufgerufen werden.

Ich habe auch eine Frage und einen Vorschlag zur Qualitätssicherung dieses Projekts. Darf ich die Betreuer fragen, warum wir kein CI verwenden, um die Integrität unserer Tests sicherzustellen? Ein CI würde es uns auch ermöglichen, Benchmark-Testfälle zu schreiben, in denen wir Leistungs- / Speicherregressionen profilieren und verfolgen können.

Ein gutes Beispiel für einen solchen Ansatz findet sich im pq-Projekt:
https://github.com/malthe/pq/blob/master/pq/tests.py#L287

Vielen Dank an alle, die sich darauf gestürzt haben und beschlossen haben zu helfen!
Wir werden weiterhin andere Theorien untersuchen, die dies verursachen.

@stas Ich möchte eines ansprechen:

Benutzer sollten diese Methoden kennen, da die Methoden in den meisten Fällen nicht implizit aufgerufen werden.

Wenn Sie PyPy für einen Moment beiseite lassen, sollten diese Methoden nicht explizit aufgerufen werden müssen. Wenn die Socket-Objekte in CPython nicht mehr erreichbar sind, werden sie automatisch überprüft, einschließlich des Schließens der Dateihandles. Dies ist kein Argument, um diese Methoden nicht zu dokumentieren, aber es ist eine Warnung, sich nicht zu sehr auf sie zu konzentrieren.

Wir sollen ein CI verwenden, aber es scheint im Moment unwohl zu sein, und nur @kennethreitz ist in der Lage, es zu beheben. Er wird es schaffen, wenn er Zeit hat. Beachten Sie jedoch, dass es äußerst schwierig ist, Benchmark-Tests so durchzuführen, dass sie nicht extrem laut werden.

Wenn Sie PyPy für einen Moment beiseite lassen, sollten diese Methoden nicht explizit aufgerufen werden müssen. Wenn die Socket-Objekte in CPython nicht mehr erreichbar sind, werden sie automatisch überprüft, einschließlich des Schließens der Dateihandles. Dies ist kein Argument, um diese Methoden nicht zu dokumentieren, aber es ist eine Warnung, sich nicht zu sehr auf sie zu konzentrieren.

Ich stimme dem zu, was Sie sagen, mit Ausnahme des Teils, den wir hier über Python diskutieren. Ich möchte kein Argument anfangen, aber beim Lesen des Zen von Python wäre der pythonische Weg, dem expliziten Ansatz zu folgen, besser als der implizite Ansatz. Ich bin auch nicht mit dieser Projektphilosophie vertraut. Bitte ignorieren Sie meine Gedanken, wenn dies nicht für requests .

Ich würde gerne bei den CI- oder Benchmark-Tests helfen, wenn sich die Gelegenheit ergibt! Vielen Dank für die Erklärung der aktuellen Situation.

Ich glaube, ich habe die Ursache des Problems bei der Verwendung der funktionalen API gefunden. Wenn Sie tun

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

Die Steckdose scheint noch offen zu sein. Der Grund, warum ich _appears_ sage, ist, dass es immer noch ein _sock -Attribut hat, weil, wenn Sie dies tun

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

Sie sehen None gedruckt. Was also passiert, ist, dass urllib3 auf jedem HTTPResponse ein Attribut enthält, das auf den Verbindungspool verweist, aus dem es stammt. Der Verbindungspool hat die Verbindung in der Warteschlange, die den nicht geschlossenen Socket hat. Das Problem für die funktionale API wäre behoben, wenn wir in requests/api.py Folgendes tun:

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

Dann ist r.raw._pool immer noch der Verbindungspool, aber r.raw._pool.pool ist None .

Der schwierige Teil wird, was passiert, wenn Leute Sitzungen verwenden. Es ist unsinnig, sie nach jeder Anfrage close der Sitzung zu haben, was den Zweck der Sitzung zunichte macht. In der Realität ist der Speicherverlust viel schwieriger zu erkennen, wenn Sie eine Sitzung (ohne Threads) verwenden und 100 Anforderungen an dieselbe Domäne (und dasselbe Schema, z. B. https ) mithilfe einer Sitzung senden, es sei denn, Sie Warten Sie etwa 30 Sekunden, bis ein neuer Socket erstellt wurde. Das Problem ist, dass r.raw._pool , wie wir bereits gesehen haben, ein sehr veränderliches Objekt ist. Es ist ein Verweis auf den Verbindungspool, der vom Pool-Manager in Anforderungen verwaltet wird. Wenn also der Socket ersetzt wird, wird er durch Verweise auf jede Antwort ersetzt, die noch erreichbar ist (im Umfang). Was ich mehr tun muss, ist herauszufinden, ob nach dem Schließen der Verbindungspools noch Verweise auf die Sockets vorhanden sind. Wenn ich etwas finde, das an Referenzen festhält, werden wir wahrscheinlich das _real_ Speicherleck finden.

Eine Idee, die ich hatte, war, mit objgraph herauszufinden, was sich nach einem Anruf bei requests.get tatsächlich auf ein SSLSocket bezieht, und ich bekam Folgendes:

socket

Das Interessante ist, dass es anscheinend 7 Verweise auf die SSLSocket aber nur zwei Rückverweise, die objgraph finden konnte. Ich denke, eine der Referenzen ist die eine, die an objgraph übergeben wurde, und die andere ist die Bindung, die ich in dem Skript mache, das dies generiert, aber immer noch 3 oder 4 für Referenzen unberücksichtigt lässt, von denen ich nicht sicher bin, woher sie kommen.

Hier ist mein Skript, um dies zu generieren:

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)

Verwenden von

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)

Das socket-after.png zeigt dies:

socket-after

Wir entfernen also einen Verweis auf den SSL-Socket. Das heißt, wenn ich mir s._sock anschaue, ist das zugrunde liegende socket.socket geschlossen.

Nachdem wir eine Reihe lang laufender Benchmarks durchgeführt haben, haben wir Folgendes gefunden:

  • Das Aufrufen von close() hilft explizit!
  • Benutzer, die mehrere Anforderungen ausführen, sollten Session und diese ordnungsgemäß schließen, nachdem sie fertig sind. Bitte führen Sie # 2326 zusammen
  • PyPy-Benutzer sind ohne JIT besser! Oder sie sollten gc.collect() explizit aufrufen!

TL; DR; requests sieht gut aus. Unten finden Sie einige Diagramme, in denen dieses Snippet ausgeführt wird:

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 ohne 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 mit 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.'

Ich glaube, einer der Gründe, warum wir alle anfangs verwirrt waren, ist, dass für die Ausführung der Benchmarks ein größerer Satz erforderlich ist, um das Verhalten von GC von einer Implementierung zur anderen auszuschließen.

Das Ausführen der Anforderungen in einer Thread-Umgebung erfordert aufgrund der Funktionsweise von Threads eine größere Anzahl von Aufrufen (nach dem Ausführen mehrerer Thread-Pools konnten keine wesentlichen Unterschiede in der Speichernutzung festgestellt werden).

In Bezug auf PyPy mit JIT sparte das Aufrufen von gc.collect() für die gleiche Anzahl von Anrufen ~ 30% des Speichers. Aus diesem Grund glaube ich, dass JIT-Ergebnisse von dieser Diskussion ausgeschlossen werden sollten, da es darum geht, wie jeder die VM optimiert und den Code für JIT optimiert.

Okay, das Problem scheint also explizit in der Art und Weise zu liegen, wie wir mit dem Speicher umgehen, der mit der PyPy-JIT interagiert. Es könnte eine gute Idee sein, einen PyPy-Experten hinzuzuziehen: @alex?

Ich kann mir wirklich nicht vorstellen, welche Anfragen (und welche Firma) dies möglicherweise tun würden. Können Sie Ihren Test mit PYPYLOG=jit-summary:- in der Umgebung ausführen und die Ergebnisse einfügen (die einige Elemente ausdrucken, wenn der Prozess endet)?

Hoffe das hilft:

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}

Ich bin auf vertrauenswürdigem 32-Bit mit dem neuesten PyPy von https://launchpad.net/~pypy/+archive/ubuntu/ppa

31 kompilierte Pfade erklären nicht, dass mehr als 200 MB RAM verwendet werden.

Können Sie etwas in Ihr Programm einfügen, um es auszuführen?
gc.dump_rpy_heap('filename.txt') während es sich in einem sehr hohen Speicher befindet
Verwendung? (Sie müssen es nur einmal ausführen, dies erzeugt einen Speicherauszug aller
Speicher, den der GC kennt).

Führen Sie dann beim Auschecken des PyPy-Quellbaums ./pypy/tool/gcdump.py filename.txt und zeigen Sie uns die Ergebnisse.

Vielen Dank!

Am Samstag, den 08. November 2014 um 15:20:52 Uhr Stas Sușcov [email protected]
schrieb:

Hoffe das hilft:

Zeile # Speicherverwendung Inkrementieren Sie den Zeileninhalt

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
Rückverfolgung: 41 0,290082
Backend: 30 0.029096
GESAMT: 1612,933400
ops: 79116
Aufgezeichnete Operationen: 23091
Anrufe: 2567
Wachen: 7081
opt ops: 5530
Opt Guards: 1400
Kräfte: 198
abbrechen: zu lange verfolgen: 2
abort: compiling: 0
Abbruch: Vable Flucht: 9
abort: bad loop: 0
Abbruch: Kraft quasi-immut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6666
Gesamtzahl der Schleifen: 23
Gesamtzahl der Brücken: 8
Freigegebene Anzahl von Schleifen: 0
Freigegebene Anzahl Brücken: 0
[2cbb7c242e8b] jit-summary}

Ich bin auf vertrauenswürdigen 32bit mit dem neuesten PyPy von
https://launchpad.net/~pypy/+archive/ubuntu/ppa
https://launchpad.net/%7Epypy/+archive/ubuntu/ppa

- -
Antworte direkt auf diese E-Mail oder sieh sie dir auf GitHub an
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62269627
.

Log:

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}

Der Speicherauszug hier: https://gist.github.com/stas/ad597c87ccc4b563211a

Vielen Dank, dass Sie sich die Zeit genommen haben, dabei zu helfen!

Das macht also vielleicht 100 MB der Nutzung aus. Es gibt zwei Orte, an denen man sich ausruhen kann
davon kann sein, im "freien Speicher" hält der GC für verschiedene Dinge herum, und
bei Nicht-GC-Zuweisungen - dies bedeutet Dinge wie die interne OpenSSL
Zuweisungen. Ich frage mich, ob es einen guten Weg gibt, um zu sehen, ob OpenSSL-Strukturen vorhanden sind
werden durchgesickert, wird hier mit TLS getestet, wenn ja, können Sie
Versuchen Sie es mit einer Nicht-TLS-Site und prüfen Sie, ob sie reproduziert wird.

Am Samstag, den 08. November 2014 um 17:38:04 Uhr Stas Sușcov [email protected]
schrieb:

Log:

Zeile # Speicherverwendung Inkrementieren Sie den Zeileninhalt

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
Rückverfolgung: 41 0,293192
Backend: 30 0.026873
GESAMT: 1615.665337
ops: 79116
Aufgezeichnete Operationen: 23091
Anrufe: 2567
Wachen: 7081
opt ops: 5530
Opt Guards: 1400
Kräfte: 198
abbrechen: zu lange verfolgen: 2
abort: compiling: 0
Abbruch: Vable Flucht: 9
abort: bad loop: 0
Abbruch: Kraft quasi-immut: 0
nvirtuals: 9318
nvholes: 1113
nvreused: 6637
Gesamtzahl der Schleifen: 23
Gesamtzahl der Brücken: 8
Freigegebene Anzahl von Schleifen: 0
Freigegebene Anzahl Brücken: 0
[3fd756c29302] jit-summary}

Der Speicherauszug hier: https://gist.github.com/stas/ad597c87ccc4b563211a

Vielen Dank, dass Sie sich die Zeit genommen haben, dabei zu helfen!

- -
Antworte direkt auf diese E-Mail oder sieh sie dir auf GitHub an
https://github.com/kennethreitz/requests/issues/1685#issuecomment -62277822
.

@alex ,

Ich glaube, dass @stas für diesen Benchmark eine reguläre http-Verbindung (Nicht-SSL / TLS) verwendet hat. Für alle Fälle habe ich auch das Benchmark-Skript von @stas verwendet und es auf meinem Mac (OSX 10.9.5 2,5 GHz i5 8 GB 1600 MHz DDR3) mit einer regulären http-Verbindung ausgeführt.

Wenn es hilft, sind hier meine Ergebnisse zum Vergleichen (unter Verwendung Ihrer Anweisungen):
https://gist.github.com/mhjohnson/a13f6403c8c3a3d49b8d

Lass mich wissen was du denkst.

Vielen Dank,

-Matt

Der reguläre Ausdruck von GitHub ist zu locker. Ich öffne das wieder, weil ich nicht denke, dass es vollständig behoben ist.

Hallo, vielleicht könnte ich helfen, darauf hinzuweisen, dass das Problem besteht. Ich habe einen Crawler, der Anforderungen verwendet, und Process mit Multiprocessing. Es kommt vor, dass mehr als eine Instanz das gleiche Ergebnis erhält. Möglicherweise ist der Ergebnispuffer oder der Sockel selbst undicht.

Lassen Sie mich wissen, ob ich ein Beispiel des Codes senden kann oder wie der Referenzbaum generiert wird, um zu identifizieren, welcher Teil der Informationen "geteilt" wird (durchgesickert).

Vielen Dank

@barroca das ist ein anderes Problem. Sie verwenden wahrscheinlich eine Thread-übergreifende Sitzung und stream=True . Wenn Sie eine Antwort schließen, bevor Sie sie gelesen haben, wird der Socket wieder in den Verbindungspool gestellt, in dem sich die Daten noch befinden (wenn ich mich richtig erinnere). Wenn dies nicht der Fall ist, ist es auch plausibel, dass Sie die letzte Verbindung abrufen und eine zwischengespeicherte Antwort vom Server erhalten. In beiden Fällen ist dies kein Hinweis auf einen Speicherverlust.

@ sigmavirus24 Danke Ian, wie Sie bereits erwähnt haben, wurde die Sitzung über ordnungsgemäß verwendet. Vielen Dank für die Erklärung und Entschuldigung für die Aktualisierung des falschen Problems.

Keine Sorge @barroca :)

Es gab keine weiteren Beschwerden darüber und ich denke, wir haben unser Bestes gegeben. Gerne öffne ich es wieder und untersuche es bei Bedarf erneut

Was ist die Lösung für dieses Problem?

@ Makecodeeasy Das will ich auch wissen

Bisher ist mein Problem mit requests nicht threadsicher.
Verwenden Sie am besten eine separate Sitzung für einen anderen Thread.

Meine laufende Arbeit für das Durchlaufen von Millionen von URLs zur Validierung der Cache-Antwort führte mich hierher

Wenn ich feststelle, dass die Speichernutzung nicht mehr zumutbar ist, wenn requests mit ThreadPoolExecutor oder threading interagieren,
Am Ende benutze ich einfach multiprocessing.Process um den Arbeiter zu isolieren und eine unabhängige Sitzung für jeden Arbeiter zu haben

@AndCycle dann ist dein Problem nicht hier. Es wurde eine PR zusammengeführt, um diesen speziellen Speicherverlustfall zu beheben. Es ist nicht zurückgegangen, da es Tests gibt. Und Ihr Problem klingt völlig anders.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen