Как мы спасали Magento 2 с 1 млн товаров и 10 млн CMS страниц от 504 ошибок
Технологический стек:
- 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 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 ошибок.
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% мусорного трафика, чем бесконечно наращивать мощности серверов для его обслуживания.