Всё хорошо
Twisted — асинхронный (событийно-ориентированный) фреймворк, написанный на Python. Мощное средство для быстрой разработки сетевых (и не только) сервисов. Он разработан с использованием паттерна проектирования Reactor. Сервисы созданные с использованием Twisted быстры и надежны, фреймворк позволяет не писать макаронный код, насыщенный непонятными коллбэками, имеет внутри себя красивые хелперы (Deferred, Transport, Protocol etc). Другими словами, делает нашу жизнь бекенд разработчиков лучше.
Но есть и проблемы
Основная проблема в том, что многочисленные, надежные, оттестированные, удобные библиотеки, использующие в своей основе синхронные модули Python (socket, os, ssl, time, select, thread, subprocess, sys, signal etc), просто возьмут и заблокируют нам основной процесс, цикл реактора и наступит беда. Такими библиотеками, к примеру, являются psycopg2, request, mysql и другие. В частности, psycopg2 используется в Django ORM как один из бекендов баз данных.
Что же делать?
Есть три пути. Сложный, приемлемый и хороший. Сложный — реализовать аналог библиотеки на Twisted. Приемлемый — использовать deferToThread и запускать синхронный код в отдельных потоках (используя пул потоков реализованный в Twisted). О хорошем пути (по моему мнению) и пойдет речь в заметке.
Используем «зеленые» потоки и события для переключения контекста!
Что нам для этого нужно?
- Greenlets — легковесные «зеленые» потоки, которые работают внутри главного процесса приложения
- Gevent — фреймворк, который позволяет переключать контекст между гринлетами, в тот момент, когда исполняемый код блокируется
- Метод реактора [deferToGreenlet], позволяющий обернуть гринлет в Deferred
Пример применения технологии в реальном проекте
Я не стал писать собственную реализацию реактора с возможностью отправлять код в гринлеты, так как нашел готовое решение, протестировал и внедрил в проект. Код реактора можно забрать отсюда.
Для использования geventreactor при инициализации приложения нужно его установить:
from geventreactor import install
install()
Теперь нам доступны новые методы:
__all__ = ['deferToGreenletPool', 'deferToGreenlet', 'callMultipleInGreenlet', 'waitForGreenlet', 'waitForDeferred',
'blockingCallFromGreenlet', 'IReactorGreenlets', 'GeventResolver', 'GeventReactor', 'install']
По аналогии с reactor.deferToThread(f, *args, **kwargs), можно вызывать reactor.deferToGreenlet(f, *args, **kwargs), где f — callable объект, а *args и **kwargs его аргументы.
Чтобы все заработало необходимо также пропатчить библиотеки в пространстве имен:
from gevent import monkey
monkey.patch_all()
После данных манипуляций, основные библиотеки Python будут пропатчены фреймворком Gevent. Смотрите документацию по Gevent
Теперь все библиотеки или код, который напрямую импортирует их, при вызове блокирующихся методов или функций, будут вызывать соответствующие события в системе Gevent. На эти события повешены коллбеки, позволяющие переключать контекст между гринлетами.
У меня в проекте используется Django ORM для манипуляции данными в PostgreSQL. Поэтому для того, чтобы методы ORM не блокировали процесс нужно использовать специальный бекенд, позволяющий создавать пул соединений с БД и переключаться между соединениями. Одним из бекендов является django-db-geventpool
Использовать django-db-geventpool не трудно. Достаточно следовать документации.
Что дальше?
Метод reactor.deferToGreenlet возвращает объект Deferred, с которым можно работать как с обычным Deferred.
Например, у нас есть модель:
class ExampleModel(models.Model):
title = models.CharField(max_length=256)
Мы хотим получить все модели и передать их какому-то обработчику внутри системы. Мы можем написать что-то вроде:
d = reactor.deferToGreenlet(ExampleModel.objects.all)
И наш код не заблокирует основной процесс. Ведь в тот момент, когда Django ORM вызовет cursor.execute(), который будет ожидать ответ от драйвера базы данных, geventreactor переключит контекст на другой Deferred.
Что в итоге?
Мы можем выполнять синхронный код внутри Twisted, не создавая при этом лишних потоков или процессов, при этом не блокируя event loop реактора. Главное следовать основным принципам работы с асинхронными системами, куски кода не должны выполняться слишком долго, gevent позволяет принудительно переключать контекст из любого места кода, там, где это нам удобно, достаточно лишь вызвать gevent.sleep().
Комментарии (11)
apatrushev
21.12.2015 19:14А ещё есть inlineCallbacks. Метод тоже из разряда «серединка на половинку».
mx2000
Мсье знает толк…
Как по мне — давно пора уже открыть для себя asyncio и выкинуть python 2 на свалку истории.
viatoriche
Не всегда asyncio подходит. Не всегда подходит python 3. Плюс ко всему asyncio позволяет работать только с сетевым IO и с модулем subprocess. Как насчет файлового IO? Как быть с библиотеками, которые реализуют множество интерфейсов и протоколов, все переписывать на asyncio? Asyncio — это светлое будущее, но в суровом настоящем это, в основном, боль, в условиях, когда приходится работать над бизнес-проектами.
mx2000
нет конечно, сидеть и ждать, когда python 3 обрастет на 87% поддержкой библиотек — наверное, тоже своеобразная стратетия, но, граждане программисты, вы программисты или где?
Не хватает какой-то либы? Сядь и напиши, будь мужиком! Андрей Светлов сидит и пилит пачку библиотек под asyncio и мы уже полгода как сидим на его aiohttp, aiopg, aioredis и нормально себя чувствуем. О какой боли вы говорите — для меня загадка. =)
viatoriche
И тут врывается такой начальник с криками: «Почему так долго? Сроки горят, клиенты уходят, конкуренты уже 100500 фич запилили, а вы тут чем занимаетесь, дармоеды?»
bosha
А профит есть? У меня в тестах блокирующий redis работал даже быстрее, чем его неблокирующий анагалог.
apatrushev
Это всё от того, что блокирующий код быстрее асинхронного. Что, вроде как, должно быть самоочевидным фактом.
bosha
Это очевидно. Если говорить точнее, то создание genTask/Future может быть затратнее и дольше чем блокировка потока. Собственно, поэтому я и задал этот вопрос. В чём смысл использовать aioredis?
viatoriche
Мое мнение по этому вопросу. Проблемы с потоками начинаются когда их становится слишком много, или когда забивается очередь пулла, а нам жизненно необходимо быстро обратиться к ресурсу. Тут и получается выигрыш от асинхронности, так как на поддержание и создание соединения не нужно выделять ресурсы на создание потока или же ожидать высвобождения потока из очереди. А так как синхронный код работает только в потоке, то общая производительность системы падает, падает rps. Если соединений не много, то потоки будут выгоднее асинхронности зачастую (особенно если используется пулл), но при возрастании нагрузки отдельно работающий быстрее код будет заторможен внешними зависимостями — переключением контекста, блокировками и другими особенностями многопоточных приложений.
bosha
А при чем тут потоки? :)
Когда я проводил тесты (с tornado, правда), то у меня обращения с использованием асинхронной библиотеки redis были более затратными по ресурсам и времени, чем использование блокирующей. Всё дело в том, что redis весьма шустрый, и получается, что блокировка потока на время ожидания ответа от redis оказывается выигрышнее, чем создание genTask/Future и выполнение асинхронного запроса. Плюс ко всему, получается, что в принципе (если других блокирующих запросов нет) заблокированный поток после разблокировки может сразу отдать данные, и идти обрабатывать запросы, не возвращаясь более к этому.
Или Вы используете aioredis для подключения к каналам?
viatoriche
Я не использую aioredis, я использую deferToGreenlet где у меня вызывается простой блокирующий код библиотеки redis (внутри которой select, os, socket, threading… ), вызовы которого перехватываются gevent. Так что не знаю, в чем разница именно aioredis (как асинхронной реализации redis) и redis. Gevent довольно реактивен (внутри libevent), в своих тестах разницы производительности не замечаю. В любом случае есть опасность заблокировать процесс при использовании целиком синхронного редиса в асинхронном приложении (без greenlets а именно на coroutine), при условии если redis вдруг затормозит, затупит, зависнет или случится какая-то беда внутри сети.