Многие люди выбирают Django из-за его простоты.
Код Django простой и лаконичный, мы меньше думаем о костылях и больше о бизнес-логике.
Gevent также выбран потому, что он прост, очень быстр и не несет в себе адских обратных вызовов.
В голове возникает отличная идея объединить две простые и удобные вещи воедино.
Мы патчим Django и наслаждаемся простотой, лаконичностью и производительностью, делаем много запросов к другим сайтам, создаём подпроцессы и вообще используем наш новый асинхронный Django по максимуму.
Но объединив их, мы незаметно расставили на своем пути несколько граблей.
Django ORM и пул подключений к базе данных
Django был создан как основа для синхронных приложений.Каждый процесс получает запрос от пользователя, полностью его обрабатывает, отправляет результат и только после этого может приступить к обработке другого запроса.
Принцип действия прост, как пареная репа.
Отличная архитектура для создания блога или новостного сайта, но мы хотим большего.
Давайте возьмем простой пример, который имитирует большую активность HTTP-запросов.
Пусть это будет сервис сокращения ссылок:
Traceback: .Без gevent этот код был бы невероятно медленным и с трудом обслуживал бы два или три одновременных запроса, но с gevent все работает. Запускаем наш проект через uwsgi (который стал де-факто стандартом для развертывания Python-сайтов:> link = LinkModel.objects.get(url=url) OperationalError: FATAL: remaining connection slots are reserved for non-replication superuser connections
OperationalError: ?????: ?????????? ????? ??????????? ??????????????? ??? ??????????? ????????????????? (?? ??? ??????????)Мы стараемся тестировать десять запросов на сокращение ссылок одновременно и довольны: все запросы обрабатываются без ошибок за минимальное время.
Мы запускаем наш новый сервис, сидим и наблюдаем за его успешным развитием.
Нагрузка растет с 10 до 75 одновременных запросов, и ему такая нагрузка не важна.
Неожиданно однажды ночью на почту приходит несколько тысяч писем следующего содержания: # testproject/__init__.py
__import__('gevent.monkey').
monkey.patch_all() # testproject/main/models.py from django.db import models class LinkModel(models.Model): url = models.URLField(max_length=256, db_index=True, unique=True) # testproject/main/views.py import urllib2, httplib from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect from .
models import LinkModel def check_url(url): request = urllib2.Request(url) request.get_method = lambda: 'HEAD' try: response = urllib2.urlopen(request) except (urllib2.URLError, httplib.HTTPException): return False response.close() return True def remember(request): url = request.GET['url'] try: link = LinkModel.objects.get(url=url) except LinkModel.DoesNotExist: if not check_url(url): return HttpResponse('Oops :(') link = LinkModel.objects.create(url=url) return HttpResponse(' http://localhost:8000 ' + reverse( go_to, args=(str(link.id).
encode('base64').
strip(), )))
def go_to(request, code):
obj = LinkModel.objects.get(id=code.decode('base64'))
return HttpResponseRedirect(obj.url)
И хорошо, если вы установите локаль ru_US.UTF-8 В postgresql.conf , потому что если вы использовали конфигурацию Ubuntu/Debian по умолчанию, вы получите тысячу электронных писем с сообщением типа: uwsgi --http-socket 0.0.0.0:8000 --gevent 1000 -M -p 2 -w testproject.wsgi
Приложение создало слишком много подключений к базе данных (по умолчанию — максимум 100 подключений), за что оно было оштрафовано.
Вот самый первый подводный камень: В Django нет пула подключений к базе данных.
, потому что в синхронном коде он просто не нужен.
Один синхронный процесс Django не может обрабатывать запросы параллельно; он обслуживает только один запрос за раз, и поэтому ему не нужно создавать более одного подключения к базе данных.
Фактически.
Фактически, Django может работать в многопоточном режиме, в котором один процесс может обрабатывать несколько запросов.
Это сервер, который запускает команда Manage.py — сервер запуска , при этом в документации написано, что этот режим совершенно непригоден для боевого применения.
Выход один: нам срочно нужен пул подключений к базе данных.
Например, существует относительно мало реализаций пула для Django. Джанго-БД-пул И Джанго-psycopg2-пул .
Первый пул основан на psycopg2.TreadedConnectionPool который выдает исключение при попытке установить соединение из пустого пула.
Приложение будет вести себя так же, как и раньше, но другие приложения смогут создавать соединение с базой данных.
Второй пул основан на gevent.Queue : Если вы попытаетесь получить соединение из пустого пула, гринлет будет заблокирован до тех пор, пока другой гринлет не установит соединение в пул.
Скорее всего, вы выберете второе решение, поскольку оно более логично.
Запросы к базе данных внутри гринлетов
Мы уже пропатчили приложение с помощью gevent и нам не хватает синхронных вызовов, так почему бы не воспользоваться гринлетами по максимуму? Мы можем выполнять несколько HTTP-запросов параллельно или создавать подпроцессы.
Возможно, мы захотим использовать базу данных в гринлетах: def some_view(request):
greenlets = [gevent.spawn(handler, i) for i in xrange(5)]
gevent.joinall(greenlets)
return HttpResponse("Done")
def handler(number):
obj = MyModel.objects.get(id=number)
obj.response = send_http_request_somewhere(obj.request)
obj.save(update_fields=['response'])
Прошло несколько часов, и вдруг наше приложение полностью перестало работать: на любой запрос, который мы получаем Ошибка 504 Время ответа сервера истекло .
Что случилось на этот раз? Для объяснения вам придется прочитать небольшой код Django. Хранит все соединения в себе django.db.connections , который является экземпляром класса django.db.utils.ConnectionHandler .
Когда ORM готов сделать запрос, он запрашивает соединение с базой данных, вызывая соединения['по умолчанию'] .
ConnectionHandler.__getattr__ в свою очередь проверяет наличие соединения в ConnectionHandler._connections , а если он пуст, то создается новое соединение.
Все открытые соединения должны быть закрыты после использования.
Вот что делает сигнал запрос_закончен , который работает в django.http.HttpResponseBase.close .
Джанго закрывает соединения с базой данных в самый последний момент, когда никто не уверен, что к ним получит доступ, что вполне логично.
Вся проблема в том, как ConnectionHandler хранит соединения с базой данных.
Для этого он использует Threading.local , который после манкипатчинга превращается в gevent.local.local .
После объявления эта структура данных действует так, как если бы она была уникальной в каждом гринлете.
Контроллер some_view стали обрабатываться в одном гринлете, а в ConnectionHandler._connections уже есть подключение к базе данных.
Мы создали несколько новых гринлетов, в которых ConnectionHandlers._connections оказался пуст, и из пула для этих гринлетов были взяты дополнительные соединения.
После того, как исчезли наши новые гринлеты, исчезло и их содержимое.
местный() , соединения с базой данных безвозвратно теряются и обратно в пул их никто не вернет. Со временем бассейн становится совершенно пустым.
При разработке с помощью Django+gevent всегда следует помнить об этом нюансе и в конце каждого гринлета закрывать соединения с базой данных, вызывая django.db.close_connection .
Его также необходимо вызывать при возникновении исключительной ситуации, для чего можно использовать небольшой декоратор контекстного менеджера.
Пример такого декоратора class autoclose(object):
def __init__(self, f=None):
self.f = f
def __call__(self, *args, **kwargs):
with self:
return self.f(*args, **kwargs)
def __enter__(self):
pass
def __exit__(self, exc_type, exc_info, tb):
from django.db import close_connection
close_connection()
return exc_type is None
Пользоваться этой оберткой нужно с умом: закрывайте все соединения перед каждым переключением гринлетов (например, перед urllib2.urlopen ), а также убедитесь, что соединения не закрываются внутри незавершенной транзакции или не проходят через итератор, например Модель.
объекты.
все() .
Использование Django ORM отдельно от Django
Мы можем столкнуться с теми же проблемами, если создадим что-то похожее на cron или Celery, которое время от времени делает запросы к базе данных.То же самое нас ждет, если мы поднимем Django с помощью gevent.WSGIServer и параллельно поднимите любые сервисы с другим протоколом, который будет использовать Django ORM. Главное, своевременно возвращать соединения к пулу базы данных, тогда приложение будет работать стабильно и приносить вам радость.
выводы
В этом посте было сказано об элементарных правилах о том, что необходимо использовать пул соединений с базой данных и о том, что соединения необходимо возвращать обратно в пул сразу после использования.Вы бы обязательно учли это, если бы использовали только gevent и psycopg2. Но Django ORM работает на настолько высокоуровневых абстракциях, что разработчику не приходится иметь дело с подключениями к базе данных, и со временем эти правила можно забыть и открыть заново.
Теги: #python #gevent #django #django orm #postgresql #python #django
-
Грызуны
19 Oct, 24 -
Будет Ли Теперь Pgp Против Elcomsoft?
19 Oct, 24 -
Основы Запуска Sun
19 Oct, 24 -
Деловой Интернет. Первый День
19 Oct, 24 -
Торрент-Трекер Посвященный Аниме
19 Oct, 24