python - Fuga de memoria en la biblioteca de Google ndb
google-app-engine memory-leaks (3)
Creo que hay una pérdida de memoria en la biblioteca ndb
, pero no puedo encontrar dónde.
¿Hay alguna manera de evitar el problema que se describe a continuación?
¿Tiene una idea más precisa de las pruebas para descubrir dónde está el problema?
Así es como reproduje el problema:
Creé una aplicación minimalista de Google App Engine con 2 archivos.
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),
])
Cargué la aplicación, llamada /create
una vez.
Después de eso, cada llamada a /
aumenta la memoria utilizada por la instancia. Hasta que se detenga debido al error Exceeded soft private memory limit of 128 MB with 143 MB after servicing 5 requests total
.
Ejemplo de gráfico de uso de memoria (puede ver el crecimiento de la memoria y los bloqueos):
Nota: El problema se puede reproducir con otro marco que con webapp2
, como web.py
Hay un problema conocido con NDB. Puedes leer sobre esto aquí y hay un trabajo por aquí :
El no determinismo observado con fetch_page se debe al orden de iteración de eventloop.rpcs, que se pasa a datastore_rpc.MultiRpc.wait_any () y apiproxy_stub_map .__ check_one selecciona el último rpc del iterador.
La obtención con page_size de 10 hace un rpc con count = 10, limit = 11, una técnica estándar para forzar al backend a determinar con mayor precisión si hay más resultados. Esto arroja 10 resultados, pero debido a un error en la forma en que se descifra QueryIterator, se agrega una RPC para recuperar la última entrada (utilizando el cursor obtenido y el recuento = 1). NDB luego devuelve el lote de entidades sin procesar este RPC. Creo que este RPC no se evaluará hasta que se seleccione al azar (si MultiRpc lo consume antes de un rpc necesario), ya que no bloquea el código del cliente.
Solución alternativa: use iter (). Esta función no tiene este problema (el recuento y el límite serán los mismos). iter () se puede usar como una solución para los problemas de rendimiento y memoria asociados con la página de búsqueda causados por lo anterior.
Después de más investigaciones y con la ayuda de un ingeniero de Google, encontré dos explicaciones sobre mi consumo de memoria.
Contexto e hilo
ndb.Context
es un objeto "thread local" y solo se borra cuando aparece una nueva solicitud en el hilo. Entonces, el hilo lo mantiene entre las solicitudes. Pueden existir muchos subprocesos en una instancia de GAE y puede tomar cientos de solicitudes antes de que un subproceso se use por segunda vez y se borre el contexto.
Esto no es una pérdida de memoria, pero el tamaño del contexto en la memoria puede exceder la memoria disponible en una pequeña instancia de GAE.
Solución:
No puede configurar la cantidad de hilos utilizados en una instancia de GAE. Por lo tanto, es mejor mantener cada contexto lo más pequeño posible. Evite el caché en contexto y desactívelo después de cada solicitud.
Cola de eventos
Parece que NDB no garantiza que la cola de eventos se vacíe después de una solicitud. De nuevo, esto no es una pérdida de memoria. Pero deja Futures
en su contexto de hilo, y vuelve al primer problema.
Solución:
Envuelva todo su código que usa NDB con @ndb.toplevel
.
Una posible solución alternativa es usar context.clear_cache () y gc.collect () en el método 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()