Pythonアプリケーションでのメモリリークのトラブルシューティング

画像 最近、私はたまたま人気のあるトルネードフレームワークでいくつかのメモリリークを整理して修正しました。 説明したことはほとんど関係ないので、一度も使用したことがない場合は関係ありません。 リークを見つけて修正するために使用した方法についてお話したいと思います。



上記のすべては、ほとんどの場合、最も一般的なPython実装であるCPythonにのみ当てはまります。 ご存知のように、メモリを解放するための2つのメカニズムがあります。 1つ目はリンクカウントです。 新しいオブジェクトを明示的または暗黙的に作成するたびに、その参照カウントは1です。 このオブジェクトを新しい変数に割り当てるか、引数として渡すと、参照カウントが増加します。 関数を終了すると、ローカル変数と引数にあったオブジェクトへの参照の数が減少します。 一部のオブジェクトでリンクの数がゼロになった場合、すぐに破棄されます。



このスキーマは、相互に参照しているオブジェクトが表示されるまで正常に機能します。 最も単純な例は、子ノードと親ノードへのリンクを格納するツリーのノードです。 ノードへの外部リンクが他にない場合でも、ノードは引き続き相互にリンクします。 最も不快なことは、そのようなノードが他のデータを参照し、それらが解放されないようにすることができるということです。 このような循環参照を排除するために、Pythonにはメモリを解放するための2番目のメカニズム-ガベージコレクタがあります。 それは時々開始し、残りのコードを一時停止し、未リリースのオブジェクトをすべて解析します。



正式には、循環リンクをリークと呼ぶことはできません。ガベージコレクションは、遅かれ早かれそのようなオブジェクトを破壊します。 唯一の問題は、Pythonが早すぎる時期を判断できないが、遅すぎる時期を判断できないことです。 私の場合、ガベージコレクションが時間通りに開始されない場合、システムは単にPythonでプロセスを釘付けにしました。



gcモジュールのドキュメントにあるように、ガベージコレクションの頻度は、新しいオブジェクトの数に対して設定されたしきい値に依存します。 私が使用できるすべてのバージョンのPythonでは、この数はデフォルトで700です。ただし、かなり簡単なテストを実行すると、強制ガベージコレクションgc.collect()



によって収集されたオブジェクトの数がこの値を簡単に超えることがgc.collect()



ます。



 class Node(object): parent = None def __init__(self, *children): self.children = list(children) for node in self.children: node.parent = self @classmethod def tree(cls, depth=1, children=1): if depth == 0: return [] return [cls(*cls.tree(depth-1, children)) for _ in range(children)] import gc from time import time for n in range(1, 21): for _ in range(n): #   . Node.tree(depth=5, children=6) start = time() print('{1} objects collected for n={0} in {2:3.6} msec'.format( n, gc.collect(), (time() - start) * 1000))
      
      





nが10と20の場合、10万7千個の未リリースのオブジェクトがありました。 したがって、gcモジュールのしきい値はソフトであり、その達成は即時のガベージコレクションを保証するものではありません(コメントのAndrey Svetlovは、そうではないことを修正し、これが発生する理由を詳細に説明しています)。 さらに、オブジェクトの数は、オブジェクトが占有するメモリについては何も言いません。 その結果、アプリケーションで、大量のメモリを消費するオブジェクトが参照カウントによって破壊されない場合、悲しい結果につながる可能性があります。



それがまさに私のアプリケーションで起こったことです。 問題を特定するコードは次のようになりました。



 from tornado import web, ioloop, gen ioloop = ioloop.IOLoop.current() class IndexHandler(web.RequestHandler): megabyte_string = "0123456789abcdef" * 64 * 1024 @web.asynchronous @gen.engine def get(self): self.write("Hello, world<br>") yield gen.Task(self.some_task, self.megabyte_string * 20) self.finish() def some_task(self, bigdata, callback): self.write("some task<br>") callback() application = web.Application([(r'/', IndexHandler)], debug=True) if __name__ == "__main__": print("Start on 8888") application.listen(8888) ioloop.start()
      
      





これにより、 IndexHandler.get()



メソッドでURL「/」を処理するサーバーが作成されます。 このメソッドは非同期で、20 MBの大きなデータを転送するタスクを設定します。 この例では既に問題があるため、タスクが何をするかはそれほど重要ではありません。リクエストごとに、Pythonプロセスが占有するメモリ量がこれらの20メガバイト増加し、決して減少することはありません。 結果は簡単なベンチマークです ab -n 100 -c 4 localhost:8888/



ab -n 100 -c 4 localhost:8888/



は、ある時点でギガバイトのメモリを消費することができます。 ただし、サーバーは負荷に簡単に耐えられるため、 yield gen.Task()



を使用してタスクコールをコールバック付きの直接呼び出しに変更する価値があります。 ab -n 1000 -c 100 localhost:8888/



ab -n 1000 -c 100 localhost:8888/



、50 mb以下のメモリを消費します。



  @web.asynchronous @gen.engine def get(self): self.write("Hello, world<br>") self.some_task(self.megabyte_string * 20, self.finish)
      
      





そのような場合をデバッグする方法は? 何がリリースされていないのかを正確に確認できたらうれしいです。 このために最初に行う必要があるのは、ガベージコレクションを手動で開始して、ガベージコレクションを呼び出してメモリを本当に解放する機能を提供することです。 gc.collect()



を呼び出し、収集されたオブジェクトの数を表示する別の要求ハンドラーを作成しました。



 class HealthHandler(web.RequestHandler): def get(self): self.write('{} objects collected'.format(gc.collect())) application = web.Application([(r'/', IndexHandler), (r'/health/', HealthHandler)], debug=True)
      
      





2番目-自動ガベージコレクションを無効にする必要があります。 これにより、実験中に安定した結果が得られます。 第三-収集されたオブジェクトに関する情報が必要です。 gcモジュールにはすでにこのための既製のツールがありますgc.collect()



呼び出し中に情報がコンソールに表示されます。



 import gc gc.disable() gc.set_debug(gc.DEBUG_LEAK)
      
      





オブジェクトをこのリークに正確に関与させることは非常に簡単です。フローするメソッドを実行し、address /health/



で収集されたオブジェクトのリストを取得する必要があります。 次に、フローしないメソッドを実行し、そのリストを取得します。 最初のリストから2番目にないオブジェクトを見つけます。 ここにあります:



 gc: collectable <cell> × 4 gc: collectable <dict> × 3 gc: collectable <function> × 2 gc: collectable <generator> gc: collectable <instancemethod> gc: collectable <Runner> gc: collectable <set> gc: collectable <Task> gc: collectable <tuple> × 3
      
      





明確にするために、同じ要素をグループ化しました。 まず第一に、非構築型が重要です。 これがTask



and Runner



です。 これは、問題がyield gen.Task()



yield gen.Task()



する呼び出しにあり、問題がガベージコレクションにあることを再度証明します。 Runner



何であるか、なぜTask



お互いを参照するのかを理解することは残っています。 オープンソースコード。



私の研究の時点では、すべての例はトルネードバージョン3.1dev2のものです。 @gen.engine



デコレータ@gen.engine



は多くのコードがありますが、そこで発生する主なことは、元の関数が呼び出され、その実行結果がジェネレーターであることが判明した場合、 Runner



クラスに渡されます(キャッチ)。 私たちのTask



は、ジェネレーターが返すものです。 そのため、ジェネレーターが反復されるRunner



クラスの
場所を探す必要があります。 これは、 yielded = self.gen.send(next)



です。 それでは、 yielded



self.yield_point



分類されるのyielded



簡単にself.yield_point



ます。 さらに、 self.yield_point



.start()



メソッドを呼び出し、 Runner



へのリンクを保存します。 Runner.run()



メソッドを実行した後、一方または他方でリンクを解除する必要があることがRunner.run()



ました。 なぜなら Runner.yield_point



は最後の要素への単なるポインターであり、 Task.runner



は親への参照であり、要素へのポインターをリセットすることは論理的です。 Runner.run()



メソッドがどこで実行を完了するかを理解するだけです。 なぜなら 竜巻は非同期であり、その中心にあるソースに移動します。上部がどこにあり、下部がどこにあるかを理解することは非常に困難です。 .run()



メソッドに.run()



5つの出口点があり、あらゆる種類のコールバックから再呼び出しされます。 何度か試行した結果、 self.finished



オブジェクトのself.finished



フラグはself.finished



ではなく、Trueに設定されている場合はself.yield_pointをself.yield_point



する必要があることに気付きました。



結果を確認する ab -n 1000 -c 100 localhost:8888/



ab -n 1000 -c 100 localhost:8888/



。 すべてが順調です。



これを終了することは可能ですが、それは私には奇妙に思えました。 リクエストが未割り当てオブジェクトをメモリに残すのはなぜですか? これで何かできるかもしれません。 それにもかかわらず、リクエストではなく、 @web.asynchronous



デコレータ@web.asynchronous



いるリクエストのみであることが判明しました。 そして、未リリースのオブジェクトのリストは次のようになりました。



 gc: collectable <dict> × 7 gc: collectable <list> × 16 gc: collectable <tuple> gc: collectable <instancemethod> gc: collectable <ChunkedTransferEncoding> gc: collectable <ExceptionStackContext> gc: collectable <HTTPHeaders> × 2 gc: collectable <HTTPRequest> gc: collectable <IndexHandler>
      
      





既に5つのビルドされていないオブジェクトがあり、どこから始めるべきかは不明です。 しかし、私IndexHandler.finish()



メソッドをIndexHandler.finish()



ことから始めました。このメソッドでは、見つかったすべてのオブジェクトへのリンクを削除しました。



 class IndexHandler(web.RequestHandler): @web.asynchronous def get(self): self.write("Hello, world<br>") self.finish() def finish(self, chunk=None): super(IndexHandler, self).finish(chunk) for k, v in self.__dict__.iteritems(): print '"{}":'.format(k), type(v) self.request = None self._headers = None self.application = None self._transforms = None
      
      





これは一定の結果をもたらしましたが、問題を完全には解決しませんでした。 IndexHandler



オブジェクトの数は、 ExceptionStackContext



IndexHandler



自体の2つに減少しました。 ExceptionStackContext



は、 self._stack_context_handle_exception



引数を使用して@web.asynchronous



デコレータの操作中に作成されます。ここで、 self



は単なるIndexHandler



です。 反対方向のリンクはありません。 ExceptionStackContext



自分自身を参照しているようです。 実装を見ると、実際に.__enter__()



メソッドにself.new_contexts = (self.old_contexts[0], self)



という行があることがself.new_contexts = (self.old_contexts[0], self)



ます。 したがって、 self.new_contexts



.__exit__()



にリセットする必要があり、ポイントは帽子にあります。



その結果、両方の変更を含むpullrequestがレビューされ、2.5時間以内にマスターに受け入れられました。これにより、引き続き有用な変更を行うことができます。 これらの2つのパッチともう1つのパッチを備えた竜巻は、リクエスト後にメモリにゴミを残すことを完全に停止しました。 これにより、多数の競合する要求の間のメモリ消費が削減され、ガベージコレクションの高速化によりメモリ消費がわずかに加速され、最も重要なことに、メモリ消費が予測可能になりました。



このようなリークを見つけて修正することは、特にアプリケーション自体のコードではなく、使用されているライブラリにある場合、非常に困難です。 それでも、少なくともアプリケーションにそれらがあるかどうかを把握し、それらのせいで重いオブジェクトがメモリにぶら下がっていないことを確認する価値があります。



All Articles