У меня есть настройка, которая использует Tornado
как http-сервер и настраиваемый http-интерфейс. Идея состоит в том, чтобы иметь одиночный обработчик торнадо, и каждый запрос, который приходит, должен быть представлен только ThreadPoolExecutor
и оставить Tornado
для прослушивания новых запросов. Когда поток завершает обработку запроса, вызывается обратный вызов, который отправляет ответ клиенту в том же потоке, где выполняется цикл IO.
Сняв код, код выглядит примерно так. Базовый класс HTTP-сервера:
class HttpServer():
def __init__(self, router, port, max_workers):
self.router = router
self.port = port
self.max_workers = max_workers
def run(self):
raise NotImplementedError()
Поддержка Tortado HttpServer:
class TornadoServer(HttpServer):
def run(self):
executor = futures.ThreadPoolExecutor(max_workers=self.max_workers)
def submit(callback, **kwargs):
future = executor.submit(Request(**kwargs))
future.add_done_callback(callback)
return future
application = web.Application([
(r'(.*)', MainHandler, {
'submit': submit,
'router': self.router
})
])
application.listen(self.port)
ioloop.IOLoop.instance().start()
Основной обработчик, который обрабатывает все запросы торнадо (реализовано только GET, но другое будет одинаковым):
class MainHandler():
def initialize(self, submit, router):
self.submit = submit
self.router = router
def worker(self, request):
responder, kwargs = self.router.resolve(request)
response = responder(**kwargs)
return res
def on_response(self, response):
# when this is called response should already have result
if isinstance(response, Future):
response = response.result()
# response is my own class, just write returned content to client
self.write(response.data)
self.flush()
self.finish()
def _on_response_ready(self, response):
# schedule response processing in ioloop, to be on ioloop thread
ioloop.IOLoop.current().add_callback(
partial(self.on_response, response)
)
@web.asynchronous
def get(self, url):
self.submit(
self._on_response_ready, # callback
url=url, method='post', original_request=self.request
)
Сервер запускается с чем-то вроде:
router = Router()
server = TornadoServer(router, 1111, max_workers=50)
server.run()
Итак, как вы можете видеть, основной обработчик просто отправляет каждый запрос в пул потоков, и когда обработка завершена, вызывается callback (_on_response_ready
), который просто планирует завершение запроса для выполнения в цикле ввода-вывода (чтобы убедиться, что он выполняется в том же потоке, где выполняется цикл IO).
Это работает. По крайней мере, похоже, что это так.
Моя проблема заключается в производительности, связанной с максимальными рабочими в ThreadPoolExecutor.
Все обработчики привязаны к IO, нет никаких вычислений (они в основном ждут БД или внешние службы), поэтому с 50 рабочими я ожидал бы, что 50 совпадающих запросов закончатся примерно в 50 раз быстрее, чем 50 совпадающих запросов, только с одним работник.
Но это не так. То, что я вижу, - это почти идентичные запросы в секунду, когда у меня есть 50 работников в пуле потоков и 1 рабочий.
Для измерения я использовал Apache-Bench с чем-то вроде:
ab -n 100 -c 10 http://localhost:1111/some_url
Кто-нибудь знает, что я делаю неправильно? Не понял ли я, как работает Tornado или ThreadPool? Или комбинация?