Про фронтенд написано много: сжать картинки, отложить скрипты, собрать критический CSS. Но у магазина с настоящим каталогом половина проблем живёт не в браузере, а на сервере — и ни одна оптимизация разметки её не лечит. Разберём серверную часть по порядку: что мерить, что настраивать и в каком порядке, чтобы ускорение сайта не превратилось в перебор настроек наугад.
Где именно теряется время
Между нажатием ссылки и появлением страницы происходит несколько этапов, и каждый из них измеряется отдельно. Сваливать их в одно слово «медленно» — главная причина, по которой оптимизация не даёт результата: чинят не то, что тормозит.
- Соединение: DNS, установка TCP, рукопожатие TLS — десятки миллисекунд, лечится настройкой один раз
- Ожидание ответа сервера: сюда входит вся работа PHP и базы — именно здесь у каталогов уходят секунды
- Передача документа: зависит от размера HTML и сжатия
- Отрисовка в браузере: блокирующие стили и скрипты, шрифты, изображения
- Дозагрузка данных после отрисовки: корзина, цены, рекомендации, которые подтягиваются запросами
Фронтенд отвечает за четвёртый и пятый пункты, сервер — за второй. Если ожидание ответа 1200 миллисекунд, то любая работа с картинками и скриптами будет улучшать картину на сотые доли: человек всё равно смотрит в пустой экран, пока сервер думает.
TTFB: первое, что надо измерить
Время до первого байта показывает, сколько сервер думал над страницей. Мерить его надо не в браузере с открытой консолью и включёнными расширениями, а запросом из командной строки, несколько раз, с разных страниц.
- Главная, категория с фильтром, карточка товара, страница корзины — у них принципиально разная цена в ресурсах
- Первый запрос после сброса кеша и второй подряд — разница показывает, работает ли кеширование вообще
- Авторизованный посетитель и анонимный: для первого кеш страниц почти всегда отключён, и именно он видит настоящую скорость сайта
- Ответ из локальной сети сервера против ответа снаружи — отделяет работу приложения от сетевых задержек
- Запрос в пиковые часы и ночью: деградация под нагрузкой — отдельный диагноз, и настраивается она иначе
Ориентир простой: для страницы, отдающейся из кеша, ожидание ответа измеряется десятками миллисекунд. Для динамической страницы каталога приемлемо до трёхсот. Если видите секунду и больше — дело не в настройках, а в количестве запросов к базе на одну страницу, и начинать надо с профилирования, а не с тюнинга.
Настройка сервера не ускоряет плохой код, она только перестаёт его замедлять. Триста запросов к базе на отрисовку категории останутся тремястами запросами на любом железе — просто на дорогом они будут выполняться чуть быстрее.
Хостинг, VPS или выделенный сервер
Признак для выбора один — нужен ли вам контроль над настройками. На виртуальном хостинге вы получаете готовую среду, в которой нельзя поменять версию PHP под себя, поднять кеш в памяти, настроить параметры базы или посмотреть, что происходит с диском.
- Виртуальный хостинг подходит для лендинга, визитки, небольшого корпоративного сайта с десятками страниц
- Магазину с каталогом нужен сервер с корневым доступом: без него половина пунктов этой статьи недоступна технически
- Соседи по хостингу влияют на скорость, и это невозможно ни измерить, ни исправить со своей стороны
- Выделенный сервер нужен не раньше, чем виртуальный упирается в ресурсы на реальном трафике — а не на всякий случай
- Географически сервер должен стоять там, где покупатели: для российской аудитории это Россия, и причина не только в скорости, но и в требованиях к хранению персональных данных
Отдельно про перенос базы на отдельный сервер. Это правильный шаг для крупного проекта, но на старте он часто делает хуже: вместо обращения к базе на том же хосте появляется сетевой round-trip, и страница с тремя сотнями запросов начинает работать медленнее, чем была.
Память важнее ядер
При выборе конфигурации обычно смотрят на процессор. Для магазина критичнее память: от неё зависит, сколько одновременных запросов сервер способен обработать и сколько данных база держит в оперативке вместо чтения с диска.
Простая арифметика: память делится между процессами PHP, буферами базы, кешем в памяти и операционной системой. Если сумма настроенных лимитов превышает физическую память, при пике сервер уходит в подкачку и перестаёт отвечать совсем — то есть вместо медленного сайта получается недоступный.
- Подкачка не решает нехватку памяти, а превращает её в деградацию: лучше честный отказ в соединении, чем тридцатисекундные ответы
- Если в журнале ядра встречается убийца процессов по нехватке памяти, никакие другие настройки обсуждать смысла нет
- Сборка проекта, выгрузки и обмен с учётной системой едят память в те же моменты, что и посетители — это надо учитывать в расчёте
- Фоновые задачи стоит выносить в отдельный пул процессов, чтобы они не отнимали рабочие процессы у посетителей
PHP-FPM: количество процессов
Главный параметр — максимальное число дочерних процессов. Считается он не по вкусу, а делением доступной под PHP памяти на средний объём одного процесса. Средний объём смотрится на работающем сервере, а не берётся из статьи в интернете: у магазина на тяжёлой платформе процесс занимает в разы больше, чем у простого сайта.
- Слишком маленькое значение — запросы встают в очередь, и под нагрузкой ожидание ответа растёт, хотя процессор свободен
- Слишком большое — память заканчивается, сервер уходит в подкачку, отваливается всё сразу
- Режим статического пула предсказуемее динамического: процессы создаются сразу и не тратят время на запуск под нагрузкой
- Ограничение числа запросов на процесс защищает от медленных утечек памяти в коде и расширениях
- Соединение через unix-сокет, а не через TCP, экономит немного на каждом запросе
- Журнал медленных запросов PHP с порогом в несколько секунд — самый дешёвый способ увидеть, где именно код зависает: в журнал пишется стек вызовов
Отдельно стоит включить страницу состояния пула. Она показывает, сколько процессов занято прямо сейчас и какая была максимальная очередь, — без этого настройка пула превращается в угадывание.
OPcache и версия PHP
Самое дешёвое ускорение, которое до сих пор встречается ненастроенным. OPcache хранит скомпилированный байт-код в памяти, чтобы не разбирать исходники на каждом запросе. Включён он почти всегда, но объёма памяти и лимита числа файлов по умолчанию крупному проекту не хватает — а при переполнении кеш просто перестаёт работать, молча.
- Число кешируемых файлов должно быть больше, чем файлов в проекте: у тяжёлых платформ это десятки тысяч, и значение по умолчанию мало
- Объём памяти под кеш стоит задавать с запасом и следить за процентом заполнения и числом вытеснений
- Проверку времени изменения файлов на боевом сервере отключают — но тогда после каждого обновления кода нужен перезапуск пула, иначе сайт продолжит работать по старому коду
- Кеш путей файловой системы тоже имеет лимит, и у проектов с глубокой структурой каталогов его стоит поднять
- Обновление версии PHP на одну мажорную даёт прирост, сопоставимый с любой ручной оптимизацией, — и обычно требует лишь проверки совместимости модулей
База: буферный пул и медленные запросы
Для каталога база — главный источник задержки. И здесь всего два действия дают почти весь результат.
Первое: буферный пул должен вмещать рабочий набор данных. Если он меньше размера активных таблиц и индексов, база читает с диска при каждом обращении, и дальше всё зависит от скорости диска, а не от скорости базы. На сервере, где кроме базы ничего нет, под пул отдают большую часть памяти; на общем сервере — считают остаток после PHP.
Второе: включить журнал медленных запросов с низким порогом и посмотреть, что в него попадает. Как правило, оказывается, что восемьдесят процентов времени съедают три-четыре однотипных запроса без подходящего индекса — и добавление индекса даёт кратное ускорение категории.
- Запросы без использования индексов стоит логировать отдельно: это готовый список задач
- Разбор плана выполнения показывает, сколько строк база перебирает фактически, — это объективнее, чем время, которое зависит от нагрузки
- Кеш запросов в современных версиях MySQL отсутствует: настройки из старых инструкций либо игнорируются, либо не дают серверу запуститься
- Фильтрация по нескольким свойствам в фасетной навигации — самый тяжёлый тип запроса в магазине, и решается он чаще структурой таблиц, а не настройкой сервера
- Таблицы в старом формате хранения без поддержки блокировок на уровне строки под нагрузкой превращаются в очередь: формат надо проверить и перевести
Отдельная статья расходов — временные таблицы на диске. Если лимиты для них малы, тяжёлые выборки с сортировкой начинают писать на диск, и это видно по счётчикам базы. Поднятие лимитов помогает, но корень обычно в самом запросе.
Кеш в памяти: сессии и объекты
Два слоя, которые часто путают. Кеш страниц отдаёт готовый HTML и не работает для авторизованных и для корзины. Объектный кеш хранит результаты отдельных вычислений и запросов — он работает всегда, в том числе для авторизованных, и именно он спасает каталог.
- Объектный кеш в памяти вместо файлов на диске — заметный выигрыш на каталоге с большим числом мелких обращений
- Сессии в памяти вместо файлов убирают блокировки файла сессии: без этого параллельные запросы одного посетителя выстраиваются в очередь
- Кеш и сессии лучше держать в разных базах хранилища: сброс кеша не должен разлогинивать людей
- У хранилища в памяти должен быть лимит и политика вытеснения, иначе оно съест память, рассчитанную под PHP
- Инвалидация важнее самого кеша: после обмена с учётной системой кеш каталога обязан сбрасываться, иначе цены на витрине будут вчерашними
Почему установка готового плагина кеширования сама по себе не ускоряет магазин, мы разбирали подробно: причина ровно в этом разделении слоёв.
Кеш на уровне веб-сервера
Самый быстрый ответ — тот, который отдаётся до запуска PHP. Веб-сервер умеет кешировать ответы сам, и для анонимных посетителей это сокращает ожидание до единиц миллисекунд. Но включать такой кеш без условий на магазине нельзя — иначе один посетитель увидит корзину другого.
- Обход кеша по признаку авторизации и по наличию непустой корзины — обязательное условие, и проверять его надо до включения кеша на боевом сайте
- Методы, отличные от GET, не кешируются никогда
- Короткое время жизни кеша даже на уровне секунд уже снимает пик с главной и категорий при всплеске трафика
- Выдача устаревшего ответа при ошибке приложения спасает сайт от белого экрана в момент сбоя
- Сжатие ответа, протокол второй версии, повторное использование соединений, кеширование открытых файловых дескрипторов — настраивается один раз и работает всегда
- Статика должна отдаваться веб-сервером напрямую с длинным временем жизни кеша и версионированием в имени файла
Диск: важна не ёмкость
Для базы данных значение имеет число операций ввода-вывода в секунду и задержка отдельной операции, а не объём. Разница между механическим диском, SSD по старому интерфейсу и современным накопителем для базы кратная — и ощущается именно тогда, когда данные не поместились в память.
Признак, что проблема в диске: при нормальной загрузке процессора растёт время ожидания ввода-вывода, а запросы к базе замедляются все разом, независимо от типа. Смотрится это стандартными средствами наблюдения за системой, и перепутать это ни с чем нельзя.
Чем мерить, чтобы не угадывать
- Журнал медленных запросов базы — главный источник конкретных задач
- Журнал медленных запросов PHP со стеком вызовов — показывает место в коде, а не симптом
- Состояние пула процессов: занятые процессы, длина очереди, максимумы за сутки
- Статистика OPcache: заполнение, вытеснения, процент попаданий
- Системные счётчики: загрузка процессора, свободная память, ожидание диска, подкачка
- Нагрузочная проверка на копии сайта, а не на боевом: она отвечает на вопрос, сколько одновременных посетителей сервер держит до деградации
- Журнал доступа веб-сервера с временем обработки каждого запроса — покажет самые дорогие адреса на реальном трафике
Последний пункт недооценивают. Добавить время обработки в формат журнала — пять минут работы, после чего видно, что половину ресурсов съедает не каталог, а, например, поиск по сайту или страница, которую обходит робот.
Порядок работ
- Снять базовые показатели: ожидание ответа на пяти типах страниц, для анонимного и авторизованного
- Проверить память: хватает ли её при текущих лимитах, нет ли подкачки
- Включить журналы медленных запросов базы и PHP, собрать данные за сутки
- Исправить то, что в них нашлось: индексы, лишние запросы в цикле, тяжёлые выборки
- Настроить OPcache и обновить версию PHP, если она старая
- Рассчитать и выставить число процессов PHP под реальный объём памяти
- Подобрать буферный пул базы под размер рабочего набора данных
- Перевести сессии и объектный кеш в память, настроить сброс кеша после обмена
- Включить кеш веб-сервера с корректными условиями обхода
- Повторить замеры и зафиксировать результат — иначе непонятно, что из этого сработало
Порядок важен: настройка кеша до исправления запросов прячет проблему, а не решает её. Первый же сброс кеша или заход робота по неоткешированным адресам возвращает сайт в исходное состояние, и выяснять причину приходится заново.
Мы занимаемся этим в таком порядке: сначала замеры и журналы, потом исправления, потом настройка, снова замеры. Разовая работа по серверной части — это ускорение сайта, постоянное наблюдение за ресурсами, обновлениями и резервными копиями — администрирование сервера. Если тормозит магазин на готовой платформе с набором расширений, начинать стоит с разбора того, что именно выполняется на каждой странице, — это входит в поддержку. Фронтенд-часть и показатели, которые видит поисковик, мы разбирали в статье про Core Web Vitals.