Кэширование в Django

Кэширование, как правило, является наиболее эффективным способом повышения производительности приложения.

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

Например, вам нужно сделать HTTP-вызов к внешнему API, чтобы получить данные, необходимые для рендеринга шаблона. Даже в идеальных условиях это увеличит время рендеринга, что приведет к увеличению общего времени загрузки. А что, если API не работает или на него наложено ограничение по скорости? В любом случае, если данные обновляются нечасто, стоит реализовать механизм кэширования, чтобы избежать необходимости выполнять HTTP-вызов для каждого клиентского запроса.

В этой статье мы рассмотрим, как это сделать, сначала рассмотрим фреймворк кэширования Django в целом, а затем подробно, шаг за шагом, объясним, как кэшировать представление Django.

Зависимости:

  1. Django v3.2.5
  2. django-redis v5.0.0
  3. Python v3.9.6
  4. pymemcache v3.5.0
  5. Requests v2.26.0

Статьи о кэшировании в Django:

  1. Кэширование в Django (эта статья)
  2. Низкоуровневый API кэша в Django

Цели

К концу этого урока вы должны уметь:

  1. Объясните, почему вы можете захотеть рассмотреть возможность кэширования представлений Django
  2. Опишите встроенные опции Django для кэширования
  3. Кэширование представления Django с помощью Redis
  4. Загрузочное тестирование приложения Django с помощью Apache Bench
  5. Кэширование представления Django с помощью Memcached

Типы кэширования в Django

Django поставляется с несколькими встроенными бэкендами кэширования, а также с поддержкой пользовательских бэкендов.

Встроенные опции:

  1. Memcached: Memcached - это основанное на памяти хранилище ключевых значений для небольших фрагментов данных. Оно поддерживает распределенное кэширование на нескольких серверах.
  2. Database: Здесь фрагменты кэша хранятся в базе данных. Таблицу для этой цели можно создать с помощью одной из команд администрирования Django. Это не самый производительный тип кэширования, но он может быть полезен для хранения сложных запросов к базе данных.
  3. Файловая система: Кэш сохраняется в файловой системе, в отдельных файлах для каждого значения кэша. Это самый медленный из всех типов кэширования, но его проще всего настроить в производственной среде.
  4. Локальная память: Кэш локальной памяти, который лучше всего подходит для локальной среды разработки или тестирования. Хотя он почти так же быстр, как Memcached, он не может масштабироваться дальше одного сервера, поэтому его не стоит использовать в качестве кэша данных для приложений, использующих более одного веб-сервера.
  5. Dummy: "Фиктивный" кэш, который на самом деле ничего не кэширует, но при этом реализует интерфейс кэша. Он предназначен для использования при разработке или тестировании, когда вам не нужно кэширование, но вы не хотите менять свой код.

Уровни кэширования Django

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

Кэш всего сайта

Это самый простой способ реализовать кэширование в Django. Для этого достаточно добавить два класса промежуточного ПО в файл settings.py:

MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',     # NEW
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',  # NEW
]

Здесь важен порядок расположения промежуточного ПО. UpdateCacheMiddleware должен быть раньше FetchFromCacheMiddleware. Для получения дополнительной информации посмотрите Order of MIDDLEWARE из документации Django.

Затем вам нужно добавить следующие настройки:

CACHE_MIDDLEWARE_ALIAS = 'default'  # which cache alias to use
CACHE_MIDDLEWARE_SECONDS = '600'    # number of seconds to cache a page for (TTL)
CACHE_MIDDLEWARE_KEY_PREFIX = ''    # should be used if the cache is shared across multiple sites that use the same Django instance

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

Кэш для вью

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

Вы можете реализовать этот тип кэша с помощью декоратора cache_page либо непосредственно на функции представления, либо в пути внутри URLConf:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def your_view(request):
    ...

# or

from django.views.decorators.cache import cache_page

urlpatterns = [
    path('object/<int:object_id>/', cache_page(60 * 15)(your_view)),
]

Сам кэш основан на URL, поэтому запросы, скажем, к object/1 и object/2 будут кэшироваться отдельно.

Стоит отметить, что реализация кэша непосредственно в представлении усложняет отключение кэша в некоторых ситуациях. Например, что если вы хотите разрешить определенным пользователям доступ к представлению без кэша? Включение кэша через URLConf дает возможность привязать к представлению другой URL, который не использует кэш:

from django.views.decorators.cache import cache_page

urlpatterns = [
    path('object/<int:object_id>/', your_view),
    path('object/cache/<int:object_id>/', cache_page(60 * 15)(your_view)),
]

Кэш фрагментов шаблонов

Если ваши шаблоны содержат части, которые часто меняются в зависимости от данных, вы, вероятно, захотите оставить их вне кэша.

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

Для кэширования списка объектов:

{% load cache %}

{% cache 500 object_list %}
  <ul>
    {% for object in objects %}
      <li>{{ object.title }}</li>
    {% endfor %}
  </ul>
{% endcache %}

Здесь {% load cache %} дает нам доступ к тегу шаблона cache, который ожидает таймаут кэша в секундах (500) вместе с именем фрагмента кэша (object_list).

API кэша низкого уровня

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

Например:

from django.core.cache import cache


def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)

    objects = cache.get('objects')

    if objects is None:
        objects = Objects.all()
        cache.set('objects', objects)

    context['objects'] = objects

    return context

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

from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver


@receiver(post_delete, sender=Object)
def object_post_delete_handler(sender, **kwargs):
     cache.delete('objects')


@receiver(post_save, sender=Object)
def object_post_save_handler(sender, **kwargs):
    cache.delete('objects')

Подробнее об использовании сигналов базы данных для аннулирования кэша читайте в статье Low-Level Cache API in Django.

Давайте рассмотрим несколько примеров.

Настройка проекта

Склонируйте базовый проект из репозитория cache-django-view, а затем проверьте базовую ветку:

$ git clone https://github.com/testdrivenio/cache-django-view.git --branch base --single-branch
$ cd cache-django-view

Создайте (и активируйте) виртуальную среду и установите требования:

$ python3.9 -m venv venv
$ source venv/bin/activate
(venv)$ pip install -r requirements.txt

Примените миграции Django, а затем запустите сервер:

(venv)$ python manage.py migrate
(venv)$ python manage.py runserver

Перейдите по адресу http://127.0.0.1:8000 в выбранном вами браузере, чтобы убедиться, что все работает так, как ожидалось.

Вы должны увидеть:

uncached webpage

Обратите внимание на свой терминал. Вы должны увидеть общее время выполнения запроса:

Total time: 2.23s

Эта метрика взята из core/middleware.py:

import logging
import time


def metric_middleware(get_response):
    def middleware(request):
        # Get beginning stats
        start_time = time.perf_counter()

        # Process the request
        response = get_response(request)

        # Get ending stats
        end_time = time.perf_counter()

        # Calculate stats
        total_time = end_time - start_time

        # Log the results
        logger = logging.getLogger('debug')
        logger.info(f'Total time: {(total_time):.2f}s')
        print(f'Total time: {(total_time):.2f}s')

        return response

    return middleware

Посмотрите на представление в apicalls/views.py:

import datetime

import requests
from django.views.generic import TemplateView


BASE_URL = 'https://httpbin.org/'


class ApiCalls(TemplateView):
    template_name = 'apicalls/home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        response = requests.get(f'{BASE_URL}/delay/2')
        response.raise_for_status()
        context['content'] = 'Results received!'
        context['current_time'] = datetime.datetime.now()
        return context

Это представление выполняет HTTP-вызов с помощью requests к httpbin.org. Чтобы имитировать длинный запрос, ответ от API задерживается на две секунды. Таким образом, на отрисовку http://127.0.0.1:8000 должно уйти около двух секунд не только при первом, но и при каждом последующем запросе. Если для первого запроса двухсекундная загрузка является вполне приемлемой, то для последующих запросов она совершенно неприемлема, поскольку данные не меняются. Давайте исправим это, кэшируя все представление с помощью уровня кэша Per-view в Django.

Рабочий процесс:

  1. Выполните полный HTTP-вызов на httpbin.org при первом запросе
  2. Кэшировать представление
  3. Последующие запросы будут браться из кэша, минуя HTTP-вызов
  4. Аннулировать кэш по истечении определенного периода времени (TTL)

Базовый бенчмарк производительности

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

Apache Bench поставляется предустановленным на Mac.

Если вы работаете в системе Linux, скорее всего, он уже установлен и готов к работе. Если нет, можно установить через APT (apt-get install apache2-utils) или YUM (yum install httpd-tools).

Пользователям Windows потребуется скачать и извлечь двоичные файлы Apache.

Добавьте Gunicorn в файл требований:

gunicorn==20.1.0

Убейте сервер Django dev и установите Gunicorn:

(venv)$ pip install -r requirements.txt

Далее, запустите приложение Django с Gunicorn (и четырьмя рабочими) следующим образом:

(venv)$ gunicorn core.wsgi:application -w 4

В новом окне терминала запустите программу Apache Bench:

$ ab -n 100 -c 10 http://127.0.0.1:8000/

Это позволит смоделировать 100 соединений в 10 одновременных потоках. Это 100 запросов, по 10 за раз.

Обратите внимание на количество запросов в секунду:

Requests per second:    1.69 [#/sec] (mean)

Имейте в виду, что Django Debug Toolbar добавит немного накладных расходов. Бенчмаркинг вообще сложно сделать идеально правильным. Главное - последовательность. Выберите метрику, на которой вы сосредоточитесь, и используйте одно и то же окружение для каждого теста.

Убейте сервер Gunicorn и восстановите сервер Django dev:

(venv)$ python manage.py runserver

В связи с этим давайте рассмотрим, как кэшировать представление.

Кэширование представления

Начните с декорирования представления ApiCalls декоратором @cache_page следующим образом:

import datetime

import requests
from django.utils.decorators import method_decorator # NEW
from django.views.decorators.cache import cache_page # NEW
from django.views.generic import TemplateView


BASE_URL = 'https://httpbin.org/'


@method_decorator(cache_page(60 * 5), name='dispatch') # NEW
class ApiCalls(TemplateView):
    template_name = 'apicalls/home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        response = requests.get(f'{BASE_URL}/delay/2')
        response.raise_for_status()
        context['content'] = 'Results received!'
        context['current_time'] = datetime.datetime.now()
        return context

Поскольку мы используем представление на основе класса, мы не можем поместить декоратор непосредственно на класс, поэтому мы использовали method_decorator и указали dispatch (как метод, который должен быть декорирован) в качестве аргумента name.

В этом примере для кэша установлен тайм-аут (или TTL) в пять минут.

Альтернативно, вы можете установить это в настройках следующим образом:

# Cache time to live is 5 minutes
CACHE_TTL = 60 * 5

Затем, возвращаясь к просмотру:

import datetime

import requests
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import TemplateView

BASE_URL = 'https://httpbin.org/'
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)


@method_decorator(cache_page(CACHE_TTL), name='dispatch')
class ApiCalls(TemplateView):
    template_name = 'apicalls/home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        response = requests.get(f'{BASE_URL}/delay/2')
        response.raise_for_status()
        context['content'] = 'Results received!'
        context['current_time'] = datetime.datetime.now()
        return context

Далее добавим бэкэнд кэша.

Redis vs Memcached

Memcached и Redis - это хранилища данных in-memory, содержащие ключевые значения. Они просты в использовании и оптимизированы для высокопроизводительного поиска. Скорее всего, вы не увидите особой разницы в производительности или использовании памяти между ними. При этом Memcached немного проще настроить, поскольку он создан для простоты и удобства использования. Redis, с другой стороны, обладает более богатым набором функций, поэтому у него есть широкий спектр применений помимо кэширования. Например, его часто используют для хранения пользовательских сессий или в качестве брокера сообщений в системе pub/sub. Благодаря своей гибкости, если только вы уже не инвестировали в Memcached, Redis будет гораздо лучшим решением.

Чтобы узнать больше об этом, просмотрите этот ответ на Stack Overflow.

Далее выберите нужное вам хранилище данных и рассмотрим, как кэшировать представление.

Вариант 1: Redis с Django

Скачайте и установите Redis.

Если вы работаете на Mac, мы рекомендуем установить Redis с помощью Homebrew:

$ brew install redis

После установки в новом окне терминала запустите сервер Redis и убедитесь, что он работает на порту по умолчанию, 6379. Номер порта будет важен, когда мы будем указывать Django, как взаимодействовать с Redis.

$ redis-server

Для того чтобы Django использовал Redis в качестве бэкенда кэша, нам сначала нужно установить django-redis.

Добавьте его в файл requirements.txt:

django-redis==5.0.0

Установить:

(venv)$ pip install -r requirements.txt

Далее добавьте пользовательский бэкэнд в файл settings.py:

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

Теперь, когда вы снова запустите сервер, Redis будет использоваться в качестве бэкэнда кэша:

(venv)$ python manage.py runserver

При запущенном сервере перейдите по адресу http://127.0.0.1:8000.

Первый запрос все равно займет около двух секунд. Обновите страницу. Страница должна загрузиться почти мгновенно. Посмотрите на время загрузки в терминале. Оно должно быть близко к нулю:

Total time: 0.01s

Любопытно, как выглядят кэшированные данные внутри Redis?

Запустите Redis CLI в интерактивном режиме в новом окне терминала:

$ redis-cli

Вы должны увидеть:

127.0.0.1:6379>

Запустите ping, чтобы убедиться, что все работает правильно:

127.0.0.1:6379> ping
PONG

Вернитесь к файлу настроек. Мы использовали базу данных Redis номер 1: 'LOCATION': 'redis://127.0.0.1:6379/1',. Поэтому выполните select 1, чтобы выбрать эту базу данных, а затем выполните keys *, чтобы просмотреть все ключи:

127.0.0.1:6379> select 1
OK

127.0.0.1:6379[1]> keys *
1) ":1:views.decorators.cache.cache_header..17abf5259517d604cc9599a00b7385d6.en-us.UTC"
2) ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"

Мы видим, что Django поместил один ключ заголовка и один ключ cache_page.

Чтобы просмотреть фактические кэшированные данные, выполните команду get с ключом в качестве аргумента:

127.0.0.1:6379[1]> get ":1:views.decorators.cache.cache_page..GET.17abf5259517d604cc9599a00b7385d6.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC"

Вы должны увидеть нечто подобное:

"\x80\x05\x95D\x04\x00\x00\x00\x00\x00\x00\x8c\x18django.template.response\x94\x8c\x10TemplateResponse
\x94\x93\x94)\x81\x94}\x94(\x8c\x05using\x94N\x8c\b_headers\x94}\x94(\x8c\x0ccontent-type\x94\x8c\
x0cContent-Type\x94\x8c\x18text/html; charset=utf-8\x94\x86\x94\x8c\aexpires\x94\x8c\aExpires\x94\x8c\x1d
Fri, 01 May 2020 13:36:59 GMT\x94\x86\x94\x8c\rcache-control\x94\x8c\rCache-Control\x94\x8c\x0
bmax-age=300\x94\x86\x94u\x8c\x11_resource_closers\x94]\x94\x8c\x0e_handler_class\x94N\x8c\acookies
\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94)\x81\x94\x8c\x06closed\x94\x89\x8c
\x0e_reason_phrase\x94N\x8c\b_charset\x94N\x8c\n_container\x94]\x94B\xaf\x02\x00\x00
<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Home</title>\n
<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\
"\n          integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\"
crossorigin=\"anonymous\">\n\n</head>\n<body>\n<div class=\"container\">\n    <div class=\"pt-3\">\n
<h1>Below is the result of the APICall</h1>\n    </div>\n    <div class=\"pt-3 pb-3\">\n
<a href=\"/\">\n            <button type=\"button\" class=\"btn btn-success\">\n
Get new data\n            </button>\n        </a>\n    </div>\n    Results received!<br>\n
13:31:59\n</div>\n</body>\n</html>\x94a\x8c\x0c_is_rendered\x94\x88ub."

Выйдите из интерактивного CLI после завершения работы:

127.0.0.1:6379[1]> exit

Перейдите к разделу "Тесты производительности".

Вариант 2: Memcached с Django

Начните с добавления pymemcache в файл requirements.txt:

pymemcache==3.5.0

Установите зависимости:

(venv)$ pip install -r requirements.txt

Далее нам нужно обновить настройки в core/settings.py, чтобы включить бэкенд Memcached:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

Здесь мы добавили бэкенд PyMemcacheCache и указали, что Memcached должен быть запущен на нашей локальной машине на localhost (127.0.0.1) на порту 11211, который является портом по умолчанию для Memcached.

Далее нам нужно установить и запустить демон Memcached. Проще всего установить его с помощью менеджера пакетов, например APT, YUM, Homebrew или Chocolatey, в зависимости от вашей операционной системы:

# linux
$ apt-get install memcached
$ yum install memcached

# mac
$ brew install memcached

# windows
$ choco install memcached

Затем запустите его в другом терминале на порту 11211:

$ memcached -p 11211

# test: telnet localhost 11211

Для получения дополнительной информации об установке и настройке Memcached ознакомьтесь с официальной wiki.

Снова перейдите по адресу http://127.0.0.1:8000 в нашем браузере. Первый запрос по-прежнему займет полные две секунды, но все последующие запросы будут использовать преимущества кэша. Таким образом, если вы обновите страницу или нажмете кнопку "Получить новые данные", она должна загрузиться практически мгновенно.

Как выглядит время выполнения в вашем терминале?

Total time: 0.03s

Тесты производительности

Если посмотреть на время загрузки первого запроса по сравнению со вторым (кэшированным) в Django Debug Toolbar, то оно будет выглядеть примерно так:

load time uncached

load time with cache

Также на панели инструментов отладки можно увидеть операции с кэшем:

cache operations

Снова запустите Gunicorn и повторно проведите тесты производительности:

$ ab -n 100 -c 10 http://127.0.0.1:8000/

Каковы новые запросы в секунду? На моей машине около 36!

Заключение

В этой статье мы рассмотрели различные встроенные опции кэширования в Django, а также различные уровни доступного кэширования. Мы также подробно рассказали о том, как кэшировать представление с помощью Per-view кэша Django с использованием Memcached и Redis.

Окончательный код для обоих вариантов, Memcached и Redis, вы можете найти в репо cache-django-view.

В общем, вы захотите обратиться к кэшированию, если рендеринг страниц происходит медленно из-за сетевых задержек при запросах к базе данных или HTTP-вызовов.

Отсюда следует, что настоятельно рекомендуется использовать пользовательский бэкенд кэша Django с Redis с типом Per-view. Если вам нужна большая детализация и контроль, потому что не все данные в шаблоне одинаковы для всех пользователей или часть данных часто меняется, то перейдите к кэшу фрагментов шаблона или низкоуровневому кэшу API.

Статьи о кэшировании в Django:

  1. Кэширование в Django (эта статья)
  2. Низкоуровневый API кэша в Django
Вернуться на верх