Подтвердить что ты не робот

Постоянное соединение с DB в приложении Django/WSGI

Я хочу, чтобы постоянное соединение было открыто для базы данных устаревшей третьей стороны в веб-приложении, основанном на django.

user - inet - webserver - persistent-db-connection

Я хочу сохранить связь между веб-приложением и устаревшей базой данных, так как создание нового соединения очень медленное для этой специальной БД.

Это не похоже на обычный пул соединений, так как мне нужно сохранить соединение для каждого пользователя сети. Пользователь "Foo" нуждается в собственном соединении между веб-сервером и устаревшим DB.

До сих пор я использую Apache и wsgi, но я мог бы изменить, если другое решение лучше подходит.

До сих пор я использую django. Здесь я тоже могу измениться. Но боль будет больше, потому что уже есть много кода, который нужно снова интегрировать.

До сих пор я использую Python. Думаю, Node.js лучше подойдет здесь, но боль в изменении слишком высока.

Конечно, потребуется какой-то тайм-аут. Если HTTP-запрос от пользователя "Foo" отсутствует в течение N минут, то постоянное соединение должно быть отключено.

Как это можно решить?

Обновление

Я называю это DB, но это не БД, который настроен через settings.DATABASES. Это странная, унаследованная не широко распространенная DB-подобная система, которую мне нужно интегрировать.

Если в настоящий момент у меня есть 50 человек в Интернете, используя веб-приложение, мне нужно иметь 50 постоянных подключений. Один для каждого пользователя.

Код для подключения к DB

Я мог бы выполнить эту строку в каждом запросе:

strangedb_connection = strangedb.connect(request.user.username)

Но эта операция медленная. Использование соединения выполняется быстро.

Конечно, strangedb_connection не может быть сериализован и не может быть сохранен в сеансе: -)

4b9b3361

Ответ 1

рабочий демон, управляющий соединением

Ваша фотография выглядит следующим образом:

user  ----------->  webserver  <--------[1]-->  3rd party DB

connection [1] is expensive.

Вы можете решить эту проблему с помощью

user ---->  webserver  <--->  task queue[1]  <--->  worker daemon  <--[2]-> 3rd party DB

[1] task queue can be redis, celery or rabbitmq.
[2] worker daemon keeps connection open.

Рабочий демон выполнит подключение к третьей стороне базы данных и сохранит соединение открытым. Это будет означать, что каждый запрос не должен оплачивать расходы на подключение. Очередь задач - это межпроцессная связь, диспетчерская работа с демоном и выполнение запросов в стороннем db. Веб-сервер должен быть как можно более легким с точки зрения обработки и позволить работникам выполнять дорогостоящие задачи.

предварительная загрузка с помощью apache + modwsgi

Фактически вы можете preload и иметь дорогое соединение, выполненное до первого запроса. Это делается с помощью WSGIImportScript. Я не помню на макушке головы, если конфигурация предварительной нагрузки + форсирование означает, что каждый запрос уже будет иметь открытое соединение и делить его; но поскольку у вас есть большая часть кода, это может быть легкий эксперимент.

предварительная загрузка с помощью uwsgi

uwsgi также поддерживает предварительную загрузку. Это делается с директивой import.

Ответ 2

Насколько я могу судить, вы исключили большинство (всех?) общих решений этой проблемы:

  • Хранить соединение в словаре... нуждаться в N рабочих и не может гарантировать, какой запрос отправляется к тому, кто работает
  • Хранить данные в кеше... слишком много данных
  • Сохранить информацию о подключении в кеше... соединение не является сериализуемым

Насколько я вижу, на самом деле существует только одно "мета" решение, используйте предложение @Gahbu словаря и гарантируйте, что запросы для данного user идут к тому же работнику. То есть выяснить способ сопоставления объекта user с данным рабочим одинаково каждый раз (может быть, хэш их имя и MOD по числу рабочих?).

Это решение не будет максимально использовать ваши рабочие N, если активные пользователи все будут сопоставлены одному и тому же работнику, но если все пользователи одинаково активны одновременно, работа должна быть одинаково распространена. (Если они не все одинаково вероятны, то отображение может учесть это).

Два возможных способа, которые я могу сделать, это:

1. Напишите специальный распределитель запросов

Я не очень хорошо знаком с интерфейсом apache/wsgi, но... возможно, будет возможно заменить компонент на вашем сервере Apache, который отправляет HTTP-запросы рабочим с некоторой пользовательской логикой, так что он всегда отправляет к тому же процессу.

2. Запустите балансировщик нагрузки/прокси-сервер перед N однопоточными рабочими

Я не уверен, что вы можете использовать готовый пакет здесь или нет, но концепция будет следующей:

  • Запустите прокси-сервер, который реализует эту логику "привязать пользователя к индексу"
  • Затем прокси-сервер перенаправляет запросы на одну из N копий вашего веб-сервера Apache/wsgi, у каждого из которых есть один рабочий.

NB: Эта вторая идея, с которой я столкнулся здесь: https://github.com/benoitc/gunicorn/issues/183

Резюме

Для обоих вариантов реализация в существующем приложении довольно проста. Приложение просто меняет использование словаря для хранения постоянного соединения (создавая его, если его еще нет). Тестирование одного экземпляра аналогично в dev, как в производстве. В процессе производства сами экземпляры не являются более мудрыми, так как их всегда спрашивают об одних и тех же пользователях.

Мне нравится Вариант 2 здесь по следующим причинам:

  • Возможно, существует существующий серверный пакет, который позволяет вам определить эту прокси-прокси.
  • Если нет, создание настраиваемого прокси-приложения для сидения перед вашим текущим приложением может быть не слишком сложным (особенно учитывая ограничения, которые вы уже используете, когда запрос достигает службы strangedb).

Ответ 3

Вместо того, чтобы иметь несколько рабочих процессов, вы можете использовать директиву WSGIDaemonProcess, чтобы иметь несколько рабочих потоков, которые все работают в одном процессе, Таким образом, все потоки могут совместно использовать одно и то же сопоставление соединений с БД.

Что-то вроде этого в вашей конфигурации apache...

# mydomain.com.conf

<VirtualHost *:80>

    ServerName mydomain.com
    ServerAdmin [email protected]

    <Directory />
        Require all granted
    </Directory>

    WSGIDaemonProcess myapp processes=1 threads=50 python-path=/path/to/django/root display-name=%{GROUP}
    WSGIProcessGroup myapp
    WSGIScriptAlias / /path/to/django/root/myapp/wsgi.py

</VirtualHost>

... вы можете использовать что-то столь же простое, как это в своем приложении Django...

# views.py

import thread
from django.http import HttpResponse

# A global variable to hold the connection mappings
DB_CONNECTIONS = {}

# Fake up this "strangedb" module
class strangedb(object):

    class connection(object):
        def query(self, *args):
            return 'Query results for %r' % args

    @classmethod
    def connect(cls, *args):
        return cls.connection()


# View for homepage
def home(request, username='bob'):

    # Remember thread ID
    thread_info = 'Thread ID = %r' % thread.get_ident()

    # Connect only if we're not already connected
    if username in DB_CONNECTIONS:
        strangedb_connection = DB_CONNECTIONS[username]
        db_info = 'We reused an existing connection for %r' % username
    else:
        strangedb_connection = strangedb.connect(username)
        DB_CONNECTIONS[username] = strangedb_connection
        db_info = 'We made a connection for %r' % username

    # Fake up some query
    results = strangedb_connection.query('SELECT * FROM my_table')

    # Fake up an HTTP response
    text = '%s\n%s\n%s\n' % (thread_info, db_info, results)
    return HttpResponse(text, content_type='text/plain')

... который, при первом попадании, производит...

Thread ID = 140597557241600
We made a connection for 'bob'
Query results for 'SELECT * FROM my_table'

... и, во втором...

Thread ID = 140597145999104
We reused an existing connection for 'bob'
Query results for 'SELECT * FROM my_table'

Очевидно, что вам нужно будет добавить что-то, чтобы срывать соединения с БД, когда они больше не требуются, но трудно узнать, как это сделать, без дополнительной информации о том, как ваше приложение должно работать.

Обновление # 1: Что касается мультиплексирования ввода-вывода и многопоточности

Я работал с потоками дважды в своей жизни, и каждый раз это был кошмар. Много времени было потрачено впустую на отладку не воспроизводимых проблемы. Я думаю, что управляемая событиями и неблокирующая архитектура ввода/вывода может быть более прочным.

Решение, использующее мультиплексирование ввода-вывода, могло бы быть лучше, но было бы более сложным, а также потребовало бы, чтобы ваша библиотека "strangedb" поддерживала его, то есть должна была бы обрабатывать EAGAIN/EWOULDBLOCK и имеют возможность повторить системный вызов, когда это необходимо.

Многопоточность в Python гораздо менее опасна, чем на большинстве других языков, из-за Python GIL, который, по сути, делает все потоки байт-кода Python -безопасно.

На практике потоки выполняются одновременно, когда базовый код C использует макрос Py_BEGIN_ALLOW_THREADS, который с его копией Py_END_ALLOW_THREADS, обычно обернуты системными вызовами и интенсивными операциями с процессором.

Поверхность этого заключается в том, что практически невозможно иметь столкновение потоков в коде Python, хотя недостатком является то, что он не всегда будет оптимально использовать несколько ядер процессора на одной машине.

Причина, по которой я предлагаю вышеизложенное решение, заключается в том, что он относительно прост и потребует минимальных изменений кода, но может быть лучше, если бы вы могли подробнее рассказать о своей библиотеке "strangedb". Кажется довольно странным иметь БД, которая требует отдельного сетевого подключения для каждого пользователя.

Обновление # 2: Что касается многопроцессорности и многопоточности

... ограничения GIL, связанные с потоками, кажутся немного проблемой. Разве это не одна из причин, почему тенденция заключается в использовании отдельных вместо процессов?

Это вполне возможно, главная причина, почему существует модуль Python multiprocessing, то есть обеспечить параллельное выполнение байт-кода Python на нескольких ядрах ЦП, хотя в этом модуле есть недокументированный ThreadPool класс, который использует потоки, а не процессы.

"Ограничения GIL", безусловно, будут проблематичными в тех случаях, когда вам действительно нужно использовать каждый отдельный цикл ЦП на каждом ядре ЦП, например. если вы пишете компьютерную игру, которая должна отображать 60 кадров в секунду в режиме высокой четкости.

Большинство веб-сервисов, однако, скорее всего, проведут большую часть своего времени, ожидая чего-то, например, сетевой ввод-вывод или дисковый ввод-вывод, которые потоки Python позволят выполнять одновременно.

В конечном счете, это компромисс между производительностью и ремонтопригодностью, и учитывая, что аппаратное обеспечение обычно намного дешевле, чем время разработки, предпочтение от обслуживания по производительности обычно более экономически выгодно.

Честно говоря, в тот момент, когда вы решили использовать язык виртуальной машины, такой как Python, вместо языка, который компилируется в настоящий машинный код, например C, вы уже говорите, что готовы принести в жертву некоторую производительность в обмен для удобства.

См. также Проблема C10K для сравнения методов масштабирования веб-сервисов.

Ответ 4

Один простой способ сделать это - заставить другой python-процесс управлять пулом постоянного соединения (по одному для каждого пользователя и может таймаут при необходимости). И тогда другой процесс python и django могут связываться с чем-то быстрым, как zeromq. interprocess communication в python