Разработка сетевых приложений с gevent
DESCRIPTION
Это слайды моего доклада на DevConf 2010.TRANSCRIPT
Разработка сетевых приложений с gevent
Андрей Попп
http://braintrace.ru
@andreypopp
Сетевые сервисы должны уметь одновременно обрабатыватьнесколько клиентских запросов.
Андрей Попп: Разработка сетевых приложений с gevent
Современные сетевые сервисы должны уметь одновременнообрабатывать огромное количество клиентских запросов.
Андрей Попп: Разработка сетевых приложений с gevent
Стратегии организации I/O
Основные стратегии обработки соединений относительноорганизации I/O:
Блокирующий I/O – необходимо несколько потоков ОС.Неблокирующий I/O + мултиплексор – достаточно дажеодного потока ОС.
Андрей Попп: Разработка сетевых приложений с gevent
Блокирующий I/O
Необходимо использовать отдельный поток на каждоеактивное соединение.Много активных соединений = много активных потоков =большое количество потребляемой памяти.Переключение контекста исполнения обходится дорого.В Python есть GIL.
Андрей Попп: Разработка сетевых приложений с gevent
Блокирующий I/O
Объективно не подходит для обслуживания большогоколичество одновременных соединений.
Андрей Попп: Разработка сетевых приложений с gevent
Неблокирующий I/O
Операции на сокетах не блокируют поток – онипроизводяться только тогда, когда доступны.Для обслуживания нескольких активных соединенийдостаточно даже одного потока.Меньшее количество потребляемой памяти.Обычно приходится выстраивать код приложения ввидеобработчиков событий на сокетах.
Андрей Попп: Разработка сетевых приложений с gevent
Неблокирующий I/O
Использвование неблокирующего I/O кажется болееподходящим решением проблемы.
Андрей Попп: Разработка сетевых приложений с gevent
Неблокирующий I/O
Но какие распространённые библиотеки/фрэймворки мы имеемдля Python: asyncore, Twisted, Tornado.
Андрей Попп: Разработка сетевых приложений с gevent
С Twisted приходится писать асинхронный код – это неудобно!
def handle_client(req):deferred = make_api_request ()deferred.addCallback(handle_api_resp , req)deferred.addErrback(handle_error)return deferred
def handle_api_resp(api_resp , req):deferred = make_db_request ()deferred.addCallback(handle_db_resp , api_resp , req)return deferred
def handle_db_resp(db_resp , api_response , req):# work with api_resp and db_resprequest.write(" success ")
def handle_error(failure , req):# handle errorrequest.write("error")return failure
Андрей Попп: Разработка сетевых приложений с gevent
Синхронный код писать проще и получается он понятнее.
def handle_client(request ):try:
api_response = make_api_request ()db_response = make_db_request ()# work with api_response and db_responserequest.write(" success ")
except Exception:request.write("error")raise
Но приходится использовать блокирующий I/O.
Андрей Попп: Разработка сетевых приложений с gevent
Что делать? Нужно искать компромис!
Андрей Попп: Разработка сетевых приложений с gevent
Микропотоки
Микропотоки или “зелёные” потоки или userspace-потоки:
Это как функции, исполнение которых можноприостановить, а потом – продолжить.Работают внутри одного или нескольких потоков ОС.Для их исполнения необходим планировщик.Обычно дёшевы в плане потребления памяти ипереключения контекста.
Андрей Попп: Разработка сетевых приложений с gevent
Микропотоки + Неблокирующий I/O
Чтобы микропотоки исполнялись им необходим планировщик.
Предлагается следующий вариант:
Как только микропоток пытается выполнить I/O, онпередаёт управление планировщику.После того, как выполнение I/O становится доступным длямикропотока – планировщик возвращает ему управление.
Андрей Попп: Разработка сетевых приложений с gevent
Блокируется только микропоток, который пытается выполнитьI/O, а не весь интерпретатор.
Андрей Попп: Разработка сетевых приложений с gevent
Это называется кооперативная многозадачность – потоки самирешают когда передать исполнение другим.
Андрей Попп: Разработка сетевых приложений с gevent
Существует также преемптивная или вытесняющаямногозадачность – поток вытесняется планировщиком послеопределённого количества выполненных инструкций или по
истичении определённого времени.
Андрей Попп: Разработка сетевых приложений с gevent
Но разве микропотоки есть в Python?
Андрей Попп: Разработка сетевых приложений с gevent
Микропотоки в Python – Генераторы
Можно реализовать микропотоки в Python c помощьюгенераторов (PEP 342, начиная с версии Python 2.5).
Чтобы передать исполнение – делаем yield.
К сожалению:
Кооперация с помощью yield – это слишком явно инеудобно, приходиться самим думать, когда отдаватьуправление.Генераторы не сохраняют весь стэк во время остановки –yield должен быть всегда на самом верху.
Андрей Попп: Разработка сетевых приложений с gevent
Микропотоки в Python – greenlet
Микропотоки с библиотекой greenlet:
Микропоток или просто гринлет это объект greenlet.Кооперация посредством вызова метода greenlet.switch.greenlet – это “выжимка” из Stackless Python.
Андрей Попп: Разработка сетевых приложений с gevent
Как работают гринлеты
from greenlet import greenlet
>>> def test1 ():... print ’one ’... gr2.switch ()... print ’two ’...>>> def test2 ():... print ’three ’... gr1.switch ()... print ’four ’...>>> gr1 = greenlet(test1)>>> gr2 = greenlet(test2)>>> gr1.switch ()onethreetwo
Андрей Попп: Разработка сетевых приложений с gevent
Микропотоки реализованные с помощью greenlet удобны – онине страдают от недостатков генераторов.
Андрей Попп: Разработка сетевых приложений с gevent
Теперь нам нужен планировщик, который будет контролироватьисполнение гринлетов, руководствуясь событиями I/O.
Андрей Попп: Разработка сетевых приложений с gevent
gevent = libevent + greenlet
Такой планировщик предоставляет нам библиотека gevent.
Андрей Попп: Разработка сетевых приложений с gevent
Почему используется libevent
Почему gevent использует libevent для обработки событий:
Это быстрая библиотека, написанная на языке C – самцикл полностью в C коде.Libevent используется длительное время и хорошо себязарекомендовала (Chromium, Memcached, Io).Предоставляет встраиваемый HTTP-сервер – evhttp.Имеет API для работы с DNS – evdns.
Андрей Попп: Разработка сетевых приложений с gevent
Как устроен geventОбщая схема
Цикл обработки событий libevent работает в отдельномгринлете – этот гринлет называется хаб.Хаб запускается неявно и только при необходимости.Кооперация между гринлетами происходит через хаб:
Гринлет может переключиться только на хаб.Гринлет может получить управление только через хаб.
Андрей Попп: Разработка сетевых приложений с gevent
Как устроен geventОрганизация I/O
Чтобы совершить I/O наш гринлет должен:
1 Отправить запрос на I/O в цикл обработки событий.2 Переключиться на хаб.3 Хаб запускает выполнение других гринлетов.4 . . .5 Как только запрос на I/O выполнен, хаб переключается
обратно на наш гринлет.
Андрей Попп: Разработка сетевых приложений с gevent
Блокируется только гринлет, который пытается выполнить I/O,а не весь интерпретатор.
Андрей Попп: Разработка сетевых приложений с gevent
Сетевой I/O с gevent
Чтобы выполнять I/O гринлеты должны использоватькооперативный gevent.socket. Его API полностью повторяет
socket стандартной библиотеки Python.
Андрей Попп: Разработка сетевых приложений с gevent
Сетевой I/O с gevent
Кстати, gevent.socket.getaddrinfo,gevent.socket.gethostbyname используют evdns и тожеявляются блокирующими только для вызывающего их
гринлета.
Андрей Попп: Разработка сетевых приложений с gevent
Пример: конкурентный эхосервер с geventРеализация
from gevent import socket , spawn
def serve ((host , port), handler ):acceptor = socket.socket(socket.AF_INET , socket.STREAM)acceptor.bind((host , port))while True:
client , address = acceptor.accept ()spawn(handler , client , address)
def handler(sock , address ):f = sock.makefile ()while True:
line = f.readline ()if not line:
breakf.write(line)f.flush()
Андрей Попп: Разработка сетевых приложений с gevent
Пример: конкурентный эхосервер с geventОбработка соединений
Обработка соединения происходит в отдельном гринлете:
from gevent import socket , spawn
def serve ((host , port), handler ):acceptor = socket.socket(socket.AF_INET , socket.STREAM)acceptor.bind((host , port))while True:
client , address = acceptor.accept ()spawn(handler, client, address)
def handler(sock , address ):f = sock.makefile ()while True:
line = f.readline ()if not line:
breakf.write(line)f.flush()
Андрей Попп: Разработка сетевых приложений с gevent
Пример: конкурентный эхосервер с geventТочки кооперации
В этих точках гринлет отдаёт управление циклу libevent:
from gevent import socket , spawn
def serve ((host , port), handler ):acceptor = socket.socket(socket.AF_INET , socket.STREAM)acceptor.bind((host , port))while True:
client, address = acceptor.accept()spawn(handler , client , address)
def handler(sock , address ):f = sock.makefile ()while True:
line = f.readline()if not line:
breakf.write(line)f.flush()
Андрей Попп: Разработка сетевых приложений с gevent
Оказалось достаточно использовать gevent.socket вместоsocket и вызвать gevent.spawn в нужном месте.
Андрей Попп: Разработка сетевых приложений с gevent
Пример: конкурентный эхосервер с geventИспользуем StreamServer
Нужно использовать gevent.server.StreamServer:
from gevent.server import StreamServer
def handler(sock , address ):f = sock.makefile ()while True:
line = f.readline ()if not line:
breakf.write(line)f.flush()
StreamServer ((’localhost ’, 6000) , handler ). serve_forever ()
Андрей Попп: Разработка сетевых приложений с gevent
Мы умеем создавать новые гринлеты (gevent.spawn) ииспользовать gevent.socket. Посмотрим, что ещё мы можем
делать с gevent.
Андрей Попп: Разработка сетевых приложений с gevent
Базовые возможности geventЖдём завершения работы гринлета
Ждём пока гринлет прекратит свою работу:
>>> task = gevent.spawn(lambda a, b: a + b, 1, 2)>>> task.join()
Если нам нужен результат работы гринлета:
>>> task = gevent.spawn(lambda a, b: a + b, 1, 2)>>> task.get()3
В случае, если гринлет прекратил работу из-за исключения:
>>> task = gevent.spawn(lambda a, b: a / b, 1, 0)>>> task.get()Traceback (most recent call last):...ZeroDivisionError: integer division or modulo by zero
Андрей Попп: Разработка сетевых приложений с gevent
Базовые возможности geventПреждевременное завершение гринлета
Чтобы завершить выполнение гринлета:
>>> task = gevent.spawn(lambda a, b: a + b, 1, 2)>>> task.kill()
Андрей Попп: Разработка сетевых приложений с gevent
Базовые возможности geventПриостанавливаем выполнение гринлета
Иногда нужно приостановить выполнение гринлета:
>>> def some_work ():... # do some work... gevent.sleep (10)... # continue
Функция gevent.sleep аналогична time.sleep, только“засыпает” не весь интерпретатор, а отдельный гринлет.
Андрей Попп: Разработка сетевых приложений с gevent
Базовые возможности geventОбработка таймаутов
Обработка таймаутов осуществляется с gevent.Timeout:
timeout = Timeout (10)timeout.start()try:
# do some workexcept gevent.Timeout:
# handle timeoutfinally:
timeout.cancel ()
. . . или как контекст-менеджер:
try:with gevent.Timeout (10):
# do some workexcept gevent.Timeout:
# handle timeout
Андрей Попп: Разработка сетевых приложений с gevent
Управляем несколькими гринлетамиОбъединяем гринлеты в группы
Иногда нужно управлять несколькими гринлетами сразу:
>>> tasks = gevent.pool.GreenletSet ()>>> for i in range (10):... tasks.spawn(do_some_work , i)>>> tasks.join()
. . . или. . .
>>> tasks = gevent.pool.GreenletSet ()>>> tasks.map(lambda a: a**2, range (10))[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Андрей Попп: Разработка сетевых приложений с gevent
Управляем несколькими гринлетамиРаботаем с пулом гринлетов
А иногда бывает нужно ограничить количество одновременновыполняемых гринлетов в группе:
>>> tasks = gevent.pool.Pool(size =5)>>> for i in range (10):... tasks.spawn(do_some_work , i)>>> tasks.join()
В данном случае будет одновременно исполняться только 5гринлетов.
Таким образом можно, например, ограничить количествоодновременно обрабатываемых соединений.
Андрей Попп: Разработка сетевых приложений с gevent
HTTP-сервисы с geventИспользуем evhttp
Модуль gevent.http предоставляет API для использованияevhttp, но нас больше интересует WSGI.
Андрей Попп: Разработка сетевых приложений с gevent
HTTP-сервисы с geventWSGI сервер
Модуль gevent.wsgi – реализация WSGI на базе gevent.http:
from gevent.wsgi import WSGIServer
def hello_world(environ , start_response ):start_response (’200 OK ’, [(’Content -Type ’, ’text/html ’)])return ["It works !"]
WSGIServer ((’localhost ’, 8000) , hello_world ). serve_forever ()
Можно использовать практически любой WSGIфрэймворк/библиотеку: Django, Werkzeug, WebOb, repoze.bfg,Pylons.
Андрей Попп: Разработка сетевых приложений с gevent
Используем gevent с другими библиотеками
Как уже говорилось, API gevent.socket полностью повторяетsocket из стандартной библиотеки Python.
Андрей Попп: Разработка сетевых приложений с gevent
Используем gevent с другими библиотекамиПредоставляем фабрику кооперативных сокетов
Если библиотека позволяет пользовательскому коду подменятькласс используемого сокета:
from somenetworklibrary import Clientfrom gevent import socket
class CooperativeGeventAwareClient(Client ):
def create_socket(self):sock = socket.socket(socket.AF_INET , socket.STREAM)return sock
Но что делать, если не позволяет?
Андрей Попп: Разработка сетевых приложений с gevent
Используем gevent с другими библиотекамиMonkey patching
gevent предоставляет возможность пропатчить модуль socketстандартной библиотеки:
from gevent import monkeymonkey.patch_socket ()
После этого, код, который использует модуль socket будеткооперироваться.
Андрей Попп: Разработка сетевых приложений с gevent
Используем gevent с другими библиотекамиMonkey patching
Кроме этого в gevent.monkey:
patch_time() – заменяем time.sleep() накооперативный gevent.sleep().patch_thread() – создаём гринлеты вместо потоков ОС,также патчит threading.local.patch_os(), patch_ssl(), patch_select() – . . .patch_all() – патчим всё.
Андрей Попп: Разработка сетевых приложений с gevent
Пример: используем gevent с urllib2
from gevent.pool import Poolfrom gevent import monkeymonkey.patch_all ()import urllib2
tasks = Pool(size =20)
urls = [’http :// www.gevent.org ’, ...]
def print_head(url):print ’Starting %s’ % urldata = urllib2.urlopen(url).read()print ’%s: %s bytes: %r’ % (url , len(data), data [:50])
for url in urls:tasks.spawn(print_head , url)
tasks.join()
Андрей Попп: Разработка сетевых приложений с gevent
Используем gevent с другими библиотеками
Я также использовал gevent совместно с SQLAlchemy, boto.
Андрей Попп: Разработка сетевых приложений с gevent
Где используется gevent
Несколько проектов, которые используют gevent:
Gunicorn – WSGI HTTP сервер, может использовать geventдля обработки запросов.pastegevent – используем gevent.wsgi для запуска WSGIприложений вместе с PasteDeploy.gevent-mysql – драйвер для MySQL, написанный на Cython,использующий API gevent.psycogreen – отдельная ветка psycopg, которая работает сасинхронными библиотеками, например с gevent.
Андрей Попп: Разработка сетевых приложений с gevent
Некоторые ограничения
Как это обычно бывает, существуют некоторые ограничения:
После os.fork() необходимо вызывать gevent.reinit().Библиотеку можно использовать только в одном потокеОС – ограничение libevent 1.4.Блокирующий stdin – вскоре будет исправлено.Библиотеки которые не используют socket блокируютинтерпретатор полностью – можно выполнять их вотдельном потоке ОС.
Андрей Попп: Разработка сетевых приложений с gevent
Какие темы я не затронул
Остались темы, которые я не затронул:
Линки между гринлетами.Примитивы синхронизации – gevent.event.Синхронные очереди – gevent.queue.
Андрей Попп: Разработка сетевых приложений с gevent
Полезные ссылки
http://gevent.org – официальный сайт и документация.
http://bitbucket.org/denis/gevent/ – исходный код.
http://groups.google.com/group/gevent – рассылка.
http://blog.gevent.org/ – блог проекта.
http://twitter.com/gevent – twitter проекта.
И наконец #gevent на irc.freenode.net.
Андрей Попп: Разработка сетевых приложений с gevent
Спасибо!
Андрей Попп: Разработка сетевых приложений с gevent