- PVSM.RU - https://www.pvsm.ru -

Технологический стек:
Magento 2.4.4
MariaDB
Redis
Varnish
OpenSearch
Размер проекта:
~1,000,000 товаров
~3,000 категорий
~10,000,000 CMS страниц
Проблема:
504 ошибки под нагрузкой поисковых роботов
Высокая нагрузка на MariaDB
Нестабильный FPC hit ratio
Год:
2022
Сразу оговорюсь: события происходили в 2022 году. На тот момент я не планировал вести блог, поэтому полноценные метрики из Grafana уже не сохранились. Но часть заметок осталась в старых тикетах, поэтому основные цифры и решения получилось восстановить.
Магазины с каталогом порядка 1 000 000 товаров, особенно на Magento 2, уже сами по себе задача не простая. А если добавить к этому ~ 3 000 категорий и более 10 000 000 CMS-страниц, можно представить, что происходит во время пиковых нагрузок и активного налета краулеров.
На старте инфраструктура выглядела как по статьям: Magento 2, Redis, Varnish и OpenSearch. Все компонентные слои были настроены в соответствии с рекомендациями по оптимизации Adobe Commerce / Magento.
Но во время активного обхода сайта клиентами с user-agent'ами вроде Googlebot/2.1, DuckDuckBot/1.0, Mozilla/5.0 (bingbot), а также многочисленными менее известными пауками, ситуация переросла в пожар. К этому всему, пиковые нагрузки пользовательского трафика, особенно в акционные дни.
Результат этого всего: 504 ошибки, высокая нагрузка на базу данных и MariaDB, которая только и пыталась выжить под этим напором.
Для оценки объёма Full Page Cache я посчитал примерный теоретический максимум. На проекте медианный размер HTML-страниц составляла около 100 KB.
Считаем:
~1,000,000 товаров
~3,000 категорий
~10,000,000 CMS-страниц
Всего: ~11,003,000 приблизительно страниц
11,003,000 × 100 KB = 1,049 GB ≈ 1 TB
Конечно, это теоретический максимум. На практике весь этот объём никогда одновременно не окажется в кеше: часть страниц устаревает имея TTL, часть вытесняется, часть просто не запрашивается.
Тем не менее именно от таких цифр я отталкивался при перепроектировании системы кэширования и выборе стратегии работы с FPC.
Возникает вопрос: сколько денег будет стоить хранение такого объема данных в Redis и вообще в RAM?
Даже если отбросить теоретический максимум в 1 TB данных для Full Page Cache (а мы его значительно режим и будем мониторить), остается очевидная проблема: Redis нужен не только для FPC. В нём также должны храниться сессии пользователей, системный кеш Magento, конфигурация, layout, block_html и другие данные.
Поэтому идея масштабировать инфраструктуру за счёт постоянного увеличения объема RAM выглядела не экономно, особенно с технической точки зрения.
Метрики Redis в Grafana лишь подтверждали это.
Очевидно, что выделять около 1 TB RAM (или хотя бы 100GB) только под Full Page Cache большинство бизнесов держащих Magento-магазины не могут быть готовы.
Поэтому я принял решение использовать Redis только для хранения системного кеша и пользовательских сессий, а для Full Page Cache я выбрал другой механизм хранения.
В итоге, я разделил ответственность между различными хранилищами.
Для системного кеша Magento использовал Redis (Cm_Cache_Backend_Redis), а для page_cache - файловый backend (MagentoFrameworkCacheBackendFile).
Позже хранение Full Page Cache будет полностью делегировано Varnish, но на данном этапе важно исключить FPC из Redis и снизить требования к объемам оперативной памяти.
'cache' => [
'frontend' => [
'default' => [
'backend' => 'Cm_Cache_Backend_Redis',
'backend_options' => [
'server' => '127.0.0.1',
'port' => '6379',
'database' => '0',
],
],
'page_cache' => [
'backend' => 'MagentoFrameworkCacheBackendFile',
],
],
],
'session' => [
'save' => 'redis',
'redis' => [
'host' => 'redis.host',
'port' => '6379',
'timeout' => '2.5',
'persistent_identifier' => '',
'database' => '2',
'compression_threshold' => '4096',
'compression_library' => 'gzip',
'log_level' => '1',
'max_concurrency' => '10',
'break_after_frontend' => '5',
'break_after_adminhtml' => '30',
'first_lifetime' => '600',
'bot_first_lifetime' => '60',
'bot_lifetime' => '7200',
'disable_locking' => '0',
'min_lifetime' => '60',
'max_lifetime' => '2592000'
]
]
В итоге распределение данных выглядело примерно так:
~около 100 ГБ - Full Page Cache в файловой системе
~2–3 GB — системный кеш Magento (config, layout, block_html, collections, db_ddl, eav, и другие типы кеша) в Redis;
~1–2 GB — пользовательские сессии в Redis.
Такой подход позволил использовать дорогую оперативную память только там, где она действительно приносит КПД.
Вместо того, чтобы удерживать сотни гигабайт HTML-кеша в RAM, я стал использовать более дешёвое файловое хранилище, благодаря чему инфраструктура перестала упираться в объемы физической памяти.
Следующий шаг - полностью делегировать Full Page Cache серверу Varnish
В Magento это настраивается через:
Stores » Configuration » Advanced » System » Full Page Cache
где необходимо выбрать:
Caching Application = Varnish Cache
После этого Magento перестаёт рассматриваться как основной участник процесса отдачи кэшированных страниц. Её задача сводится к генерации контента и управлению инвалидацией кеша, тогда как хранение и доставка FPC передаются Varnish.
Теперь Varnish становится ключевым игроком, через который проходит основной поток трафика.
По умолчанию Varnish хранит объекты в оперативной памяти (malloc). Для большинства проектов это вполне разумный выбор, как и Redis. Однако в нашем случае уже ясно, что хранить сотни гигабайт Full Page Cache в RAM экономически нецелесообразно.
Достаточно взглянуть на предыдущие метрики Redis, чтобы понять, что путь постоянного наращивания объема памяти нам не подходит.
При этом стоит учитывать задержки различных вариантов хранения данных:
|
Storage |
Типичная задержка |
|---|---|
|
Varnish (malloc) |
~0.1–1 µs |
|
Varnish (file) + объект находится в Linux Page Cache |
~1–10 µs |
|
Varnish (file) + чтение с NVMe |
~50–200 µs |
|
Magento File Cache |
~100–1000+ µs |
|
Генерация страницы через PHP |
10–500+ ms |
Даже в худшем случае чтение объекта из NVMe остаётся на несколько порядков быстрее, чем генерация страницы через PHP и последующие запросы к базе данных.
Поэтому вместо хранения Full Page Cache в RAM, я решил использовать файловое хранилище Varnish (file storage), работающее через механизм Memory Mapped Files.
Для запуска Varnish в таком режиме можно использовать такую конфигурацию:
ExecStart=/usr/sbin/varnishd
-a :80
-f /etc/varnish/default.vcl
-s file,/var/lib/varnish/cache.bin,100G
Для локальных экспериментов в Docker это может выглядеть следующим образом.
Для образа varnish:6.6.1 это выглядело бы так:
CMD ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-s", "file,/var/lib/varnish/cache.bin,100G"]
Потому что теоретический объём в районе 1 TB никогда не будет находиться в кеше. Часть страниц устаревает по TTL, часть просто не запрашивается пользователями или поисковыми роботами.
При проектировании кеша имеет смысл ориентироваться не на теоретический максимум, а на реальный рабочий набор данных (working set), который действительно участвует в обслуживании клиентского трафика.
В результате получаем следующую схему:
Varnish
↓
Linux Page Cache (RAM)
↓
HDD / SSD / NVMe
В такой конфигурации, операционная система сама управляет тем, какие объекты должны находиться в памяти, а какие могут быть выгружены на диск.
Это позволяет использовать RAM максимально эффективно и не резервировать сотни гигабайт памяти исключительно под Full Page Cache.
После настройки необходимо убедиться, что страницы обслуживает именно Varnish.
Проверяем заголовки ответа:
X-Varnish: XXXXXX XXXXX
X-Magento-Cache-Debug: HIT
Заголовок X-Varnish должен присутствовать (если он включён в VCL), а X-Magento-Cache-Debug должен возвращать значение HIT.
Для быстрой проверки производительности я использовал Apache Benchmark:
ab -n 100 https://xxx.xxx.xxx.xxx/
В моём случае после прогрева кэша, среднее время ответа составило около 3 мс на запрос.
Для сравнения я провёл подобный тест со встроенным Full Page Cache Magento.
На тех же 100 запросах среднее время ответа составило около 97 мс против 3 мс у Varnish.
Конечно, конкретные цифры зависят от инфраструктуры, конфигурации сервера и состояния кеша. Однако даже такой простой тест хорошо демонстрирует разницу между отдачей страницы напрямую из Varnish и обработкой запроса алгоритмами Magento.
Но главное здесь даже не разница между 97 мс и 3 мс, а то, что каждый запрос, обслуженный Varnish, не доходит до PHP-FPM и MariaDB. Именно это позволяет выдерживать большие объёмы crawler-трафика без появления 504 ошибок.
После настройки Varnish может показаться, что проблема решена. Но на практике, это не всегда так.
Дело в том, что связка Magento + Varnish активно использует ESI (Edge Side Includes) для динамических блоков. Если на странице присутствуют некэшируемые элементы, Varnish всё равно будет вынужден обращаться к PHP для их генерации.
Именно здесь начинается самая сложная часть работы.
Для достижения максимально эффективного Full Page Cache мне пришлось последовательно переводить блоки в режим:
cacheable="true"
Важно понимать, что в Magento наличие даже одного блока с атрибутом cacheable="false" может сделать всю страницу не кэшируемой.
Поэтому необходимо внимательно анализировать layout и добиваться максимально возможного покрытия блоками с cacheable="true".
Однако здесь возникает другая проблема.
Нельзя просто заменить cacheable="false" на cacheable="true" и считать задачу решенной.
Большинство таких блоков работают с персональными данными пользователя:
корзина
генератор ключей для защиты от CSRF
информация о клиенте
персональные цены
специальные предложения
данные авторизации
различные пользовательские состояния
checkout
Если такие данные случайно попадут в Full Page Cache, может оказаться больно - от отображения чужих данных до утечек чувствительной информации.
Поэтому перед переводом блока в кэшируемый режим, его необходимо переработать таким образом, чтобы он не зависел от пользовательского контекста.
По сути, это означает перенос динамики на клиентскую сторону.
Например:
пользовательский блок можно загружать через AJAX/XHR, а точнее, данные клиента можно получать через стандартный механизм Magento_Customer/js/customer-data
form_key используемый для защиты от CSRF-атак, можно получать отдельным запросом через AJAX или Fetch API
элементы интерфейса, зависящие от состояния пользователя, можно собирать уже после загрузки страницы средствами JavaScript, рисовать с помощью Knockout.js или даже отрисовывать средствами React или Vue.js
В результате HTML страницы становится полностью статическим и могут храниться в Full Page Cache.
Насколько далеко стоит заходить в такой переработке - зависит от требований бизнеса, от того как бизнес считает ROI.
В некоторых проектах достаточно кешировать только CMS-страницы. В других имеет смысл полностью кешировать каталог и карточки товаров. В наиболее агрессивных сценариях оптимизации переработке может подвергаться даже checkout, хотя такие изменения требуют особенно тщательного подхода и времени, однако может оказаться, что целесообразнее придерживаться принципа YAGNI.
Отдельно хочу упомянуть интересную особенность Magento 2.4.4, артефакт на который наткнулся.
На момент работы над проектом атрибут cacheable="false" анализировался на уровне полного набора layout-файлов, а не итогового результата после их объединения. По крайней мере именно такое поведение наблюдалось в версии 2.4.4.
Не исключаю, что в более поздних версиях ребята из Adobe это исправили, однако при внедрении подобных оптимизаций, стоит учитывать вероятность того, что для может потребоваться собственный плагин.
В качестве финальной проверки стоит контролировать значение заголовка: X-Magento-Cache-Debug
Ожидается, что прогретые страницы генерируют X-Magento-Cache-Debug: HIT как результат.
Значение MISS допустимо при первом обращении к странице после очистки кеша или истечения TTL..
После того как добился стабильного Full Page Cache на стороне Varnish, появился следующий уровень оптимизации.
Если страница стабильно отдаётся из FPC, имеет смысл позволить кешировать её ещё ближе к пользователю:
Cloudflare
Amazon CloudFront
корпоративные прокси
браузеры пользователей
Для этого необходимо корректно настроить заголовки Cache-Control и Pragma.
Тем, не менее здесь есть нюанс.
Стандартная конфигурация Varnish для Magento часто удаляет эти заголовки:
sub vcl_backend_response {
unset beresp.http.Cache-Control;
unset beresp.http.Pragma;
}
sub vcl_deliver {
unset resp.http.Cache-Control;
unset resp.http.Pragma;
}
Если ваша стратегия кэширования предполагает управление TTL непосредственно из Magento, эти строки стоит удалить.
В таком случае Magento будет самостоятельно формировать эти заголовки, а Cloudflare, CloudFront и браузеры пользователей смогут использовать их.
Фактически получается многоуровневая схема кэширования:
Browser Cache
↓
Cloudflare / CloudFront
↓
Varnish
↓
Magento
Чем выше процент попаданий на верхних уровнях этой цепочки, тем меньше запросов доходит до PHP-FPM, базы данных и нашей генерирующей инфраструктуры в целом.
На проектах такого масштаба нужно оптимизировать не скорость PHP, а количество запросов, которые вообще доходят до PHP
В результате удалось существенно снизить нагрузку на всю инфраструктуру.
До внедренных изменений MariaDB регулярно упиралась в CPU и RAM, а во время активной индексации поисковыми роботами, рекламных кампаний и сезонных пиков нагрузки сайт периодически отвечал ошибкой 504 Gateway Timeout.
После перевода Full Page Cache на Varnish File Storage и устранения проблем с cacheable="false" ситуация изменилась.
Страницы начали стабильно отдавать:
X-Magento-Cache-Debug: HIT
Большое количество запросов к каталогам, карточкам товаров и CMS-страницам перестали доходить до PHP-FPM.
Основной поток трафика обслуживался непосредственно CloudFlare и Varnish, что значительно снизило нагрузку на PHP и MariaDB и практически устранило проблемы с недоступностью.
Разумеется, тема масштабирования Magento значительно глубже, чем может поместиться в одну статью.
Здесь мы рассмотрели лишь один из аспектов - организацию эффективного Full Page Cache и снижение нагрузки на инфраструктуру.
Если статья окажется полезной и вызовет интерес у читателей, в продолжении можно будет разобрать другие практические вопросы, углубиться в техническую часть или предложенные в обсуждении:
параметрические URL и их влияние на индексацию
ограничение crawler-трафика
настройку robots.txt
crawl-delay
борьбу с бесконечными URL-комбинациями
защиту инфраструктуры от бесполезного бот-трафика
Потому что на проектах такого масштаба иногда оказывается выгоднее устранить 90% мусорного трафика, чем бесконечно наращивать мощности серверов для его обслуживания.
Оригинальная версия статьи: https://tereta.dev/ru/magento2/optimisations [1]
Автор: AlexanderTereta
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/redis/453742
Ссылки в тексте:
[1] https://tereta.dev/ru/magento2/optimisations: https://tereta.dev/ru/magento2/optimisations
[2] Источник: https://habr.com/ru/articles/1050416/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1050416
Нажмите здесь для печати.