31 votes

psycopg2 perd de la mémoire après une grosse requête

J'exécute une grande requête dans un script python contre ma base de données postgres en utilisant psycopg2 (j'ai mis à jour à la version 2.5). Une fois la requête terminée, je ferme le curseur et la connexion, et je lance même gc, mais le processus consomme toujours une tonne de mémoire (7,3 Go pour être exact). Ai-je manqué une étape de nettoyage ?

import psycopg2
conn = psycopg2.connect("dbname='dbname' user='user' host='host'")
cursor = conn.cursor()
cursor.execute("""large query""")
rows = cursor.fetchall()
del rows
cursor.close()
conn.close()
import gc
gc.collect()

63voto

joeblog Points 859

J'ai rencontré un problème similaire et après quelques heures de sang, de sueur et de larmes, j'ai découvert que la réponse nécessite simplement l'ajout d'un paramètre.

Au lieu de

cursor = conn.cursor()

écrire

cursor = conn.cursor(name="my_cursor_name")

ou plus simple encore

cursor = conn.cursor("my_cursor_name")

Les détails se trouvent à l'adresse suivante http://initd.org/psycopg/docs/usage.html#server-side-cursors

J'ai trouvé les instructions un peu confuses dans la mesure où j'ai pensé que je devais réécrire mon SQL pour inclure "DECLARE my_cursor_name ...." et ensuite un "FETCH count 2000 FROM my_cursor_name" mais il s'avère que psycopg fait tout cela pour vous sous le capot si vous remplacez simplement le paramètre par défaut "name=None" lors de la création d'un curseur.

La suggestion ci-dessus d'utiliser fetchone ou fetchmany ne résout pas le problème puisque, si vous ne définissez pas le paramètre name, psycopg essaiera par défaut de charger la requête entière dans la RAM. La seule autre chose que vous pouvez faire (en plus de déclarer un paramètre de nom) est de changer l'attribut cursor.itersize de 2000 par défaut à 1000 si vous avez toujours trop peu de mémoire.

12voto

Craig Ringer Points 72371

Veuillez consulter le réponse suivante par @joeblog pour la meilleure solution.


Tout d'abord, vous ne devriez pas avoir besoin de toute cette RAM en premier lieu. Ce que vous devriez faire ici est de récupérer morceaux de l'ensemble des résultats. Ne faites pas de fetchall() . Au lieu de cela, utilisez la méthode beaucoup plus efficace cursor.fetchmany méthode. Voir la documentation de psycopg2 .

Maintenant, l'explication de pourquoi il n'est pas libéré, et pourquoi ce n'est pas une fuite de mémoire dans l'utilisation formellement correcte de ce terme.

La plupart des processus ne restituent pas la mémoire au système d'exploitation lorsqu'elle est libérée, ils la rendent disponible pour une réutilisation ailleurs dans le programme.

La mémoire ne peut être libérée pour le système d'exploitation que si le programme peut compacter les objets restants dispersés dans la mémoire. Ceci n'est possible que si des références indirectes sont utilisées, car sinon le déplacement d'un objet invaliderait les pointeurs existants vers cet objet. Les références indirectes sont plutôt inefficaces, en particulier sur les processeurs modernes où le fait de courir après les pointeurs a des effets néfastes sur les performances.

Ce qui se passe généralement, à moins que le programme ne fasse preuve d'une grande prudence, c'est que chaque gros morceau de mémoire alloué avec l'option brk() se retrouve avec quelques petites pièces encore utilisées.

Le système d'exploitation ne peut pas dire si le programme considère que cette mémoire est toujours utilisée ou non, il ne peut donc pas la réclamer. Comme le programme n'a pas tendance à accéder à la mémoire, le système d'exploitation la libère généralement au fil du temps, libérant ainsi la mémoire physique pour d'autres utilisations. C'est l'une des raisons pour lesquelles vous devez disposer d'un espace d'échange.

Il est possible d'écrire des programmes qui rendent la mémoire au système d'exploitation, mais je ne suis pas sûr qu'on puisse le faire avec Python.

Voir aussi :

Donc : ce n'est pas vraiment un souvenir fuite . Si vous faites autre chose qui utilise beaucoup de mémoire, le processus ne devrait pas grandir beaucoup, voire pas du tout, il réutilisera la mémoire précédemment libérée lors de la dernière grosse allocation.

11voto

Le Droid Points 455

Joeblog a la bonne réponse. La façon dont vous traitez la récupération est importante mais beaucoup plus évidente que la façon dont vous devez définir le curseur. Voici un exemple simple pour illustrer cela et vous donner quelque chose à copier-coller pour commencer.

import datetime as dt
import psycopg2
import sys
import time

conPG = psycopg2.connect("dbname='myDearDB'")
curPG = conPG.cursor('testCursor')
curPG.itersize = 100000 # Rows fetched at one time from the server

curPG.execute("SELECT * FROM myBigTable LIMIT 10000000")
# Warning: curPG.rowcount == -1 ALWAYS !!
cptLigne = 0
for rec in curPG:
   cptLigne += 1
   if cptLigne % 10000 == 0:
      print('.', end='')
      sys.stdout.flush() # To see the progression
conPG.commit() # Also close the cursor
conPG.close()

Comme vous le verrez, les points sont venus par groupe rapidement, que la pause pour obtenir un tampon de lignes (itersize), de sorte que vous n'avez pas besoin d'utiliser fetchmany pour les performances. Lorsque je l'exécute avec /usr/bin/time -v Avec le curseur côté client, j'obtiens le résultat en moins de 3 minutes, en utilisant seulement 200 Mo de RAM (au lieu de 60 Go avec le curseur côté client) pour 10 millions de lignes. Le serveur n'a pas besoin de plus de RAM car il utilise une table temporaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X