Багатопроцесорна обробка Django та підключення до бази даних


83

Передумови:

Я працюю над проектом, який використовує Django з базою даних Postgres. Ми також використовуємо mod_wsgi на випадок, коли це важливо, оскільки деякі мої пошукові запити в Інтернеті згадували про це. Під час подання веб-форми подання Django запускає роботу, яка займе значну кількість часу (більше, ніж користувач хотів би зачекати), тому ми починаємо роботу за допомогою системного дзвінка у фоновому режимі. Завдання, яке виконується зараз, має мати можливість читати та записувати в базу даних. Оскільки ця робота триває так довго, ми використовуємо багатопроцесорну обробку, щоб паралельно запускати її частини.

Проблема:

Сценарій верхнього рівня має підключення до бази даних, і коли він породжує дочірні процеси, здається, що батьківський зв’язок доступний для дітей. Тоді є виняток щодо того, як НАБІР РІВНЯ ІЗОЛЯЦІЇ ОПЕРАЦІЙ повинен викликатися перед запитом. Дослідження показали, що це пов’язано зі спробою використовувати одне і те ж підключення до бази даних у кількох процесах. Один потік, який я знайшов, пропонував викликати connection.close () на початку дочірніх процесів, щоб Django автоматично створив нове з'єднання, коли йому це потрібно, і тому кожен дочірній процес матиме унікальне з'єднання - тобто не буде спільним. Це не спрацювало для мене, оскільки виклик connection.close () у дочірньому процесі змусив батьківський процес скаржитися на те, що зв’язок втрачено.

Інші висновки:

Деякі речі, які я прочитав, вказували на те, що ви насправді цього не можете зробити, а багатопроцесорність, mod_wsgi та Django погано грають разом. Напевно, здається, у це важко повірити.

Деякі пропонували використовувати селеру, що може бути довгостроковим рішенням, але наразі я не можу встановити селеру, очікуючи деяких процедур затвердження, тому зараз це не варіант.

Знайшов кілька посилань на SO та в інших місцях щодо постійних з'єднань з базами даних, що, на мою думку, є іншою проблемою.

Також знайдено посилання на psycopg2.pool та pgpool та щось про вишибалу. Слід визнати, що я не розумів більшості того, що читав на цих темах, але це, звичайно, не стрибало на мене як на те, що я шукав.

Поточне "усунення":

Наразі я повернувся до простого запуску речей послідовно, і це працює, але повільніше, ніж хотілося б.

Будь-які пропозиції щодо того, як я можу використовувати багатопроцесорну роботу для паралельного запуску? Здається, якби я міг мати батьків і двох дітей, всі вони мали незалежні підключення до бази даних, все було б нормально, але я, здається, не маю такої поведінки.

Дякую, і вибачте за довжину!

Відповіді:


70

Багатопроцесорне копіювання об’єктів зв’язку між процесами, оскільки воно розгалужує процеси, а отже, копіює всі дескриптори файлів батьківського процесу. З огляду на це, підключення до SQL-сервера - це лише файл, ви можете побачити його в Linux під / proc // fd / .... будь-який відкритий файл буде спільним для розгалужених процесів. Детальніше про роздвоєння ви можете знайти тут .

Моє рішення було просто закрити db-з'єднання безпосередньо перед запуском процесів, кожен процес відтворив сам підключення, коли йому буде потрібно (перевірено в django 1.4):

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

Pgbouncer / pgpool не пов'язаний з потоками у значенні багатопроцесорної обробки. Це швидше рішення для не закриття з'єднання при кожному запиті = пришвидшення підключення до postgres під великим навантаженням.

Оновлення:

Для повного усунення проблем із підключенням до бази даних просто перемістіть всю логіку, пов’язану з базою даних, у db_worker - я хотів передати QueryDict як аргумент ... Краща ідея - просто передати список ідентифікаторів ... Див. QueryDict та values_list ('id', flat = Правда), і не забудьте передати його в список! список (QueryDict) перед передачею до db_worker. Завдяки цьому ми не копіюємо підключення до бази даних моделей.

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

чи не могли б ви пояснити цей біт про передачу ідентифікаторів із набору запитів на запитання з самовідповіддю?
Jharwood

1
багатопроцесорне копіювання об'єктів з'єднання між процесами, оскільки воно розгалужує процеси, а отже, копіює всі дескриптори файлів батьківського процесу. З огляду на це, підключення до сервера mysql - це лише файл, ви можете побачити його в linux під / proc / <PID> / fd / .... будь-який відкритий файл буде спільним для розгалужених процесів AFAIK. stackoverflow.com/questions/4277289 / ...
влад-Ardelean

1
Це стосується і ниток? Напр. закрити db conn в основному потоці, потім отримати доступ до db у кожному потоці, чи кожен потік отримає своє власне з'єднання?
James Lin

1
Вам слід скористатися, django.db.connections.close_all()щоб розірвати всі з'єднання одним дзвінком.
Денис Малиновський

1
Хм ... Ось досить цікава розмова між людьми з django: code.djangoproject.com/ticket/20562, можливо, це проллє трохи світла на цю тему? В основному підключення "не підлягають перестановці" ... Кожен процес повинен мати своє підключення.
лечуп

18

При використанні декількох баз даних слід закрити всі з'єднання.

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

РЕДАГУВАТИ

Будь ласка, використовуйте те саме, що @lechup згадується, щоб закрити всі підключення (не впевнений, з якої версії django був доданий цей метод):

from django import db
db.connections.close_all()

9
це просто дзвінок db.close_connection кілька разів
ibz

2
Я не розумію, як це може працювати, не використовуючи ніде псевдонім чи інформацію.
RemcoGerlich

Це ... не може працювати. @Mounir, ви повинні змінити його використовувати aliasабо infoв forтілі циклу, якщо dbабо close_connection()підтримує це.
0atman

5

Для Python 3 і Django 1.9 ось що мені вдалося:

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)

Зауважте, що без django.setup () я не міг змусити це працювати. Я припускаю, що щось потрібно знову ініціалізувати для багатопроцесорної обробки.


Дякую! Це спрацювало для мене, і, мабуть, зараз має бути прийнятою відповіддю для нових версій django.
krischan

Шлях django полягає у створенні команди управління, а не створення окремого сценарію обгортки. Якщо ви не використовуєте команду управління, вам потрібно використовувати setupdjango.
лечуп

2
Цикл for насправді нічого не робить db.connections.databases.items()- він просто перериває з'єднання кілька разів. db.connections.close_all()відмінно працює, доки це називається робочою функцією.
tao_oat

2

У мене були проблеми із закритим зв’язком при послідовному запуску тестових випадків Django . На додаток до тестів, існує також інший процес, навмисно модифікуючи базу даних під час виконання тесту. Цей процес запускається в кожному тестовому випадку setUp ().

Просте виправлення полягало в тому, щоб успадкувати мої тестові класи TransactionTestCaseзамість TestCase. Це гарантує, що база даних була фактично записана, а інший процес має сучасний вигляд даних.


1

(не чудове рішення, але можливе обхідне рішення)

якщо ви не можете користуватися селерою, можливо, ви могли б реалізувати власну систему черг, додавши завдання до якоїсь таблиці завдань і маючи звичайний cron, який їх відбирає та обробляє? (за допомогою команди управління)


можливо - сподівався уникнути такого рівня складності, але якщо це єдине рішення, то, можливо, мені доведеться піти цим шляхом - дякую за пропозицію. Чи найкраща відповідь на селеру? якщо так, я, можливо, зможу натиснути, щоб отримати, але це займе деякий час. Я пов'язую селеру з розподіленою обробкою, на відміну від паралельної обробки на одній машині, але, можливо, це лише мій недостатній досвід роботи з нею ..
daroo

2
селера добре підходить для будь-якої обробки, необхідної поза циклом відповіді на запит
друга

1

Привіт, я зіткнувся з цією проблемою і зміг її вирішити, виконавши наступне (ми впроваджуємо обмежену систему завдань)

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

Чесно кажучи, для запобігання перегоновим умовам (з кількома одночасними користувачами) було б найкраще зателефонувати до database.close () якомога швидше після того, як ви розгалужите процес. Можливо, все ще існує ймовірність того, що інший користувач десь у рядку повністю зробить запит на базу даних, перш ніж у вас буде можливість очистити базу даних.

Чесно кажучи, було б безпечніше і розумніше, якщо б ваш форк не викликав команду безпосередньо, а замість цього викликав скрипт в операційній системі, щоб породжене завдання працювало у власній оболонці django!


Я використав вашу ідею закрити всередині виделки замість раніше, щоб зробити декоратор, який я додаю до своїх робочих функцій.
Ребс

1

Ви можете дати більше ресурсів Postgre, у Debian / Ubuntu ви можете редагувати:

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

замінивши 9.4 на вашу версію postgre.

Ось декілька корисних рядків, які слід оновити з допомогою прикладів значень, імена говорять самі за себе:

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

Будьте обережні, щоб не надто збільшити ці параметри, оскільки це може призвести до помилок, коли Postgre намагається взяти більше ресурсів, ніж доступно. Наведені вище приклади чудово працюють на машині Debian 8GB Ram, обладнаній 4 ядрами.


0

Якщо вам потрібен лише паралелізм вводу-виводу, а не обробка паралелізму, ви можете уникнути цієї проблеми, переключивши процеси на потоки. Замінити

from multiprocessing import Process

з

from threading import Thread

ThreadОб'єкт має той же інтерфейс,Procsess


0

Якщо ви також використовуєте пул з'єднань, для нас працювало наступне, примусово закриваючи з'єднання після розгалуження. Раніше нібито не допомагало.

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

connections[DEFAULT_DB_ALIAS].dispose()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.