Система кэширования Django¶
Фундаментальный компромисс динамических веб-сайтов заключается в том, что они динамичны. Каждый раз, когда пользователь запрашивает страницу, веб-сервер выполняет всевозможные вычисления — от запросов к базе данных до рендеринга шаблонов и бизнес-логики — чтобы создать страницу, которую увидит посетитель вашего сайта. С точки зрения затрат на обработку это намного дороже, чем стандартная схема сервера с чтением файла вне файловой системы.
Для большинства веб-приложений эти накладные расходы не имеют большого значения. Большинство веб-приложений не являются washingtonpost.com или slashdot.org; это сайты малого и среднего размера с посредственным трафиком. Но для сайтов со средней и высокой посещаемостью важно максимально сократить накладные расходы.
С этого момента в дело вступает кэширование.
Кэшировать что-либо — значит сохранить результат дорогостоящего вычисления, чтобы вам не пришлось выполнять его в следующий раз. Вот некоторый псевдокод, объясняющий, как это будет работать для динамически генерируемой веб-страницы:
given a URL, try finding that page in the cache
if the page is in the cache:
return the cached page
else:
generate the page
save the generated page in the cache (for next time)
return the generated page
Django поставляется с надёжной системой кэширования, которая позволяет вам сохранять динамические страницы так, что не потребуется их создавать для каждого запроса. Для удобства, Django даёт возможность тонкого управления уровнем кэширования: вы можете кэшировать результат работы представления, вы можете кэшировать только те куски, которые трудно вычислять или вы можете кэшировать весь ваш сайт.
Django также хорошо работает с «нисходящими» кешами, такими как Squid и кешами на основе браузера. Это типы кешей, которыми вы не управляете напрямую, но которым вы можете подсказывать (через HTTP-заголовки) о том, какие части вашего сайта следует кэшировать и как.
См.также
Философия дизайна кэширующих фреймворков объясняет некоторые дизайн решения фреймворка.
Настройка кэша¶
Система кэширования требует небольшой настройки. А именно, надо указать где должны располагаться закэшированные данные – в базе данных, на файловой системе или прямо в памяти. Это важное решение, которое повлияет на производительность вашего кэша. Да, типы кэшей различаются по скорости работы.
Настройки кэша определяются параметром конфигурации CACHES. Ниже приведено объяснение всех возможных значений этого параметра.
Memcached¶
Memcached__ — это кэш-сервер, полностью основанный на памяти, первоначально разработанный для обработки высоких нагрузок на LiveJournal.com, а затем открытый исходный код Danga Interactive. Он используется такими сайтами, как Facebook и Wikipedia, для уменьшения доступа к базе данных и значительного повышения производительности сайта.
Memcached работает как демон и захватывает определённый объём оперативной памяти. Его задачей является представление быстрого интерфейса для добавления, получения и удаления определённых данных в кэше. Все данные хранятся прямо в оперативной памяти, таким образом нет никакой дополнительной нагрузки на базу данных или файловую систему.
После установки самого Memcached вам потребуется установить привязку Memcached. Доступно несколько привязок Python Memcached; Django поддерживает два: pylibmc и pymemcache.
Для использования Memcached с Django:
Установите для
BACKENDзначениеdjango.core.cache.backends.memcached.PyMemcacheCacheилиdjango.core.cache.backends.memcached.PyLibMCCache(в зависимости от выбранной вами привязки memcached).Определите для
LOCATIONзначениеip:port(гдеip— это IP адрес, на котором работает демон Memcached,port— его порт) илиunix:path(гдеpathявляется путём к файлу-сокету Memcached).
В этом примере Memcached работает на локальном хосте (127.0.0.1) с портом 11211, используя привязку pymemcache:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
}
}
В этом примере Memcached доступен через локальный файл сокета Unix:file:/tmp/memcached.sock с использованием привязки pymemcache:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "unix:/tmp/memcached.sock",
}
}
Одной из замечательных особенностей Memcached является возможность распределять кэш по нескольким серверам. Это означает, что вы можете запустить демоны Memcached на нескольких машинах и программа будет рассматривать эту группу машин как единый кэш, без необходимости копирования всех значений кэша на каждую машину. Для того, чтобы воспользоваться этой особенностью, укажите адреса всех машин в LOCATION, в виде списка или строки, разделённой запятыми.
В данном примере, кэш распределён по экземплярам Memcached, работающим на IP адресах 172.19.26.240 и 172.19.26.242, на порту 11211 оба:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": [
"172.19.26.240:11211",
"172.19.26.242:11211",
],
}
}
В следующем примере, кэш распределён по экземплярам Memcached, работающим на IP адресах 172.19.26.240 (порт 11211), 172.19.26.242 (порт 11212) и на 172.19.26.244 (порт 11213):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": [
"172.19.26.240:11211",
"172.19.26.242:11212",
"172.19.26.244:11213",
],
}
}
По умолчанию серверная часть PyMemcacheCache устанавливает следующие параметры (вы можете переопределить их в своих OPTIONS):
"OPTIONS": {
"allow_unicode_keys": True,
"default_noreply": False,
"serde": pymemcache.serde.pickle_serde,
}
В конце рассказа о Memcached следует сказать, что этот тип кэша имеет один недостаток: Кэш располагается в оперативной памяти и уничтожается при сбое сервера. Очевидно, что оперативная память не предназначена для постоянного хранения информации, поэтому не следует на неё рассчитывать в этом смысле. Несомненно, ни один из модулей кэширования не должен использоваться как постоянное хранилище — они предназначены для кэширования, не для хранения — мы особенно это отмечаем для данного типа кэша.
Редис¶
Redis__ — это база данных в памяти, которую можно использовать для кэширования. Для начала вам понадобится сервер Redis, работающий локально или на удаленном компьютере.
После настройки сервера Redis вам необходимо установить привязки Python для Redis. redis-py — это привязка, изначально поддерживаемая Django. Также рекомендуется установить пакет hiredis-py.
Чтобы использовать Redis в качестве сервера кэширования с Django:
Установите для
BACKENDзначениеdjango.core.cache.backends.redis.RedisCache.Задайте для
LOCATIONURL-адрес, указывающий на ваш экземпляр Redis, используя соответствующую схему. Подробную информацию о доступных схемах см. в документацииredis-py<https://redis.readthedocs.io/en/stable/connections.html#redis.connection.ConnectionPool.from_url>`__.
Например, если Redis работает на локальном хосте (127.0.0.1), порт 6379:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
Часто серверы Redis защищены аутентификацией. Чтобы указать имя пользователя и пароль, добавьте их в LOCATION вместе с URL-адресом:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://username:password@127.0.0.1:6379",
}
}
Если у вас есть несколько серверов Redis, настроенных в режиме репликации, вы можете указать серверы либо в виде строки, разделенной точкой с запятой или запятой, либо в виде списка. При использовании нескольких серверов операции записи выполняются на первом сервере (лидере). Операции чтения выполняются на других серверах (репликах), выбранных случайным образом:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": [
"redis://127.0.0.1:6379", # leader
"redis://127.0.0.1:6378", # read-replica 1
"redis://127.0.0.1:6377", # read-replica 2
],
}
}
Кэширование в базу данных¶
Кэширование в базу данных отлично работает в случае, если у вас есть быстрый сервер баз данных с поддержкой индексирования.
Для использования таблицы базы данных в качестве бэкэнда кэша:
Установите
BACKENDвdjango.core.cache.backends.db.DatabaseCacheУстановите
LOCATIONвtablename, имя таблицы базы данных. Это имя может быть любым, пока оно не противоречит правилам именования таблиц в вашей базе данных и не занято другой таблицей.
В данном примере, именем таблицы для кэша будет my_cache_table:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "my_cache_table",
}
}
В отличие от других механизмов кэширования, кэш базы данных не поддерживает автоматический отбор записей с истекшим сроком действия на уровне базы данных. Вместо этого записи кэша с истекшим сроком действия отбираются каждый раз при вызове add(), set() или touch().
Создание таблицы для кэша¶
Прежде чем использовать кэш базы данных, вы должны создать таблицу кэша с помощью этой команды:
python manage.py createcachetable
В результате в базе данных будет создана таблица, структура которой соответствует ожиданиям системы кэширования. Имя для таблицы будет взято из параметра LOCATION.
При использовании нескольких БД кэшей, команда createcachetable создаст по одной таблице для каждого кэша.
Если вы используете множество баз данных, то команда createcachetable обратится к методу allow_migrate() роутера вашей базы данных (см. далее).
Аналогично команде migrate, команда createcachetable не внесёт изменения в существующую таблицу. Она создаёт только отсутствующие таблицы.
Чтобы вывести SQL, который был бы выполнен, без его выполнения, используйте опцию :djadminopt:`--dry-run`.
Использование нескольких баз данных¶
Если у вас несколько баз данных и вы планируете использовать кэширование, потребуется прописать инструкции роутинга для таблицы кэширования. В целях роутинга таблица кэширования представлена моделью CacheEntry в приложении django_cache. Эта модель не отобразится в модельном кэше, но содержимое модели может быть использовано для роутинга.
Например, представленный ниже роутер будет перенаправлять все операции чтения из кэша на cache_replica, а всё операции записи на cache_primary. Таблица кэширования будет синхронизироваться только с cache_primary:
class CacheRouter:
"""A router to control all database cache operations"""
def db_for_read(self, model, **hints):
"All cache read operations go to the replica"
if model._meta.app_label == "django_cache":
return "cache_replica"
return None
def db_for_write(self, model, **hints):
"All cache write operations go to primary"
if model._meta.app_label == "django_cache":
return "cache_primary"
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
"Only install the cache model on primary"
if app_label == "django_cache":
return db == "cache_primary"
return None
Если вы не настроите роутинг для кэширования, то модуль кэширования будет использовать базу default.
А если вы не используете серверную часть кэша базы данных, вам не нужно беспокоиться о предоставлении инструкций маршрутизации для модели кэша базы данных.
Кэширование на файловую систему¶
Файловый бэкэнд сериализует и сохраняет каждое закэшированное значение в отдельном файле. Для использования этого бэкэнда, установите BACKEND в "django.core.cache.backends.filebased.FileBasedCache", а для LOCATION укажите подходящий каталог. Например, для хранения закэшированных данных в /var/tmp/django_cache, используйте такую настройку:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
}
}
Если вы используете Windows, то подставьте букву диска в начало пути, вот так:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "c:/foo/bar",
}
}
Путь до каталога должен быть абсолютным, т.е. он должен начинаться от корня файловой системы. Завершающий слэш не имеет значения.
Убедитесь, что каталог, на который указывает этот параметр, либо существует и доступен для чтения и записи, либо что он может быть создан пользователем системы, под которым работает ваш веб-сервер. Продолжая приведенный выше пример, если ваш сервер работает от имени пользователя «apache», убедитесь, что каталог «/var/tmp/django_cache» существует и доступен для чтения и записи пользователю «apache», или что он может быть создан пользователем «apache».
Предупреждение
Когда кэш LOCATION содержится в MEDIA_ROOT, STATIC_ROOT или STATICFILES_FINDERS, конфиденциальные данные могут быть раскрыты.
Злоумышленник, получивший доступ к файлу кэша, может не только фальсифицировать HTML-контент, которому будет доверять ваш сайт, но и удаленно выполнить произвольный код, поскольку данные сериализуются с помощью pickle.
Предупреждение
Кэширование файловой системы может замедляться при хранении большого количества файлов. Если вы столкнулись с этой проблемой, рассмотрите возможность использования другого механизма кэширования. Вы также можете создать подкласс FileBasedCache и улучшить стратегию отсеивания.
Кэширование в оперативной памяти¶
Это стандартный кэш, который применяется, если другой не определён в вашем файле конфигурации. Если вам требуется высокая скорость работы кэша, но у вас нет возможности развернуть Memcached, рассмотрите вариант использования кэша в оперативной памяти. Этот кэш по-процессный (см. далее) и потокобезопасный. Для его использования надо параметру конфигурации BACKEND присвоить значение "django.core.cache.backends.locmem.LocMemCache". Например:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
Параметр конфигурации LOCATION используется для идентификации отдельных хранилищ в памяти. Если у вас только один такой кэш, то вы можете пропустить этот параметр. Однако, в случае нескольких кэшей, вам потребуется назначить имя хотя бы для одного из них, для их различия.
Кэш использует стратегию отбраковки наименее недавно использованных данных (LRU).
Обратите внимание, что каждый процесс будет иметь свой собственный экземпляр частного кэша, что означает, что межпроцессное кэширование невозможно. Это также означает, что кэш локальной памяти не особенно эффективен с точки зрения использования памяти, поэтому он, вероятно, не является хорошим выбором для производственных сред. Это приятно для развития.
Псевдокэширование (для разработки)¶
Наконец, Django поставляется с «псевдо» кэшем, который не выполняет собственно кэширование. Он просто реализует интерфейс кэша, не делая больше ничего.
Он полезен в случае, когда у вас есть боевой сайт , который плотно использует кэширование в различных местах, а также окружение разработки или тестирование, где кэшировать ничего не надо. Для активации псевдо кэширования, установите BACKEND:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}
}
Использование собственного модуля кэширования¶
Несмотря на то, что Django предоставляет ряд модулей кэширования, вам может потребоваться использовать свой кэширующий модуль. Для подключения внешнего кэширующего модуля используйте путь до него в качестве значения BACKEND параметра конфигурации CACHES, вот так:
CACHES = {
"default": {
"BACKEND": "path.to.backend",
}
}
Если вы создаете собственную серверную часть, вы можете использовать стандартные серверные части кэша в качестве эталонных реализаций. Вы найдете код в каталоге django/core/cache/backends/ исходного кода Django.
Следует отметить, если нет важной причины для использования собственного кэширующего модуля, вы должны использовать кэширующие модули, поставляемые с Django. Они протестированы и просты в использовании.
Параметры кэша¶
После определения типа кэша и имени для каждого из них, для каждого модуля можно указать дополнительные аргументы для управления поведением кэша. Эти аргументы представляются в виде дополнительных ключей в параметре конфигурации CACHES. Приведём список допустимых аргументов:
TIMEOUT: время устаревания кэша по умолчанию, в секундах. По умолчанию300секунд (5 минут). Вы можете установитьTIMEOUTвNone, тогда кэш никогда не устареет. Если указать0, все ключи будут сразу устаревать (таким образом можно заставить «не кэшировать»).OPTIONS: Любая опция, которая должна быть передана модулю. Список допустимых опций варьируется от модуля к модулю и передается непосредственно в библиотеку для кэширования.Модули кэширования, которые реализуют собственную стратегию очистки (т.е., модули
locmem,filesystemиdatabase) учитывают следующие опции:MAX_ENTRIES: максимальное количество элементов в кэше перед началом удаления старых значений. Обычно,300элементов.CULL_FREQUENCY: Часть элементов, которые надо удалить при достиженииMAX_ENTRIES. Обычное соотношение —1/CULL_FREQUENCY, таким образом, надо установитьCULL_FREQUENCYв2, чтобы удалять половину значений кэша при достиженииMAX_ENTRIES. Аргумент должен быть целым числом и по умолчанию равен3.Значение
0дляCULL_FREQUENCYозначает, что весь кэш должен быть сброшен при достиженииMAX_ENTRIES. Это делает очистку значительно быстрее для определенных бэкендов(в частностиdatabase) ценой увеличения промахов кэша.
Серверные части Memcached и Redis передают содержимое
OPTIONSв качестве аргументов ключевых слов конструкторам клиента, что позволяет более детально контролировать поведение клиента. Пример использования см. ниже.KEY_PREFIX: Строка, которая автоматически включается (предваряет, по умолчанию) во все ключи кэша, используемые сервером Django.Обратитесь к документации на кэш для подробностей.
VERSION: Номер версии про умолчанию для ключей кэша, созданных сервером Django.Обратитесь к документации на кэш для подробностей.
KEY_FUNCTIONСтрока, содержащая путь до функции, которая определяет правила объединения префикса, версии и ключа в итоговый ключ кэша.Обратитесь к документации на кэш для подробностей.
В этом примере, модуль кэширования на файловую систему настроен на таймаут в 60 секунд и ёмкость в 1000 элементов:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
"TIMEOUT": 60,
"OPTIONS": {"MAX_ENTRIES": 1000},
}
}
Вот пример конфигурации для бэкенда на основе pylibmc, который включает двоичный протокол, аутентификацию SASL и режим поведения ketama:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
"LOCATION": "127.0.0.1:11211",
"OPTIONS": {
"binary": True,
"username": "user",
"password": "pass",
"behaviors": {
"ketama": True,
},
},
}
}
Вот пример конфигурации для бэкэнда на основе pymemcache, который включает объединение клиентов в пул (что может улучшить производительность за счет поддержания соединения клиентов), обрабатывает ошибки memcache/сетевые ошибки как промахи в кэше и устанавливает флаг TCP_NODELAY в сокете соединения:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
"OPTIONS": {
"no_delay": True,
"ignore_exc": True,
"max_pool_size": 4,
"use_pooling": True,
},
}
}
Вот пример конфигурации для серверной части на основе Redis, которая выбирает базу данных 10 (по умолчанию Redis поставляется с 16 логическими базами данных) и устанавливает собственный класс пула соединений (по умолчанию используется redis.ConnectionPool):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"db": "10",
"pool_class": "redis.BlockingConnectionPool",
},
}
}
Кэш для каждого сайта¶
После настройки кэша, простейшим способом его использования будет кэширование всего сайта. Вам надо будет добавить 'django.middleware.cache.UpdateCacheMiddleware' и 'django.middleware.cache.FetchFromCacheMiddleware' в параметр конфигурации MIDDLEWARE_CLASSES, как это показано в примере:
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
Примечание
Нет, это не ошибка: мидлварь «update» должна идти первой в списке, а мидлварь «fetch» — последней. Подробности тут не приводятся, обратитесь к Порядку классов MIDDLEWARE, проведенному далее, если они вам нужны.
Затем, добавьте следующие обязательные параметры в файл конфигурации проекта:
CACHE_MIDDLEWARE_ALIAS– Метка кэша, используемая для хранилища.CACHE_MIDDLEWARE_SECONDS– Целое число секунд, в течение которого должна кэшироваться каждая страница.CACHE_MIDDLEWARE_KEY_PREFIX– Если кэш разделён между несколькими сайтами внутри одной инсталляции Django, установите в имя сайта или любую другую строку, которая уникальна для этой инсталляции, чтобы предотвратить совпадения. Используйте пустую строку, если это не ваш случай.
Мидлварь FetchFromCacheMiddleware кэширует GET и HEAD отклики со статусом 200, если заголовки запроса и отклика это позволяют. Ответы на запросы для одного URL с разными параметрами запроса считаются уникальными и кэшируются раздельно. Мидлварь кэша ожидает, что запрос HEAD возвращает такие же заголовки, как и соответствующий GET запрос; в этом случае он может вернуть закэшированный GET отклик для запроса HEAD.
Кроме того, UpdateCacheMiddleware автоматически устанавливает несколько заголовков в каждом HttpResponse, которые влияют на нисходящие кэши:
Устанавливает в заголовке
Expiresтекущие дату и время, добавляя к ним значение, определённое вCACHE_MIDDLEWARE_SECONDS.Устанавливает заголовок
Cache-Control, определяя максимальный возраст для страницы, также используя значение из параметра конфигурацииCACHE_MIDDLEWARE_SECONDS.
Обратитесь к Промежуточный слой (Middleware) для подробностей.
Если представление устанавливает собственное время кэширования (т.е. определяет значение max-age в заголовке Cache-Control), то страница будет закэширована на указанное время, вместо CACHE_MIDDLEWARE_SECONDS. Используя декораторы из django.views.decorators.cache, вы можете легко определять время кэширования представлений (с помощью декоратора cache_control) или отключать кэширование для представления (с помощью декоратора never_cache). Обратитесь к разделу Использование других заголовков для получения дополнительной информации об этих декораторах.
Если параметр конфигурации USE_I18N установлен в True, то созданный ключ значения в кэше будет содержать имя активного языка – обратитесь к определению Django языковой настройки). Такое поведение позволяет легко кэшировать мультиязычные сайты, не требуя функционала для создания ключей кэша.
Ключи кэша также включают текущий часовой пояс, когда USE_TZ установлен в True.
Кэширование на уровне представлений¶
- django.views.decorators.cache.cache_page(timeout, *, cache=None, key_prefix=None)¶
Более детальный способ использования системы кэширования возможен за счет кэширования вывода отдельных представлений. Модуль django.views.decorators.cache определяет декоратор cache_page, который автоматически кэширует вывод представления. Использовать его несложно:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def my_view(request): ...
Декоратор cache_page принимает единственный аргумента: длительность кэширования, в секундах. В предыдущем примере, результат представления my_view() будет закэширован на 15 минут. (Следует отметить, что мы задали значения в виде 60 * 15 в целях читаемости. 60 * 15 будет вычислено в 900, т.е. 15 минут умножается на 60 секунд в минуте.)
Тайм-аут кэша, установленный в cache_page, имеет приоритет над директивой max-age из заголовка Cache-Control.
Кэш уровня представления, аналогично кэшу уровня сайта, использует ключи на основе URL. Если несколько URL указывают на одно представление, каждый URL будет закэширован отдельно. Продолжая работу с примером my_view, если ваш URLconf выглядит так:
urlpatterns = [
path("foo/<int:code>/", my_view),
]
то запросы к /foo/1/ и /foo/23/ будут закэшированы отдельно, как вы могли предполагать. Но как только определённый URL (например, /foo/23/) будет запрошен, следующие запросы к этому URL будут использовать кэш.
Декоратор cache_page также может принимать необязательный именованный аргумент, cache, который указывает декоратору использовать определённый кэш (из списка параметра конфигурации CACHES) для кэширования результатов. По умолчанию используется кэш default, но вы можете указать любой:
@cache_page(60 * 15, cache="special_cache")
def my_view(request): ...
Вы также можете переопределить префикс кэша для каждого представления. cache_page принимает необязательный аргумент ключевого слова key_prefix, который работает так же, как параметр CACHE_MIDDLEWARE_KEY_PREFIX для промежуточного программного обеспечения. Его можно использовать так:
@cache_page(60 * 15, key_prefix="site1")
def my_view(request): ...
Аргументы key_prefix и cache можно указать вместе. Аргумент key_prefix и параметр KEY_PREFIX настройки CACHES будут объединены.
Кроме того, cache_page автоматически устанавливает в ответе заголовки Cache-Control и Expires, которые влияют на нисходящие кэши.
Определение кэша уровня представления в URLconf¶
В примерах из предыдущего раздела тот факт, что представление кэшируется, жестко запрограммирован, поскольку cache_page изменяет функцию my_view на месте. Этот подход связывает ваше представление с системой кэширования, которая не идеальна по нескольким причинам. Например, вы можете захотеть повторно использовать функции представления на другом сайте без кэша или распространить представления среди людей, которые захотят использовать их без кэширования. Решением этих проблем является указание кэша для каждого представления в URLconf, а не рядом с самими функциями представления.
Сделать это несложно: просто оберните функцию представления с помощью cache_page, при обращении к ней в URLconf. Вот так выглядела старая конфигурация URL:
urlpatterns = [
path("foo/<int:code>/", my_view),
]
Вот так должно быть в случае, когда my_view обёрнута с помощью cache_page:
from django.views.decorators.cache import cache_page
urlpatterns = [
path("foo/<int:code>/", cache_page(60 * 15)(my_view)),
]
Кэширование фрагментов шаблона¶
Если требуется более тонкий контроль, вы также можете кэшировать фрагменты шаблона с помощью шаблонного тега cache. Для работы данного тега необходимо указать {% load cache %} в начале шаблона.
Шаблонный тег {% cache %} кэширует содержимое блока на указанный период времени. Он принимает как минимум два аргумента: время кэширования в секундах и имя для закэшированного фрагмента. Имя будет использовано как есть, не используйте переменную. Например:
{% load cache %}
{% cache 500 sidebar %}
.. sidebar ..
{% endcache %}
Временами требуется кэшировать многочисленные копии одного фрагмента, различающиеся только небольшим куском динамических данных. Например, может потребоваться кэширование копии сайдбара из предыдущего примера для каждого пользователя вашего сайта. Это можно сделать, передав дополнительные аргументы в шаблонный тег {% cache %}, что однозначно будет идентифицировать фрагмент:
{% load cache %}
{% cache 500 sidebar request.user.username %}
.. sidebar for logged-in user ..
{% endcache %}
Если параметр конфигурации USE_I18N установлен в True, то мидлварь кэширования сайта будет учитывать активных язык. Для шаблонного тега cache можно использовать одну из переменных, предназначенных для перевода, доступных шаблонам для достижения того же результата:
{% load i18n %}
{% load cache %}
{% get_current_language as LANGUAGE_CODE %}
{% cache 600 welcome LANGUAGE_CODE %}
{% translate "Welcome to example.com" %}
{% endcache %}
Время жизни фрагмента в кэше также может быть шаблонной переменной, до тех пор пока эта переменная разрешается в целое значение. Например, если шаблонная переменная my_timeout имеет значение 600, то следующие два примера эквивалентны:
{% cache 600 sidebar %} ... {% endcache %}
{% cache my_timeout sidebar %} ... {% endcache %}
Эта особенность полезна для уменьшения количества повторений в шаблонах. Вы можете установить время жизни в переменной, в одном месте, и затем использовать это значение.
По умолчанию, кэширующий тег будет пытаться использовать кэш с именем «template_fragments». При отсутствии такого кэша, будет происходить переключение на стандартный кэш. Вы можете выбрать использование альтернативного бэкэнда кэша с помощью аргумента using, который должен быть последним аргументом в теге.
{% cache 300 local-thing ... using="localcache" %}
Указание имени несконфигурированного кэша является ошибкой.
- django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None)¶
Чтобы получить ключ кэша для кэшированного фрагменты шаблона, можно использовать функцию make_template_fragment_key. fragment_name – второй аргумент, передаваемый в тег cache, vary_on – список дополнительных аргументов, передаваемых в тег. Эту функцию можно использовать для переназначения или удаления кэшированных фрагментов шаблона:
>>> from django.core.cache import cache
>>> from django.core.cache.utils import make_template_fragment_key
# cache key for {% cache 500 sidebar username %}
>>> key = make_template_fragment_key("sidebar", [username])
>>> cache.delete(key) # invalidates cached template fragment
True
API низкого уровня для кэширования¶
Временами кэширование всей созданной страницы не даёт нужного результата и обычно является неудобным излишеством.
Допустим, ваш сайт содержит представление, итог работы которого зависит от нескольких ресурсоёмких запросов, результаты которых периодически изменяются. В этом случае, кэширование всей страницы будет только вредить, но вот кэширование редко изменяемых фрагментов было бы хорошим подходом.
Для подобных случаев Django предоставляет API низкоуровневого кэша. Вы можете использовать этот API для хранения объектов в кеше с любым уровнем детализации, который вам нравится. Вы можете кэшировать любой объект Python, который можно безопасно консервировать: строки, словари, списки объектов модели и т. д. (Большинство распространенных объектов Python можно консервировать; дополнительную информацию о травлении см. в документации Python.)
Доступ к кэшу¶
- django.core.cache.caches¶
Вы можете иметь доступ к кэшам, определённым в параметре конфигурации
CACHES, через словарно-подобный объектdjango.core.cache.caches. Повторяющиеся запросы по одинаковому псевдониму в одном и том же потоке будут возвращать одинаковый результат.>>> from django.core.cache import caches >>> cache1 = caches["myalias"] >>> cache2 = caches["myalias"] >>> cache1 is cache2 True
Если указанного именованного ключа не существует, то будет вызвано исключение
InvalidCacheBackendError.Для обеспечения потоковой независимости, каждый поток будет получать разные экземпляры бэкэнда кэша.
- django.core.cache.cache¶
В качестве ярлыка кэш по умолчанию доступен как django.core.cache.cache:
>>> from django.core.cache import cache
Этот объект эквивалентен
caches['default'].
Использование¶
Базовый интерфейс:
- cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)¶
>>> cache.set("my_key", "hello, world!", 30)
- cache.get(key, default=None, version=None)¶
>>> cache.get("my_key")
'hello, world!'
key должен быть str, а value может быть любым поддающимся выбору объектом Python.
Аргумент timeout является необязательным и обычно равен аргументу timeout соответствующего бэкенда кэша из параметре конфигурации CACHES (читайте выше). Аргумент определяет период в секундах, в течение которого значение должно храниться в кэше. Передав None в timeout можно закэшировать данные навсегда. При timeout равном 0 значение никогда не будет кэшироваться.
Если объект не существует в кеше, cache.get() возвращает None:
>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key")
None
Если вам нужно определить, существует ли объект в кеше, и вы сохранили буквальное значение None, используйте дозорный объект по умолчанию:
>>> sentinel = object()
>>> cache.get("my_key", sentinel) is sentinel
False
>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key", sentinel) is sentinel
True
cache.get() может принимать аргумент default. Это указывает, какое значение возвращать, если объект не существует в кеше:
>>> cache.get("my_key", "has expired")
'has expired'
- cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)¶
Чтобы добавить ключ, только если он еще не существует, используйте метод add(). Он принимает те же параметры, что и set(), но не будет пытаться обновить кеш, если указанный ключ уже присутствует:
>>> cache.set("add_key", "Initial value")
>>> cache.add("add_key", "New value")
>>> cache.get("add_key")
'Initial value'
Если вам надо определить сохранил ли метод add() значение в кэше, вы можете проверить значение, которое он возвращает. True указывает, что значение было сохранено, а False – наоборот.
- cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)¶
Если вы хотите получить значение ключа или установить значение, если ключа нет в кеше, существует метод get_or_set(). Он принимает те же параметры, что и get(), но по умолчанию устанавливается новое значение кэша для этого ключа, а не возвращается:
>>> cache.get("my_new_key") # returns None
>>> cache.get_or_set("my_new_key", "my new value", 100)
'my new value'
Вы также можете передать любой вызываемый объект в качестве значения по умолчанию:
>>> import datetime
>>> cache.get_or_set("some-timestamp-key", datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)
- cache.get_many(keys, version=None)¶
Также существует интерфейс get_many(), который обращается к кешу только один раз. get_many() возвращает словарь со всеми запрошенными вами ключами, которые действительно существуют в кеше (и срок действия которых не истек):
>>> cache.set("a", 1)
>>> cache.set("b", 2)
>>> cache.set("c", 3)
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}
- cache.set_many(dict, timeout)¶
Чтобы более эффективно устанавливать несколько значений, используйте set_many() для передачи словаря пар ключ-значение:
>>> cache.set_many({"a": 1, "b": 2, "c": 3})
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}
Аналогично методу cache.set(), метод set_many() принимает необязательный параметр timeout.
На поддерживаемых бэкэндах (memcached) set_many() возвращает список ключей, которые не удалось вставить.
- cache.delete(key, version=None)¶
Вы можете удалить ключи явно с помощью delete(), чтобы очистить кеш для определенного объекта:
>>> cache.delete("a")
True
delete() возвращает True, если ключ был успешно удален, False в противном случае.
- cache.delete_many(keys, version=None)¶
Если вы хотите очистить сразу несколько ключей, delete_many() может взять список ключей, которые нужно очистить:
>>> cache.delete_many(["a", "b", "c"])
- cache.clear()¶
Наконец, если вы хотите удалить все ключи в кеше, используйте cache.clear(). Будьте осторожны с этим; clear() удалит из кэша все, а не только ключи, установленные вашим приложением:
>>> cache.clear()
- cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)¶
cache.touch() устанавливает новый срок действия ключа. Например, чтобы обновить ключ, срок действия которого истекает через 10 секунд:
>>> cache.touch("a", 10)
True
Как и другие методы, аргумент timeout является необязательным и по умолчанию равен опции TIMEOUT соответствующего бэкэнда в настройке CACHES.
touch() возвращает True, если клавиша была успешно нажата, False в противном случае.
- cache.incr(key, delta=1, version=None)¶
- cache.decr(key, delta=1, version=None)¶
Вы также можете увеличить или уменьшить уже существующий ключ, используя методы incr() или decr() соответственно. По умолчанию существующее значение кэша будет увеличено или уменьшено на 1. Другие значения увеличения/уменьшения можно указать, указав аргумент для вызова увеличения/уменьшения. Ошибка ValueError будет выдана, если вы попытаетесь увеличить или уменьшить несуществующий ключ кэша:
>>> cache.set("num", 1)
>>> cache.incr("num")
2
>>> cache.incr("num", 10)
12
>>> cache.decr("num")
11
>>> cache.decr("num", 5)
6
Примечание
Методы incr() и decr() не гарантируют атомарность операции. Тут всё зависит от бэкенда. К примеру, Memcached гарантирует атомарность этих операций. Другие бэкенды делают это через двойную операцию считывания и сохранения нового значения.
- cache.close()¶
Можно закрыть подключение к кэшу методом close(), если бэкенд кэширования предоставляет его.
>>> cache.close()
Примечание
Если кэш не реализует метод close, он будет пустым.
Примечание
Асинхронные варианты базовых методов имеют префикс a, например: cache.aadd() или cache.adelete_many(). Подробнее см. Асинхронная поддержка.
Прификсы ключей кэша¶
Если ваш кэш разделяется между несколькими серверами или между окружениями разработки и продакшена, возможна ситуация, когда данные, закэшированные одним сервером, будут использованы другим сервером. Если формат закэшированных данных различен между серверами, то это может привести к трудно выявляемым проблемам.
Чтобы избежать такой ситуации, Django предоставляет возможность назначения префиксов для всех ключей кэша, используемых сервером. При работе с ключом Django автоматически добавляет к нему префикс, определённый параметром конфигурации KEY_PREFIX.
Назначив различные префиксы для каждого экземпляра Django с помощью параметра конфигурации KEY_PREFIX, вы гарантируете отсутствие коллизий в значениях кэша.
Версионирование кэша¶
При изменении работающего кода, который использует закэшированные значения, может потребоваться сброс всех закэшированных данных. Простым способом является отчистка всего кэша, но это может привести к потере закэшированных данных, которые всё ещё верны и полезны.
Django предоставляет отличный способ выделения отдельных значений в кэше. Система кэширования Django обладает глобальным идентификатором версии, определённым в параметре кэша VERSION. Значение этого параметра автоматически объединяется с префиксом кэша и пользователем, которые предоставил ключ, получая итоговый ключ для обращения к кэшу.
По умолчанию любой запрос ключа автоматически включает версию ключа кэша сайта по умолчанию. Однако все примитивные функции кэша включают аргумент «версия», поэтому вы можете указать конкретную версию ключа кэша для установки или получения. Например:
>>> # Set version 2 of a cache key
>>> cache.set("my_key", "hello world!", version=2)
>>> # Get the default version (assuming version=1)
>>> cache.get("my_key")
None
>>> # Get version 2 of the same key
>>> cache.get("my_key", version=2)
'hello world!'
Версию определенного ключа можно увеличивать и уменьшать с помощью методов incr_version() и decr_version(). Это позволяет преобразовать определенные ключи в новую версию, не затрагивая другие ключи. Продолжая наш предыдущий пример:
>>> # Increment the version of 'my_key'
>>> cache.incr_version("my_key")
>>> # The default version still isn't available
>>> cache.get("my_key")
None
# Version 2 isn't available, either
>>> cache.get("my_key", version=2)
None
>>> # But version 3 *is* available
>>> cache.get("my_key", version=3)
'hello world!'
Преобразование ключа кэша¶
Как описано в предыдущих двух разделах, система кэширования никогда не использует напрямую ключ, предоставленный пользователем. Ключ всегда комбинируется с префиксом кэша и версией ключа. По-умолчанию, эти три части объединяются через двоеточия, создавая итоговый ключ:
def make_key(key, key_prefix, version):
return "%s:%s:%s" % (key_prefix, version, key)
Если вам потребуется объединить эти части другим способом или создать ключ другим способом (т.е. приняв хеш ключа за сам ключ), вы можете указать функцию, которая будет выполнять эту работу.
Параметр кэша KEY_FUNCTION определяет путь в точечной нотации к функции, соответствующей прототипу make_key() из предыдущего примера. При наличии такой функции она будет использоваться вместо стандартной функции построения итогового ключа.
Предупреждения о проблемах с ключами¶
Memcached, наиболее используемый в продакшене бэкенд кэширования, не поддерживает ключи длиной более 250 символов или содержащих пробелы или управляющие символы. Использование таких ключей приводит к вызову исключения. Для обеспечения уверенности в портируемости кода между кэширующими бэкендами и для уменьшения количества неприятных сюрпризов, другие встроенные кэширующие бэкенды выбрасывают предупреждение (django.core.cache.backends.base.CacheKeyWarning), если используемый ключ может привести к ошибке при работе с Memcached.
Если вы используете на продакшене кэширующий бэкенд, который может принимать широкий диапазон ключей (собственный бэкенд или один из встроенных бэкендов, кроме Memcached), и не желаете видеть подобные предупреждения, вы можете заглушить CacheKeyWarning с помощью следующего кода в модуле management одного из ваших приложений:
import warnings
from django.core.cache import CacheKeyWarning
warnings.simplefilter("ignore", CacheKeyWarning)
Если вам требуется изменить логику проверки ключа для одного из встроенных бэкендов кэширования, вы можете унаследовать свой класс от него и переопределить метод validate_key. Затем следуйте инструкциям по использованию собственного бэкенда кэширования. Например, для реализации подобного для бэкенда locmem, поместите следующий код в модуль:
from django.core.cache.backends.locmem import LocMemCache
class CustomLocMemCache(LocMemCache):
def validate_key(self, key):
"""Custom validation, raising exceptions or warnings as needed."""
...
…и используйте путь в точечной нотации к этому классу в ключе BACKEND параметра конфигурации CACHES.
Асинхронная поддержка¶
Django разрабатывает поддержку асинхронного кэширования, но пока не поддерживает асинхронное кэширование. Это появится в будущем выпуске.
django.core.cache.backends.base.BaseCache имеет асинхронные варианты всех базовых методов. По соглашению асинхронные версии всех методов имеют префикс a. По умолчанию аргументы для обоих вариантов одинаковы:
>>> await cache.aset("num", 1)
>>> await cache.ahas_key("num")
True
«Даунстрим» кэши¶
До сих пор этот документ был посвящен кэшированию ваших собственных данных. Но для веб-разработки актуален и другой тип кэширования: кэширование, выполняемое «нисходящими» кэшами. Это системы, которые кэшируют страницы пользователей еще до того, как запрос достигнет вашего сайта.
Рассмотрим несколько примеров таких «даунстрим» кэшей:
При использовании HTTP ваш ISP может кэшировать определенные страницы, поэтому, если вы запросили страницу с
http://example.com/, ваш интернет-провайдер отправит вам эту страницу без необходимости прямого доступа к example.com. Сопровождающие сайта example.com ничего не знают об этом кэшировании; Интернет-провайдер находится между example.com и вашим веб-браузером, прозрачно обрабатывая все кэширование. Такое кэширование невозможно в рамках HTTPS, поскольку оно представляет собой атаку «человек посередине».Ваше Django приложение может находится за прокси-кэшем, таким как Squid Web Proxy Cache (http://www.squid-cache.org/), который кэширует страницы для обеспечения производительности. В таком случае, каждый запрос сначала будет обработан прокси, а затем при необходимости будет передан вашему приложению.
Ваш веб-браузер тоже кэширует страницы. Если веб-страница отправляет соответствующие заголовки, ваш браузер будет использовать локальную кэшированную копию для последующих запросов к этой странице, даже не обращаясь к веб-странице повторно, чтобы узнать, изменилась ли она.
Последующее кэширование является хорошим повышением эффективности, но оно таит в себе опасность: содержимое многих веб-страниц различается в зависимости от аутентификации и множества других переменных, а системы кэширования, которые слепо сохраняют страницы исключительно на основе URL-адресов, могут предоставлять неверные или конфиденциальные данные последующим посетителям этих страниц.
Например, если вы используете систему веб-почты, то содержимое страницы «Входящие» зависит от того, какой пользователь вошел в систему. Если интернет-провайдер слепо кэшировал ваш сайт, то первый пользователь, вошедший в систему через этого интернет-провайдера, будет кэшировать свою страницу входящих сообщений для последующих посетителей сайта. Это не круто.
К счастью, протокол HTTP предоставляет решение для этой проблемы. Существует ряд HTTP заголовков, которые инструктируют «даунстрим» кэши как именно следует различать страницы в зависимости от определённых переменных, также указывая что некоторые страницы кэшировать вообще не следует. Мы рассмотрим некоторые из этих заголовков в следующих секциях.
Использование заголовков Vary¶
Заголовок Vary определяет, какие заголовки запросов механизм кэширования должен учитывать при построении своего ключа кэша. Например, если содержимое веб-страницы зависит от языковых предпочтений пользователя, говорят, что страница «зависит от языка».
По-умолчанию, кэширующая система Django создаёт свои ключи, используя запрошенный путь и запрос, т.е., "https://www.example.com/stories/2005/?order_by=author". Это означает, что каждый запрос по этому URL-у будет использовать одну закэшированную версию, независимо от различия в куках или языковых настройках. Однако, если эта страница создаёт своё содержимое, основываясь на разнице в заголовках запроса, таких как куки или язык или тип браузера, вам следует использовать заголовок Vary, чтобы указать кэширующему механизму, что вывод данной страница зависит от этих параметров.
Чтобы сделать это в Django следует использовать декоратор представления django.views.decorators.vary.vary_on_headers(), так:
from django.views.decorators.vary import vary_on_headers
@vary_on_headers("User-Agent")
def my_view(request): ...
В этом случае кэширующий механизм (такой как кэширующий мидлварь Django) закэширует отдельную версию этой страницы для каждого уникального типа браузера.
Преимущество использования декоратора vary_on_headers вместо ручной установки заголовка Vary (используя что-то вроде response.headers['Vary'] = 'user-agent') заключается в том, что декоратор добавляет к заголовку Vary (который может уже существовать), а не устанавливает его с нуля и потенциально переопределяет все, что там уже было.
Вы можете передавать несколько заголовков в vary_on_headers():
@vary_on_headers("User-Agent", "Cookie")
def my_view(request): ...
Это указывает «даунстрим» кэшам, что надо обращать внимание на оба, т.е. любая комбинация типа браузера и куки будет кэшироваться отдельно. Например, запрос от Mozilla с кукой foo=bar будет рассматриваться как отличный от запроса от Mozilla с кукой foo=ham.
Так как зависимость от куки является настолько стандартной, что существует декоратор django.views.decorators.vary.vary_on_cookie(). Следующие два представления эквивалентны:
@vary_on_cookie
def my_view(request): ...
@vary_on_headers("Cookie")
def my_view(request): ...
Заголовки, которые вы передаёте в vary_on_headers нечувствительны к регистру, т.е. "User-Agent" аналогичен "user-agent".
Также вы можете использовать вспомогательную функцию django.utils.cache.patch_vary_headers() напрямую. Эта функция устанавливает или добавляет заголовок Vary. Например:
from django.shortcuts import render
from django.utils.cache import patch_vary_headers
def my_view(request):
...
response = render(request, "template_name", context)
patch_vary_headers(response, ["Cookie"])
return response
Функция patch_vary_headers принимает экземпляр HttpResponse в качестве первого аргумента и список/кортеж имён заголовков в качестве второго аргумента.
Дополнительную информацию о заголовках Vary см. в официальной спецификации Vary.
Управление кэшированием: Использование других заголовков¶
Другими проблемами с кэшированием являются приватность данных и вопрос от том, где следует выполнять кэширование в каскаде кэшей.
Пользователь обычно сталкивается с двумя типами кешей: собственным кешем браузера (частный кеш) и кешем своего провайдера (публичный кеш). Публичный кеш используется несколькими пользователями и контролируется кем-то еще. Это создает проблемы с конфиденциальными данными — вы не хотите, чтобы, скажем, номер вашего банковского счета хранился в общедоступном кеше. Поэтому веб-приложениям нужен способ сообщать кэшам, какие данные являются частными, а какие общедоступными.
Решением является указание того, что страница должна кэшироваться в личном кэше. Для этого в Django следует использовать декоратор представления cache_control. Например:
from django.views.decorators.cache import cache_control
@cache_control(private=True)
def my_view(request): ...
Этот декоратор обеспечивает автоматическую отправку соответствующих HTTP заголовков.
Следует отметить, что настройки открытости и закрытости взаимоисключающие. Декоратор обеспечивает эту проверку. Примером использования таких настроек будет блог, который содержит закрытые и открытые записи. Открытые записи могут кэшироваться везде. Следующий код использует django.utils.cache.patch_cache_control(), ручной способ изменения управляющего заголовка (обычно эта функция вызывается в декораторе cache_control):
from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_cookie
@vary_on_cookie
def list_blog_entries_view(request):
if request.user.is_anonymous:
response = render_only_public_entries()
patch_cache_control(response, public=True)
else:
response = render_private_and_public_entries(request.user)
patch_cache_control(response, private=True)
return response
Вы можете управлять нисходящими кэшами и другими способами (подробнее о HTTP-кэшировании см. RFC 9111). Например, даже если вы не используете структуру кэширования на стороне сервера Django, вы все равно можете указать клиентам кэшировать представление на определенный период времени с помощью директивы max-age:
from django.views.decorators.cache import cache_control
@cache_control(max_age=3600)
def my_view(request): ...
(Следует отметить, что кэширующая мидлварь уже устанавливает заголовок max-age равным значению параметра конфигурации CACHE_MIDDLEWARE_SECONDS. Если вы используете определённый max-age в декораторе cache_control, декоратор применит его, правильно объединив значения заголовка.)
Любая верная HTTP директива Cache-Control верна в cache_control(). Приведём полный список:
no_transform=Truemust_revalidate=Trues_maxage=num_secondsno_cache=True
Полный список известных директив можно найти в реестре IANA (обратите внимание, что не все из них применимы к ответам).
Если потребуется использовать заголовки, которые полностью отключают кэширование, то воспользуйтесь декоратором django.views.decorators.cache.never_cache(). Например:
from django.views.decorators.cache import never_cache
@never_cache
def myview(request): ...
Порядок записей в MIDDLEWARE_CLASSES¶
Если вы используете кэширующую мидлварь, важно правильно её разместить в параметре конфигурации MIDDLEWARE_CLASSES. Так происходит из-за того, что кэширующая мидлварь должна знать какие заголовки как влияют на кэширующие системы. Мидлварь всегда добавляет что-то в заголовок Vary, когда может.
UpdateCacheMiddleware работает во время выдачи отклика, там где мидлвари применяются с конца списка, т.е. элемент наверху списка выполняется последним. Таким образом, надо удостовериться, что UpdateCacheMiddleware указан до любых мидлварей, которые могут добавить что-то в заголовок Vary. Следующие модули как раз такие:
SessionMiddlewareдобавляетCookieGZipMiddlewareдобавляетAccept-EncodingLocaleMiddlewareдобавляетAccept-Language
FetchFromCacheMiddleware, с другой стороны, выполняется во время запроса, когда мидлвари применяются от начала списка, т.е. элемент наверху списка выполняется первым. FetchFromCacheMiddleware также должен выполняться после других мидлварей, которые изменяют заголовок Vary.