Александр Кошелев: Препарирование работы...
DESCRIPTION
Что происходит внутри асинхронного кода? Как быть, когда логика становится cpu-bound? Можно ли сделать гибридную синхронно-асинхронную архитектуру? Я попробую ответить на эти вопросы на примере приложения на Tornado. Сделаю визуализацию работы приложения и предложу пути решения некоторых проблем.TRANSCRIPT
ПРЕПАРИРОВАНИЕ РАБОТЫАСИНХРОННОГО КОДА
Александр Кошелев, ЯндексPyCon Россия, 2013
ЗАЧЕМAsynchronous is a new coolЭффективностьМного io-boundМало cpu-bound
Высокие нагрузкиМного клиентов
КАКИнструменты на выбор
TwistedTornadoAsyncoreGevent...Node.js
Можно написать своё
EVENT LOOPОпрос сокетовВызов хендлеров
Обработка таймаутовВызов колбеков
EVENT LOOPwhile running: do_handle_sockets()
do_handle_timeouts()
do_handle_callbacks()
ЭТО ЗНАЧИТВсе запросы обрабатываются в одном процессеОбработка одного запроса влияет на другиеТребуется изоляция контекста каждого запроса
ГЛАВНЫЕ ПРАВИЛАНельзя блокироватьсяРазбивка на короткие этапыОбязательно io-bound
ЭКСПЕРИМЕНТTornadocpu-boundОснован на реальной задаче
ЭКСПЕРИМЕНТТЕСТОВОЕ ПРИЛОЖЕНИЕ
Принять запросПараллельно опросить бекэнды — принципиальноОбработать данныеОтдать ответ
ЭКСПЕРИМЕНТТЕСТОВОЕ ПРИЛОЖЕНИЕ
ЭКСПЕРИМЕНТХЕНДЛЕР
class AppHandler(object): @gen.engine def process(self, callback): yield gen.Task(self.do_io) self.do_cpu()
results = yield [gen.Task(self.do_branch), gen.Task(self.do_branch)] self.do_cpu()
yield gen.Task(self.do_io) self.do_cpu()
callback('ok')
ЭКСПЕРИМЕНТХЕНДЛЕР
class AppHandler(object): # ...
def do_cpu(self, cycles=1000000): for _ in xrange(cycles): pass
@gen.engine def do_io(self, callback): yield gen.Task(self.http_client.fetch, 'http://localhost:5001/')
callback(None)
@gen.engine def do_branch(self, callback): yield [gen.Task(self.do_io) for _ in range(5)] self.do_cpu()
callback(None)
ЭКСПЕРИМЕНТПРИЛОЖЕНИЕ
from tornado import web
from handler import AppHandler
class RootHandler(web.RequestHandler): @web.asynchronous def get(self): handler = AppHandler()
handler.process(self.return_response)
def return_response(self, response): self.finish(response)
application = web.Application([ (r'/', RootHandler),])
1 ПАРАЛЛЕЛЬНЫЙ ЗАПРОС
1 ПАРАЛЛЕЛЬНЫЙ ЗАПРОС
N ПАРАЛЛЕЛЬНЫХ ЗАПРОСОВ
3 ПАРАЛЛЕЛЬНЫХ ЗАПРОСА
ВЫВОДЫЗапросы мешают друг другу из-за cpu-bound задачРастет время ответаЖадность
WORKAROUNDFLASK + TORNADO IOLOOP
Синхронный воркерНе жадныйНо нужно несколько процессов
Event loop внутриПараллельный опрос бекэндов
FLASK + TORNADO IOLOOPРЕАКТОР
def do(handler, args=(), kwargs={}): io_loop = tornado.ioloop.IOLoop.instance()
result = [None]
def callback(result_): result[0] = result_
io_loop.stop()
kwargs['callback'] = callback handler(*args, **kwargs)
io_loop.start()
return result[0]
FLASK + TORNADO IOLOOPПРИЛОЖЕНИЕ
from flask import Flask
import handler, reactor
application = Flask(__name__)
@application.route('/')def root(): hndl = handler.AppHandler()
response = reactor.do(hndl.process)
return response
1 ПАРАЛЛЕЛЬНЫЙ ЗАПРОС
1 ПАРАЛЛЕЛЬНЫЙ ЗАПРОС
N ПАРАЛЛЕЛЬНЫХ ЗАПРОСОВ
3 ПАРАЛЛЕЛЬНЫХ ЗАПРОСА
ИТОГИВремя не зависит от числа параллельных запросовБолее предсказуемое поведение
ВЫВОДЫБаланс cpu-bound/io-boundВсё зависит от задачи
ВОПРОСЫ?Спасибо
Александр Кошелев, Яндекс