У меня была схожая проблема на OpenCart 3 (MySQL 5.6, PHP 7.1): ~30 000 товаров, медленные страницы, тяжёлые запросы, периодические падения по метрике.
Скриншот консоли Chrome DevTools: TTFB в PWA. Ответ из кэша Service Worker.
Цели были несколько другие, но то что касается "ускорения", вот что сделал, в общих чертах:
Железо
Обязательно NVMe, остальное по потребности, в моем случае - 6 CPU, 12 ГБ RAM
Перевод с MyISAM на InnoDB - можно и нужно
Перевёл все таблицы:
ALTER TABLE oc_product ENGINE=InnoDB
ALTER TABLE oc_product_description ENGINE=InnoDB
Почему это важно:
InnoDB поддерживает блокировку строк, а не целых таблиц
Работает innodb_buffer_pool - кэширует данные в RAM
Лучше масштабируется под нагрузку
Также, обязательно, периодически оптимизировать все таблицы
Настройки в my.cnf:
[mysqld]
# Основной кэш InnoDB должен быть ~70-80% от RAM.
innodb_buffer_pool_size=8G
# Разделяет буферный пул на независимые экземпляры (= CPU) - снижает конкуренцию потоков.
innodb_buffer_pool_instances=6
# Баланс скорости и безопасности: лог пишется в файл каждую секунду, не после каждой транзакции.
innodb_flush_log_at_trx_commit=2
# Прямая запись на диск, минуя кэш ОС - повышает предсказуемость производительности.
innodb_flush_method=O_DIRECT
# Ускоряет доступ к часто используемым данным через хеш-индексы (авто).
innodb_adaptive_hash_index=ON
# Размер файла журнала транзакций. Больше = меньше записи на диск, лучше производительность.
innodb_log_file_size=2G
# Буфер лога транзакций в памяти. Помогает сгладить пиковые нагрузки.
innodb_log_buffer_size=256M
# При старте MySQL загружает в пул ранее сохранённый кэш - быстрый выход на пиковую скорость.
innodb_buffer_pool_load_at_startup=ON
# При остановке MySQL сохраняет "горячий" кэш - ускоряет восстановление после перезапуска.
innodb_buffer_pool_dump_at_shutdown=ON
# Контролирует скорость фоновой записи данных.
innodb_io_capacity=2000
# Увеличивает параллелизм чтения.
innodb_read_io_threads=8
# Увеличивает параллелизм записи.
innodb_write_io_threads=8
# Время ожидания освобождения блокировки перед ошибкой.
innodb_lock_wait_timeout=120
# Максимальный процент "грязных" страниц в буфере.
innodb_max_dirty_pages_pct=70
# Автоматически регулирует скорость записи изменённых страниц - стабилизирует нагрузку.
innodb_adaptive_flushing=ON
# Задержка перед вытеснением "старых" блоков из кэша - защищает от случайного сканирования.
innodb_old_blocks_time=1000
# Отключает сбор статистики при запросах к INFORMATION_SCHEMA - ускоряет метазапросы.
innodb_stats_on_metadata=OFF
# Уменьшает блокировки, подходит для репликации и высокой параллельности.
transaction-isolation=READ-COMMITTED
# Query Cache устарел: при записи блокируется полностью - убивает производительность.
# Обязательно отключить при нагрузке.
query_cache_type=0
query_cache_size=0
# Временные таблицы в памяти - ускоряют сложные JOIN и ORDER BY.
tmp_table_size=512M
max_heap_table_size=512M
# Директория для временных файлов. /tmp - обычно в RAM (если смонтирован как tmpfs).
tmpdir=/tmp
Обязательно настроить веб-сервер и раскомпилировать релевантные модули
Вот пример для Apache 2.4:
httpd.conf
MPM Worker:
StartServers 6 # начальное число серверных процессов
MinSpareThreads 50 # минимальное число свободных потоков
MaxSpareThreads 100 # максимальное число свободных потоков
ThreadsPerChild 25 # потоков на процесс - параллельная обработка
MaxRequestWorkers 300 # общее число одновременных подключений
ServerLimit 12 # макс. число процессов (ограничивает масштабирование)
MaxRequestsPerChild 0 # не перезапускать процессы (риск утечки памяти, но выше скорость)
# Прямая передача файлов ядром ОС - минуя буферизацию, ускоряет отдачу статики
EnableSendfile on
# Время ожидания повторного запроса по соединению - меньше = быстрее освобождение ресурсов
KeepAliveTimeout 1
# Отключение .htaccess - критично для скорости: Apache не проверяет каждый каталог
<Directory "/var/www">
AllowOverride None # отключает чтение .htaccess
Require all granted
</Directory>
# Минимизация записи логов - снижает I/O нагрузку на диск
LogLevel warn # пишутся только ошибки и предупреждения
CustomLog "logs/access_log" combined # лог только запросов, без debug
# Модуль сжатия Gzip - уменьшает размер HTML/CSS/JS
LoadModule deflate_module modules/mod_deflate.so
# Модуль сжатия Brotli - лучше Gzip, меньший размер (современные браузеры)
LoadModule brotli_module modules/mod_brotli.so
# Кэширование через заголовки Expires - снижает количество запросов от браузера
LoadModule expires_module modules/mod_expires.so
# Управление HTTP-заголовками - важно для кэширования, CORS, безопасности
LoadModule headers_module modules/mod_headers.so
Корректно настроить глобальный пул PHP-FPM в php-fpm.d/*.conf (тут тоже согласуем с железом)
Вот пример под Apache и PHP 7.1:
# Максимальное число ожидающих соединений к сокету. При высокой нагрузке - увеличивать.
# Значение 4096 предотвращает потерю запросов при всплесках трафика.
listen.backlog = 4096
# Стратегия управления процессами: dynamic - гибко подстраивается под нагрузку.
pm = dynamic
# Максимальное число дочерних процессов PHP. Ограничивает общую нагрузку на CPU и RAM.
# 120 - подходит для сервера с 8–16G RAM (при ~100–150M на процесс).
pm.max_children = 120
# Количество процессов при старте PHP-FPM. Ускоряет обработку первых запросов.
pm.start_servers = 20
# Минимум свободных процессов. Поддерживает "резерв" для быстрого отклика.
pm.min_spare_servers = 10
# Максимум свободных процессов. Превышение - лишние процессы завершаются.
pm.max_spare_servers = 30
# После 500 запросов процесс перезапускается - предотвращает утечки памяти.
pm.max_requests = 500
# Время простоя процесса перед завершением (актуально для ondemand/dynamic).
pm.process_idle_timeout = 60s;
# Лимит открытых файлов на процесс. Если высокая нагрузка и множество соединений.
rlimit_files = 50000
# Разрешить создание core-дампов при аварии - полезно для отладки.
rlimit_core = 'unlimited'
# Убить скрипт, если он выполняется дольше 20 секунд - предотвращает зависания.
request_terminate_timeout = 20s;
# Разрешает обработку только .php файлов - защита от выполнения сторонних скриптов.
security.limit_extensions = .php
OPcache - включить и настроить
Без него каждый запрос перекомпилирует PHP - это убивает CPU
Настройки в php.ini (и тут тоже нужно согласовать с железом):
; Включает кэширование скомпилированного байт-кода PHP. Без этого - нет ускорения.
opcache.enable=1
; Выделяет 2 ГБ RAM для хранения скомпилированных скриптов.
opcache.memory_consumption=2048
; Максимальное число файлов, которые можно закэшировать.
opcache.max_accelerated_files=100000
; Частота проверки изменений в файлах (в секундах). 0 - отключает автоматическую проверку.
opcache.revalidate_freq=0
; Полностью отключает проверку временных меток файлов - максимальная скорость.
; Кэш обновляется только вручную (перезагрузка FPM или opcache_reset()).
opcache.validate_timestamps=0
; Использует huge pages (2M вместо 4K) - снижает нагрузку на TLB, ускоряет доступ к памяти.
; Требует настройки ОС: echo 'vm.nr_hugepages = 1280' >> /etc/sysctl.conf
opcache.huge_code_pages=1
После этого PHP существенно снижает нагрузку на CPU
Кэширование HTML на уровне приложения
Memcached не поможет - он кэширует данные, а не готовые страницы
Реализовал кэширование целых HTML-страниц до запуска контроллеров. Решение работает через APCu, кэширует ответы с учётом URL, сессии и устройства
Ключевые особенности:
Кэшируются страницы категорий, товаров, корзины
Умная инвалидация при изменении данных
Сжатие GZIP на этапе кэширования - снижает нагрузку на CPU при отдаче
+ Brotli оставляем только для статики (JS, CSS, JSON) - там выигрыш в сжатии оправдан
Прогрев кэша - через cron + curl
Даже с кэшем первые посетители после простоя будут попадать на "холодные" страницы
Самый простой и достаточно эффективный способ "жарить" кроном (у меня их получилось пять, разные группы страниц, разные тайминги - зависит от вариативности поведения пользователей):
*/15 * * * * /home/scripts/preload_pages.sh
Вот пример скрипта:
#!/bin/bash
PAGES_FILE="./pages_to_warm.txt"
if [ ! -f "$PAGES_FILE" ] || [ ! -s "$PAGES_FILE" ]; then
echo "❌ Файл $PAGES_FILE не найден или пуст"
exit 1
fi
readarray -t PAGES < "$PAGES_FILE"
echo " Прогрев десктоп..."
for PAGE in "${PAGES[@]}"; do
curl -fs -o /dev/null "$PAGE" > /dev/null 2>&1 && sleep 0.5
done
echo " Прогрев мобильных..."
for PAGE in "${PAGES[@]}"; do
curl -fs -o /dev/null \
-A "Mozilla/5.0 (iPhone; ... Safari/604.1" \
"$PAGE" > /dev/null 2>&1 && sleep 0.5
done
echo "✅ Прогрев завершён"
Теперь "горячие" страницы всегда в кэше. Я запускал с двумя User-Agent, смартфон и десктоп (нужно если кэши разные)
На клиенте
Чтобы сделать переходы ещё быстрее:
Добавил Service Worker - кэширует HTML на устройстве. Я у себя разделил на PWA и браузер (смартфоны и ПК) чтобы можно было управлять кэшем более адресно.
Внедрил предиктивную навигацию: анализируем, какие переходы чаще всего делаются - и предзагружаем эти страницы
Часто посещаемые страницы уже закэшированы на устройстве пользователя, до того как он их посетил, что дает очень хорошую скорость особенно в веб-приложении.
Но про это ещё дольше расписывать и возможно для вас это будет избыточным.
Если будет интересно, здесь описал все что делал:
https://uxengineer.ru/pe-uxe-sre