Ускорили сайт в 10 раз с помощью кэширования Redis

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

Ускорили сайт в 10 раз с помощью кэширования Redis

По сайту пользователь судит о надежности всей компании. Каким бы эффектным ни был дизайн, если сайт тормозит — это сильно портит впечатление. А если сайт и вовсе перестает работать, клиенты просто уйдут к конкурентам. Кроме того, от стабильности работы зависит ранжирование сайта в поисковой выдаче. Поэтому не стоит жалеть силы на тестирование и оптимизацию.

Во время тестирования одного из проектов мы заметили, что 90% времени загрузки страницы уходит на генерацию HTML — сборку документа из шаблонов на сервере. Эта избыточная задержка может вызвать проблемы при большой нагрузке на сайт. Мы избавились от нее, подключив кэширование: сначала попробовали более простой путь с помощью Nginx, а затем настроили более сложную систему c Redis.

Почему обработка шаблонов становится проблемой?

Большинство проектов мы пишем на Symfony — универсальном и мощном PHP-фреймворке, который по умолчанию использует шаблонизатор Twig.

Шаблонизатор — это инструмент, упрощающий работу с повторяющимися элементами HTML разметки. Они выносятся в отдельные шаблоны, которые можно многократно использовать. Это сильно сокращает дублирование кода и придает проекту более четкую логику.

Допустим, на сайте 100 карточек товаров с одинаковой структурой (название, фото, цена), но разным контентом. Вам нужно изменить дизайн карточек, например поменять местами название и фото. Без шаблонов пришлось бы править каждую карточку. С шаблонами — достаточно изменить один файл, и правки применятся ко всем.

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

Сбор единого HTML документа из шаблонов называется генерацией. Когда пользователь запрашивает страницу, Symfony генерирует ее на сервере, прежде чем отправить.

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

Медленная генерация страниц чревата тем, что сайт будет падать, когда нагрузка вырастет, даже если сразу после запуска он работает превосходно. Например, удачная рекламная кампания может дать резкий приток пользователей. Кроме того, поисковые и спам-боты обычно приходят волнами, резко увеличивая нагрузку на короткий срок.

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

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

Nginx кэш

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

Как устроено кэширование в Nginx

  1. Когда пользователь запрашивает страницу, Nginx передает запрос на бэкенд, где из шаблонов генерируется HTML документ.
  2. Nginx сохраняет его в кэше и передает пользователю.
  3. При повторном запросе той же страницы, Nginx не обращается на бэкенд, а отдает документ из кэша.

Кэшировать можно все страницы сайта?

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

Мы нашли решение в том, чтобы обрабатывать индивидуальные данные динамически, через Ajax запросы, то есть вообще отдельно от загрузки страницы. Когда пользователь запрашивает подобную страницу, то одинаковый для всех HTML он получает из кэша, а на бэкенд уходят только запросы, касающиеся индивидуальных данных. Такой подход дает существенный выигрыш в производительности.

Проблема 1: Nginx не учитывает нормализацию URL

Запрос, поступающий на Nginx, содержит полный адрес страницы — URL.

URL включает query-параметры, которые отражают дополнительную информацию, но не влияют на путь к странице. Они могут содержать, например, настройки языка, региона, поисковый запрос или маркетинговые метки (UTM). Маркетинговые метки не влияют на контент, но множат варианты URL — уникальных комбинаций query параметров получается много. Полный адрес одной и той же страницы может отличаться, в зависимости от того, перешел на нее пользователь по ссылке из гугла, яндекса или изнутри сайта.

Nginx по умолчанию кэширует страницы целиком, включая query-параметры. Если 1000 пользователей пришли с разными UTM-метками — в кэше окажется 1000 версий одной и той же страницы.

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

Проблема 2: Инвалидация кэша в Nginx

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

В Nginx нет сложных механизмов инвалидации кэша. Для сохраненного документа устанавливается TTL (Time to Live) — время жизни, период, в который он считается валидным. Когда TTL истекает, документ удаляется. Установить оптимальное время жизни кэша тяжело: слишком длинное даст высокий риск, что пользователь получит устаревшую информацию, а короткое не снизит нагрузку.

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

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

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

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

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

Обработка URL

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

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

Инвалидация и прогрев кэша в Redis

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

  • Точечная инвалидация. Нет необходимости сбрасывать кэш целиком при каждом обновлении. Redis позволяет отслеживать, какие именно данные были изменены на сайте и отмечать затронутые документы в кэше.
  • Фоновый прогрев кэша — предварительная генерация страниц до запроса пользователя. В процессе инвалидации кэш не удаляется, а помечается как устаревший. Фоновый процесс проходит по отмеченным документам и заново их генерирует.
  • Приоритизация на основе статистики. C Redis можно настроить счетчик, чтобы следить за частотой запроса страниц, а затем ранжировать их в очереди кэширования. Популярные страницы кэшируются сразу после обновлений на сервере — вероятность, что пользователь получит устаревшие данные низкая. Редко запрашиваемые страницы обновляются, только когда нагрузка на сервер снижается.

Настройка дополнительного кэширования оказалась эффективной и универсальной — мы стали применять ее на большинстве проектов. Такая конфигурация требует немного времени и ресурсов и дает стабильный результат:

  • Пользователи не ждут загрузки.
  • Риск получить устаревшие данные минимален.
  • Сервер работает стабильно даже при высокой нагрузке.

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

Vide Infra — дизайн и разработка для тех, кому нужно лучшее: диджитал-айдентика, продукты, сайты, корпоративные порталы, e-commerce.

2
Начать дискуссию