web-dev-qa-db-ja.com

強制的にDjangoモデルをメモリから解放する方法

管理コマンドを使用して、マサチューセッツの建物の1回限りの分析を実行したいと考えています。問題のコードを、発生した問題を示す8行のスニペットに減らしました。コメントは、なぜ私がこれをしたいのかを説明するだけです。空白の管理コマンドで、以下のコードを逐語的に実行しています

_zips = ZipCode.objects.filter(state='MA').order_by('id')
for Zip in zips.iterator():
    buildings = Building.objects.filter(boundary__within=Zip.boundary)
    important_buildings = []
    for building in buildings.iterator():
        # Some conditionals would go here
        important_buildings.append(building)
    # Several types of analysis would be done on important_buildings, here
    important_buildings = None
_

この正確なコードを実行すると、反復ループの外側のループごとにメモリ使用量が着実に増加していることがわかります(print('mem', process.memory_info().rss)を使用してメモリ使用量をチェックしています)。

範囲外になった後でも、_important_buildings_リストがメモリを消費しているようです。 important_buildings.append(building)を__ = building.pk_に置き換えると、多くのメモリを消費しなくなりますが、一部の分析にはそのリストが必要です。

だから、私の質問は次のとおりです:Pythonで強制的にDjangoモデルが範囲外になったときにモデルのリストを解放するように強制するには?

編集:スタックオーバーフローで少しキャッチ22があるように感じます-あまりにも多くの詳細を書いた場合、誰もそれを読むために時間をかけたくありません(そしてそれはあまり適用されない問題になります)が、私が少なすぎると詳細、私は問題の一部を見落とすリスクがあります。とにかく、私は答えに本当に感謝しています。そして、私が最終的にこれに戻る機会を得たときに、今週末にいくつかの提案を試す予定です!!

16
Teddy Ward

モデルの大きさやモデル間のリンクについてはあまり情報を提供していないため、以下にいくつかのアイデアを示します。

デフォルトでは、QuerySet.iterator()_2000_要素をメモリにロードします (Django> = 2.0を使用している場合)。Buildingモデルには多くの情報が含まれているため、メモリが大量に消費される可能性があります。_chunk_size_パラメータをより低い値に変更してみてください。

Buildingモデルには、gcが検出できない参照循環を引き起こす可能性のあるインスタンス間のリンクがありますか? gcデバッグ機能を使用して、詳細を取得できます。

または、上記のアイデアを短絡させて、ガベージコレクションを強制するために、すべてのループの最後でdel(important_buildings)およびdel(buildings)に続けてgc.collect()を呼び出すだけですか?

変数のスコープは、forループだけではなく関数です。そのため、コードを小さな関数に分割すると役立つ場合があります。 pythonガベージコレクターが常にメモリをOSに返すとは限らないことに注意してください。そのため、 この答え で説明されているように、より残忍な対策を講じて確認する必要がある場合があります。 rssがダウンします。

お役に立てれば!

編集:

どのコードがメモリをどの程度使用しているかを理解するのに役立つように、提案されたコードなどを使用して tracemalloc モジュールを使用できます。

_import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))

tracemalloc.start()

# ... run your code ...

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)
_
8
Laurent S

非常に簡単な回答:メモリが解放されています。rssは、正確な通知ツールではありませんメモリが消費されている場所rssプロセスが持っているメモリの測定値を示します使用、プロセスが持っているメモリではありません使用(デモを見るために読み続けてください)、パッケージを使用できます memory-profiler 行ごと、関数のメモリ使用状況をチェックするため。

では、Djangoモデルをメモリから強制的に解放する方法は?process.memory_info().rssを使用しただけでは、このような問題があるとは言えません。

ただし、コードを最適化するためのソリューションを提案できます。そして、なぜprocess.memory_info().rssがコードのブロックで使用されているメモリを測定するための非常に正確なツールではないのかについてのデモを書きます

提案された解決策:この同じ投稿の後半で示されているように、delをリストに適用することは解決策ではなく、_chunk_size_ for iteratorを使用した最適化役立つでしょう(iteratorの_chunk_size_オプションがDjango 2.0で追加されました)に注意してください)、それは確かですが、ここでの本当の敵はその厄介なリストです。

つまり、分析を実行するために必要なフィールドのみのリストを使用できます(一度に1つの建物で分析に取り組むことができないと想定しています)。そのリストに格納されるデータの量を減らすことができます。

外出先で必要な属性だけを取得し、DjangoのORMを使用して対象の建物を選択してください。

_for Zip in zips.iterator(): # Using chunk_size here if you're working with Django >= 2.0 might help.
    important_buildings = Building.objects.filter(
        boundary__within=Zip.boundary,
        # Some conditions here ... 

        # You could even use annotations with conditional expressions
        # as Case and When.

        # Also Q and F expressions.

        # It is very uncommon the use case you cannot address 
        # with Django's ORM.

        # Ultimately you could use raw SQL. Anything to avoid having
        # a list with the whole object.
    )

    # And then just load into the list the data you need
    # to perform your analysis.

    # Analysis according size.
    data = important_buildings.values_list('size', flat=True)

    # Analysis according height.
    data = important_buildings.values_list('height', flat=True)

    # Perhaps you need more than one attribute ...
    # Analysis according to height and size.
    data = important_buildings.values_list('height', 'size')

    # Etc ...
_

非常に重要ですこのようなソリューションを使用する場合、data変数にデータを入力するときにのみデータベースにアクセスすることに注意してください。そしてもちろん、あなたはあなたの分析を達成するために必要な最小限のメモリを持っています。

事前に考えます。

このような問題にぶつかったとき、並列処理、クラスタ化、ビッグデータなどについて考え始める必要があります... ElasticSearch についても読んでください。非常に優れた分析機能があります。

デモ

process.memory_info().rss解放されているメモリについては通知しません。

私はあなたの質問とあなたがここで説明する事実に本当に興味をそそられました:

それはスコープ外に出た後でも、important_buildingsリストがメモリを消費しているようです。

確かに、そうですが、そうではありません。次の例を見てください。

_from psutil import Process

def memory_test():
    a = []
    for i in range(10000):
        a.append(i)
    del a

print(process.memory_info().rss)  # Prints 29728768
memory_test()
print(process.memory_info().rss)  # Prints 30023680
_

したがって、aメモリが解放されても、最後の数値は大きくなります。これは、memory_info.rss()がプロセスの総メモリhasであり、現在のメモリではないsingがドキュメントに記載されているためです。- memory_info

次の画像は、以前と同じコードのプロット(メモリ/時間)ですが、range(10000000)を使用しています

Image against time. このグラフの生成には、 memory-profiler に含まれるスクリプトmprofを使用します。

メモリが完全に解放されていることがわかります。これは、process.memory_info().rssを使用してプロファイリングしたときに表示されるものではありません。

important_buildings.append(building)を_ = buildingに置き換えると、メモリ使用量が少なくなります

それは常にそうです。オブジェクトのリストは常に単一のオブジェクトより多くのメモリを使用します。

また、その一方で、使用したメモリが予想どおりに直線的に増加しないこともわかります。どうして?

この優れた site から、次のように読むことができます。

Appendメソッドは「償却」されたO(1)です。ほとんどの場合、新しい値を追加するために必要なメモリは既に割り当てられており、厳密にO(1)です。リストの基礎となるC配列が使い果たされたら、それ以上の追加に対応するために拡張する必要があります。この定期的な拡張プロセスは、新しい配列のサイズに対して線形であり、追加がO(1)であるという私たちの主張に矛盾しているようです。

ただし、拡張率は、配列の以前のサイズの3倍になるように巧妙に選択されています;この追加スペースによって提供される追加の追加ごとに拡張コストを分散する場合、追加あたりのコストは、償却ベースでO(1))です。

高速ですが、メモリコストがかかります。

本当の問題はthe Djangoモデルがメモリから解放されないではありません。問題は、実装したアルゴリズム/ソリューションであり、メモリを使いすぎています。もちろん、リストは悪役です。

Django最適化のための黄金律:クエリセットのリストの使用を可能な限り置き換えます。

9
Raydel Miranda

ローランSの答えはかなり重要です(+1と私からよくできました:D)。

メモリ使用量を削減するために考慮すべき点がいくつかあります。

  1. iterator 使用法:

    イテレータの_chunk_size_パラメータを、できるだけ小さなもの(チャンクごとに500アイテムなど)に設定できます。
    (イテレータのすべてのステップがクエリを再評価するため)クエリは遅くなりますが、メモリの消費量が削減されます。

  2. only および defer オプション:

    defer():複雑なデータモデリングの状況では、モデルに多数のフィールドには、多くのデータ(たとえば、テキストフィールド)が含まれている場合や、Pythonオブジェクトに変換するために高コストの処理が必要な場合があります。最初にデータをフェッチするときに特定のフィールドが必要かどうかわからない状況でクエリセットの結果を使用している場合、Djangoにデータベースから取得しないように指示できます。

    only()多かれ少なかれdefer()の逆です。モデルの取得時に延期してはならないフィールドを使用して呼び出します。ほとんどすべてのフィールドを遅延させる必要があるモデルがある場合、only()を使用して補完的なフィールドのセットを指定すると、コードが単純になります。

    したがって、イテレーターの各ステップでモデルから取得するものを削減し、操作に必要なフィールドのみを保持できます。

  3. それでもクエリのメモリが多すぎる場合は、_building_id_リストに_important_buildings_のみを保持し、このリストを使用してBuildingのモデルから必要なクエリを作成できます。操作ごとに(これにより操作が遅くなりますが、メモリ使用量が削減されます)。

  4. 分析の一部(または全体)を解決するほどクエリを改善することができますが、現時点での質問の状態では、はっきりとはわかりません([〜#〜] ps [〜#〜]この回答の終わりに))

次に、上記のすべてのポイントをサンプルコードにまとめてみましょう。

_# You don't use more than the "boundary" field, so why bring more?
# You can even use "values_list('boundary', flat=True)"
# except if you are using more than that (I cannot tell from your sample)
zips = ZipCode.objects.filter(state='MA').order_by('id').only('boundary')
for Zip in zips.iterator():
    # I would use "set()" instead of list to avoid dublicates
    important_buildings = set()

    # Keep only the essential fields for your operations using "only" (or "defer")
    for building in Building.objects.filter(boundary__within=Zip.boundary)\
                    .only('essential_field_1', 'essential_field_2', ...)\
                    .iterator(chunk_size=500):
        # Some conditionals would go here
        important_buildings.add(building)
_

これでもお好みに応じてメモリを多く消費している場合は、上記の3番目のポイントを次のように使用できます。

_zips = ZipCode.objects.filter(state='MA').order_by('id').only('boundary')
for Zip in zips.iterator():
    important_buildings = set()
    for building in Building.objects.filter(boundary__within=Zip.boundary)\
                    .only('pk', 'essential_field_1', 'essential_field_2', ...)\
                    .iterator(chunk_size=500):
        # Some conditionals would go here

        # Create a set containing only the important buildings' ids
        important_buildings.add(building.pk)
_

次に、そのセットを使用して、残りの操作について建物にクエリを実行します。

_# Converting set to list may not be needed but I don't remember for sure :)
Building.objects.filter(pk__in=list(important_buildings))...
_

PS:モデルの構造や実行しようとしている分析操作など、より具体的な回答で更新できる場合は、あなたを助けるためのより具体的な答えを提供することができる!

3
John Moutafis

メモリを解放するには、内側のループの建物のそれぞれの重要な詳細を新しいオブジェクトに複製し、後で使用するために、不適切なものを排除する必要があります。元の投稿に示されていないコードには、内部ループへの参照が存在します。したがって、メモリの問題。関連するフィールドを新しいオブジェクトにコピーすることにより、元のオブジェクトを意図したとおりに削除できます。

0
Strom

nion を検討しましたか?投稿したコードを見ると、そのコマンド内で多くのクエリが実行されていますが、Unionを使用してデータベースにオフロードできます。

combined_area = FooModel.objects.filter(...).aggregate(area=Union('geom'))['area']
final = BarModel.objects.filter(coordinates__within=combined_area)

上記を調整すると、この関数に必要なクエリを基本的に1つに絞り込むことができます。

DjangoDebugToolbar -まだ見ていない場合は、一見の価値があります。

0
slajma