Привет, Хабр! Это мой первый пост, поэтому поделюсь с вами кейсом ускорения работы одного сайта на WP + WooCommerce. Сам занимаюсь веб-разработкой, последние два года только на фрилансе. Не претендную на идеальную статью, я сам каждый день учусь новому, но в статье максимально стараюсь избегать неточностей.

Статья будет полезна джунам и миддлам кто разрабатывает сайты, кто занимается оптимизацией сайтов и кто хочет посмотреть на работу php кода "с высоты". Для себя из полезного можно узнать как связать вместе OpenServer, PhpStorm и xDebug. Один раз настраиваете и можно потом запросто делать отладку. И так, начнём.

Описание проблемы

Есть сайт. Работает на WP + wooCommerce. Количество продуктов ~21 000, категорий ~4 000. Куча своих таксономий, пользовательских типов записей. Генерация одной страницы занимает 4-5 секунд, иногда доходит до 10. Пользователи со всей статикой и с задержками сети ждут ещё 2-3 секунды. Задача: надо выявить что так тормозит сайт.

Конфигурация сервера: CentOS 8, ISPmanager 6 Lite. RAM 16 Гб, 4 процессора (именно какие не смог узнать, там VDS не показал с теми доступами что у меня были, а я сильно не интересовался, ну тут это не особо важной роли играет).

Для такой конфигурации и трафика который не превышает в сутки 500 уников такая долгая генерация однозначно патология. Взялся изучать...

Первичный анализ

Для начала сделал бекапы. Установил плагин Query Monitor который абы как показывает запросы в БД. Зацепка есть: около ~15000 запросов.

У WooCommerce есть такая проблема - по умолчанию при выводе категорий он показывает сколько там находится продуктов рядом с названием категории в скобках. Если кому интересно, issue. Так вот, Query Monitor сможет показать свойственный этой особенности запросы и одной строчкой можно исправить ситуацию. Но на этот раз проблема в другом.

На первый взгялд не понять в чем проблема. Тяжелых плагинов нет, тема не на каком-то Elementor или WP Bakery, создан с нуля и расширена плагином Advanced Custom Fields, все файлы разбиты по папкам и названы адекватно. Копаем глубже.

Разворачиваем среду отладки

С помощью плагина Duplicator быстро взял копию сайта. Исключил все медиа, изображения, архивы и папку wp-content/uploads целиком. Они нам не нужны для анализа, но могут здорово раздуть архив что сервер упрётся в лимит, либо браузер покажет 504 Gateway time out.

У меня установлен OpenServer версии 5.3.8, создаю директорию, закидываю файл с архивом и с файлом installer.php что выдаёт duplicator. Перехожу по адресу mydevsite.domain/installer.php и заполняю все данные что просит dup-installer. Не буду тут долго останавливаться, поднять копию сайта проще простого.

Открываю проект в phpStorm. IDE довольно удобный, но для текущего кейса можно было и без него обойтись. Поэтому если у вас другой IDE не торопитесь уходить, нам он нужен только для просмотра логов xDebug.

Включить xDebug в OpenServer

Если у вас как и у меня установлен OpenServer то отдельно устанавливать расширение xDebug не надо, он уже установлен, достаточно в конфигурации php.ini раскомментировать нужные строчки и немного настроить. Если что-то другое документация к xdebug тут.

Чтобы открыть php.ini надо проделать следующее:

Клик по иконке OpenServer в нижнем меню -> Дополнительно -> Конфигурация -> php7.4.

У меня php версии 7.4, если у вас другая версия там будет соответствующее название.

В открывшемся окне с конфигами находим строчку ;zend_extension = xdebug и убираем с начала строки точку с запятой (раскомментируем). Чуть ниже в окне будут уже сами конфиги xdebug. Опускаемся или находим поиском строку [xdebug] и вносим правки ниже:

xdebug.mode = profile,trace
xdebug.start_with_request = "trigger"

Тут xdebug.mode указывает что мы хотим профилировать и трассировать код. По умолчанию там стоит off. Есть ещё другие режимы, но на них не буду останавливаться, этих двух нам достаточно. xdebug.start_with_request указывает что включать режим отладки надо не всегда, а только если передать специальный параметр (через POST, GET или COOKIE, удобнее всего GET параметр). И да, этот параметр по умолчанию закомментирован, не забудьте раскомментировать.

Как выглядит конфиг php.ini у меня.
Как выглядит конфиг php.ini у меня.

Всё. xDebug готов, перезапускаем OpenServer. Теперь при переходе на любую страницу нам достаточно прописать ключ get параметра XDEBUG_PROFILE и php будет собирать отладочную информацию и записывать в папку что указан по пути xdebug.output_dir (у меня стоит путь по умолчанию). Про другие особенности можно прочитать тут.

Вид URL будет иметь такой вид mydevsite.domain/?XDEBUG_PROFILE если надо собрать информацию с главной страницы. В моем случае я снял снепшот с главной страницы.

Просмотр логов в красивом виде

Для просмотра логов я использую phpStorm. Но гугл подсказывает что для vsCode или Atom тоже есть решения. В самом верхнем меню находим раздел Tools и в списке Analyze Xdebug Profiler Snapshot. Нас попросит выбрать файл, переходим по пути где указали запись файлов, у меня по умолчанию это {{ папка openServer где он установлен }}/userdata/temp/xdebug/

Если всё сделано правильно там будет записанный файл с названием cachegrind.out.{{ какие-то цифры }}. Выбираем, ждём когда phpStorm обработает всё и вуаля, у нас есть работа скриптов с высоты.

Выявляем проблемный участок кода

phpStorm показывает логи в красивом и удобном виде. На первом окне смотрим сколько вообще времени заняло выполнение скрипта. Но нас интересует вкладка Call Tree (ниже выделил куда кликать).

И так у нас уже есть какие-то цифры. Раскрываем по одному файлы чтобы понять где лежит проблемный участок. Нас интересует ветка с файлом template-loader.php, нам надо ускорить написанную с нуля тему, а не копать в сторону ядра WP/

Ориентируясь цифрами, сколько процентов какая ветка сожрала, опускаемся до нужного файла и участка кода. В моем случае функция get_heder использовала 42% от всего времени. Смотря ещё глубже нахожу что это функция get_catalog_menu который жрал 36%. Кавабанга!

Файл header.php
Файл header.php
Опускаемся ниже и находим функцию тяжеловес
Опускаемся ниже и находим функцию тяжеловес

Находим файл в котором находится функция. Можно кликнуть по ПКМ и сделать jump to source. В нашем случае это оказался файл menu.php.

Вкратце - функция выбирает каждый раз все 4 000 категории и как-то делая проверки строит из них меню. Каждая проверка условии для одной категории это одно обращение к БД. В целом кусок кода довольно тяжелый.

Разработчик учёл что эта функция сильно нагружает код и сделал кеширование через WP Object Cache. Но не учёл что кеш работает только если сервер поддерживает кеширование и на сайте WP установлен плагин для объектного кеширования. А в моем случае на сервере он не работал и кеш не создавался. Про эту особенность можно прочитать тут.

Решение

Заказчик сказал что сайт мало когда редактируется, категории тем более, поэтому не стали переделывать код и оставили всё как есть для удобства. Только вместо объектного кеширования сделал хранение кеша в транзиентах.

И была такая же функция, только для менюшек в сайдбаре. Их тоже сделал чтобы кешировались. Итог - сайт летит.

Поверх всего провёл ещё ряд других оптимизаций и настройку сервера (сжатие gzip, время жизни кеша для статики, апдейт версии php до 8.0, очистка базы данных от мусора, вынесение стилей и скриптов в отдельные файлы).

Итог

Результаты ДО. ~9 секунд на выполнение.
Результаты ДО. ~9 секунд на выполнение.
Результат после. Выполение занимает ~6 секунд.
Результат после. Выполение занимает ~6 секунд.

Результаты до/после тут только после исправления всего двух функций. После были выполнены ещё другие мелкие оптимизации, настроен плагин полностраничного кеширования и суммарно для конечного пользователя сайт работает раза 4-5 быстрее. Отклик кешированных страниц занимает ~400мс, а не кешированных в зависимости от типа 1,5-2 секунд.

В целом заказчик доволен. Мы не ставили цель разогнать сайт до максимума на данном этапе. В будущем возможно с ростом трафика будем ещё переделывать кодовую базу. Пока на этом всё. Мне будет интересно прочесть комментарии от других разработчиков что можно было делать лучше, а где были допущены ошибки.

Комментарии (15)


  1. Hayate
    22.09.2021 20:11
    +5

    Когда-то давно, из любопытства, как оно там у буржуев, взял заказ на апворке. Был поражён тем какой гавнокод плещется даже в популярных плагинах для вордпресса. Например какой-то популярный плагин для локализации, мне помнится, просто во вложенных циклах обходил буферизированный контент производя замены слов на "локализованные". Конечно оно страшно тормозило, отчёт профилировщика мог вызвать у неподготовленного человека шок.


    1. arkamax
      28.09.2021 07:31
      +2

      WordPress? Вчера разгребал милейший race condition в стоковой поставке 5.7.3. В библиотеке генерации паролей для новых аккаунтов:

      	function generatePassword() {
      		if ( typeof zxcvbn !== 'function' ) {
      			setTimeout( generatePassword, 50 );
      			return;
          } ....

      Т.е. проверяем, подгружена ли определенная функциональность, и если нет, давайте подождем 50 миллисекунд и попробуем еще раз (это вместо того, чтобы нормально завязаться на инициализацию, или исправить проблему с тем, почему код вызывается до полной инициализации). И так в Вордпрессе сделано чуть более, чем почти всё.


  1. drinkmaker
    22.09.2021 22:58
    +2

    Для подобных задач пользуюсь инструментами от blackfire. Могу смело рекомендовать. Показывает всё тоже, но в более удобной форме.


  1. mSnus
    23.09.2021 02:03

    Нет, серьезно, после оптимизации получилось всего 6 секунд и заказчик доволен? /sarcasm

    на этот раз проблема в другом

    Проблема в том, что мыши колются, плачут, но продолжают грызть WP-кактус..


    1. cosmoCoder Автор
      23.09.2021 04:29
      +1

      Это мой локальный сервер где в ноуте кроме OpenServer работает ещё куча других программ. Я потому и под конец написал что после ещё донастроек боевого сервера результат ~400мс, а без кеширования 1,5-2 сек, только эти донастройки касаются только этого сайта и для других мало пользы от этой информации.

      Заказчик доволен хотя бы тем что он не просил меня сделать из его сайта суперджет. Он попросил посмотреть что так тормозит его сайт. Статью пишу в надежде что пользователи могут узнать что WP Object Cache может тупить, и что транзиенты можно использовать как хранилище кеша (для вот таких маленьких кусков).

      Хорош или плох WP - не тема этой статьи. Спасибо что хоть язык php не поругали)


      1. mSnus
        23.09.2021 04:35
        +1

        Да, применение WP+WooCommerce для большого каталога — это ошибка.


        Пытаться лечить его, наклеивая пластырь поверх гипса поверх подорожника устанавливая плагин, который должен кешировать, но не работает, поэтому поверх сделаем кеширование, но не совсем кеширование… ну, костыль знатный вышел.


        Я бы ещё понял, если бы вы настроили нормальное кеширование через Nginx. Но — нет, пошли по пути костылестроения. Лучше настройте джинкс, главное, корзину не закешируйте ))


        1. cosmoCoder Автор
          23.09.2021 07:33

          Найду человека который написал тему для сайта, передам ему) Не понимаю причём тут нжинкс с кешированием, я же отлавливаю тяжелый кусок кода

          Хотя справедливости ради да, кеш от nginx лучше чем плагинами. Спасибо


          1. mSnus
            23.09.2021 21:29
            +1

            Я понимаю вашу боль, мне приходилось возиться с сайтом на Dupal 5 с теми же проблемами. Я пару лет его потихоньку костылил, но единственное нормальное решение — это переложить кеширование всего, что можно, на Nginx. Тогда кривые и тяжелые запросы (а Друпал в этом плане ещё хуже ВП) просто не запускаются, и после прогрева кеша сайт летает. Особенно это актуально для больших каталогов.


  1. shushu
    23.09.2021 02:30
    +1

    посмотрите в сторону: xhprof или tideway (https://github.com/tideways/php-xhprof-extension)


    1. Dangetsu-PK
      27.09.2021 21:09

      Что касается xhprof, то он врет на проектах с большим количеством обрабатываемых сущностей и вложенностей. Я так замерял наш проект на ларе через xdebug и xhprof, и эти инструменты дали наводку что больше всего времени занимает трансформация моделей в апи ответ. Я сперва поверил, оптимизации там крутил, а потом проверил старым дедовским методом через echo time() в контроллере, и оказалось что трансформеры вообще немного отжирали. Судя по всему логирование каждой вложенности дается очень дорого, вот если бы можно было регулировать уровень до которого анализ должен доходить...


  1. zuborg
    23.09.2021 12:31

    Может кто-то знает способ получить стэктрейс для работающего в текущий момент php-fpm процесса? Чтобы понять в каком месте он выполняется в текущий момент.


    1. vtvz_ru
      23.09.2021 13:05

      Раз, два, если я правильно понял вопрос


      1. zuborg
        23.09.2021 13:11

        Спасибо, но речь не о том, чтобы процесс получил свой стэктрейс, с этим проблем нет.

        Вопрос в том, как снаружи узнать, в каком месте кода сейчас выполняется php-fpm процесс (возможна соотв модификация кода). Ну, ждет ответ от базы, например, или вычисляет что-то в какой-то функции.. С точностью до строки - вообще идеально, с точностью до функции - тоже годится. Есть ли какой-то модуль для этого?


        1. dimti
          25.09.2021 15:41

          Никогда этого не делал, но иногда, хотел, потрогать.

          Не знаю как оно, даст ли прийти к вашей цели. Но есть, как я это понимаю, штука для мониторинга приложений на php (и не только на php, а ещё и есть штука для отслеживания пользовательских ошибок в исполняемом в их же браузерах javascript).

          Все, это, конечно стоит безумных денег для сколь либо приемлемой аналитики. И с поддержкой в России у них "так себе" (никак). Но сервис запили знатный https://docs.newrelic.com/docs/agents/php-agent/getting-started/introduction-new-relic-php/


  1. Dangetsu-PK
    27.09.2021 21:14
    +1

    К сожалению, xdebug не является серебряной пулей в определении узкого горлышка, и не подойдет для каждого проекта. Но вот для просмотра стектрейса - шикарная штука!
    Пробовал еще xhprof, но он тоже сильно грешил при работе с большим кол-вом вложенностей...