
Технологический стек:
-
Magento 2.4.4
-
MariaDB
-
Redis
-
Varnish
-
OpenSearch
Размер проекта:
-
~1,000,000 товаров
-
~3,000 категорий
-
~10,000,000 CMS страниц
Проблема:
-
504 ошибки под нагрузкой поисковых роботов
-
Высокая нагрузка на MariaDB
-
Нестабильный FPC hit ratio
Год:
-
2022
Let’s go!
Сразу оговорюсь: события происходили в 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
Возникает вопрос: сколько денег будет стоить хранение такого объема данных в 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 я выбрал другой механизм хранения.
FPC мы не держим в Redis
В итоге, я разделил ответственность между различными хранилищами.
Для системного кеша Magento использовал Redis (Cm_Cache_Backend_Redis), а для page_cache — файловый backend (Magento\Framework\Cache\Backend\File).
Позже хранение 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' => 'Magento\Framework\Cache\Backend\File', ], ],],'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, я стал использовать более дешёвое файловое хранилище, благодаря чему инфраструктура перестала упираться в объемы физической памяти.
Varnish
Следующий шаг — полностью делегировать Full Page Cache серверу Varnish
В Magento это настраивается через:
Stores » Configuration » Advanced » System » Full Page Cache
где необходимо выбрать:
Caching Application = Varnish Cache
После этого Magento перестаёт рассматриваться как основной участник процесса отдачи кэшированных страниц. Её задача сводится к генерации контента и управлению инвалидацией кеша, тогда как хранение и доставка FPC передаются Varnish.
Теперь Varnish становится ключевым игроком, через который проходит основной поток трафика.
Varnish: RAM или Memory Mapped File?
По умолчанию 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"]
Почему 100 GB?
Потому что теоретический объём в районе 1 TB никогда не будет находиться в кеше. Часть страниц устаревает по TTL, часть просто не запрашивается пользователями или поисковыми роботами.
При проектировании кеша имеет смысл ориентироваться не на теоретический максимум, а на реальный рабочий набор данных (working set), который действительно участвует в обслуживании клиентского трафика.
В результате получаем следующую схему:
Varnish ↓Linux Page Cache (RAM) ↓HDD / SSD / NVMe
В такой конфигурации, операционная система сама управляет тем, какие объекты должны находиться в памяти, а какие могут быть выгружены на диск.
Это позволяет использовать RAM максимально эффективно и не резервировать сотни гигабайт памяти исключительно под Full Page Cache.
Проверка
После настройки необходимо убедиться, что страницы обслуживает именно Varnish.
Проверяем заголовки ответа:
X-Varnish: XXXXXX XXXXXX-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 ошибок.
ESI (Edge Side Includes) и Full-Stack разработка
После настройки 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..
Cloudflare, CloudFront и Browser Cache
После того как добился стабильного 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
ссылка на оригинал статьи https://habr.com/ru/articles/1050416/