Ода кешированию в Django
Mar 22, 2017 23:02 · 681 words · 4 minute read
Как известно, есть 2 проблемы программирования: выбор имени переменной и инвалидация кеша. Вторая меня на этой неделе прям достала. Извините, наболело… Итак, кеширование - классная штука, она позволяет существенно ускорить работу приложения, но привносит свои проблемы. Главная из них - поддержание кеша в консистентном состоянии. Вроде бы ничего сложного - на сигнал post_save вешаем функцию по перерассчёту и радуемся жизни, но не всё так просто. В Django есть несколько адаптеров для работы с кешем, рассмотрим парочку из django.core.cache.
cache.backends.locmem.LocMemCache
Первая проблема возникла с кешированием в локальной памяти при работе с библиотекой sorl-thumbnail. В админке при смене картинки у объекта пересоздаются также превью с разными разрешениями, а ссылки сохраняются в кеше процесса. Я не зря упоминул место сохранения кеша, т.к. их может быть несколько. И если при смене изображения в худшем случае можем увидеть старое (сменили в одном процессе, а во втором остались ссылки на старые файлы), то при удалении изображения пользователь получит исключение, связанное с отсутствием файла. Ссылки на него удалили ж только в одном процессе, а запрос пришёл на другой, который ни сном ни духом что что-то изменилось.
cache.backends.db.DatabaseCache
Хорошо, раз кеш должен быть един, то пусть это будет таблица в БД. Да, не оптимально, но нагрузки у нас пока нет - потянет. Была задача собрать порядка 1000 json объектов и хранить их где-нибудь, т.к. на формирование одного объекта уходит уж очень много времени. Вроде бы пока всё логично… Выбрали для этого DatabaseCache - памяти много, скорость устраивает, даже индексы создавать не обязательно. Написал процедурку, поставил выполняться, пришёл - всего 100 объектов. Команда отработала, но всего 100 объектов??? Хорошо, добавил отладочный вывод, запустил - всё обработано, в базе по-прежнему 100. Ладно, дебаг так дебаг… Монотонно давлю F8, но тут замечаю константу MAX_ENTRIES. Серьёзно? Для кеша в базе данных максимальное кол-во записей 300? Ах это ограничение для всех типов кешей?! Ну как бы 2017 год на дворе, откуда такая цифра?! Я бы понял 2, 3 или 4, я бы понял 100500, но 300!!! Да, я слышал про ротацию кеша и его протухание, но чтобы так. В общем, вывод - читайте документацию к настройкам, хотя бы бегло.
Не всё так плохо
Однако, без кеша бывает просто не обойтись. В своём проекте GeoPuzzle все полигоны закодированы в gmaps и навечно сохранены в кеше. Теперь нет никакой вычислительной нагрузки - всё просто летает, но я пошёл дальше и закешировал страницы с помощью декораторов:
MIDDLEWARE = ('django.middleware.cache.UpdateCacheMiddleware',
*MIDDLEWARE,
'django.middleware.cache.FetchFromCacheMiddleware')
Но стоит ли говорить, что после деплоя я про это забыл и не сразу понял отчего же у меня не показывается новая разметка страниц?! Кстати, для кеширования полигонов я написал простенький декоратор, который кеширует свойство объекта:
def cacheable(func):
def new_func(*args, **kwargs):
self = args[0]
cache_key = self.caches[func.__name__].format(id=self.id)
result = cache.get(cache_key)
if result is None:
result = func(*args, **kwargs)
cache.set(cache_key, result, timeout=None)
return result
return new_func
Теперь в модели достаточно прописать всего пару дополнительных строк:
class Region(CacheablePropertyMixin, models.Model):
...
caches = {
'polygon_bounds': 'region{id}bounds',
}
@property
@cacheable
def polygon_bounds(self) -> List:
return self.polygon.extent
Настройка кеширования в Redis
Для себя оптимальным я выбрал кеширование в Redis - установка этого сервиса совсем ничего не стоит, он умеет работать с разными типами данных и даже выполнять lua-скрипты. Вы можете расположить его на локальной машине, или же в качестве отдельного сервиса.
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": 'redis://{}:6379/1'.format(REDIS_HOST),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"COMPRESSOR": "django_redis.compressors.lzma.LzmaCompressor",
"SOCKET_CONNECT_TIMEOUT": 2,
"SOCKET_TIMEOUT": 2,
}
}
}
Также в нём есть встроенное сжатие данных и удобная маршализация. Ещё одним плюсом является возможность сохранять данные на винте. Когда упала машина в сервисе на AWS мне достаточно было указать rdb-файл с сохранёнными полигонами, и инстанс восстановился из него!
Заключение
Так или иначе, рано или поздно любому программисту придётся столкнуться с кешированием. Будь то хранение предрасчитанных значений, сессий в redis, результатов ajax-запросов в local storage, картинок где-то на диске… Пожалуйста, отложите этот момент! Но уж если не выходит, то протестируйте, а потом ещё передайте кому-нибудь для тестов. Это очень опасное место. Я знал системы, в которых прогрев всего хозяйства занимал часы, знал где использовались сразу и redis, и memcached. Поверьте, так было сделано не от хорошей жизни и не от прихоти заказчика. Кеширование, увы, - финальная стадия оптимизации.