Successfully reported this slideshow.
Your SlideShare is downloading. ×

Gevent rabbit rpc

Upcoming SlideShare
Cooking pies with Celery
Cooking pies with Celery
Loading in …3

Check these out next

1 of 49 Ad

More Related Content

Slideshows for you (20)

Similar to Gevent rabbit rpc (20)


Recently uploaded (20)

Gevent rabbit rpc

  1. 1. Асинхронный RPC с помощью Gevent и RabbitMQ Александр Мокров
  2. 2. О чем доклад Некоторые ограничения Celery Как их обойти Gevent RabbitMQ (некоторые особенности) Будет предложена модель асинхронного RPC
  3. 3. Пример построения приложения на Celery workflow get flour bake pie get meat seal pie create dough order pie get milk get aggs
  4. 4. ... entry point 2 entry point 1 entry point 3 service n app service 2 service 1
  5. 5. sommelier winery сhateau Service of degustation 3 app Service of degustation 2 Service of degustation 1
  6. 6. Дегустация вин с gevent и RabbitMQ
  7. 7. entry task service task 1 DB callback task 1 service task n callback task n
  8. 8. Что хотелось бы получить entry task service task 1 service task 2 service task n
  9. 9. long task services time
  10. 10. Celery AsyncResult async_result = task.apply_async() print(async_result.status) False result = async_result.wait()
  11. 11. long task service task persistant queue exclusive queue reply_to=amq.gen-E6. correlation_id Request correlation_id Response reply_to=amq.gen-E6... RabbitMQ RPC
  12. 12. greenlet greenlet long task service response listener service service service reply_to exclusive queue services queues
  13. 13. Workers ● solo ● prefork ● eventlet ● gevent
  14. 14. Gevent gevent is a concurrency library based around libev. It provides a clean API for a variety of concurrency and network related tasks.
  15. 15. Greenlet The primary pattern used in gevent is the Greenlet, a lightweight coroutine provided to Python as a C extension module. Greenlets all run inside of the OS process for the main program but are scheduled cooperatively. Only one greenlet is ever running at any given time. Spin-off of Stackless, a version of CPython that supports micro-threads called “tasklets”. Tasklets run pseudo-concurrently (typically in a single or a few OS-level threads) and are synchronized with data exchanges on “channels”. Its coroutine
  16. 16. Event loop
  17. 17. def foo(): print('Running in foo') gevent.sleep(0) print('Explicit context switch to foo') def bar(): print('Explicit context to bar') gevent.sleep() print('Implicit context switch to bar') gevent.joinall([ gevent.spawn(foo), gevent.spawn(bar), ]) Running in foo Explicit context to bar Explicit context switch to foo Implicit context switch to bar
  18. 18. def task(pid): gevent.sleep(random.randint(0,2)*0.001) print('Task %s done' % pid) def synchronous(): for i in range(1, 8): task(i) def asynchronous(): threads = [gevent.spawn(task, i) for i in range(10)] gevent.joinall(threads) Synchronous: Task 1 done Task 2 done Task 3 done Task 4 done Task 5 done Task 6 done Task 7 done Asynchronous: Task 1 done Task 5 done Task 6 done Task 2 done Task 4 done Task 7 done Task 0 done Task 3 done
  19. 19. def echo(i): time.sleep(0.001) return i # Non Deterministic Process Pool from multiprocessing.pool import Pool p = Pool(10) run1 = [a for a in p.imap_unordered(echo, xrange(10))] run2 = [a for a in p.imap_unordered(echo, xrange(10))] run3 = [a for a in p.imap_unordered(echo, xrange(10))] run4 = [a for a in p.imap_unordered(echo, xrange(10))] print(run1 == run2 == run3 == run4) False
  20. 20. # Deterministic Gevent Pool from gevent.pool import Pool p = Pool(10) run1 = [a for a in p.imap_unordered(echo, xrange(10))] run2 = [a for a in p.imap_unordered(echo, xrange(10))] run3 = [a for a in p.imap_unordered(echo, xrange(10))] run4 = [a for a in p.imap_unordered(echo, xrange(10))] print(run1 == run2 == run3 == run4) True
  21. 21. Spawning Greenlets from gevent import Greenlet thread1 = Greenlet.spawn(foo, "message", 1) thread2 = gevent.spawn(foo, "message", 2) thread3 = gevent.spawn(lambda x: (x+1), 2) threads = [thread1, thread2, thread3] # Block until all threads complete. gevent.joinall(threads)
  22. 22. class MyGreenlet(Greenlet): def __init__(self, message, n): Greenlet.__init__(self) self.message = message self.n = n def _run(self): print(self.message) gevent.sleep(self.n) g = MyGreenlet("Hi there!", 3) g.start() g.join()
  23. 23. Greenlet State started -- Boolean, indicates whether the Greenlet has been started ready() -- Boolean, indicates whether the Greenlet has halted successful() -- Boolean, indicates whether the Greenlet has halted and not thrown an exception value -- arbitrary, the value returned by the Greenlet exception -- exception, uncaught exception instance thrown inside the greenlet
  24. 24. greenlet greenlet long task service response listener subscribe(task_id)
  25. 25. Timeouts
  26. 26. from gevent import Timeout seconds = 10 timeout = Timeout(seconds) timeout.start() def wait(): gevent.sleep(10) try: gevent.spawn(wait).join() except Timeout: print('Could not complete')
  27. 27. time_to_wait = 5 # seconds class TooLong(Exception): pass with Timeout(time_to_wait, TooLong): gevent.sleep(10)
  28. 28. class Queue(maxsize=None, items=None) empty() full() get(block=True, timeout=None) get_nowait() next() peek(block=True, timeout=None) peek_nowait() put(item, block=True, timeout=None) put_nowait(item) qsize()
  29. 29. greenlet greenlet greenlet service results dispatcher gevent.queues task_id reply_to, results_queue
  30. 30. Events Groups and Pools Locks and Semaphores Subprocess Thread Locals Actors
  31. 31. Monkey patching guerrilla patch gorilla patch monkey patch
  32. 32. import socket print(socket.socket) from gevent import monkey monkey.patch_socket() print("After monkey patch") print(socket.socket) import select print( monkey.patch_select() print("After monkey patch") print( <class 'socket.socket'> After monkey patch <class 'gevent._socket3.socket'> <built-in function select> After monkey patch <function select at 0x7ff7e111c378>
  33. 33. Stack layout for a greenlet | ^^^ | | older data | | | stack_stop . |_______________| . | | . | greenlet data | . | in stack | . * |_______________| . . _____________ stack_copy + stack_saved . | | | | . | data | |greenlet data| . | unrelated | | saved | . | to | | in heap | stack_start . | this | . . |_____________| stack_copy | greenlet | | | | newer data | | vvv |
  34. 34. greenlet greenlet greenlet service results dispatcher service service service reply_to exclusive queue services queues subscribe gevent.queues
  35. 35. Service Result Dispatcher
  36. 36. greenlet greenlet greenlet service results dispatcher reply_to exclusive queue reply_to, results_queue gevent.queues task_id task_id, reply_to
  37. 37. class ServiceResultsDispatcher(Greenlet): def __init__(self): … self.reply_to = None Greenlet.__init__(self) def create_connection(self): ... result = self.reply_to = result.method.queue
  38. 38. def subscribe(self, task_id): service_results_queue = gevent.queue.Queue() self.service_results[task_id] = service_results_queue return service_results, self.reply_to def unsubscribe(self, task_id): self.service_results.pop(task_id, None)
  39. 39. def _run(self): while True: try: for method_frame, properties, body in, no_ack=True): if properties.correlation_id in self.tasks: self.tasks[properties.correlation_id].put_nowait((method_frame, properties, body)) except ...
  40. 40. Greenlet task
  41. 41. greenlet greenlet greenlet service results dispatcher services queues subscribe gevent.queues
  42. 42. self.results_queue, self.reply_to = self.service_publisher.subscribe(self.task_id)'', routing_key=service_queue, properties=BasicProperties( reply_to=self.reply_to, correlation_id=self.task_id ), body=request)
  43. 43. try: method_frame, properties, body = self.results_queue.get(block=True, timeout=self.timeout) except Empty:'timeout') break else:'body = {}'.format(body))
  44. 44. Services response = channel.basic_publish( exchange='', routing_key=props.reply_to, properties=BasicProperties(correlation_id=request.task_id), body=response )
  45. 45. Альтернативы? Почему gevent? 1. Встроенная поддержка в Celery (малыми силами) 2. Хотелось рассмотреть в докладе именно gevent. Ничто не мешает переделать, к примеру, на asyncio.
  46. 46. Вывод
  47. 47. Ссылки
  48. 48. Спасибо за внимание!

Editor's Notes

  • Сразу оговорюсь, что тут Celery можно вообще выкинуть из доклада и общая суть не изменится. Но хотелось бы показать, как нередко приходится сталкиваться с ограничениями существующих инструментов и возникает вопрос либо их (инструментов) расширения либо отказа в пользу чего-то более подходящего. В данном докладе я расскажу о некоторых трудностях, с которыми можно столкнуться при построении распределенной системы. Приведу приближенный проблемный пример и предложу свой вариант решения, который базируется на RabbitMQ и библиотеке gevent. Сделаю предварительно небольшой обзор библиотеки gevent и упомяну о некоторых особенностях которые понадобятся в предлагаемом решении.
  • На экране приведен пример построения приложения базирующегося на Celery workflow. Я как-то уже приводил в прошлом докладе. Что она из себя представляет? Логика приложения разбита на отдельные задачи, которые выполняются либо последовательно, либо параллельно, выполнение одних задач зависит от других. В данном примере все выглядит хорошо и особых проблем нет. Но рассмотрим другой вариант.
  • Когда на необходимо общаться с большим количество различных сервисов. Здесь есть у нас некие точки входа, от которых могут приходить какие-то задачи и результат их выполнения нужно возвращать. Иногда за ограниченное время. Есть некий аппликешен, логика работы которого может быть разбита на много задач, но это в данном случае не важно. И этому аппликейшену надо общаться с большим количеством как разных так и однотипных сервисов, результаты работы сервисов надо аккумулировать и формировать некий ответ для точек входа. В прошлом случае у меня был пример с пирожком, сейчас можно представить, что это система для дегустации вин.
  • Вот у нас есть какие-то винные заводы, шато, который хотят получать от нас оценку их вина. А мы строим свою работу через общение с сервисами по дегустации. Это могут быть свои сервисы на которые мы можем влиять, а могут быть сервисы сторонние, партнерские, к которым надо приспосабливаться. Они все работают по разному, разное время, отдают разные оценки по разным параметрам. И иногда надо отвечать за ограниченное время и не обязательно по результатам работы всех сервисов. И желательно самим вносить минимальные задержки. Впрочем, некоторые виноделы могут и подождать когда наши эксперты опробуют их великолепные вина. Да и о чем собственно разговор, доклад должен был називаться так.
  • Celery предлагает для подобного взаимодействия механизм callback’ов. Как это работает. В задаче, которую мы назначаем сервисам, укказываем какую задачу с каким набором аргументов надо выполнить при успешном завершении задачи, и аналогично при неуспешном. Т.е. в итоги получается цепочка из трех задач, последняя из которых зависит от успешности выполнения предыдущей. В правой части мы видим, что entry task создает n сервисных задач, в нашем варианте они выполняются на стороне сервисов, каждая из сервисных задач порождает callback task’и, которые уже выполняются нашим апп. И тут появляется проблемное место - каждая из call back задач не знает результаты выполнения предыдущих и есть ли результаты, а нам надо как-то аккумулировать результаты. И каждой задаче в итоге приходиться запрашивать результаты из некоего общего хранилища и писать туда же результаты своей работы. И еще быть готовыми в любой момент отдаь результат не дожидаясь дальнейших ответов от сервисов. И для этого нет нормального механизма.
  • А что бы хотелось получить? Во-первых, хранить весь контекст управления в одном месте, во-вторых, контролировать выполнение и в любой момент быть готовыми завершить работу, либо отдать промежуточный вариант. Ну а в третьих, исключить необязательное постоянное взаимодействие с хранилищем и уменьшить латенси. В конце-концов система распределенная и запросы могут занимать значительное время между разными узлами. Т.е. неплохо, чтобы у нас была одна управляющая задача со контекстом выполнения, которая и будет получать результаты рпботы сервисов.
  • Во временном разрезе это примерно будет выглядеть так.
  • Что нам предоставляет Celery. С одной стороны есть AsyncResult, который позволяет получит статус задачи и собственно сам результат. Но нам надо дожидаться сразу много результатов, и как-то постоянно опрашивать статусы - не вариант. Т.е. тут надо что-то придумывать.
  • С другой стороны кролик нам предлагает некую модель RPC. Клиентская часть, в данном случае наша длинная такса, при поднятии декларирует эксклюзивную очередь и в дальнейшем её слушает. Такая очередь будет создана со случайным именем и будет удалена после потери соединения с потребителем/клиентом. При отправке серверной части запроса необходима указать в параметре reply_to имя созданной эксклюзивной очереди и correlation_id для согласования запроса с ответом. Серверная часть выполняет задачу и отдает ответ в reply_to очередь с указанием свойствах ответа correlation_id. Клиент получает ответ и по correlation_id сопоставляет его с соответствующим запросом
  • Такой подход вполне применим. У насть есть некоторое количество длинных задач, набор сервисов, которые слушают свои очереди. Но длинных тасок может быть одновременно много и потому для каждой из них создавать эксклюзивную очередь - плохой вариант. Потому логичнее выделить отдельную задачу, которая и будет получать ответы от сервисов и как-то отдавать их длинным задачам. Последние же должны как-то подписываться у этого слушателя на получение ответов и получать имя reply_to очереди. Последнее можно сделать реализовав интерфейс подписки, вопрос остается как получать ответы и как иметь одновременно большое количество задач, сотни, тысячи...
  • Что может предложить celery? Есть несколько типов воркеров: solo - понятно, что не то, что нам требуется, и до кучи имеет дополнительные ограничения. Префорк - держать сотни и тем более тысячи процессов одновременно - по 70 мб в оперативеи выше каждый - дорого. Евентлеты и гевенты - похожие вещи и мы рассмотрим последние.
  • Гевент или Джиэвент - это конкаренси библиотека, построенная вокруг libev
  • Центральным понятием и неким кирпичиком в ней служат гринлеты. Это такие легковесный сопрограммы предоставляемые в питон как с-экстеншен модуль. Все гринлеты крутятся внутри главного процесса, но шедулятся кооперативно. Только один гринлет работает в отдельно взятый момент времени. Это некий спинов стэклеса. И это, еще раз, корутин или легковесные треды, как их часто называют.
  • Ою=б эвент лупе я скажу лишь, что он основан на libev, и по сути является ее оберткой. И крутиться в отдельном гринлете, называемом Hub,
  • Одна из основных идей конкурентности в том, что большую задачу можно разбить на подзадачи выполнение которых планировать одновременно или асинхронно. Переключение между двумя подзадачами называют переключением контекста. Переключение контекста в гевентах происходить через yeilding (уступление). В данном примере у нас есть два контекста, которые переключаются через вызов gevent.sleep()
  • Детерменизм
  • Но даже если гевенты детерминированы, источник недетерменированности может скрывать во взаимодействие с внешними сервисами, такими как сокеты и файлы. И собственно они из-за это сталкиваются с теми же проблемами, что и треды и процессы. Например, race condition - состояние гонки. В простом варианте - когда несколько потоков/процессов зависят от одного ресурса и пытаются каждый взаимодействовать на него(изменять значение). Лучший вариант - избегать глобальные состояния.
  • Есть несколько способов пораждать гринлеты. Инициализваия нового инстанса Greenlet, вызов метода и передача неименованной функции.
  • Ну и, кончено, можно наследоваться от гринлета и переопределить метод инит, и _run. Ну и обращу внимание на последние 3 строчки.
  • Queues are ordered sets of data that have the usual put / get operations but are written in a way such that they can be safely manipulated across Greenlets.
    For example if one Greenlet grabs an item off of the queue, the same item will not be grabbed by another Greenlet executing simultaneously.
    На основе collections.deque
  • Вообще библиотек гевент имеет весь джентельминский набор, тут и эвенты, и группы с пулами, и локи с семафорами и сабпроцессы, и тред локалс, позволяющиие указывать, какие данные локальны для текущего гринлета и реализовывать модель экторов в духе эрланга. Вот така вот замечательная библиотека. Но...
  • Alas we come to dark corners of Gevent. I've avoided mentioning monkey patching up until now to try and motivate the powerful coroutine patterns, but the time has come to discuss the dark arts of monkey-patching. If you noticed above we invoked the command monkey.patch_socket(). This is a purely side-effectful command to modify the standard library's socket library.
  • Python's runtime allows for most objects to be modified at runtime including modules, classes, and even functions. This is generally an astoudingly bad idea since it creates an "implicit side-effect" that is most often extremely difficult to debug if problems occur, nevertheless in extreme situations where a library needs to alter the fundamental behavior of Python itself monkey patches can be used. In this case gevent is capable of patching most of the blocking system calls in the standard library including those in socket, ssl, threading andselect modules to instead behave cooperatively.
    For example, the Redis python bindings normally uses regular tcp sockets to communicate with the redis-server instance. Simply by invoking gevent.monkey.patch_all() we can make the redis bindings schedule requests cooperatively and work with the rest of our gevent stack.
    This lets us integrate libraries that would not normally work with gevent without ever writing a single line of code. While monkey-patching is still evil, in this case it is a "useful evil".
  • Spawn - вновь порождунный гринлет ассоцииурется с адресом в стэке, в котором мы в данный момент находися и больше ничего особенного не происходит. Питоновский код гринлета работает с кучей, интерпретатор продолжает работать с с-стэко
    Switch - когда гринлет переключается с переключаемого гринлета, то соответствующая часть с-стэка копируется (начиная с начального адреса) в кучу, а на освобожденное место копируются ранее сохраненная часть стэка переключаемого гринлета. И питоновский код переключенного гринлета продолжает работать с кучей.
    Return - стэк не трогается, а данные в куче очищаются сборщиком мусора
  • С одной стороны в докладе показано, как используя некоторые мощные инструменты все равно можно столкнуться с ограничениями. И здесь приведен пример, который можно воспринимать и как расширение возможностей этого инструмента, так и, как ни странно, плавный отказ от него.
    Рассмотре конкурентную бибилиотеку gevent, постарался показать её сильные стороны и заглянуть в её темную поднаготную. Но это опять как к этому относиться. Инструмент с долгой историей, хорошей репутацие и достаточно мощном, но как то, мне показалось, о нем больше помалкивают и используют молча.
    Ну и собственно привел некоторое видение построения асинхронного RPC с помощью упомянутых инструментов, но никак не привязанное к ним.