Abbiamo trovato la risposta a questo enigma, almeno lo speriamo. Se continui con qualsiasi domanda, condividila in un commento, ti aiuteremo volentieri
Soluzione:
Utilizzando la funzione di Python gc
del garbage collector e sys.getsizeof()
è possibile scaricare tutti gli oggetti di Python e le loro dimensioni. Ecco il codice che sto usando in produzione per risolvere una perdita di memoria:
rss = psutil.Process(os.getpid()).get_memory_info().rss
# Dump variables if using more than 100MB of memory
if rss > 100 * 1024 * 1024:
memory_dump()
os.abort()
def memory_dump():
dump = open("memory.pickle", 'wb')
xs = []
for obj in gc.get_objects():
i = id(obj)
size = sys.getsizeof(obj, 0)
# referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
if hasattr(obj, '__class__'):
cls = str(obj.__class__)
xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
cPickle.dump(xs, dump)
Si noti che sto salvando solo i dati degli oggetti che hanno un valore __class__
perché sono gli unici oggetti che mi interessano. Dovrebbe essere possibile salvare l'elenco completo degli oggetti, ma si dovrà fare attenzione a scegliere altri attributi. Inoltre, ho scoperto che ottenere i referenti per ogni oggetto era estremamente lento, quindi ho scelto di salvare solo i referenti. In ogni caso, dopo il crash, i dati decapati risultanti possono essere letti in questo modo:
with open("memory.pickle", 'rb') as dump:
objs = cPickle.load(dump)
Aggiunto 2017-11-15
La versione di Python 3.6 è qui:
import gc
import sys
import _pickle as cPickle
def memory_dump():
with open("memory.pickle", 'wb') as dump:
xs = []
for obj in gc.get_objects():
i = id(obj)
size = sys.getsizeof(obj, 0)
# referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
if hasattr(obj, '__class__'):
cls = str(obj.__class__)
xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
cPickle.dump(xs, dump)
Espanderò la risposta di Brett dalla mia recente esperienza. Il pacchetto Dozer è ben mantenuto, e nonostante i progressi, come l'aggiunta di tracemalloc
a stdlib in Python 3.4, il suo pacchetto gc.get_objects
è il mio strumento preferito per affrontare le perdite di memoria. Qui di seguito uso dozer > 0.7
che non è stato rilasciato al momento della stesura di questo articolo (anche perché recentemente ho contribuito con un paio di correzioni).
Esempio
Vediamo una perdita di memoria non banale. Utilizzerò Celery 4.4 e alla fine scoprirò una caratteristica che causa la perdita (e poiché si tratta di un bug/caratteristica, può essere definita una semplice configurazione errata, causata dall'ignoranza). Quindi c'è un Python 3.6 venv dove io pip install celery < 4.5
. E ho il seguente modulo.
demo.py
import time
import celery
redis_dsn = 'redis://localhost'
app = celery.Celery('demo', broker=redis_dsn, backend=redis_dsn)
@app.task
def subtask():
pass
@app.task
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
if __name__ == '__main__':
task.delay().get()
In pratica un task che pianifica un gruppo di sottotask. Cosa può andare storto?
Userò procpath
per analizzare il consumo di memoria del nodo Celery. pip install procpath
. Ho 4 terminali:
procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]"
per registrare le statistiche dell'albero dei processi del nodo Celery.docker run --rm -it -p 6379:6379 redis
per eseguire Redis che servirà come broker di Celery e backend dei risultaticelery -A demo worker --concurrency 2
per eseguire il nodo con 2 lavoratoripython demo.py
per eseguire infine l'esempio
(4) finirà in meno di 2 minuti.
Poi uso sqliteviz (versione precostituita) per visualizzare ciò che procpath
ha il registratore. Lascio cadere il celery.sqlite
e uso questa query:
SELECT datetime(ts, 'unixepoch', 'localtime') ts, stat_pid, stat_rss / 256.0 rss
FROM record
E in sqliteviz creo una traccia di grafico a linee con X=ts
, Y=rss
e aggiungo la trasformazione divisa By=stat_pid
. Il grafico risultante è:
Questa forma è probabilmente abbastanza familiare a chiunque abbia combattuto con le perdite di memoria.
Trovare gli oggetti che perdono memoria
Ora è il momento di dozer
. Mostrerò un caso non strumentato (e voi potete strumentare il vostro codice in modo simile, se potete). Per iniettare il server Dozer nel processo target userò Pyrasite. Ci sono due cose da sapere su di esso:
- Per funzionare, ptrace deve essere configurato come "permessi classici di ptrace":
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
il che può essere un rischio per la sicurezza - Ci sono possibilità non nulle che il processo Python di destinazione vada in crash
Con questo avvertimento io:
pip install https://github.com/mgedmin/dozer/archive/3ca74bd8.zip
(è il to-be 0.8 di cui ho parlato sopra)pip install pillow
(chedozer
utilizza per i grafici)pip install pyrasite
Dopo di che posso ottenere la shell Python nel processo di destinazione:
pyrasite-shell 26572
E iniettare quanto segue, che eseguirà l'applicazione WSGI di Dozer utilizzando stdlib wsgiref
del server.
import threading
import wsgiref.simple_server
import dozer
def run_dozer():
app = dozer.Dozer(app=None, path='/')
with wsgiref.simple_server.make_server('', 8000, app) as httpd:
print('Serving Dozer on port 8000...')
httpd.serve_forever()
threading.Thread(target=run_dozer, daemon=True).start()
Apertura http://localhost:8000
in un browser si dovrebbe vedere qualcosa come:
Dopo di che eseguo python demo.py
da (4) e aspetto che finisca. Poi in Dozer imposto "Floor" a 5000 ed ecco cosa vedo:
Due tipi relativi a Celery grow come sottoattività sono programmati:
celery.result.AsyncResult
vine.promises.promise
weakref.WeakMethod
ha la stessa forma e gli stessi numeri e deve essere causato dalla stessa cosa.
Trovare la causa principale
A questo punto, dai tipi di perdite e dalle tendenze potrebbe essere già chiaro cosa sta succedendo nel vostro caso. Se così non fosse, Dozer ha un link "TRACE" per tipo, che consente di tracciare (ad esempio, vedere gli attributi dell'oggetto) i referrer dell'oggetto scelto (gc.get_referrers
) e i referenti (gc.get_referents
) e di continuare il processo attraversando nuovamente il grafo.
Ma un'immagine dice mille parole, giusto? Quindi mostrerò come usare objgraph
per rendere il grafico delle dipendenze di un oggetto scelto.
pip install objgraph
apt-get install graphviz
Poi:
- Eseguo
python demo.py
da (4) di nuovo - in Dozer ho impostato
floor=0
,filter=AsyncResult
- e faccio clic su "TRACE" che dovrebbe produrre
Poi in Pyrasite shell eseguo:
objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')
Il file PNG dovrebbe contenere:
Fondamentalmente ci sono alcuni Context
contenente un oggetto list
chiamato _children
che a sua volta contiene molte istanze di celery.result.AsyncResult
, che perdono. Cambiare Filter=celery.*context
in Dozer ecco cosa vedo:
Quindi il colpevole è celery.app.task.Context
. La ricerca di questo tipo porterebbe certamente alla pagina dei task di Celery. Facendo una rapida ricerca di "children", ecco cosa dice:
trail = True
Se abilitata, la richiesta terrà traccia delle sottoattività avviate da questo task e queste informazioni saranno inviate con il risultato (
result.children
).
Disabilitare la traccia impostando trail=False
come:
@app.task(trail=False)
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
Quindi riavviare il nodo Celery da (3) e python demo.py
da (4) ancora una volta, mostra questo consumo di memoria.
Problema risolto!
Si potrebbe registrare il traffico (tramite un log) sul sito di produzione, quindi riprodurlo sul server di sviluppo con un debugger di memoria python (consiglio dozer: http://pypi.python.org/pypi/Dozer).
Alla fine del post trovi le dimensioni di altri amministratori, hai anche la facoltà di inserire la tua se vuoi.