2. Сетевые сервисы должны уметь одновременно обрабатывать
несколько клиентских запросов.
Андрей Попп: Разработка сетевых приложений с gevent
3. Современные сетевые сервисы должны уметь одновременно
обрабатывать огромное количество клиентских запросов.
Андрей Попп: Разработка сетевых приложений с gevent
4. Стратегии организации I/O
Основные стратегии обработки соединений относительно
организации I/O:
Блокирующий I/O – необходимо несколько потоков ОС.
Неблокирующий I/O + мултиплексор – достаточно даже
одного потока ОС.
Андрей Попп: Разработка сетевых приложений с gevent
5. Блокирующий I/O
Необходимо использовать отдельный поток на каждое
активное соединение.
Много активных соединений = много активных потоков =
большое количество потребляемой памяти.
Переключение контекста исполнения обходится дорого.
В Python есть GIL.
Андрей Попп: Разработка сетевых приложений с gevent
6. Блокирующий I/O
Объективно не подходит для обслуживания большого
количество одновременных соединений.
Андрей Попп: Разработка сетевых приложений с gevent
7. Неблокирующий I/O
Операции на сокетах не блокируют поток – они
производяться только тогда, когда доступны.
Для обслуживания нескольких активных соединений
достаточно даже одного потока.
Меньшее количество потребляемой памяти.
Обычно приходится выстраивать код приложения ввиде
обработчиков событий на сокетах.
Андрей Попп: Разработка сетевых приложений с gevent
8. Неблокирующий I/O
Использвование неблокирующего I/O кажется более
подходящим решением проблемы.
Андрей Попп: Разработка сетевых приложений с gevent
9. Неблокирующий I/O
Но какие распространённые библиотеки/фрэймворки мы имеем
для Python: asyncore, Twisted, Tornado.
Андрей Попп: Разработка сетевых приложений с gevent
10. С Twisted приходится писать асинхронный код – это неудобно!
def handle_client ( req ):
deferred = m a k e _ a p i _ r e q u e s t ()
deferred . addCallback ( handle_api_resp , req )
deferred . addErrback ( handle_error )
return deferred
def h and le _a p i_ re s p ( api_resp , req ):
deferred = m ak e _d b_ r eq ue s t ()
deferred . addCallback ( handle_db_resp , api_resp , req )
return deferred
def handle _db_re sp ( db_resp , api_response , req ):
# work with api_resp and db_resp
request . write (" success ")
def handle_error ( failure , req ):
# handle error
request . write (" error ")
return failure
Андрей Попп: Разработка сетевых приложений с gevent
11. Синхронный код писать проще и получается он понятнее.
def handle_client ( request ):
try :
api_response = m a k e _ a p i _ r e q u e s t ()
db_response = ma k e_ d b_ re q ue st ()
# work with api_response and db_response
request . write (" success ")
except Exception :
request . write (" error ")
raise
Но приходится использовать блокирующий I/O.
Андрей Попп: Разработка сетевых приложений с gevent
12. Что делать? Нужно искать компромис!
Андрей Попп: Разработка сетевых приложений с gevent
13. Микропотоки
Микропотоки или “зелёные” потоки или userspace-потоки:
Это как функции, исполнение которых можно
приостановить, а потом – продолжить.
Работают внутри одного или нескольких потоков ОС.
Для их исполнения необходим планировщик.
Обычно дёшевы в плане потребления памяти и
переключения контекста.
Андрей Попп: Разработка сетевых приложений с gevent
14. Микропотоки + Неблокирующий I/O
Чтобы микропотоки исполнялись им необходим планировщик.
Предлагается следующий вариант:
Как только микропоток пытается выполнить I/O, он
передаёт управление планировщику.
После того, как выполнение I/O становится доступным для
микропотока – планировщик возвращает ему управление.
Андрей Попп: Разработка сетевых приложений с gevent
15. Блокируется только микропоток, который пытается выполнить
I/O, а не весь интерпретатор.
Андрей Попп: Разработка сетевых приложений с gevent
16. Это называется кооперативная многозадачность – потоки сами
решают когда передать исполнение другим.
Андрей Попп: Разработка сетевых приложений с gevent
17. Существует также преемптивная или вытесняющая
многозадачность – поток вытесняется планировщиком после
определённого количества выполненных инструкций или по
истичении определённого времени.
Андрей Попп: Разработка сетевых приложений с gevent
18. Но разве микропотоки есть в Python?
Андрей Попп: Разработка сетевых приложений с gevent
19. Микропотоки в Python – Генераторы
Можно реализовать микропотоки в Python c помощью
генераторов (PEP 342, начиная с версии Python 2.5).
Чтобы передать исполнение – делаем yield.
К сожалению:
Кооперация с помощью yield – это слишком явно и
неудобно, приходиться самим думать, когда отдавать
управление.
Генераторы не сохраняют весь стэк во время остановки –
yield должен быть всегда на самом верху.
Андрей Попп: Разработка сетевых приложений с gevent
20. Микропотоки в Python – greenlet
Микропотоки с библиотекой greenlet:
Микропоток или просто гринлет это объект greenlet.
Кооперация посредством вызова метода greenlet.switch.
greenlet – это “выжимка” из Stackless Python.
Андрей Попп: Разработка сетевых приложений с gevent
21. Как работают гринлеты
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 ()
one
three
two
Андрей Попп: Разработка сетевых приложений с gevent
22. Микропотоки реализованные с помощью greenlet удобны – они
не страдают от недостатков генераторов.
Андрей Попп: Разработка сетевых приложений с gevent
23. Теперь нам нужен планировщик, который будет контролировать
исполнение гринлетов, руководствуясь событиями I/O.
Андрей Попп: Разработка сетевых приложений с gevent
24. gevent = libevent + greenlet
Такой планировщик предоставляет нам библиотека gevent.
Андрей Попп: Разработка сетевых приложений с gevent
25. Почему используется libevent
Почему gevent использует libevent для обработки событий:
Это быстрая библиотека, написанная на языке C – сам
цикл полностью в C коде.
Libevent используется длительное время и хорошо себя
зарекомендовала (Chromium, Memcached, Io).
Предоставляет встраиваемый HTTP-сервер – evhttp.
Имеет API для работы с DNS – evdns.
Андрей Попп: Разработка сетевых приложений с gevent
26. Как устроен gevent
Общая схема
Цикл обработки событий libevent работает в отдельном
гринлете – этот гринлет называется хаб.
Хаб запускается неявно и только при необходимости.
Кооперация между гринлетами происходит через хаб:
Гринлет может переключиться только на хаб.
Гринлет может получить управление только через хаб.
Андрей Попп: Разработка сетевых приложений с gevent
27. Как устроен gevent
Организация I/O
Чтобы совершить I/O наш гринлет должен:
1 Отправить запрос на I/O в цикл обработки событий.
2 Переключиться на хаб.
3 Хаб запускает выполнение других гринлетов.
4 ...
5 Как только запрос на I/O выполнен, хаб переключается
обратно на наш гринлет.
Андрей Попп: Разработка сетевых приложений с gevent
28. Блокируется только гринлет, который пытается выполнить I/O,
а не весь интерпретатор.
Андрей Попп: Разработка сетевых приложений с gevent
29. Сетевой I/O с gevent
Чтобы выполнять I/O гринлеты должны использовать
кооперативный gevent.socket. Его API полностью повторяет
socket стандартной библиотеки Python.
Андрей Попп: Разработка сетевых приложений с gevent
30. Сетевой I/O с gevent
Кстати, gevent.socket.getaddrinfo,
gevent.socket.gethostbyname используют evdns и тоже
являются блокирующими только для вызывающего их
гринлета.
Андрей Попп: Разработка сетевых приложений с gevent
31. Пример: конкурентный эхосервер с 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 :
break
f . write ( line )
f . flush ()
Андрей Попп: Разработка сетевых приложений с gevent
32. Пример: конкурентный эхосервер с 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 :
break
f . write ( line )
f . flush ()
Андрей Попп: Разработка сетевых приложений с gevent
33. Пример: конкурентный эхосервер с 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 :
break
f.write(line)
f.flush()
Андрей Попп: Разработка сетевых приложений с gevent
34. Оказалось достаточно использовать gevent.socket вместо
socket и вызвать gevent.spawn в нужном месте.
Андрей Попп: Разработка сетевых приложений с gevent
35. Пример: конкурентный эхосервер с 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 :
break
f . write ( line )
f . flush ()
StreamServer (( ’ localhost ’ , 6000) , handler ). serve_forever ()
Андрей Попп: Разработка сетевых приложений с gevent
36. Мы умеем создавать новые гринлеты (gevent.spawn) и
использовать gevent.socket. Посмотрим, что ещё мы можем
делать с gevent.
Андрей Попп: Разработка сетевых приложений с gevent
37. Базовые возможности 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 ):
...
Z e r o D i vi s i o n E r r o r : integer division or modulo by zero
Андрей Попп: Разработка сетевых приложений с gevent
38. Базовые возможности gevent
Преждевременное завершение гринлета
Чтобы завершить выполнение гринлета:
>>> task = gevent . spawn ( lambda a , b : a + b , 1 , 2)
>>> task . kill ()
Андрей Попп: Разработка сетевых приложений с gevent
39. Базовые возможности gevent
Приостанавливаем выполнение гринлета
Иногда нужно приостановить выполнение гринлета:
>>> def some_work ():
... # do some work
... gevent . sleep (10)
... # continue
Функция gevent.sleep аналогична time.sleep, только
“засыпает” не весь интерпретатор, а отдельный гринлет.
Андрей Попп: Разработка сетевых приложений с gevent
40. Базовые возможности gevent
Обработка таймаутов
Обработка таймаутов осуществляется с gevent.Timeout:
timeout = Timeout (10)
timeout . start ()
try :
# do some work
except gevent . Timeout :
# handle timeout
finally :
timeout . cancel ()
. . . или как контекст-менеджер:
try :
with gevent . Timeout (10):
# do some work
except gevent . Timeout :
# handle timeout
Андрей Попп: Разработка сетевых приложений с gevent
41. Управляем несколькими гринлетами
Объединяем гринлеты в группы
Иногда нужно управлять несколькими гринлетами сразу:
>>> 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
42. Управляем несколькими гринлетами
Работаем с пулом гринлетов
А иногда бывает нужно ограничить количество одновременно
выполняемых гринлетов в группе:
>>> tasks = gevent . pool . Pool ( size =5)
>>> for i in range (10):
... tasks . spawn ( do_some_work , i )
>>> tasks . join ()
В данном случае будет одновременно исполняться только 5
гринлетов.
Таким образом можно, например, ограничить количество
одновременно обрабатываемых соединений.
Андрей Попп: Разработка сетевых приложений с gevent
43. HTTP-сервисы с gevent
Используем evhttp
Модуль gevent.http предоставляет API для использования
evhttp, но нас больше интересует WSGI.
Андрей Попп: Разработка сетевых приложений с gevent
44. HTTP-сервисы с gevent
WSGI сервер
Модуль gevent.wsgi – реализация WSGI на базе gevent.http:
from gevent . wsgi import WSGIServer
def hello_world ( environ , star t_resp onse ):
star t_resp onse ( ’200 OK ’ , [( ’ Content - Type ’ , ’ text / html ’)])
return [" It works !"]
WSGIServer (( ’ localhost ’ , 8000) , hello_world ). serve_forever ()
Можно использовать практически любой WSGI
фрэймворк/библиотеку: Django, Werkzeug, WebOb, repoze.bfg,
Pylons.
Андрей Попп: Разработка сетевых приложений с gevent
45. Используем gevent с другими библиотеками
Как уже говорилось, API gevent.socket полностью повторяет
socket из стандартной библиотеки Python.
Андрей Попп: Разработка сетевых приложений с gevent
46. Используем gevent с другими библиотеками
Предоставляем фабрику кооперативных сокетов
Если библиотека позволяет пользовательскому коду подменять
класс используемого сокета:
from s om e n e t w o r k l i b r a r y import Client
from gevent import socket
class C o o p e r a t i v e G e v e n t A w a r e C l i e n t ( Client ):
def create_socket ( self ):
sock = socket . socket ( socket . AF_INET , socket . STREAM )
return sock
Но что делать, если не позволяет?
Андрей Попп: Разработка сетевых приложений с gevent
47. Используем gevent с другими библиотеками
Monkey patching
gevent предоставляет возможность пропатчить модуль socket
стандартной библиотеки:
from gevent import monkey
monkey . patch_socket ()
После этого, код, который использует модуль socket будет
кооперироваться.
Андрей Попп: Разработка сетевых приложений с gevent
48. Используем gevent с другими библиотеками
Monkey patching
Кроме этого в gevent.monkey:
patch_time() – заменяем time.sleep() на
кооперативный gevent.sleep().
patch_thread() – создаём гринлеты вместо потоков ОС,
также патчит threading.local.
patch_os(), patch_ssl(), patch_select() – . . .
patch_all() – патчим всё.
Андрей Попп: Разработка сетевых приложений с gevent
49. Пример: используем gevent с urllib2
from gevent . pool import Pool
from gevent import monkey
monkey . patch_all ()
import urllib2
tasks = Pool ( size =20)
urls = [ ’ http :// www . gevent . org ’ , ...]
def print_head ( url ):
print ’ Starting %s ’ % url
data = 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
50. Используем gevent с другими библиотеками
Я также использовал gevent совместно с SQLAlchemy, boto.
Андрей Попп: Разработка сетевых приложений с gevent
51. Где используется gevent
Несколько проектов, которые используют gevent:
Gunicorn – WSGI HTTP сервер, может использовать gevent
для обработки запросов.
pastegevent – используем gevent.wsgi для запуска WSGI
приложений вместе с PasteDeploy.
gevent-mysql – драйвер для MySQL, написанный на Cython,
использующий API gevent.
psycogreen – отдельная ветка psycopg, которая работает с
асинхронными библиотеками, например с gevent.
Андрей Попп: Разработка сетевых приложений с gevent
52. Некоторые ограничения
Как это обычно бывает, существуют некоторые ограничения:
После os.fork() необходимо вызывать gevent.reinit().
Библиотеку можно использовать только в одном потоке
ОС – ограничение libevent 1.4.
Блокирующий stdin – вскоре будет исправлено.
Библиотеки которые не используют socket блокируют
интерпретатор полностью – можно выполнять их в
отдельном потоке ОС.
Андрей Попп: Разработка сетевых приложений с gevent
53. Какие темы я не затронул
Остались темы, которые я не затронул:
Линки между гринлетами.
Примитивы синхронизации – gevent.event.
Синхронные очереди – gevent.queue.
Андрей Попп: Разработка сетевых приложений с gevent
54. Полезные ссылки
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