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

#python #postgresql #tornado #python-asyncio #peewee

#python #postgresql #торнадо #python-asyncio #peewee

Вопрос:

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

Я разработал небольшой тест для сравнения традиционного Peewee и Peewee-async, но почему-то 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:

 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)
  

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

Ответ №1:

Для длинного объяснения:

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

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

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

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

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

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

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

Комментарии:

1. итак, асинхронный веб-фреймворк, такой как Sanic github.com/channelcat/sanic может ускорить? Он использует Python3.5 uvloop

Ответ №2:

ORM базы данных создают много сложностей для асинхронных архитектур. В 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 в сопрограммах, но для меня это было немного проблематично. Кроме того, я еще не очень хорошо разбираюсь в сопрограммах. Этот фрагмент должен хорошо работать с Tornado, если разработчики будут осторожны с взаимоблокировками, сеансами БД и транзакциями. Эти факторы могут действительно замедлить работу вашего приложения, если что-то пойдет не так в потоке.

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

Комментарии:

1. Я могу согласиться с большей частью вашего ответа, но это предложение: «MagicStack (компания, стоящая за asyncio)» ошибочно наводит на мысль, что они несут ответственность или авторы asyncio. Они внесли свой вклад в async / await, но это делает их ничем иным, как еще одним участником, еще одной частью системы. В любом случае, я поддержал вас, поскольку ваш пример полезен и может помочь другим операторам исследовать эту область.