Ода кешированию в Django

Mar 22, 2017 23:02 · 681 words · 4 minute read django

Нельзя просто так взять и подключить кеширование

Как известно, есть 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. Поверьте, так было сделано не от хорошей жизни и не от прихоти заказчика. Кеширование, увы, - финальная стадия оптимизации.