Оптимизация и кеширование в Django

Оптимизация — это настройка проекта, нацеленная на уменьшение нагрузки и ускорение работы. Универсальных критериев нет, они определяются, исходя из специфики сервиса.

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

Мозг человека довольно ленив и стремится заниматься простыми или придуманными задачами вместо того, чтобы решать сложные, скучные, но важные задачи. Разработчики часто пишут: «мы работаем над высоконагруженным проектом», но чаще всего это означает, что команда придумала себе интересную игру в оптимизацию. «Строить архитектуру высоконагруженного проекта»™ интереснее, чем работать над сложной и скучной задачей по превращению проекта в коммерчески успешный сервис. Это распространенное когнитивное искажение, о нём надо знать, чтобы вовремя заметить это за собой. Хочется чувствовать себя причастным к чему-то великому, и рассказывать «мы сделали проект, оптимизированный под 10 миллионов посещений в день» приятнее, чем «мы сделали каталог товаров и пытались прикрутить к нему корзину, но проект не окупился».

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

Чем плоха преждевременная оптимизация? Представьте, что вы проектируете особый гоночный автомобиль, например, для ралли на Луне. У вас есть общее представление о том, какие части механизма будут необходимы для работы, но вы пока не решили, где их разместить и как скомпоновать. И вот кто-то говорит, что при разработке нужно оптимизировать длину проводов, и теперь вы мучаетесь, размещая блоки механизма как можно ближе один к другому, у вас нет свободы перекомпоновать систему, а кресло пилота вам придётся установить задом наперёд: ведь это позволит укоротить кабели. Задача по оптимизации решена, а проект провален.

Стратегии оптимизации

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

Но в реальности не критично, если добавленная на сайт новость появится на главной не сразу, а минуту спустя (даже если в техзадании было сказано «мгновенное отображение новостей»).

Редакция тратит десятки минут на подготовку даже самой «горящей» новости, а пользователь обновляет страницу раз в несколько минут или просматривает ленту событий утром и вечером перед сном; получается — «мгновенное появление новости» не так уж принципиально. Главную страницу можно генерировать не при каждом запросе, а раз в минуту, сохраняя её в памяти и отдавая пользователям сохранённую версию: так проект будет работать быстрее. Заплатить за быстрый отзыв страницы и уменьшение нагрузки на БД минутной отсрочкой публикации новости — достойная цена.

Когда становится понятно, что именно надо оптимизировать — можно применить разные стратегии. И надо понимать, какую цену придётся платить.

  • Сделать так, чтобы медленная работа делалась быстро. В другом месте будет что-то будет работать медленнее.
  • Сделать так, чтобы медленная работа делалась один раз. Потребуется дополнительное место для хранения результатов работы.
  • Сделать так, чтобы медленная работа делалась заранее. Потребуется сложный механизм планирования для запуска подготовки данных, добавятся сложности отладки... и вся эта система может оказаться невостребованной.
  • Сделать так, чтобы медленная работа не делалась вообще, если не нужны результаты. Так работают ORM запросы: они не выполняются до тех пор, пока не потребует обратиться к данным.

Оптимизация работы сайта

В проекте Yatube пока не видно узких мест, потому что у него один-единственный посетитель. Но представим себе, что на сайт обрушилась волна пользователей (совсем скоро так и будет). Первые шаги по оптимизации нашего проекта должны быть такими:

  • Уменьшить количество генераций сложных страниц за счет кеширования.
  • Оптимизировать работу с базой, учитывая особенности проекта.
  • Использовать специальный веб-сервер для раздачи статики и сжатия содержимого страниц.

Использование кеширования

При каждом обращении к серверу любая страница проекта Yatube генерируется с нуля: идут обращения к базе, запрашивается шаблон — и пользователю отдаётся страница. В большинстве случаев цена отрисовки страниц не столь высока, чтобы обращать на неё внимание. Но если посещаемость возрастёт, стоит подумать, как сократить расходы на генерацию HTML.

Представим себе, что к главной странице обращаются 100 раз в секунду, 24 часа в сутки. Арифметика показывает, что за сутки страницу запросят 24*60*60*100 = 8 640 000 раз. Если после всех оптимизаций и применения select_related и prefetch_related для генерации главной страницы будет нужно сделать всего пару десятков запросов к БД, то за сутки к базе будет сделано несколько десятков миллионов однотипных запросов с почти одинаковым результатом.

Если мы будем генерировать главную страницу не при каждом запросе каждого пользователя, а лишь один раз в 15 секунд и станем отдавать сохранённую страницу из памяти, то к базе мы будем обращаться не 100 раз в секунду, а один раз в 15 секунд. Профит: в 1500 раз выгоднее, дёшево и без потерь.

Временное сохранение результатов с целью повторного использования и называется кеширование.

Фреймворк кеширования Django

В Django есть встроенный кеширующий бэкенд locmem.LocMemCache. Он проверяет, закешированы ли запрошенные данные в памяти или специализированной базе данных.

Если кеша нет — бэкенд генерирует запрошенные данные (это может быть результат работы view-функции или даже часть шаблона) и отдаёт их пользователю, и, одновременно, на определённое время сохраняет результат в кеш.

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

Для подключения бэкенда кеширования добавьте в settings.py следующие строки:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    }
} 

Этот бэкенд годится для разработки, а на боевом сервере обычно используют Memcached или Redis.

Теперь для кеширования работы view-функции можно применить специальный декоратор:

from django.views.decorators.cache import cache_page

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

Число в аргументе указывает, сколько секунд надо хранить значение в кеше.

Включить кеширование можно и в шаблоне, при этом можно кешировать не весь шаблон, а только его часть:

{% load cache %}
{% cache 500 sidebar %}
    .. колонка сайта ..
{% endcache %} 

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

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. колонка сайта для залогиненного пользователя ..
{% endcache %} 

Оптимизация базы данных

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

Добавление индексов существенно увеличивает «стоимость» добавления новых записей в базу: после добавления или изменения записей надо будет обновить индекс. Процесс ресурсоёмкий, но это разумная плата за быстродействие системы. Чтение данных в приоритете, ведь посетители чаще читают, чем пишут, и выгоднее редко тратить ресурсы на составление индекса, чем часто — на длительный поиск по БД.

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

Может показаться, что чем больше индексов — тем лучше, но это не так. Лишние индексы в таблице занимают много места на диске. Индексы бывают составными, то есть один индекс может строиться по объединенным данным из нескольких столбцов таблицы, и тогда количество возможных индексов будет равно факториалу количества столбцов, что потребует бесконечного дискового пространства. Кроме того, лишние индексы затормозят работу базы, если планировщик запроса ошибётся в построении плана запроса.

За добавление индекса отвечает аргумент db_index в свойствах модели. Для ForeignKey это поле по умолчанию имеет значение True.

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

Если бы в базе Yatube были миллионы записей, небольшую оптимизацию дало бы добавление индекса к свойству pub_date в модели Post:

class Post(models.Model):
    # ...
    pub_date = models.DateTimeField(
        'Дата публикации',
        auto_now_add=True,
        db_index=True
    )
    # ... 

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



Вы можете оставить комментарий, или Трекбэк с вашего сайта.

Оставить комментарий