Торнадо с помощью ThreadPoolExecutor

У меня есть настройка, которая использует 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? Или комбинация?

Ответ 1

Оболочка momoko для postgres устраняет эту проблему, как полагает kwarunek. Если вы хотите запросить дальнейшие рекомендации по отладке внешних соавторов, это поможет опубликовать timestamped журналы отладки из тестовой задачи, которая выполняет спящий режим (10) перед каждым доступом к БД.