7

ndbライブラリにメモリリークがありますが、どこに見つけることができないと思います。Google ndbライブラリのメモリリーク

下記の問題を回避する方法はありますか?
問題がどこにあるのかをテストするためのより正確な考え方がありますか?

私は、問題を再現する方法です

私は2つのファイルでミニマルなGoogle App Engineアプリケーションを作成しました。
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), 
]) 

私は/create一度と呼ばれるアプリケーションを、アップロードしました。
その後、/を呼び出すたびに、インスタンスによって使用されるメモリが増加します。エラーExceeded soft private memory limit of 128 MB with 143 MB after servicing 5 requests totalにより停止するまで。

メモリ使用量グラフのExemple(あなたは、メモリの成長とクラッシュを見ることができます): enter image description here

注:この問題は既知の問題がありweb.py

+2

おそらく[コンテキスト内のキャッシュ](https://cloud.google.com/appengine/docs/python/ndb/cache)だと思います。 –

+0

私はあなたの 'ndb.put_multi'が1つのトランザクションに100個のエンティティを挿入しようとしているので、Pythonについては分かりませんが、あなたのコードを読んでいます。これはおそらく、多くのメモリが割り当てられる原因になります。ソフトプライベートメモリの制限を超えているのは、次回のリクエストがメモリ負荷を増やしてもトランザクションがまだ実行されているためです。これは、コールの間にしばらく待ってから(トランザクションが完了するまでそれぞれ待つ)発生しません。また、応答時間が大幅に増加した場合、App Engineは追加のインスタンスを開始する必要があります。 – konqi

+0

@DanielRoseman "コンテキスト内キャッシュは、単一スレッドの間だけ持続します。"コンテキスト内キャッシュをクリアしたり、キャッシュを無効にするポリシーを設定したりすると、メモリ使用量の増加は緩やかになりますが、リークは持続します。 – greg

答えて

4

多くの調査とGoogleエンジニアの助けを借りて、私は2つのメモリ消費量の説明を見つけました。

コンテキストとスレッド

ndb.Context「スレッドローカル」オブジェクトであり、新しい要求がスレッドに来たときにのみクリアされます。したがって、スレッドはリクエスト間でスレッドを保持します。 GAEインスタンスには多くのスレッドが存在し、スレッドが2度目に使用されてコンテキストがクリアされる前に何百もの要求がかかることがあります。
これはメモリリークではありませんが、メモリ内のコンテキストサイズは、小さなGAEインスタンスで使用可能なメモリを超えている可能性があります。

回避方法:
GAEインスタンスで使用するスレッドの数を設定することはできません。したがって、各コンテキストを最小限に抑えることが最善です。コンテキスト内のキャッシュを避け、各要求後にクリアします。

イベントキュー

NDBは、そのイベントキューが要求した後、空にされて保証するものではないようです。これもメモリリークではありません。しかし、スレッドのコンテキストにはFuturesが残っています。最初の問題に戻ります。

回避策:
@ndb.toplevelとNDBを使用するすべてのコードを包みます。

+0

Greg、Googleのエンジニアが、これが意図した動作かバグかどうかわかりますか?確かに私にはバグのようだ。 –

+0

リクエストごとにどのようにコンテキストをクリアしますか? – diogovk

+0

私は上記のことをすべて行い、問題についてGoogleのサポートに連絡しても、それが存在することを認めない。私はまだリークが非常に極端なため、ndbエントリを繰り返し処理し、結果をbigqueryにキューイングし、500Mのメモリを2〜3分でリークさせるプロセスはほとんどありません。その他の可能な説明は? – Sniggerfardimungus

3

のように、webapp2よりも、別の枠組みで再現することができますNDBとあなたはおよそit hereを読むことができ、作業around hereがあります:fetch_pageで観察

非決定論がdatastore_rpc.MultiRpc.wait_any()とapiproxy_stub_mapに渡されeventloop.rpcsの繰り返し順序によるものであるが。 __check_oneはイテレータからの最後の rpcを選択します。

page_sizeを10にフェッチすると、結果がさらにあるかどうかをより正確に判断する標準的な手法であるcount = 10、limit = 11のrpcが実行されます。これは10個の結果を返しますが、QueryIteratorが解読される方法のバグにより、最後のエントリを取得するためにRPCが追加されます(取得されたカーソルとcount = 1を使用して)。 NDBは、このRPCを処理せずにエンティティのバッチを返します。私は、このRPCは、クライアントコードをブロックしないので、ランダムに選択するまで(MultiRpcが必要なrpcの前にそれを消費する場合)評価されないと考えています。

回避方法:iter()を使用してください。この機能にはこの問題はありません(カウントとリミットは同じになります)。上記に起因するページの取得に関連するパフォーマンスとメモリの問題の回避策として、iter()を使用できます。

+0

私はこれらのスレッドを読みましたが、 'iter()'を使用してもメモリリークは防げません。 – greg

+0

エンジニアが見ることができるように、結果をスレッドに投稿する必要があります。 – Ryan

+0

グレッグ、あなたとパリでおしゃべりしています。代わりに "iter()"を使用してコードを編集し、メモリリークの証拠を提供することをお勧めします。 – Riccardo

1

可能な回避策は、GETメソッドにcontext.clear_cache()gc.collect()を使用することです。

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()