Я не понимаю, зачем вам здесь кэшированное соединение, и почему бы просто не переподключаться при каждом запросе кеширования учетных данных пользователя где-нибудь, но в любом случае я постараюсь наметить решение, которое может соответствовать вашим требованиям.
Я бы предложил сначала рассмотреть более общую задачу - кэшировать что-то между последующими запросами, которые ваше приложение должно обработать, и не может сериализоваться в сеансы django
.
В вашем конкретном случае это общее значение будет соединение с базой данных (или несколько соединений).
Давайте начнем с простой задачи разделения простой счетной переменной между запросами, просто чтобы понять, что на самом деле происходит под капотом.
Удивительно, но ни в одном ответе не упоминалось ничего о веб-сервере, который вы могли бы использовать!
На самом деле существует несколько способов обработки одновременных подключений в веб-приложениях:
- Имея несколько процессов , каждый запрос поступает в один из них случайным образом
- Имея несколько потоков , каждый запрос обрабатывается случайным потоком
- стр.1 и стр.2 в сочетании
- Различные асинхронные методы, когда есть один процесс + цикл обработки событий обработка запросов с предупреждением, что обработчики запросов не должны блок на долгое время
Исходя из собственного опыта, стр. 1-2 подходят для большинства типичных веб-приложений.
Apache1.x
может работать только с п.1, Apache2.x
может обрабатывать все 1-3.
Давайте запустим следующее приложение django
и запустим однопроцессный веб-сервер gunicorn .
Я собираюсь использовать gunicorn
, потому что его довольно легко настроить в отличие от apache
(личное мнение: -)
views.py
import time
from django.http import HttpResponse
c = 0
def main(self):
global c
c += 1
return HttpResponse('val: {}\n'.format(c))
def heavy(self):
time.sleep(10)
return HttpResponse('heavy done')
urls.py
from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.main, name='main'),
path('heavy/', views.heavy, name='heavy')
]
Запуск в режиме одного процесса:
gunicorn testpool.wsgi -w 1
Вот наше дерево процессов - есть только один рабочий, который будет обрабатывать ВСЕ запросы
pstree 77292
-+= 77292 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1
\--- 77295 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1
Попытка использовать наше приложение:
curl 'http://127.0.0.1:8000'
val: 1
curl 'http://127.0.0.1:8000'
val: 2
curl 'http://127.0.0.1:8000'
val: 3
Как видите, вы можете легко разделить счетчик между последующими запросами.
Проблема в том, что вы можете обслуживать только один запрос параллельно. Если вы запрашиваете / heavy / в одной вкладке, / не будет работать, пока / heavy не будет выполнено
Теперь давайте используем 2 рабочих процесса:
gunicorn testpool.wsgi -w 2
Вот так будет выглядеть дерево процессов:
pstree 77285
-+= 77285 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
|--- 77288 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
\--- 77289 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
Тестирование нашего приложения:
curl 'http://127.0.0.1:8000'
val: 1
curl 'http://127.0.0.1:8000'
val: 2
curl 'http://127.0.0.1:8000'
val: 1
Первые два запроса были обработаны первым worker process
, а 3-й - вторым рабочим процессом, который имеет собственное пространство памяти, поэтому вместо этого вы видите 1 3 .
Обратите внимание, что ваш вывод может отличаться, потому что процессы 1 и 2 выбираются случайным образом. Но рано или поздно вы попадете на другой процесс.
Это не очень полезно для нас, потому что нам нужно обрабатывать несколько одновременных запросов, и нам нужно каким-то образом обрабатывать наш запрос конкретным процессом, который не может быть выполнен в общем случае.
Большинство пулирующих технологий, выходящих из коробки, будут кэшировать соединения только в рамках одного процесса, если ваш запрос обслуживается другим процессом - NEW соединение должно быть сделано.
Позволяет перейти к темам
gunicorn testpool.wsgi -w 1 --threads 2
Опять же - только 1 процесс
pstree 77310
-+= 77310 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2
\--- 77313 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2
Теперь, если вы запустите / heavy на одной вкладке, вы все равно сможете запросить / , и ваш счетчик будет сохраняться между запросами!
Даже если количество потоков увеличивается или уменьшается в зависимости от вашей рабочей нагрузки, оно все равно должно работать нормально.
Проблемы : вам нужно синхронизировать доступ к общей переменной, как это, используя технику синхронизации потоков Python ( read more ).
Другая проблема заключается в том, что одному и тому же пользователю может потребоваться выполнить несколько запросов параллельно - то есть открыть несколько вкладок.
Чтобы справиться с этим, вы можете открывать несколько соединений по первому запросу, когда у вас есть доступные учетные данные БД.
Если пользователю нужно больше подключений, чем ваше приложение может подождать, пока соединение не станет доступным.
Вернуться к вашему вопросу
Вы можете создать класс, который будет иметь следующие методы:
from contextlib import contextmanager
class ConnectionPool(object):
def __init__(self, max_connections=4):
self._pool = dict()
self._max_connections = max_connections
def preconnect(self, session_id, user, password):
# create multiple connections and put them into self._pool
# ...
@contextmanager
def get_connection(sef, session_id):
# if have an available connection:
# mark it as allocated
# and return it
try:
yield connection
finally:
# put it back to the pool
# ....
# else
# wait until there's a connection returned to the pool by another thread
pool = ConnectionPool(4)
def some_view(self):
session_id = ...
with pool.get_connection(session_id) as conn:
conn.query(...)
Это не полное решение - вам нужно каким-то образом удалить устаревшие соединения, которые долгое время не использовались.
Если пользователь возвращается через длительное время и его соединение закрыто, ему нужно будет снова предоставить свои учетные данные - надеюсь, это нормально с точки зрения вашего приложения.
Также имейте в виду, что у python threads
есть свои потери производительности, не уверен, что это проблема для вас.
Я не проверял его на apache2
(слишком большая нагрузка на конфигурацию, я не использовал его целую вечность и обычно использую uwsgi ), но он также должен работать там - был бы рад услышать от вас ответ
если вам удастся запустить его)
И также не забывайте о p.4 (асинхронный подход) - вряд ли вы сможете использовать его на apache, но это стоит изучить - ключевые слова: django + gevent , Джанго + Асинсио . У него есть свои плюсы и минусы, и он может сильно повлиять на реализацию вашего приложения, поэтому трудно предложить какое-либо решение, не зная деталей вашего приложения подробно