Skip to content

Come posso trovare cosa sta utilizzando la memoria in un processo Python in un sistema di produzione?

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:

  1. procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]" per registrare le statistiche dell'albero dei processi del nodo Celery.
  2. docker run --rm -it -p 6379:6379 redis per eseguire Redis che servirà come broker di Celery e backend dei risultati
  3. celery -A demo worker --concurrency 2 per eseguire il nodo con 2 lavoratori
  4. python 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=rsse aggiungo la trasformazione divisa By=stat_pid. Il grafico risultante è:

Perdita del nodo di sedano

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_scopeil 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 (che dozer 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 wsgirefdel 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:

Dozer

Dopo di che eseguo python demo.py da (4) e aspetto che finisca. Poi in Dozer imposto "Floor" a 5000 ed ecco cosa vedo:

Dozer mostra la perdita di sedano

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

traccia

Poi in Pyrasite shell eseguo:

objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')

Il file PNG dovrebbe contenere:

grafico backref

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:

Contesto di sedano

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.

risolto

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.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.