Python: Verhalten des Garbage Collectors

9

Ich habe eine Django-Anwendung, die seltsames Garbage Collection-Verhalten aufweist. Vor allem gibt es eine Ansicht, die die VM-Größe bei jedem Aufruf deutlich erhöht - bis zu einem bestimmten Limit, bei dem die Nutzung wieder zurückfällt. Das Problem ist, dass es ziemlich lange dauert, bis dieser Punkt erreicht ist. Tatsächlich verfügt der virtuelle Computer, auf dem meine App ausgeführt wird, nicht über genügend Arbeitsspeicher, damit alle FCGI-Prozesse so viel Speicher belegen können, wie sie es manchmal tun.

Ich habe die letzten zwei Tage damit verbracht, dies zu untersuchen und etwas über die Python-Müllsammlung zu lernen, und ich glaube, ich verstehe, was jetzt passiert - größtenteils. Bei Verwendung von

%Vor%

Dann sehe ich für eine einzelne Anfrage die folgende Ausgabe:

%Vor%

Im Wesentlichen wird also eine riesige Menge an Objekten zugewiesen, die jedoch anfänglich in Generation 1 verschoben werden. Wenn Gen 1 während der gleichen Anfrage durchlaufen wird, werden sie in Generation 2 verschoben. Wenn ich ein manuelles gc.collect (2 ) Danach werden sie entfernt. Und, wie ich bereits erwähnt habe, da entfernt sich auch der nächste automatische Gen 2 Sweep, der, wenn ich das richtig verstehe, in diesem Fall etwa alle 10 Anfragen (an dieser Stelle benötigt die App etwa 150MB).

Okay, zunächst dachte ich, dass es bei der Verarbeitung einer Anfrage eine zyklische Referenzierung geben könnte, die verhindert, dass eines dieser Objekte bei der Bearbeitung dieser Anfrage gesammelt wird. Allerdings habe ich Stunden damit verbracht, einen zu finden, der pympler.muppy und objgraph verwendet, sowohl nach als auch durch das Debugging innerhalb der Anfrageverarbeitung, und es scheint keine zu geben. Vielmehr scheinen die 14.000 Objekte oder so, die während der Anfrage erzeugt werden, alle innerhalb einer Referenzkette zu einem Objekt mit Anfrage-Global zu sein, d. H. Sobald die Anfrage verschwindet, können sie freigegeben werden.

Das war mein Versuch, es auf jeden Fall zu erklären. Wenn dies jedoch der Fall ist und tatsächlich keine zyklischen Abhängigkeiten vorhanden sind, sollte nicht der gesamte Baum der Objekte freigegeben werden, sondern nur aufgrund der Referenzzählungen, welches Anfrageobjekt auch immer gehalten wird, ohne dass der Müllsammler beteiligt ist auf Null gehen?

Mit diesem Setup sind hier meine Fragen:

  • Macht das Obige überhaupt Sinn, oder muss ich anderswo nach dem Problem suchen? Ist es nur ein unglücklicher Zufall, dass in diesem speziellen Anwendungsfall so lange Daten gespeichert werden?

  • Gibt es etwas, was ich tun kann, um das Problem zu vermeiden? Ich sehe bereits ein gewisses Potenzial, um die Ansicht zu optimieren, aber das scheint eine Lösung mit begrenztem Umfang zu sein - obwohl ich mir nicht sicher bin, was ich allgemein wäre; Wie empfiehlt es sich, gc.collect () oder gc.set_threshold () manuell aufzurufen?

In Bezug darauf, wie der Garbage Collector selbst funktioniert:

  • Verstehe ich richtig, dass ein Objekt immer in die nächste Generation verschoben wird, wenn ein Sweep es betrachtet und feststellt, dass die Referenzen nicht zyklisch sind, aber tatsächlich verfolgt werden können zu einem Wurzelobjekt.

  • Was passiert, wenn der GC einen Sweep der Generation 1 ausführt und ein Objekt findet, auf das von einem Objekt innerhalb der Generation 2 verwiesen wird? Folgt es dieser Beziehung innerhalb der zweiten Generation oder wartet es auf einen zweiten Generation-Sweep, bevor die Situation analysiert wird?

  • Bei der Verwendung von gc.DEBUG_STATS interessiert mich in erster Linie die Information "Objekte in jeder Generation"; Allerdings bekomme ich immer Hunderte von "gc: 0.0740s verstrichen.", "gc: 1258233035.9370s verstrichen." Mitteilungen; Sie sind völlig unpraktisch - sie brauchen viel Zeit, um gedruckt zu werden, und sie machen die interessanten Dinge viel schwieriger zu finden. Gibt es eine Möglichkeit, sie loszuwerden?

  • Ich nehme nicht an, dass es eine Möglichkeit gibt, eine gc.get_objects () nach Generation zu erstellen, d. h. nur die Objekte aus Generation 2 abzurufen, zum Beispiel?

miracle2k 16.11.2009, 06:20
quelle

2 Antworten

3
  

Macht das Obige überhaupt Sinn, oder muss ich anderswo nach dem Problem suchen? Ist es nur ein unglücklicher Zufall, dass signifikante Daten in diesem speziellen Anwendungsfall so lange aufbewahrt werden?

Ja, es macht Sinn. Und ja, es gibt noch andere Fragen, die es zu überdenken gibt. Django verwendet threading.local als Basis für DatabaseWrapper (und einige Beiträge verwenden es, um das Anfrageobjekt von Orten zugänglich zu machen, an denen es nicht explizit übergeben wird). Diese globalen Objekte überleben Anfragen und können Verweise auf Objekte behalten, bis eine andere Ansicht im Thread behandelt wird.

  

Gibt es etwas, was ich tun kann, um das Problem zu vermeiden? Ich sehe bereits ein gewisses Potenzial, um die Ansicht zu optimieren, aber das scheint eine Lösung mit begrenztem Umfang zu sein - obwohl ich mir nicht sicher bin, was ich allgemein wäre; Wie sinnvoll ist es beispielsweise, gc.collect () oder gc.set_threshold () manuell aufzurufen?

Allgemeiner Hinweis (wahrscheinlich wissen Sie es, aber trotzdem): Vermeiden Sie zirkuläre Referenzen und globale (einschließlich threading.local ). Versuche, Zyklen zu durchbrechen und Globals zu löschen, wenn Django-Design es schwierig macht, sie zu vermeiden. gc.get_referrers(obj) kann Ihnen helfen, Orte zu finden, die Aufmerksamkeit erfordern. Eine weitere Möglichkeit, den Garbage Collector zu deaktivieren und nach jeder Anforderung manuell aufzurufen, wenn dies der beste Ort ist (dies verhindert, dass Objekte in die nächste Generation verschoben werden).

  

Ich nehme nicht an, dass es eine Möglichkeit gibt, eine gc.get_objects () nach Generation zu erstellen, d. h. nur die Objekte aus Generation 2 abzurufen, zum Beispiel?

Leider ist das mit gc interface nicht möglich. Aber es gibt mehrere Wege zu gehen. Sie können nur das Ende der Liste berücksichtigen, die von gc.get_objects() zurückgegeben wird, da die Objekte in dieser Liste nach Generierung sortiert sind. Sie können die Liste mit einer aus dem vorherigen Aufruf zurückgegebenen vergleichen, indem Sie zwischen den Aufrufen schwache Referenzen auf sie speichern (z. B. in WeakKeyDictionary ). Sie können gc.get_objects() in Ihr eigenes C-Modul schreiben (es ist einfach, hauptsächlich Copy-Paste-Programmierung!), Da sie intern gespeichert werden oder sogar auf interne Strukturen mit ctypes zugreifen (erfordert ziemlich tiefes ctypes Verständnis) / p>     

Denis Otkidach 16.11.2009 09:55
quelle
2

Ich denke, Ihre Analyse sieht gut aus. Ich bin kein Experte für die gc , also wenn ich ein Problem wie dieses habe, füge ich einfach einen Aufruf zu gc.collect() an einem geeigneten, nicht zeitkritischen Ort hinzu und vergesse es.

Ich würde vorschlagen, dass Sie in Ihren Views gc.collect() aufrufen und sehen, wie sich dies auf Ihre Antwortzeit und Ihre Speichernutzung auswirkt.

Beachten Sie auch diese Frage , die darauf hinweist, dass das Festlegen von DEBUG=True Speicher wie fast verbraucht nach seinem Verfalldatum.

    
Nick Craig-Wood 16.11.2009 09:01
quelle