Contexte:
Je travaille sur un projet qui utilise Django avec une base de données Postgres. Nous utilisons également mod_wsgi au cas où cela serait important, car certaines de mes recherches sur le Web en ont fait mention. Lors de la soumission d'un formulaire Web, la vue Django lance un travail qui prendra beaucoup de temps (plus que l'utilisateur ne voudrait attendre). Nous lançons donc le travail via un appel système en arrière-plan. Le travail en cours d'exécution doit pouvoir lire et écrire dans la base de données. Comme ce travail prend beaucoup de temps, nous utilisons le multitraitement pour en exécuter certaines parties en parallèle.
Problème:
Le script de niveau supérieur comporte une connexion à une base de données et, lorsqu'il génère des processus enfants, il semble que la connexion du parent soit disponible pour les enfants. Il existe ensuite une exception concernant la manière dont SET TRANSACTION ISOLATION LEVEL doit être appelé avant une requête. La recherche a indiqué que cela est dû à la tentative d'utiliser la même connexion à la base de données dans plusieurs processus. Un fil que j’ai trouvé suggère d’appeler connection.close () au début des processus enfants pour que Django crée automatiquement une nouvelle connexion s’il en a besoin, et donc chaque processus enfant disposera d’une connexion unique - c’est-à-dire non partagée. Cela n'a pas fonctionné pour moi, car l'appel de connection.close () dans le processus enfant a amené le processus parent à se plaindre de la perte de la connexion.
Autres constatations:
Certaines choses que j'ai lues semblaient indiquer que vous ne pouvez pas vraiment faire cela, et que le multitraitement, mod_wsgi et Django ne fonctionnent pas bien ensemble. Cela semble difficile à croire, je suppose.
Certains ont suggéré d'utiliser le céleri, ce qui pourrait être une solution à long terme, mais je ne parviens pas à installer le céleri pour le moment, dans l'attente de certains processus d'approbation, de sorte que ce n'est pas une option pour le moment.
Nous avons trouvé plusieurs références sur SO et ailleurs concernant les connexions persistantes à la base de données, problème qui, à mon avis, est différent.
On a également trouvé des références à psycopg2.pool et pgpool et quelque chose à propos de videur. Certes, je ne comprenais pas la plupart de ce que je lisais sur ceux-ci, mais cela ne m'a certainement pas échappé comme étant ce que je cherchais.
"Contournement" actuel:
Pour le moment, je suis revenu à la gestion en série, et cela fonctionne, mais est plus lent que je ne le souhaiterais.
Des suggestions sur la façon dont je peux utiliser le multitraitement pour s'exécuter en parallèle? On dirait que si je pouvais avoir le parent et deux enfants ayant tous des connexions indépendantes à la base de données, tout irait bien, mais je n'arrive pas à avoir ce comportement.
Merci et désolé pour la longueur!
Le multitraitement copie les objets de connexion entre les processus car il divise les processus et, par conséquent, copie tous les descripteurs de fichier du processus parent. Cela étant dit, une connexion au serveur SQL est juste un fichier, vous pouvez le voir sous Linux sous/proc // fd/.... tout fichier ouvert sera partagé entre des processus forkés. Vous pouvez en savoir plus sur le forking ici .
Ma solution consistait simplement à fermer la connexion à la base de données juste avant le lancement des processus, chaque processus recrée lui-même la connexion quand il en aura besoin (testé dans Django 1.4):
from Django import db
db.connections.close_all()
def db_worker():
some_paralell_code()
Process(target = db_worker,args = ())
Pgbouncer/pgpool n'est pas connecté aux threads dans le sens du multitraitement. C'est plutôt une solution pour ne pas fermer la connexion à chaque requête = accélérer la connexion à postgres sous forte charge.
Mettre à jour:
Pour supprimer complètement les problèmes de connexion à la base de données, déplacez simplement toute la logique connectée à la base de données vers db_worker - Je voulais passer QueryDict en tant qu'argument ... Une meilleure idée serait simplement de passer la liste des identifiants ... Voir QueryDict et values_list ('id ', flat = True), et n'oubliez pas de le transformer en liste! list (QueryDict) avant de passer à db_worker. Grâce à cela, nous ne copions pas les modèles de connexion à la base de données.
def db_worker(models_ids):
obj = PartModelWorkerClass(model_ids) # here You do Model.objects.filter(id__in = model_ids)
obj.run()
model_ids = Model.objects.all().values_list('id', flat=True)
model_ids = list(model_ids) # cast to list
process_count = 5
delta = (len(model_ids) / process_count) + 1
# do all the db stuff here ...
# here you can close db connection
from Django import db
db.connections.close_all()
for it in range(0:process_count):
Process(target = db_worker,args = (model_ids[it*delta:(it+1)*delta]))
Lorsque vous utilisez plusieurs bases de données, vous devez fermer toutes les connexions.
from Django import db
for connection_name in db.connections.databases:
db.connections[connection_name].close()
MODIFIER
Veuillez utiliser la même chose que @lechup mentionné pour fermer toutes les connexions (vous ne savez pas depuis quelle version de Django cette méthode a été ajoutée):
from Django import db
db.connections.close_all()
Pour Python 3 et Django 1.9, voici ce qui a fonctionné pour moi:
import multiprocessing
import Django
django.setup() # Must call setup
def db_worker():
for name, info in Django.db.connections.databases.items(): # Close the DB connections
Django.db.connection.close()
# Execute parallel code here
if __== '__main__':
multiprocessing.Process(target=db_worker)
Notez que sans Django.setup (), je ne pourrais pas le faire fonctionner. Je suppose que quelque chose doit être réinitialisé pour le multitraitement.
J'avais des problèmes de "connexion fermée" lors de l'exécution séquentielle de Django test cas. En plus des tests, il existe également un autre processus modifiant intentionnellement la base de données pendant l'exécution du test. Ce processus est lancé dans chaque cas de test setUp ().
Une solution simple consistait à hériter de mes classes de test de TransactionTestCase
au lieu de TestCase
. Cela garantit que la base de données a bien été écrite et que l'autre processus dispose d'une vue à jour des données.
(pas une bonne solution, mais une solution de contournement possible)
si vous ne pouvez pas utiliser le céleri, vous pourriez peut-être mettre en place votre propre système de file d'attente, en ajoutant des tâches à une table de tâches et en ayant un cron régulier qui les sélectionne et les traite? (via une commande de gestion)
Vous pouvez donner plus de ressources à Postgre. Dans Debian/Ubuntu, vous pouvez éditer:
nano /etc/postgresql/9.4/main/postgresql.conf
en remplaçant 9.4 par votre version postgre.
Voici quelques lignes utiles qui doivent être mises à jour avec des exemples de valeurs. Les noms parlent d’eux-mêmes:
max_connections=100
shared_buffers = 3000MB
temp_buffers = 800MB
effective_io_concurrency = 300
max_worker_processes = 80
Veillez à ne pas trop amplifier ces paramètres car cela pourrait entraîner des erreurs avec Postgre qui essaie de prendre plus de ressources que ce qui est disponible. Les exemples ci-dessus fonctionnent correctement sur une machine Ram 8 Go de Debian équipée de 4 cœurs.
J'ai rencontré ce problème et j'ai pu le résoudre en procédant comme suit (nous implémentons un système de tâches limité)
from Django.db import connection
def as_task(fn):
""" this is a decorator that handles task duties, like setting up loggers, reporting on status...etc """
connection.close() # this is where i kill the database connection VERY IMPORTANT
# This will force Django to open a new unique connection, since on linux at least
# Connections do not fare well when forked
#...etc
from Django.db import connection
def run_task(request, job_id):
""" Just a simple view that when hit with a specific job id kicks of said job """
# your logic goes here
# ...
processor = multiprocessing.Queue()
multiprocessing.Process(
target=call_command, # all of our tasks are setup as management commands in Django
args=[
job_info.management_command,
],
kwargs= {
'web_processor': processor,
}.items() + vars(options).items()).start()
result = processor.get(timeout=10) # wait to get a response on a successful init
# Result is a Tuple of [TRUE|FALSE,<ErrorMessage>]
if not result[0]:
raise Exception(result[1])
else:
# THE VERY VERY IMPORTANT PART HERE, notice that up to this point we haven't touched the db again, but now we absolutely have to call connection.close()
connection.close()
# we do some database accessing here to get the most recently updated job id in the database
Honnêtement, pour éviter les situations de concurrence (avec plusieurs utilisateurs simultanés), il serait préférable d'appeler database.close () le plus rapidement possible après le lancement du processus. Il se peut qu’un autre utilisateur, quelque part sur la ligne, adresse une requête à la base de données avant que vous ayez la possibilité de vider la base de données.
En toute honnêteté, il serait probablement plus sûr et plus intelligent} [ de ne pas appeler directement votre commande fork, mais d'appeler un script sur le système d'exploitation afin que la tâche générée s'exécute dans son propre shell Django !
Si tout ce dont vous avez besoin est d'un parallélisme d'E/S et non d'un parallélisme de traitement, vous pouvez éviter ce problème en basculant vos processus vers des threads. Remplacer
from multiprocessing import Process
avec
from threading import Thread
L'objet Thread
a la même interface que Procsess