Django Multiprocessing und Datenbankverbindungen

82

Hintergrund:

Ich arbeite an einem Projekt, das Django mit einer Postgres-Datenbank verwendet. Wir verwenden auch mod_wsgi, falls dies wichtig ist, da einige meiner Websuchen dies erwähnt haben. Beim Senden eines Webformulars startet die Django-Ansicht einen Job, der viel Zeit in Anspruch nimmt (mehr als der Benutzer warten möchte). Daher starten wir den Job über einen Systemaufruf im Hintergrund. Der Job, der jetzt ausgeführt wird, muss lesen und in die Datenbank schreiben können. Da dieser Job so lange dauert, verwenden wir Multiprocessing, um Teile davon parallel auszuführen.

Problem:

Das Skript der obersten Ebene verfügt über eine Datenbankverbindung. Wenn untergeordnete Prozesse ausgelöst werden, scheint die Verbindung des übergeordneten Elements für die untergeordneten Prozesse verfügbar zu sein. Dann gibt es eine Ausnahme darüber, wie SET TRANSACTION ISOLATION LEVEL vor einer Abfrage aufgerufen werden muss. Untersuchungen haben ergeben, dass dies auf den Versuch zurückzuführen ist, dieselbe Datenbankverbindung in mehreren Prozessen zu verwenden. Ein Thread, den ich gefunden habe, schlug vor, connection.close () zu Beginn der untergeordneten Prozesse aufzurufen, damit Django automatisch eine neue Verbindung erstellt, wenn eine benötigt wird, und daher jeder untergeordnete Prozess eine eindeutige Verbindung hat - dh nicht gemeinsam genutzt wird. Dies hat bei mir nicht funktioniert, da der Aufruf von connection.close () im untergeordneten Prozess dazu führte, dass sich der übergeordnete Prozess beschwerte, dass die Verbindung unterbrochen wurde.

Weitere Ergebnisse:

Einige Dinge, die ich gelesen habe, scheinen darauf hinzudeuten, dass man das nicht wirklich kann und dass Multiprocessing, mod_wsgi und Django nicht gut zusammenspielen. Das scheint nur schwer zu glauben, denke ich.

Einige schlugen vor, Sellerie zu verwenden, was eine langfristige Lösung sein könnte, aber ich kann Sellerie derzeit nicht installieren, bis einige Genehmigungsverfahren abgeschlossen sind. Daher ist dies derzeit keine Option.

In SO und anderswo wurden mehrere Referenzen zu dauerhaften Datenbankverbindungen gefunden, die meines Erachtens ein anderes Problem darstellen.

Auch Verweise auf psycopg2.pool und pgpool und etwas über Türsteher gefunden. Zugegeben, ich habe das meiste, was ich darüber las, nicht verstanden, aber es hat mich mit Sicherheit nicht als das herausgesprungen, wonach ich gesucht habe.

Aktuelle "Work-Around":

Im Moment habe ich mich wieder darauf konzentriert, Dinge nur seriell auszuführen, und es funktioniert, ist aber langsamer als ich es gerne hätte.

Irgendwelche Vorschläge, wie ich Multiprocessing verwenden kann, um parallel zu laufen? Es scheint, als ob die Eltern und zwei Kinder unabhängige Verbindungen zur Datenbank haben könnten. Die Dinge wären in Ordnung, aber ich kann dieses Verhalten anscheinend nicht verstehen.

Danke und Entschuldigung für die Länge!

Daroo
quelle

Antworten:

69

Durch die Mehrfachverarbeitung werden Verbindungsobjekte zwischen Prozessen kopiert, da sie Prozesse verzweigt und daher alle Dateideskriptoren des übergeordneten Prozesses kopiert. Davon abgesehen ist eine Verbindung zum SQL Server nur eine Datei. Sie können sie unter Linux unter / proc // fd / .... sehen. Jede geöffnete Datei wird von gegabelten Prozessen gemeinsam genutzt. Weitere Informationen zum Gabeln finden Sie hier .

Meine Lösung bestand darin, die Datenbankverbindung kurz vor dem Starten von Prozessen zu schließen. Jeder Prozess erstellt die Verbindung selbst neu, wenn er eine benötigt (getestet in django 1.4):

from django import db
db.connections.close_all()
def db_worker():      
    some_paralell_code()
Process(target = db_worker,args = ())

Pgbouncer / pgpool ist nicht mit Threads im Sinne von Multiprocessing verbunden. Es ist eher eine Lösung, um die Verbindung nicht bei jeder Anforderung zu schließen = die Verbindung zu Postgres unter hoher Last zu beschleunigen.

Aktualisieren:

Um Probleme mit der Datenbankverbindung vollständig zu beseitigen, verschieben Sie einfach die gesamte mit der Datenbank verbundene Logik nach db_worker. Ich wollte QueryDict als Argument übergeben. Eine bessere Idee ist, einfach die Liste der IDs zu übergeben. Siehe QueryDict und values_list ('id', flat = Richtig), und vergessen Sie nicht, es zur Liste zu machen! list (QueryDict) vor der Übergabe an db_worker. Aus diesem Grund kopieren wir keine Modelldatenbankverbindung.

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]))   
Lechup
quelle
Können Sie das etwas über die Übergabe von IDs von einem Abfragesatz an eine selbst beantwortete Frage erklären?
Jharwood
1
Multiprocessing kopiert Verbindungsobjekte zwischen Prozessen, da es Prozesse verzweigt, und kopiert daher alle Dateideskriptoren des übergeordneten Prozesses. Davon abgesehen ist eine Verbindung zum MySQL-Server nur eine Datei. Sie können sie unter Linux unter / proc / <PID> / fd / .... sehen. Jede geöffnete Datei wird von AFAIK-Forked-Prozessen gemeinsam genutzt. stackoverflow.com/questions/4277289/…
vlad-ardelean
1
Gilt das auch für Threads? Z.B. Schließen Sie db conn im Hauptthread und greifen Sie dann in jedem Thread auf db zu. Bekommt jeder Thread eine eigene Verbindung?
James Lin
1
Sie sollten verwenden django.db.connections.close_all(), um alle Verbindungen mit einem Anruf zu schließen.
Denis Malinovsky
1
Hm ... Hier ist ein ziemlich interessantes Gespräch zwischen Leuten aus Django: code.djangoproject.com/ticket/20562 Vielleicht wird es etwas Licht in dieses Thema bringen? Grundsätzlich sind Verbindungen nicht abzweigbar ... Jeder Prozess sollte eine eigene Verbindung haben.
Lechup
18

Wenn Sie mehrere Datenbanken verwenden, sollten Sie alle Verbindungen schließen.

from django import db
for connection_name in db.connections.databases:
    db.connections[connection_name].close()

BEARBEITEN

Bitte verwenden Sie dasselbe wie @lechup, um alle Verbindungen zu schließen (nicht sicher, seit welcher Django-Version diese Methode hinzugefügt wurde):

from django import db
db.connections.close_all()
Mounir
quelle
9
Dies ruft nur mehrmals
db.close_connection auf
2
Ich sehe nicht, wie dies funktionieren kann, ohne irgendwo Alias ​​oder Informationen zu verwenden.
RemcoGerlich
Das ... kann nicht funktionieren. @Mounir, sollten Sie es zu verwenden , modifizieren , um aliasoder infoin dem forSchleifenkörper, wenn dboder close_connection()Stützen , dass.
0atman
5

Für Python 3 und Django 1.9 hat dies für mich funktioniert:

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 __name__ == '__main__':
    multiprocessing.Process(target=db_worker)

Beachten Sie, dass ich dies ohne django.setup () nicht zum Laufen bringen könnte. Ich vermute, dass etwas für die Mehrfachverarbeitung erneut initialisiert werden muss.

Kevin Nasto
quelle
Vielen Dank! Dies hat bei mir funktioniert und sollte jetzt wahrscheinlich die akzeptierte Antwort für neuere Versionen von Django sein.
Krischan
Die Django-Methode besteht darin, einen Verwaltungsbefehl zu erstellen und kein eigenständiges Wrapper-Skript zu erstellen. Wenn Sie den Verwaltungsbefehl nicht verwenden, müssen Sie setupdjango verwenden.
Lechup
2
Ihre for-Schleife macht eigentlich nichts damit db.connections.databases.items()- sie schließt die Verbindung nur mehrmals. db.connections.close_all()funktioniert gut, solange es die Worker-Funktion genannt wird.
Tao_oat
2

Ich hatte Probleme mit der "geschlossenen Verbindung", als ich Django -Testfälle nacheinander ausführte. Zusätzlich zu den Tests gibt es einen weiteren Prozess, der die Datenbank während der Testausführung absichtlich ändert. Dieser Vorgang wird in jedem Testfall setUp () gestartet.

Eine einfache Lösung bestand darin, meine Testklassen von TransactionTestCasestatt zu erben TestCase. Dadurch wird sichergestellt, dass die Datenbank tatsächlich geschrieben wurde und der andere Prozess eine aktuelle Ansicht der Daten hat.

Juuso Ohtonen
quelle
1

(keine gute Lösung, aber eine mögliche Problemumgehung)

Wenn Sie keinen Sellerie verwenden können, könnten Sie vielleicht Ihr eigenes Warteschlangensystem implementieren, indem Sie einer Aufgabentabelle Aufgaben hinzufügen und einen normalen Cron haben, der sie aufnimmt und verarbeitet? (über einen Verwaltungsbefehl)

zweite
quelle
möglicherweise - hatte gehofft, diese Komplexität zu vermeiden, aber wenn es die einzige Lösung ist, muss ich diesen Weg gehen - danke für den Vorschlag. Ist Sellerie die beste Antwort? Wenn ja, kann ich vielleicht Druck machen, um es zu bekommen, aber es wird eine Weile dauern. Ich verbinde Sellerie mit verteilter Verarbeitung im Gegensatz zur parallelen Verarbeitung auf einer Maschine, aber vielleicht ist das nur meine mangelnde Erfahrung damit.
Daroo
2
Sellerie passt gut zu jeder Verarbeitung, die außerhalb des Anforderungs- / Antwortzyklus erforderlich ist
zweiter
1

Hey, ich bin auf dieses Problem gestoßen und konnte es durch Ausführen der folgenden Schritte beheben (wir implementieren ein System mit eingeschränkten Aufgaben).

task.py

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

ScheduledJob.py

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

Um Rennbedingungen (mit mehreren gleichzeitigen Benutzern) zu vermeiden, ist es am besten, database.close () so schnell wie möglich aufzurufen, nachdem Sie den Vorgang abgebrochen haben. Möglicherweise besteht immer noch die Möglichkeit, dass ein anderer Benutzer irgendwo auf der ganzen Linie eine Anfrage an die Datenbank stellt, bevor Sie die Datenbank leeren können.

Um ehrlich zu sein, wäre es wahrscheinlich sicherer und intelligenter , wenn Ihre Gabel den Befehl nicht direkt aufruft, sondern stattdessen ein Skript auf dem Betriebssystem aufruft, damit die erzeugte Aufgabe in einer eigenen Django-Shell ausgeführt wird!

Mike McMahon
quelle
Ich habe Ihre Idee, statt vorher in der Gabel zu schließen, verwendet, um einen Dekorateur zu erstellen, den ich meinen Arbeiterfunktionen hinzufüge.
Rebs
1

Sie könnten Postgre mehr Ressourcen geben, in Debian / Ubuntu können Sie Folgendes bearbeiten:

nano /etc/postgresql/9.4/main/postgresql.conf

indem Sie 9.4 durch Ihre Postgre-Version ersetzen.

Hier sind einige nützliche Zeilen, die mit Beispielwerten aktualisiert werden sollten. Namen sprechen für sich:

max_connections=100
shared_buffers = 3000MB
temp_buffers = 800MB
effective_io_concurrency = 300
max_worker_processes = 80

Achten Sie darauf, diese Parameter nicht zu stark zu erhöhen, da dies zu Fehlern führen kann, wenn Postgre versucht, mehr Ressourcen als verfügbar zu verwenden. Die obigen Beispiele funktionieren einwandfrei auf einem Debian 8 GB Ram-Computer mit 4 Kernen.

Ashwini Chaudhary
quelle
0

Wenn Sie lediglich E / A-Parallelität benötigen und keine Parallelität verarbeiten, können Sie dieses Problem vermeiden, indem Sie Ihre Prozesse auf Threads umstellen. Ersetzen

from multiprocessing import Process

mit

from threading import Thread

Das ThreadObjekt hat die gleiche Schnittstelle wieProcsess

Zags
quelle
0

Wenn Sie auch Verbindungspooling verwenden, hat das Folgende für uns funktioniert und die Verbindungen nach dem Verzweigen zwangsweise geschlossen. Vorher schien nicht zu helfen.

from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS

connections[DEFAULT_DB_ALIAS].dispose()
Kevin Parker
quelle