peewee и peewee-async: почему асинхронность медленнее

Я пытаюсь обернуть голову вокруг Торнадо и асинхронных подключений к Postgresql. Я нашел библиотеку, которая может сделать это в http://peewee-async.readthedocs.io/en/latest/.

Я разработал небольшой тест для сравнения традиционных Peewee и Peewee-async, но как-то асинхронно работает медленнее.

Это мое приложение:

import peewee
import tornado.web
import logging
import asyncio
import peewee_async
import tornado.gen
import tornado.httpclient
from tornado.platform.asyncio import AsyncIOMainLoop

AsyncIOMainLoop().install()
app = tornado.web.Application(debug=True)
app.listen(port=8888)

# ===========
# Defining Async model
async_db = peewee_async.PooledPostgresqlDatabase(
    'reminderbot',
    user='reminderbot',
    password='reminderbot',
    host='localhost'
)
app.objects = peewee_async.Manager(async_db)
class AsyncHuman(peewee.Model):
    first_name = peewee.CharField()
    messenger_id = peewee.CharField()
    class Meta:
        database = async_db
        db_table = 'chats_human'


# ==========
# Defining Sync model
sync_db = peewee.PostgresqlDatabase(
    'reminderbot',
    user='reminderbot',
    password='reminderbot',
    host='localhost'
)
class SyncHuman(peewee.Model):
    first_name = peewee.CharField()
    messenger_id = peewee.CharField()
    class Meta:
        database = sync_db
        db_table = 'chats_human'

# defining two handlers - async and sync
class AsyncHandler(tornado.web.RequestHandler):

    async def get(self):
        """
        An asynchronous way to create an object and return its ID
        """
        obj = await self.application.objects.create(
            AsyncHuman, messenger_id='12345')
        self.write(
            {'id': obj.id,
             'messenger_id': obj.messenger_id}
        )


class SyncHandler(tornado.web.RequestHandler):

    def get(self):
        """
        An traditional synchronous way
        """
        obj = SyncHuman.create(messenger_id='12345')
        self.write({
            'id': obj.id,
            'messenger_id': obj.messenger_id
        })


app.add_handlers('', [
    (r"/receive_async", AsyncHandler),
    (r"/receive_sync", SyncHandler),
])

# Run loop
loop = asyncio.get_event_loop()
try:
    loop.run_forever()
except KeyboardInterrupt:
    print(" server stopped")

и это то, что я получаю от Apache Benchmark:

ab -n 100 -c 100 http://127.0.0.1:8888/receive_async

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        2    4   1.5      5       7
Processing:   621 1049 256.6   1054    1486
Waiting:      621 1048 256.6   1053    1485
Total:        628 1053 255.3   1058    1492

Percentage of the requests served within a certain time (ms)
  50%   1058
  66%   1196
  75%   1274
  80%   1324
  90%   1409
  95%   1452
  98%   1485
  99%   1492
 100%   1492 (longest request)




ab -n 100 -c 100 http://127.0.0.1:8888/receive_sync
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        2    5   1.9      5       8
Processing:     8  476 277.7    479    1052
Waiting:        7  476 277.7    478    1052
Total:         15  481 276.2    483    1060

Percentage of the requests served within a certain time (ms)
  50%    483
  66%    629
  75%    714
  80%    759
  90%    853
  95%    899
  98%   1051
  99%   1060
 100%   1060 (longest request)

почему синхронизация быстрее? где мое узкое место? не хватает?

2 ответов


долго объяснять:

http://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/

для краткого объяснения: синхронный код Python прост и в основном реализован в модуле сокета стандартной библиотеки, который является чистым C. асинхронный код Python сложнее, чем синхронный код. Каждый запрос требует нескольких исполнений основного кода цикла событий, который написан на Python (в asyncio случай здесь) и поэтому имеет значительные накладные расходы по сравнению с C-код.

такие тесты, как ваш, показывают накладные расходы async резко, потому что нет сетевой задержки между вашим приложением и вашей базой данных, и вы делаете большое количество очень небольших операций с базой данных. С каждым другое аспект бенчмарка быстрый, эти многие выполнения логики цикла событий добавляют большую часть общей среды выполнения.

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

Async лучше всего подходит для сценариев с высокой задержкой, таких как websockets и веб-искатели, где приложение проводит большую часть своего времени в ожидании однорангового узла, а не тратит большую часть своего времени на выполнение Python.

В заключение: если у вашего приложения есть веская причина быть асинхронным( он имеет дело с медленными сверстниками), наличие драйвера асинхронной базы данных является хорошим идея ради согласованного кода, но ожидайте некоторых накладных расходов.

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


ORMs базы данных вводят много сложностей для асинхронных архитектур. В ORM есть несколько мест, где блокировка может иметь место и может быть подавляющей для изменения асинхронной формы. Места, где происходит блокировка, также могут варьироваться в зависимости от базы данных. Я предполагаю, почему ваши результаты так медленны, потому что есть много неоптимизированных вызовов цикла событий и из него (я мог бы сильно ошибаться, я в основном использую SQLAlchemy или raw SQL в эти дни). По моему опыту, это обычно быстрее выполнить код базы данных в потоке и получить результат, когда он доступен. Я не могу говорить за PeeWee, но SQLAlchemy хорошо подходит для работы в нескольких потоках, и не слишком много сторон вниз (но те, которые существуют, очень очень раздражают).

Я бы рекомендовал вам попробовать свой эксперимент, используя ThreadPoolExecutor и синхронный модуль Peewee и запуск функций базы данных в потоке. Вам придется внести изменения в свой основной код, однако, если вы спросите меня, оно того стоит. Например, предположим, вы решили использовать код обратного вызова, тогда ваши запросы ORM могут выглядеть так:

from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=10)

def queryByName(name):
    query = executor.submit(db_model.findOne, name=name)
    query.add_done_callback(processResult)

def processResult(query):
    orm_obj = query.results()
    # do stuff with the results

вы могли бы использовать yeild from или await в coroutines, но это было немного проблематично для меня. Кроме того, я не очень хорошо разбираюсь в сопрограммах еще. Этот фрагмент должен хорошо работать с Tornado, пока разработчики осторожны с блокировками, сеансами БД и транзакциями. Эти факторы действительно могут замедлить ваше приложение, если что-то пойдет не так нить.

если вы чувствуете себя очень авантюрным, MagicStack (компания за asyncio) имеет проект под названием asyncpg и это должно быть очень быстро! Я хотел попробовать, но не нашел времени : (