Профилирование корзины магазина на Битрикс

Магазин на Бириксе с 510 товарами в корзине тормозит при ее пересчете, наблюдалась задержка от 10 секунд до 2 минут (в моменты сильной нагрузки сервера).

Результаты профилирования PHP с помощью XHProf

  • Самая тормозная функция mysql_query (запрос к БД) - 27% общего времени выполнения скрипта, 32% - загрузка CPU, причем эта функция вызывается только в 0.5% случаев от общего количества вызовов
  • Следущие «долгоиграющие» функции CAllDBResult::GetNext, CIBElement::GetProperties - не являются критичными, занимая порядка 4% общего времени каждая. По сути они занимаются «разбором» полученных от БД данных.
  • Остальные функции выполняются 2% времени каждая, в сумме составляя остальную часть времени выполнения.
  • Самые «популярные» функции: is_array (534283 вызовов), array_key_exists (338574), preg_match (233013) являются «нативными» для PHP, выполняются быстро

Таким образом, самое узкое место - выполнение запросов к БД.

Результаты профилирования запросов к БД с помощью Percona

  • Практически все запросы кешируются (это хорошо)
  • Время выполнения медленных запросов: 128 мс в 95% случаев (это довольно медленно), 560 мс - общее время выполнения медленных запросов (это хорошо), т.е. медленные запросы есть, но это не критично
  • Общее количество запросов для перестроения страницы в корзине: 20956 (это очень плохо)

Таким образом, запросы к БД в целом выполняются быстро, но они берут «количеством».

«Количественные» выборки отлично кешируются в memcached, что гораздо быстрее кеша запросов MySQL.

Анализ кода

50% времени уходит на получение товаров из корзины:

CSaleBasket::DoGetUserShoppingCart(SITE_ID, intval($USER->GetID()), intval(CSaleBasket::GetBasketUserID()), $arErrors, $arCupon);

Это - нативная функция Битрикс. Пути ее оптимизации:

  • переписать - нереально
  • закешировать вывод - нельзя, так как она оперирует актуальными данными
  • ускорить выполнение - только за счет ускорения выполнения запросов к БД

Выявлены грубые ошибки:

  • CSaleBasket::GetList() вызывается 515 раз, занимая 7% времени. Причем возвращаемое значение всегда одно и тоже. Следует вызвать этот метод один раз и дальше уже использовать переменную.
  • Аналогично: CAllSaleBasket::GetBasketUserID() (1025 раз, 0,3% времени).

Предложения

  • использовать более производительный MySQL сервер:
    • задействовать SSD-диски, так как именно дисковая подсистема является главным тормозом
    • установить Percona Server (он является клоном MySQL), и использовать его вместо MySQL - производительность выше в 2-3 раза
  • не производить «динамический» пересчет при количестве позиций более 50, выводить вместо этого внизу кнопку «пересчитать» - тогда пересчет, хоть и долгий, будет произведен один раз
  • переписать систему скидок (реализовано)