Оптимизация — это настройка проекта, нацеленная на уменьшение нагрузки и ускорение работы. Универсальных критериев нет, они определяются, исходя из специфики сервиса.
Лучше всего запланировать оптимизацию на финальный этап работы: то, что кажется важным в процессе разработки, может оказаться непринципиальным при эксплуатации или решаться совсем другими средствами.
Мозг человека довольно ленив и стремится заниматься простыми или придуманными задачами вместо того, чтобы решать сложные, скучные, но важные задачи. Разработчики часто пишут: «мы работаем над высоконагруженным проектом», но чаще всего это означает, что команда придумала себе интересную игру в оптимизацию. «Строить архитектуру высоконагруженного проекта»™ интереснее, чем работать над сложной и скучной задачей по превращению проекта в коммерчески успешный сервис. Это распространенное когнитивное искажение, о нём надо знать, чтобы вовремя заметить это за собой. Хочется чувствовать себя причастным к чему-то великому, и рассказывать «мы сделали проект, оптимизированный под 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
)
# ...
Для оптимизации поиска по тексту записей можно настроить полнотекстовый индекс базы данных или подключить специализированную базу, ориентированную на работу с текстовыми данными.