2009-11-16 17 views
9

Ho un'applicazione Django che presenta alcuni strani comportamenti di raccolta dei dati inutili. C'è in particolare una visione che continuerà a far crescere la dimensione della VM in modo significativo ogni volta che viene chiamata - fino a un certo limite, momento in cui l'utilizzo si riduce nuovamente. Il problema è che ci vuole molto tempo prima che venga raggiunto quel punto, e in effetti la macchina virtuale che esegue la mia app non ha memoria sufficiente per tutti i processi FCGI per occupare la stessa quantità di memoria che a volte fanno.Python: comportamento del garbage collector

Ho passato gli ultimi due giorni a indagare su questo e a conoscere la garbage collection di Python, e penso di capire cosa sta succedendo ora - per la maggior parte. Quando si utilizza

gc.set_debug(gc.DEBUG_STATS) 

Poi per una singola richiesta, viene visualizzato il seguente output:

>>> c = django.test.Client() 
>>> c.get('/the/view/') 
gc: collecting generation 0... 
gc: objects in each generation: 724 5748 147341 
gc: done. 
gc: collecting generation 0... 
gc: objects in each generation: 731 6460 147341 
gc: done. 
[...more of the same...]  
gc: collecting generation 1... 
gc: objects in each generation: 718 8577 147341 
gc: done. 
gc: collecting generation 0... 
gc: objects in each generation: 714 0 156614 
gc: done. 
[...more of the same...] 
gc: collecting generation 0... 
gc: objects in each generation: 715 5578 156612 
gc: done. 

Quindi, in sostanza, una grande quantità di oggetti sono allocati, ma sono inizialmente trasferito in generazione 1, e quando gen 1 viene spazzato durante la stessa richiesta, vengono spostati alla generazione 2. Se faccio un manuale gc.collect (2) in seguito, vengono rimossi. E, come ho già detto, viene rimosso anche quando si verifica il successivo passaggio automatico di gen 2, che, se ho capito bene, in questo caso sarebbe tipo ogni 10 richieste (a questo punto l'app ha bisogno di circa 150 MB).

OK, quindi inizialmente ho pensato che potrebbe esserci qualche riferimento ciclico all'interno dell'elaborazione di una richiesta che impedisce a questi oggetti di essere raccolti all'interno della gestione di tale richiesta. Tuttavia, ho passato ore a provare a trovarne uno usando pympler.muppy e objgraph, sia dopo che eseguendo il debug all'interno dell'elaborazione della richiesta, e non sembra che ce ne siano. Piuttosto, sembra che i 14.000 oggetti creati durante la richiesta si trovino tutti all'interno di una catena di riferimenti a qualche oggetto globale di richiesta, cioè, una volta che la richiesta scompare, possono essere liberati.

Questo è stato il mio tentativo di spiegarlo, comunque. Tuttavia, se ciò è vero e non vi sono effettivamente dipendenze cicliche, non dovrebbe essere liberato l'intero albero degli oggetti una volta che qualsiasi oggetto della richiesta che li fa ritirare va via, senza che il garbage collector sia coinvolto, semplicemente in base al conteggio dei riferimenti scendendo a zero?

Con questo setup, qui sono le mie domande:

  • Condivide la sopra nemmeno senso, o devo cercare il problema altrove? E 'solo uno sfortunato incidente che dati significativi siano tenuti in giro così a lungo in questo particolare caso d'uso?

  • C'è qualcosa che posso fare per evitare il problema. Vedo già alcune potenzialità per ottimizzare la vista, ma questa sembra essere una soluzione con uno scopo limitato - sebbene non sia sicuro di quale sia la mia generica; quanto è consigliabile, ad esempio, chiamare manualmente gc.collect() o gc.set_threshold()?

In termini di come il garbage collector stesso funziona:

  • Ho capito bene che un oggetto è sempre spostato alla prossima generazione, se una spazzata guarda e determina che i riferimenti IT è non ciclico, ma può infatti essere tracciato su un oggetto radice.

  • Cosa succede se il gc esegue, per esempio, una scansione di generazione 1 e trova un oggetto a cui fa riferimento un oggetto all'interno della generazione 2; segue quella relazione all'interno della generazione 2, o aspetta che si verifichi uno sweep di generazione 2 prima di analizzare la situazione?

  • Quando si utilizza gc.DEBUG_STATS, mi preoccupo principalmente delle informazioni "oggetti in ogni generazione"; tuttavia, continuo a ricevere centinaia di "gc: 0.0740 secondi trascorsi.", "gc: 1258233035.9370 secondi trascorsi." messaggi; sono totalmente scomodi - ci vuole un tempo considerevole per essere stampati, e rendono le cose interessanti molto più difficili da trovare. C'è un modo per sbarazzarsi di loro?

  • Suppongo che non esista un modo per eseguire gc.get_objects() per generazione, ad esempio recuperare gli oggetti dalla generazione 2, ad esempio?

risposta

2

Penso che la tua analisi sembri valida. Non sono un esperto dello gc, quindi ogni volta che ho un problema di questo tipo, aggiungo una chiamata allo gc.collect() in un luogo appropriato, non critico, e non ci penso.

Ti suggerisco di chiamare gc.collect() nella tua vista e vedere quale effetto ha sul tempo di risposta e sull'utilizzo della memoria.

Nota anche this question che suggerisce che l'impostazione DEBUG=True mangia memoria in quanto è quasi esaurita per data di vendita.

+0

+1 per menzionare l'impostazione DEBUG = False in modo che Django non registri tutte le query SQL. – Kekoa

+0

wow, che immagine mentale di Django che mangia molto per il cibo –

3

Questo ha senso, oppure devo cercare il problema altrove? E 'solo uno sfortunato incidente che dati significativi siano tenuti in giro così a lungo in questo particolare caso d'uso?

Sì, ha senso. E sì, ci sono altri problemi da considerare. Django utilizza threading.local come base per DatabaseWrapper (e alcuni contributi lo usano per rendere l'oggetto richiesta accessibile da luoghi in cui non è passato esplicitamente). Questi oggetti globali sopravvivono alle richieste e possono mantenere i riferimenti agli oggetti fino a che un'altra vista non viene gestita nel thread.

C'è qualcosa che posso fare per evitare il problema. Vedo già alcune potenzialità per ottimizzare la vista, ma questa sembra essere una soluzione con uno scopo limitato - sebbene non sia sicuro di quale sia la mia generica; quanto è consigliabile, ad esempio, chiamare manualmente gc.collect() o gc.set_threshold()?

Consiglio generale (probabilmente lo si conosce, ma comunque): evitare riferimenti circolari e globali (incluso threading.local). Cerca di interrompere i cicli e cancellare i globali quando il design del django rende difficile evitarli. gc.get_referrers(obj) potrebbe aiutarti a trovare luoghi che richiedono attenzione. Un altro modo per disabilitare il garbage collector e chiamarlo manualmente dopo ogni richiesta, quando è il posto migliore da fare (questo impedirà agli oggetti di passare alla generazione successiva).

Suppongo che non esista un modo per eseguire gc.get_objects() per generazione, ad esempio recuperare gli oggetti dalla generazione 2, ad esempio?

Sfortunatamente questo non è possibile con l'interfaccia gc. Ma ci sono diversi modi per andare. È possibile considerare solo la fine dell'elenco restituito da gc.get_objects(), poiché gli oggetti in questo elenco sono ordinati per generazione.È possibile confrontare l'elenco con quello restituito dalla chiamata precedente memorizzando riferimenti deboli a tali indirizzi (ad esempio in WeakKeyDictionary) tra le chiamate. È possibile riscrivere gc.get_objects() nel proprio modulo C (è facile, in gran parte la programmazione di copia-incolla!) Poiché sono memorizzati internamente alla generazione, o addirittura accedere alle strutture interne con ctypes (richiede una comprensione piuttosto profonda ctypes).

+0

get_objects() in fase di ordinamento è abbastanza buono, grazie per il suggerimento. – miracle2k