Comments 22
Почему async? Просто потому, что популярно.
Прошу прощения, но проследуйте вон из профессии. Приближался к концу 2021й год -- а разработчикам всё ещё нужно объяснять, чем синхронный код отличается от асинхронного. Оказывается, это просто мода нынче такая
Простите, не хотел никого обидеть, когда писал свое видение такой дикой популярности использования async/await в приложениях.
Дикая популярность обусловлена не менее дикой производительностью асинхронного кода. О производительности пишу я в комментариях, а не вы в статье. Должно быть наоборот.
Странно, что не упомянуты дефолтные functools.lru_cache
/functools.cache
Ну во первых , о чем бы была тогда статья. Кроме того, нужно все же упомянуть, что lru_cache с асинхронным кодом работать не будет. Но, как мне кажется, кому было нужно, уже имеют модифицированый async_lru_cache
Ну во первых , о чем бы была тогда статья.
Я давно стою на позиции, что если нечего говорить -- не говори, нечего отвечать -- не отвечай, нечего писать -- не пиши
Кроме того, нужно все же упомянуть, что lru_cache с асинхронным кодом работать не будет.
С этого и надо было начинать. Всё именно так. Вот это и есть та крупица разумного-доброго-вечного, ради чего и написана эта статья
Но, как мне кажется, кому было нужно, уже имеют модифицированый async_lru_cache
Пакет удобен только до тех пор, пока у вас только один инстанс. Потом приходится в redis переезжать
lru_cache (или его асинхронный аналог) удобен простотой и чаше используется для вполне конкретных целей, например локальных оптимизаций, рекурсивных расчетов или хранени] часто используемых данныъ и вовсе не выступает соперником ни какой системы хранения данных, поэтому его применение или не применение не мешает использовать ни redis ни tarantool ни что то иное.
А вот как раз реализации, описанные в статье имею меньший потенциал, так как для части людей они будут избыточны, а для другой не достаточно гибки и производительны.
А вот как раз реализации, описанные в статье имею меньший потенциал, так как для части людей они будут избыточны, а для другой не достаточно гибки и производительны.
А можете пояснить почему они будут избыточны? А для других недостаточно производительны?
А вот как раз реализации, описанные в статье имею меньший потенциал, так
как для части людей они будут избыточны, а для другой не достаточно
гибки и производительны.
Когда овладел молотком, всё вокруг кажется гвоздём :)
Кэш на уровне приложения (LRU) и кэш в Redis нужны для разных вещей. Я в Redis не буду кидать объекты типа `sqlalchemy.Select`, хотя при этом агрессивно их кэширую на уровне приложения
functools.lru_cache
/functools.cache
Да, это неплохо. Но в реальных приложениях очень скоро возникает вопрос масштабирования. И с локальными кэшами вас ждём много сюрприров
Кстати вылетело из головы. Оберните в cache/lru_cache corofunctions (асинхронную функцию) -- и закэшируете только корутину. Во второй раз уже мёртвую (выполненную). Поэтому lru_cache просто сломает асинхронное приложение
На одном из прошлых проектов я развлекался следующим образом:
Создал класс RedisWrapper -- он предоставлял все абстракции для работы с примитивами типа set, list, flat (int/float/str), hash, и другие звери. Задачей этого класса была сериализация значения, запихивание его в Redis, вычитывание и десериализация. Очевидно, раз есть десериализация, то есть и схемы -- decimal.Decimal плохо хранится в Redis.
Предсказуемо, что изпользовался этот класс в хвост и гриву везде, где мы лезли в RedisСоздал класс RedisLRU -- его методом я декорировал функции и методы классов, которые нужно прозрачно кэшировать. Через интроспекцию я в момент инициализации приложения получал число аргументов функции и генерировал шаблон ключа в Redis, в момент вызова я через %s подставлял в шаблон аргументы и получал итоговый ключ
Таким образом у нас была огромная реюзаемость кода и кэширование подключалось просто декорированием функции
Сам писал такие обертки и работал с чужими, сам делал баги и покрывал эти обертки тестами - в результат куча кода, который надо поддерживать. Зачем если можно использовать готовое? Именно в результате осознания этого решил вынести это в библиотеку, о которой в последней части статьи идет речь.
Зачем если можно использовать готовое?
Как минимум, обёртки я писал года полтора назад)
По итогу скажу следующее -- весьма недурно! Оно не решает проблему сериализации/десериализации, но решение вписывается в архитектуру.
Что насчёт возможности декорирования методов класса?
Про args/kwargs и производительность всего этого каверзных вопросов не задаю) Я сам очень скрипел зубами, если что-то лишнее приходилось делать в рантайме, ибо удобство шло в конфликт с производительностью
Что насчёт возможности декорирования методов класса?
Если вы насчет моей библиотки то их возможно декорировать. Есть маленькая проблема: библиотека дает возможность не указывать ключ для кеша и тогда он будет сгенерирован самой библиотекой
class MyClass:
@cache(ttl="10s")
async def method(self, arg):
...
в таком случае ключ сформируется не совсем валидный: main:method:self:<main.myclass object at 0x10545eeb0>:arg:test
Согласен, что такое поведение не комельфо, но когда разработчик декорирует метод, то не ясно, хочет ли он кеш для класса или для обьекта этого класса. Поэтому правильней будет если разработчик будет указывать конкретный шаблон для ключа:
@cache(ttl="10s", key="method:{self.id}:{arg}")
async def method(self, arg):
...
# vs
@cache(ttl="10s", key="method:{arg}")
async def method(self, arg):
...
Про args/kwargs и производительность всего этого каверзных вопросов не задаю) Я сам очень скрипел зубами, если что-то лишнее приходилось делать в рантайме, ибо удобство шло в конфликт с производительностью
Да я понимаю о чем вы, о необходимости получать сигнатуру через интроспекцию? - это дорого, но есть же кэш )
@lru_cache(maxsize=100)
def _get_func_signature(func):
return inspect.signature(func)
в результат куча кода, который надо поддерживать
Только зачем писать кучу кода, и главное, зачем её потом нещадно менять?
Тот проект был прикольный тем, что в нём от основной бизнес-логики отделялся очень жирный common -- библиотеки проекта. Чаще всего библиотеки писались один раз и после их не или трогали вообще, или максимум дополняли. На моей памяти модификация функционала, прямо ломающая обратную совместимость, потребовалась единожды. Малая изменчивость кода (или отсутствие осей изменений), и при этим широкое использование этого кода в проекте, лишало целесообразности тестов именно для common -- вся работоспособность проверялась другими тестами -- тестами бизнес-логики
Видимо под поддержкой имеется ввиду исправление багов:
Сам писал такие обертки и работал с чужими, сам делал баги и покрывал эти обертки тестами
Но как бы это в поддержке любого кода есть. Будет этот код в основном проекте или будет вынесен в библиотеку — не важно, тесты надо писать всегда, а ошибки исправлять сразу же как обнаружены. И нет, какой-то серебряной пули от багов нет, только внимательность и опыт
Спасибо за интересную статью. Мы тоже активно применяем различные паттерны для кэширования в своих приложениях. Подскажите, а вы не сравнивали производительность redis vs keydb? Какие еще движки планируете поддержать в Cashews?
Если честно, я скептически отношусь к keydb, возможно из-за того что не встречал серьезных проблем с redis и его всегда хватало.
Насчет поддержки других хранилишь: думал про memcached
конечноже, но решил подождать пока кому-нибудь понадобится так как он стремительно теряет свою популярность. А других претендентов пока не вижу.
А что насчет "гонок"? Между попыткой чтения из кеша и установкой нового значения. Просто игнорируете?
Хотелось бы большей деталей: к чему эта гонка может привести?
Но могу сказать, что в работе с кешом обычно вопрос гонки важен в случае высокой нагрузки на кеш, которая может привести к cache stampede: когда есть кеш в высоким рейтом вызова и вдруг происходит инвалидация, которая приводит к паралеьному пересчету данных для кэша. Можно про него еще вот тут еще почитать.
Для зашиты от этой проблемы есть в библиотеке несколько решений:
1)cache.early
декоратор, который кроме обычного ttl имеет второй ttl - ранней экпирации, после которого кэш обновится в бекграунде, причем перерасчет будет гарантировано сделан 1 раз, достигается засчет атомарности операции incr если мы храним кеш в redis. Можно сказать, что это аналог XFetch алгоритма.
2) cache.hit
декоратор, который с указанием update_after
будет действовать
также как cache.early,
отлько ориентиром для раннего обновления кеша будет количество хитов.
3) cache.locked
декоратор, который поставит лок на исполнении декорируемой функции - таким образом при совмещении 2 декораторов можно будет гарантировать что паралельного перерасчета кеша не будут
@cache(ttl="10m")
@cache.locked(ttl="5s")
async def func():
Кэш в асинхронных python приложениях