Постоянное подключение к базе данных Django
Я использую django с apache и mod_wsgi и PostgreSQL (все на одном хосте), и мне нужно обрабатывать множество простых динамических запросов страниц (сотни в секунду). Я столкнулся с проблемой, что узким местом является то, что django не имеет постоянного соединения с базой данных и повторно подключается к каждому запросу (что занимает около 5 мс).
Выполняя бенчмарк, я получил это с постоянным подключением, я могу обрабатывать около 500 р/с, пока я не получаю только 50 р/с.
У кого-нибудь есть совет? Как изменить django для использования постоянного соединения? Или ускорить подключение от python к DB
Спасибо заранее.
Ответы
Ответ 1
Django 1.6 добавил поддержку постоянных подключений (ссылка на doc для django 1.9):
Постоянные соединения избегают накладных расходов при восстановлении подключение к базе данных в каждом запросе. Они контролируются CONN_MAX_AGE, который определяет максимальное время жизни подключение. Он может быть установлен независимо для каждой базы данных.
Ответ 2
Попробуйте PgBouncer - легкий пул соединений для PostgreSQL.
Особенности:
- Несколько уровней жестокости при вращении соединений:
- Объединение сеансов
- Объединение транзакций
- Составление отчетов
- Низкие требования к памяти (по умолчанию 2k на соединение).
Ответ 3
В Django trunk отредактируйте django/db/__init__.py
и закомментируйте строку:
signals.request_finished.connect(close_connection)
Этот обработчик сигнала заставляет его отключиться от базы данных после каждого запроса. Я не знаю, какие побочные эффекты будут делать это, но не имеет смысла начинать новое соединение после каждого запроса; он разрушает производительность, как вы заметили.
Я использую это сейчас, но я не выполнил полный набор тестов, чтобы увидеть, что-нибудь сломается.
Я не знаю, почему все думают, что для этого нужен новый бэкэнд или специальный пул соединений или другие сложные решения. Это кажется очень простым, хотя я не сомневаюсь, что есть некоторые неясные ошибки, которые заставляют их делать это в первую очередь - с этим нужно бороться более разумно; Как вы заметили, накладные расходы 5 мс для каждого запроса довольно много для высокопроизводительного сервиса. (Мне требуется 150 мс - я пока не понял почему.)
Изменить: другое необходимое изменение - в django/middleware/transaction.py; удалите два теста transaction.is_dirty() и всегда вызывайте commit() или rollback(). В противном случае он не будет совершать транзакцию, если она только считывает из базы данных, которая будет закрывать блокировки, которые должны быть закрыты.
Ответ 4
Я создал небольшой Django patch, который реализует объединение пулов MySQL и PostgreSQL с помощью объединения sqlalchemy.
Это отлично работает при создании http://grandcapital.net/ в течение длительного периода времени.
Патч был написан после немного поглаживания темы.
Ответ 5
Отказ от ответственности: я не пробовал это.
Я считаю, что вам нужно реализовать пользовательский конец базы данных. В Интернете есть несколько примеров, показывающих, как реализовать конец базы данных с пулом соединений.
Использование пула соединений, вероятно, будет хорошим решением для вас, поскольку сетевые подключения сохраняются открытыми, когда соединения возвращаются в пул.
- Этот пост достигает этого, исправляя Django (один из комментариев указывает, что лучше реализовать пользовательский задний конец за пределами core django code)
- Этот пост представляет собой реализацию пользовательского db back end
Оба сообщения используют MySQL - возможно, вы можете использовать похожие методы с Postgresql.
Edit:
- В книге Django упоминается пул соединений Postgresql, используя pgpool (tutorial).
- Кто-то отправил патч для бэкэнд psycopg2, который реализует объединение пулов. Я предлагаю создать копию существующего конца в вашем собственном проекте и исправить это.
Ответ 6
Я создал небольшой пользовательский интерфейс psycopg2, который реализует постоянное соединение с использованием глобальной переменной.
Благодаря этому я смог улучшить количество запросов в секунду с 350 до 1600 (на очень простой странице с несколькими выборами)
Просто сохраните его в файле с именем base.py
в любом каталоге (например, postgresql_psycopg2_persistent) и установите в настройках
DATABASE_ENGINE для projectname.postgresql_psycopg2_persistent
Внимание!!! код не является потокобезопасным - вы не можете использовать его с потоками python из-за непредсказуемых результатов, в случае mod_wsgi используйте режим демона prefork с потоками = 1
# Custom DB backend postgresql_psycopg2 based
# implements persistent database connection using global variable
from django.db.backends.postgresql_psycopg2.base import DatabaseError, DatabaseWrapper as BaseDatabaseWrapper, \
IntegrityError
from psycopg2 import OperationalError
connection = None
class DatabaseWrapper(BaseDatabaseWrapper):
def _cursor(self, *args, **kwargs):
global connection
if connection is not None and self.connection is None:
try: # Check if connection is alive
connection.cursor().execute('SELECT 1')
except OperationalError: # The connection is not working, need reconnect
connection = None
else:
self.connection = connection
cursor = super(DatabaseWrapper, self)._cursor(*args, **kwargs)
if connection is None and self.connection is not None:
connection = self.connection
return cursor
def close(self):
if self.connection is not None:
self.connection.commit()
self.connection = None
Или вот потокобезопасный, но потоки python не используют несколько ядер, поэтому вы не получите такого повышения производительности, как в предыдущем. Вы можете использовать этот вариант с несколькими процессами.
# Custom DB backend postgresql_psycopg2 based
# implements persistent database connection using thread local storage
from threading import local
from django.db.backends.postgresql_psycopg2.base import DatabaseError, \
DatabaseWrapper as BaseDatabaseWrapper, IntegrityError
from psycopg2 import OperationalError
threadlocal = local()
class DatabaseWrapper(BaseDatabaseWrapper):
def _cursor(self, *args, **kwargs):
if hasattr(threadlocal, 'connection') and threadlocal.connection is \
not None and self.connection is None:
try: # Check if connection is alive
threadlocal.connection.cursor().execute('SELECT 1')
except OperationalError: # The connection is not working, need reconnect
threadlocal.connection = None
else:
self.connection = threadlocal.connection
cursor = super(DatabaseWrapper, self)._cursor(*args, **kwargs)
if (not hasattr(threadlocal, 'connection') or threadlocal.connection \
is None) and self.connection is not None:
threadlocal.connection = self.connection
return cursor
def close(self):
if self.connection is not None:
self.connection.commit()
self.connection = None