web-dev-qa-db-fra.com

Comment forcer les modèles Django à être libérés de la mémoire

Je souhaite utiliser une commande de gestion pour exécuter une analyse ponctuelle des bâtiments du Massachusetts. J'ai réduit le code incriminé à un extrait de 8 lignes qui illustre le problème que je rencontre. Les commentaires expliquent simplement pourquoi je veux le faire. J'exécute le code ci-dessous textuellement, dans une commande de gestion sinon vide

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

Lorsque j'exécute ce code exact, je constate que l'utilisation de la mémoire augmente régulièrement à chaque boucle externe d'itération (j'utilise print('mem', process.memory_info().rss) pour vérifier l'utilisation de la mémoire).

Il semble que la liste important_buildings Accapare de la mémoire, même après avoir été hors de portée. Si je remplace important_buildings.append(building) par _ = building.pk, Il ne consomme plus beaucoup de mémoire, mais j'ai besoin de cette liste pour une partie de l'analyse.

Donc, ma question est: Comment puis-je forcer Python pour libérer la liste des modèles Django modèles quand il sort du domaine?

Edit: j'ai l'impression qu'il y a un petit problème avec le débordement de pile - si j'écris trop de détails, personne ne veut prendre le temps de le lire (et cela devient un problème moins applicable), mais si j'écris trop peu détail, je risque d'oublier une partie du problème. Quoi qu'il en soit, j'apprécie vraiment les réponses et je prévois d'essayer certaines des suggestions ce week-end quand j'aurai enfin la chance d'y revenir !!

16
Teddy Ward

Vous ne fournissez pas beaucoup d'informations sur la taille de vos modèles, ni sur les liens entre eux, alors voici quelques idées:

Par défaut, QuerySet.iterator() chargera 2000 Éléments en mémoire (en supposant que vous utilisez Django> = 2.0). Si votre Building le modèle contient beaucoup d'informations, cela peut éventuellement accaparer beaucoup de mémoire. Vous pouvez essayer de changer le paramètre chunk_size en quelque chose de plus bas.

Votre modèle Building a-t-il des liens entre les instances qui pourraient provoquer des cycles de référence que le gc ne peut pas trouver? Vous pouvez utiliser les fonctionnalités de débogage gc pour obtenir plus de détails.

Ou court-circuiter l'idée ci-dessus, peut-être simplement appeler del(important_buildings) et del(buildings) suivi de gc.collect() à la fin de chaque boucle pour forcer la collecte des ordures?

La portée de vos variables est la fonction, pas seulement la boucle for, donc la décomposition de votre code en fonctions plus petites peut être utile. Bien que notez que le python garbage collector ne retournera pas toujours la mémoire au système d'exploitation, donc comme expliqué dans cette réponse vous devrez peut-être prendre des mesures plus brutales pour voir le rss descend.

J'espère que cela t'aides!

MODIFIER:

Pour vous aider à comprendre quel code utilise votre mémoire et combien, vous pouvez utiliser le module tracemalloc , par exemple en utilisant le code suggéré:

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

Réponse très rapide : la mémoire est libérée, rss n'est pas un outil très précis pour le dire où la mémoire est consommée , rss donne une mesure de la mémoire utilisée par le processus , pas la mémoire utilisée par le processus en utilisant (continuez à lire pour voir une démo), vous pouvez utiliser le package memory-profiler afin de vérifier ligne par ligne, l'utilisation de la mémoire de votre fonction.

Alors, comment forcer les modèles Django à être libérés de la mémoire? Vous ne pouvez pas dire que vous avez un tel problème en utilisant simplement process.memory_info().rss.

Je peux cependant vous proposer une solution pour optimiser votre code. Et écrivez une démo expliquant pourquoi process.memory_info().rss n'est pas un outil très précis pour mesurer la mémoire utilisée dans un bloc de code.

Solution proposée : comme démontré plus loin dans ce même article, appliquer del à la liste ne sera pas la solution, l'optimisation utilisant chunk_size Pour iterator aidera (soyez conscient que l'option chunk_size Pour iterator a été ajoutée dans Django 2.0), c'est sûr , mais le véritable ennemi ici est cette liste désagréable.

Cela dit, vous pouvez utiliser une liste de champs uniquement dont vous avez besoin pour effectuer votre analyse (je suppose que votre analyse ne peut pas être abordée un bâtiment à la fois) afin de réduire la quantité de données stockées dans cette liste.

Essayez d'obtenir uniquement les attributs dont vous avez besoin en déplacement et sélectionnez les bâtiments ciblés à l'aide de l'ORM de Django.

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 ...

Il est très important de noter que si vous utilisez une solution comme celle-ci, vous ne frapperez la base de données qu'en remplissant la variable data. Et bien sûr, vous n'aurez en mémoire que le minimum requis pour réaliser votre analyse.

Penser à l'avance.

Lorsque vous rencontrez des problèmes comme celui-ci, vous devriez commencer à penser au parallélisme, à la clusterisation, au big data, etc ... Lisez également à propos de ElasticSearch il a de très bonnes capacités d'analyse.

Démo

process.memory_info().rss Ne vous dira pas que la mémoire est libérée.

J'ai été vraiment intrigué par votre question et le fait que vous décrivez ici:

Il semble que la liste important_buildings accapare de la mémoire, même après avoir été hors de portée.

En effet, il semble mais ne l'est pas. Regardez l'exemple suivant:

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

Ainsi, même si la mémoire a est libérée, le dernier nombre est plus grand. En effet, memory_info.rss() est la mémoire totale utilisée par le processus , et non la mémoire utilisant pour le moment, comme indiqué ici dans la documentation: memory_info .

L'image suivante est un tracé (mémoire/temps) pour le même code qu'auparavant mais avec range(10000000)

Image against time. J'utilise le script mprof fourni memory-profiler pour cette génération de graphe.

Vous pouvez voir que la mémoire est complètement libérée, ce n'est pas ce que vous voyez lorsque vous profilez en utilisant process.memory_info().rss.

Si je remplace important_buildings.append (building) par _ = building, utilisez moins de mémoire

C'est toujours ainsi, une liste d'objets utilisera toujours plus de mémoire qu'un seul objet.

Et d'autre part, vous pouvez également voir que la mémoire utilisée ne croît pas linéairement comme vous vous y attendez. Pourquoi?

De cet excellent site on peut lire:

La méthode de l'ajout est O (1) "amorti". Dans la plupart des cas, la mémoire requise pour ajouter une nouvelle valeur a déjà été allouée, qui est strictement O (1). Une fois que le tableau C sous-jacent à la liste a été épuisé, il doit être étendu afin de prendre en compte d'autres ajouts. Ce processus d'expansion périodique est linéaire par rapport à la taille du nouveau tableau, ce qui semble contredire notre affirmation selon laquelle annexer est O (1).

Cependant, le taux d'expansion est habilement choisi pour être trois fois la taille précédente de la matrice ; lorsque nous répartissons le coût d'expansion sur chaque ajout supplémentaire fourni par cet espace supplémentaire, le coût par ajout est O(1) sur une base amortie).

Il est rapide mais a un coût mémoire.

Le vrai problème n'est pas le Django ne sont pas libérés de la mémoire . Le problème est l'algorithme/la solution que vous ' ve implémenté, il utilise trop de mémoire. Et bien sûr, la liste est le méchant.

Une règle d'or pour l'optimisation Django: remplacez l'utilisation d'une liste de querisets partout où vous le pouvez.

9
Raydel Miranda

La réponse de Laurent S est tout à fait pertinente (+1 et bravo de ma part: D).

Il y a quelques points à considérer afin de réduire votre utilisation de la mémoire:

  1. L'utilisation iterator :

    Vous pouvez définir le paramètre chunk_size De l'itérateur sur quelque chose d'aussi petit que possible (par ex. 500 éléments par bloc).
    Cela rendra votre requête plus lente (puisque chaque étape de l'itérateur réévaluera la requête) mais cela réduira votre consommation de mémoire.

  2. Les options only et defer :

    defer(): Dans certaines situations complexes de modélisation de données, vos modèles peuvent contenir beaucoup de , dont certains pourraient contenir beaucoup de données (par exemple, des champs de texte) , ou nécessiter un traitement coûteux pour les convertir en objets Python. Si vous utilisez les résultats d'un ensemble de requêtes dans certaines situations où vous ne savez pas si vous avez besoin de ces champs particuliers lorsque vous récupérez initialement les données, vous pouvez dire à Django de ne pas les récupérer dans la base de données.

    only(): Est plus ou moins l'opposé de defer(). Vous l'appelez avec les champs qui ne doivent pas être différés lors de la récupération d'un modèle. Si vous avez un modèle où presque tous les champs doivent être différés, l'utilisation de seulement () pour spécifier l'ensemble complémentaire de champs peut entraîner un code plus simple.

    Par conséquent, vous pouvez réduire ce que vous récupérez de vos modèles à chaque étape de l'itérateur et ne conserver que les champs essentiels pour votre opération.

  3. Si votre requête reste encore trop chargée en mémoire, vous pouvez choisir de ne conserver que building_id Dans votre liste important_buildings, Puis utiliser cette liste pour créer les requêtes dont vous avez besoin à partir de vos Building modèle, pour chacune de vos opérations (cela ralentira vos opérations, mais réduira l'utilisation de la mémoire).

  4. Vous pouvez améliorer vos requêtes au point de résoudre des parties (ou même la totalité) de votre analyse mais avec l'état de votre question en ce moment, je ne peux pas en être sûr (voir [ ~ # ~] ps [~ # ~] à la fin de cette réponse)

Essayons maintenant de rassembler tous les points ci-dessus dans votre exemple de code:

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

Si cela monopolise encore trop de mémoire à votre goût, vous pouvez utiliser le 3ème point ci-dessus comme ceci:

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)

puis utilisez cet ensemble pour interroger vos bâtiments pour le reste de vos opérations:

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

PS: Si vous pouvez mettre à jour votre réponse avec plus de détails, comme la structure de vos modèles et certaines des opérations d'analyse que vous essayez d'exécuter, nous pouvons être en mesure de fournir des réponses plus concrètes pour vous aider!

3
John Moutafis

Pour libérer de la mémoire, vous devez dupliquer les détails importants de chacun dans les bâtiments de la boucle intérieure dans un nouvel objet, à utiliser plus tard, tout en éliminant ceux qui ne conviennent pas. Dans le code non affiché dans la publication d'origine, des références à la boucle interne existent. Ainsi, les problèmes de mémoire. En copiant les champs pertinents vers de nouveaux objets, les originaux peuvent être supprimés comme prévu.

0
Strom

Avez-vous envisagé nion ? En regardant le code que vous avez publié, vous exécutez de nombreuses requêtes dans cette commande, mais vous pouvez les décharger dans la base de données avec Union.

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

Ajuster ce qui précède pourrait essentiellement réduire à un les requêtes nécessaires pour cette fonction.

Cela vaut également la peine de regarder DjangoDebugToolbar - si vous ne l'avez pas déjà regardé.

0
slajma