55
Разработка сетевых приложений с gevent Андрей Попп [email protected] http://braintrace.ru @andreypopp

Разработка сетевых приложений с gevent

Embed Size (px)

DESCRIPTION

Это слайды моего доклада на DevConf 2010.

Citation preview

Page 1: Разработка сетевых приложений с gevent

Разработка сетевых приложений с gevent

Андрей Попп

[email protected]

http://braintrace.ru

@andreypopp

Page 2: Разработка сетевых приложений с gevent

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

Андрей Попп: Разработка сетевых приложений с gevent

Page 3: Разработка сетевых приложений с gevent

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

Андрей Попп: Разработка сетевых приложений с gevent

Page 4: Разработка сетевых приложений с gevent

Стратегии организации I/O

Основные стратегии обработки соединений относительноорганизации I/O:

Блокирующий I/O – необходимо несколько потоков ОС.Неблокирующий I/O + мултиплексор – достаточно дажеодного потока ОС.

Андрей Попп: Разработка сетевых приложений с gevent

Page 5: Разработка сетевых приложений с gevent

Блокирующий I/O

Необходимо использовать отдельный поток на каждоеактивное соединение.Много активных соединений = много активных потоков =большое количество потребляемой памяти.Переключение контекста исполнения обходится дорого.В Python есть GIL.

Андрей Попп: Разработка сетевых приложений с gevent

Page 6: Разработка сетевых приложений с gevent

Блокирующий I/O

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

Андрей Попп: Разработка сетевых приложений с gevent

Page 7: Разработка сетевых приложений с gevent

Неблокирующий I/O

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

Андрей Попп: Разработка сетевых приложений с gevent

Page 8: Разработка сетевых приложений с gevent

Неблокирующий I/O

Использвование неблокирующего I/O кажется болееподходящим решением проблемы.

Андрей Попп: Разработка сетевых приложений с gevent

Page 9: Разработка сетевых приложений с gevent

Неблокирующий I/O

Но какие распространённые библиотеки/фрэймворки мы имеемдля Python: asyncore, Twisted, Tornado.

Андрей Попп: Разработка сетевых приложений с gevent

Page 10: Разработка сетевых приложений с 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

Page 11: Разработка сетевых приложений с 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

Page 12: Разработка сетевых приложений с gevent

Что делать? Нужно искать компромис!

Андрей Попп: Разработка сетевых приложений с gevent

Page 13: Разработка сетевых приложений с gevent

Микропотоки

Микропотоки или “зелёные” потоки или userspace-потоки:

Это как функции, исполнение которых можноприостановить, а потом – продолжить.Работают внутри одного или нескольких потоков ОС.Для их исполнения необходим планировщик.Обычно дёшевы в плане потребления памяти ипереключения контекста.

Андрей Попп: Разработка сетевых приложений с gevent

Page 14: Разработка сетевых приложений с gevent

Микропотоки + Неблокирующий I/O

Чтобы микропотоки исполнялись им необходим планировщик.

Предлагается следующий вариант:

Как только микропоток пытается выполнить I/O, онпередаёт управление планировщику.После того, как выполнение I/O становится доступным длямикропотока – планировщик возвращает ему управление.

Андрей Попп: Разработка сетевых приложений с gevent

Page 15: Разработка сетевых приложений с gevent

Блокируется только микропоток, который пытается выполнитьI/O, а не весь интерпретатор.

Андрей Попп: Разработка сетевых приложений с gevent

Page 16: Разработка сетевых приложений с gevent

Это называется кооперативная многозадачность – потоки самирешают когда передать исполнение другим.

Андрей Попп: Разработка сетевых приложений с gevent

Page 17: Разработка сетевых приложений с gevent

Существует также преемптивная или вытесняющаямногозадачность – поток вытесняется планировщиком послеопределённого количества выполненных инструкций или по

истичении определённого времени.

Андрей Попп: Разработка сетевых приложений с gevent

Page 18: Разработка сетевых приложений с gevent

Но разве микропотоки есть в Python?

Андрей Попп: Разработка сетевых приложений с gevent

Page 19: Разработка сетевых приложений с gevent

Микропотоки в Python – Генераторы

Можно реализовать микропотоки в Python c помощьюгенераторов (PEP 342, начиная с версии Python 2.5).

Чтобы передать исполнение – делаем yield.

К сожалению:

Кооперация с помощью yield – это слишком явно инеудобно, приходиться самим думать, когда отдаватьуправление.Генераторы не сохраняют весь стэк во время остановки –yield должен быть всегда на самом верху.

Андрей Попп: Разработка сетевых приложений с gevent

Page 20: Разработка сетевых приложений с gevent

Микропотоки в Python – greenlet

Микропотоки с библиотекой greenlet:

Микропоток или просто гринлет это объект greenlet.Кооперация посредством вызова метода greenlet.switch.greenlet – это “выжимка” из Stackless Python.

Андрей Попп: Разработка сетевых приложений с gevent

Page 21: Разработка сетевых приложений с 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

Page 22: Разработка сетевых приложений с gevent

Микропотоки реализованные с помощью greenlet удобны – онине страдают от недостатков генераторов.

Андрей Попп: Разработка сетевых приложений с gevent

Page 23: Разработка сетевых приложений с gevent

Теперь нам нужен планировщик, который будет контролироватьисполнение гринлетов, руководствуясь событиями I/O.

Андрей Попп: Разработка сетевых приложений с gevent

Page 24: Разработка сетевых приложений с gevent

gevent = libevent + greenlet

Такой планировщик предоставляет нам библиотека gevent.

Андрей Попп: Разработка сетевых приложений с gevent

Page 25: Разработка сетевых приложений с gevent

Почему используется libevent

Почему gevent использует libevent для обработки событий:

Это быстрая библиотека, написанная на языке C – самцикл полностью в C коде.Libevent используется длительное время и хорошо себязарекомендовала (Chromium, Memcached, Io).Предоставляет встраиваемый HTTP-сервер – evhttp.Имеет API для работы с DNS – evdns.

Андрей Попп: Разработка сетевых приложений с gevent

Page 26: Разработка сетевых приложений с gevent

Как устроен geventОбщая схема

Цикл обработки событий libevent работает в отдельномгринлете – этот гринлет называется хаб.Хаб запускается неявно и только при необходимости.Кооперация между гринлетами происходит через хаб:

Гринлет может переключиться только на хаб.Гринлет может получить управление только через хаб.

Андрей Попп: Разработка сетевых приложений с gevent

Page 27: Разработка сетевых приложений с gevent

Как устроен geventОрганизация I/O

Чтобы совершить I/O наш гринлет должен:

1 Отправить запрос на I/O в цикл обработки событий.2 Переключиться на хаб.3 Хаб запускает выполнение других гринлетов.4 . . .5 Как только запрос на I/O выполнен, хаб переключается

обратно на наш гринлет.

Андрей Попп: Разработка сетевых приложений с gevent

Page 28: Разработка сетевых приложений с gevent

Блокируется только гринлет, который пытается выполнить I/O,а не весь интерпретатор.

Андрей Попп: Разработка сетевых приложений с gevent

Page 29: Разработка сетевых приложений с gevent

Сетевой I/O с gevent

Чтобы выполнять I/O гринлеты должны использоватькооперативный gevent.socket. Его API полностью повторяет

socket стандартной библиотеки Python.

Андрей Попп: Разработка сетевых приложений с gevent

Page 30: Разработка сетевых приложений с gevent

Сетевой I/O с gevent

Кстати, gevent.socket.getaddrinfo,gevent.socket.gethostbyname используют evdns и тожеявляются блокирующими только для вызывающего их

гринлета.

Андрей Попп: Разработка сетевых приложений с gevent

Page 31: Разработка сетевых приложений с 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

Page 32: Разработка сетевых приложений с 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

Page 33: Разработка сетевых приложений с 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

Page 34: Разработка сетевых приложений с gevent

Оказалось достаточно использовать gevent.socket вместоsocket и вызвать gevent.spawn в нужном месте.

Андрей Попп: Разработка сетевых приложений с gevent

Page 35: Разработка сетевых приложений с 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

Page 36: Разработка сетевых приложений с gevent

Мы умеем создавать новые гринлеты (gevent.spawn) ииспользовать gevent.socket. Посмотрим, что ещё мы можем

делать с gevent.

Андрей Попп: Разработка сетевых приложений с gevent

Page 37: Разработка сетевых приложений с 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

Page 38: Разработка сетевых приложений с gevent

Базовые возможности geventПреждевременное завершение гринлета

Чтобы завершить выполнение гринлета:

>>> task = gevent.spawn(lambda a, b: a + b, 1, 2)>>> task.kill()

Андрей Попп: Разработка сетевых приложений с gevent

Page 39: Разработка сетевых приложений с gevent

Базовые возможности geventПриостанавливаем выполнение гринлета

Иногда нужно приостановить выполнение гринлета:

>>> def some_work ():... # do some work... gevent.sleep (10)... # continue

Функция gevent.sleep аналогична time.sleep, только“засыпает” не весь интерпретатор, а отдельный гринлет.

Андрей Попп: Разработка сетевых приложений с gevent

Page 40: Разработка сетевых приложений с 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

Page 41: Разработка сетевых приложений с 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

Page 42: Разработка сетевых приложений с gevent

Управляем несколькими гринлетамиРаботаем с пулом гринлетов

А иногда бывает нужно ограничить количество одновременновыполняемых гринлетов в группе:

>>> tasks = gevent.pool.Pool(size =5)>>> for i in range (10):... tasks.spawn(do_some_work , i)>>> tasks.join()

В данном случае будет одновременно исполняться только 5гринлетов.

Таким образом можно, например, ограничить количествоодновременно обрабатываемых соединений.

Андрей Попп: Разработка сетевых приложений с gevent

Page 43: Разработка сетевых приложений с gevent

HTTP-сервисы с geventИспользуем evhttp

Модуль gevent.http предоставляет API для использованияevhttp, но нас больше интересует WSGI.

Андрей Попп: Разработка сетевых приложений с gevent

Page 44: Разработка сетевых приложений с 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

Page 45: Разработка сетевых приложений с gevent

Используем gevent с другими библиотеками

Как уже говорилось, API gevent.socket полностью повторяетsocket из стандартной библиотеки Python.

Андрей Попп: Разработка сетевых приложений с gevent

Page 46: Разработка сетевых приложений с 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

Page 47: Разработка сетевых приложений с gevent

Используем gevent с другими библиотекамиMonkey patching

gevent предоставляет возможность пропатчить модуль socketстандартной библиотеки:

from gevent import monkeymonkey.patch_socket ()

После этого, код, который использует модуль socket будеткооперироваться.

Андрей Попп: Разработка сетевых приложений с gevent

Page 48: Разработка сетевых приложений с 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

Page 49: Разработка сетевых приложений с 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

Page 50: Разработка сетевых приложений с gevent

Используем gevent с другими библиотеками

Я также использовал gevent совместно с SQLAlchemy, boto.

Андрей Попп: Разработка сетевых приложений с gevent

Page 51: Разработка сетевых приложений с gevent

Где используется gevent

Несколько проектов, которые используют gevent:

Gunicorn – WSGI HTTP сервер, может использовать geventдля обработки запросов.pastegevent – используем gevent.wsgi для запуска WSGIприложений вместе с PasteDeploy.gevent-mysql – драйвер для MySQL, написанный на Cython,использующий API gevent.psycogreen – отдельная ветка psycopg, которая работает сасинхронными библиотеками, например с gevent.

Андрей Попп: Разработка сетевых приложений с gevent

Page 52: Разработка сетевых приложений с gevent

Некоторые ограничения

Как это обычно бывает, существуют некоторые ограничения:

После os.fork() необходимо вызывать gevent.reinit().Библиотеку можно использовать только в одном потокеОС – ограничение libevent 1.4.Блокирующий stdin – вскоре будет исправлено.Библиотеки которые не используют socket блокируютинтерпретатор полностью – можно выполнять их вотдельном потоке ОС.

Андрей Попп: Разработка сетевых приложений с gevent

Page 53: Разработка сетевых приложений с gevent

Какие темы я не затронул

Остались темы, которые я не затронул:

Линки между гринлетами.Примитивы синхронизации – gevent.event.Синхронные очереди – gevent.queue.

Андрей Попп: Разработка сетевых приложений с gevent

Page 54: Разработка сетевых приложений с 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

Page 55: Разработка сетевых приложений с gevent

Спасибо!

Андрей Попп: Разработка сетевых приложений с gevent