Django Orm, Gevent И Грабли Зеленого Цвета

Многие люди выбирают Django из-за его простоты.

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

Gevent также выбран потому, что он прост, очень быстр и не несет в себе адских обратных вызовов.

В голове возникает отличная идея объединить две простые и удобные вещи воедино.

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

Но объединив их, мы незаметно расставили на своем пути несколько граблей.



Django ORM и пул подключений к базе данных

Django был создан как основа для синхронных приложений.

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

Принцип действия прост, как пареная репа.

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

Давайте возьмем простой пример, который имитирует большую активность HTTP-запросов.

Пусть это будет сервис сокращения ссылок:

  
   
 
 Traceback:
    .

> link = LinkModel.objects.get(url=url) OperationalError: FATAL: remaining connection slots are reserved for non-replication superuser connections

Без gevent этот код был бы невероятно медленным и с трудом обслуживал бы два или три одновременных запроса, но с gevent все работает. Запускаем наш проект через uwsgi (который стал де-факто стандартом для развертывания Python-сайтов:
  
  
 
 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

Вместе с данным постом часто просматривают: