7

Penso che ci sia una perdita di memoria nella libreria ndb ma non riesco a trovare dove.Perdita di memoria nella libreria ndb di Google

C'è un modo per evitare il problema descritto di seguito?
Hai un'idea più precisa dei test per capire dove si trova il problema?


È così che ho riprodotto il problema:

ho creato un minimalista applicazione Google App Engine con 2 file.
app.yaml:

application: myapplicationid 
version: demo 
runtime: python27 
api_version: 1 
threadsafe: yes 


handlers: 
- url: /.* 
    script: main.APP 

libraries: 
- name: webapp2 
    version: latest 

main.py:

# -*- coding: utf-8 -*- 
"""Memory leak demo.""" 
from google.appengine.ext import ndb 
import webapp2 


class DummyModel(ndb.Model): 

    content = ndb.TextProperty() 


class CreatePage(webapp2.RequestHandler): 

    def get(self): 
     value = str(102**100000) 
     entities = (DummyModel(content=value) for _ in xrange(100)) 
     ndb.put_multi(entities) 


class MainPage(webapp2.RequestHandler): 

    def get(self): 
     """Use of `query().iter()` was suggested here: 
      https://code.google.com/p/googleappengine/issues/detail?id=9610 
     Same result can be reproduced without decorator and a "classic" 
      `query().fetch()`. 
     """ 
     for _ in range(10): 
      for entity in DummyModel.query().iter(): 
       pass # Do whatever you want 
     self.response.headers['Content-Type'] = 'text/plain' 
     self.response.write('Hello, World!') 


APP = webapp2.WSGIApplication([ 
    ('/', MainPage), 
    ('/create', CreatePage), 
]) 

ho caricato l'applicazione, chiamata /create una volta.
Successivamente, ogni chiamata a / aumenta la memoria utilizzata dall'istanza. Fino a quando non si arresta a causa dell'errore Exceeded soft private memory limit of 128 MB with 143 MB after servicing 5 requests total.

Esempio di grafico di utilizzo della memoria (si può vedere la crescita della memoria e va in crash): enter image description here

Nota: Il problema può essere riprodotto con un altro quadro di webapp2, come web.py

+2

Probabilmente il [NDB cache in-context] (https://cloud.google.com/appengine/docs/python/ndb/cache), mi aspetto. –

+0

Non conosco niente di Python ma leggendo il tuo codice direi che stai esaurendo la memoria perché il tuo 'ndb.put_multi' tenta di inserire 100 entità in una singola transazione. Questo è probabilmente ciò che causa l'allocazione di molta memoria. Il superamento del limite di memoria privata limitata è probabilmente dovuto al fatto che le transazioni sono ancora in esecuzione quando la richiesta successiva viene aggiunta al carico di memoria. Ciò non dovrebbe verificarsi se si attende un po 'tra le chiamate (rispettivamente attendere fino al completamento della transazione). Anche App Engine dovrebbe avviare un'istanza aggiuntiva se i tempi di risposta aumentano drasticamente. – konqi

+0

@DanielRoseman "La cache nel contesto persiste solo per la durata di un singolo thread." Se si cancella la cache nel contesto o si imposta un criterio per disabilitare la memorizzazione nella cache, l'utilizzo della memoria aumenta più lentamente ma la perdita persiste. – greg

risposta

4

Dopo ulteriori indagini e con l'aiuto di un ingegnere google, ho trovato due spiegazioni sul consumo di memoria.

Contesto e filo

ndb.Context è un oggetto "filo locale" e vengono cancellati solo quando una nuova richiesta di venire nel thread. Quindi il thread lo tiene tra le richieste. Molti thread possono esistere in un'istanza di GAE e potrebbero essere necessarie centinaia di richieste prima che un thread venga utilizzato una seconda volta e il contesto venga cancellato.
Questa non è una perdita di memoria, ma le dimensioni dei contesti nella memoria possono superare la memoria disponibile in una piccola istanza GAE.

Soluzione:
Non è possibile configurare il numero di thread utilizzati in un'istanza GAE. Quindi è meglio mantenere ogni contesto il più piccolo possibile. Evita la cache nel contesto e cancellala dopo ogni richiesta.

coda di eventi

Sembra che NDB non garantisce che coda di evento viene svuotata dopo una richiesta. Ancora una volta questa non è una perdita di memoria. Ma lascia il Futures nel contesto del thread e si torna al primo problema.

Soluzione:
Wrap tutto il codice che utilizzano NDB con @ndb.toplevel.

+0

Greg, l'ingegnere di Google ti ha dato qualche indicazione se questo è un comportamento previsto o un bug? Sicuramente mi sembra un insetto. –

+0

Come si cancella il contesto dopo ogni richiesta? – diogovk

+0

Ho fatto tutto quanto sopra, e ho persino contattato l'assistenza di Google per il problema ... e non riconoscono nemmeno che esiste. Ho ancora una perdita che è così estrema che un processo che fa un po 'di più che itera attraverso le voci ndb e accoda i risultati a bigquery, perde 500M di memoria nel giro di un paio di minuti. Altre possibili spiegazioni? – Sniggerfardimungus

3

C'è un problema noto con NDB. Si può leggere su it here e c'è un lavoro around here:

La non-determinismo osservato con fetch_page è dovuta all'ordine iterazione del eventloop.rpcs, che è passato a datastore_rpc.MultiRpc.wait_any() e apiproxy_stub_map. __check_one seleziona lo rpc dell'dall'iteratore.

Il recupero con page_size di 10 fa un rpc con count = 10, limit = 11, una tecnica standard per forzare il backend a determinare con maggiore precisione se ci sono più risultati. Questo restituisce 10 risultati, ma a causa di un bug nel modo in cui il QueryIterator viene svelato, viene aggiunto un RPC per recuperare l'ultima voce (utilizzando il cursore ottenuto e count = 1). NDB restituisce quindi il batch di entità senza elaborare questo RPC. Credo che questo RPC non verrà valutato fino a quando non viene selezionato casualmente (se MultiRpc lo consuma prima di un rpc necessario), poiché non blocca il codice client.

Soluzione alternativa: utilizzare iter(). Questa funzione non ha questo problema (il conteggio e il limite saranno gli stessi). iter() può essere utilizzato come soluzione alternativa per i problemi di prestazioni e memoria associati alla pagina di recupero causata da quanto sopra.

+0

Ho letto questi thread, ma l'uso di 'iter()' non impedisce la perdita di memoria. – greg

+0

Dovresti pubblicare i tuoi risultati sui thread in modo che gli Ingegneri possano vederli. – Ryan

+0

Greg, è bello chiacchierare con te a Parigi. Vorrei suggerire di modificare il codice utilizzando "iter()" invece e fornire prove della perdita di memoria. – Riccardo

1

Una possibile soluzione è quella di utilizzare context.clear_cache() e GC.Collect() sul metodo get.

def get(self): 

    for _ in range(10): 
     for entity in DummyModel.query().iter(): 
      pass # Do whatever you want 
    self.response.headers['Content-Type'] = 'text/plain' 
    self.response.write('Hello, World!') 
    context = ndb.get_context() 
    context.clear_cache() 
    gc.collect()