На очень большом и тяжелом сайте столкнулся с просадкой производительности, при запуске какой-либо рекламной компании. Вроде бы это нормальная история, но в один прекрасный день случилось что-то странное. Не с того ни с сего сайт на Bitrix стал ощутимо тормозить. Что ж, капнем.

Начало истории

Как оказалось по мониторингу начала захлебываться БД. При этом количество запросов, идущих к сайту было в статистической норме для этого времени дня. Покопавшись в запросах увидел что запрашиваются один и те же страницы, но с добавлением странных utm меток. Тут тоже вопросов бы не возникло, мало ли откуда идет трафик, мало ли кто решил отследить свою компанию.

Проблема заключалась в том, что эти несколько страниц являются очень тяжелыми (сотни запросов в MySQL), но в то же время совершенно не зависела от GET параметров запроса, в расчет брался только путь страницы. Эта проблема должна была решиться тем, что после первого захода страница (а точнее компоненты на ней) кешировались. После этого количество запросов падало менее десяти (при этом первые два были стандартной настройкой соединения Bitrix).

Проверка

Что бы абстрагироваться от конкретного сайта подниму стенд с битриксом. Для тестов установлю стандартный интернет магазин. Он конечно небольшой (не более 20к товаров с сотнями свойств и разделов) и пользоваться им будут я один (и он не будет постоянно находиться под около 300rps только php запросов). Но зато можно в любой момент поднять “такой же” и посмотреть “своими руками”.

Итак, включим статистику SQL через параметр show_sql_stat=Y в url (для этого нужно авторизоваться и получить доступ к функционалу).

Список товаров

Запрашиваем первый раз страницу: 246 запроса и 0.1902 времени генерации.

Список товаров с пустым кешем

И тут же повторяем запрос, что бы поглядеть на кешированную версию: 32 запроса и 0.0781 секунды генерации. Страница реально почти полностью получена из кеша и база может отдохнуть.

Закешированная страница списка товаров

Но что если добавить какой-либо get параметр. А тут мы получаем очень странные значения: 133 запроса и 0.1433 секунды. Это почти в 4 раза больше, чем на странице с кешем.

Список товаров с get параметром в url

Видимо не все компоненты на странице сбросили кеш (или промахнулись).

Детальная карточка товара

Тут ничего удивительного, нет кеша: 282 запроса и 0.3329 секунды на страницу. После этого запросим страницу еще раз, теперь то она должна отдаваться из кеша.

Карточка товара с пустым кешем

Как ни странно, но да, из кеша: 30 запросов и 0.0765 секунды на генерацию страницы.

Кешированная карточка товара

Но если мы опять добавим какой-то get параметр (например asd=asd), то наша страница опять будет частично “собрана с нуля”. Мы получаем 185 запросов и 0.1722 секунды на генерацию страницы.

Детальная страница с get параметром в url

В этом случае такая же история, какие-то компоненты просто промахнулись мимо кеша и выполнились заново.

Объяснение

Кеш компонента в Bitrix ВСЕГДА полагается на свой уникальный идентификатор (md5 строка). Именно так компонент понимает и сравнивает вызовы себя. Сам этот расчет происходит еще в старой реализации \CBitrixComponent::getCacheID. Тут собирается длинная строка, в которую добавляются все зависимости кеша. Внутри кеш будет зависеть от:

  • идентификатора текущего сайта;
  • идентификатора текущего языка;
  • идентификатора текущего шаблона;
  • названия компонента;
  • шаблона компонента;
  • списка параметров компонента (учитываются и значения и порядок этих параметров);
  • смещения часового пояса;
  • дополнительных параметров компонента;
  • порядкового номера вызова компонента с данным именем.

По сути ни про какие get параметры тут не упоминается. Разве что разработчики запихнули их в виде дополнительных параметров кеша ($additionalCacheID). Но оказывается нет, если начать разбирать строку кеши для компонентов, то станет видно, что при изменении набора и значений get параметров - уникальная строка кеширования тоже меняется.

Раскопав все хитросплетения формирования строки кеша на примере этих двух страниц, был найден инициатор этого праздника жизни \Bitrix\Iblock\Component\Base. Оказывается в нем, есть маленький кусочек кода, который и дробит кеш компонента под все возможные get параметры. Это переопределенная функция \Bitrix\Iblock\Component\Base::onPrepareComponentParams, а точнее ее часть:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function onPrepareComponentParams($params) {
    /** ... */
    if (!isset($params['CURRENT_BASE_PAGE']))
    {
        $uri = new Main\Web\Uri($this->request->getRequestUri());
        $uri->deleteParams(Main\HttpRequest::getSystemParameters());
        $params['CURRENT_BASE_PAGE'] = $uri->getUri();
    }
    /** ... */
}

То есть, если мы не задаем параметр компонента CURRENT_BASE_PAGE, то компонент сам берет полный текущий url включая все get параметры, кроме “системных” и зашитых внутри \Bitrix\Main\HttpRequest::getSystemParameters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static function getSystemParameters()
{
    static $params = array(
        "login",
        "login_form",
        "logout",
        "register",
        "forgot_password",
        "change_password",
        "confirm_registration",
        "confirm_code",
        "confirm_user_id",
        "bitrix_include_areas",
        "clear_cache",
        "show_page_exec_time",
        "show_include_exec_time",
        "show_sql_stat",
        "show_cache_stat",
        "show_link_stat",
        "sessid",
    );
    return $params;
}

Цепочка получается такая:

  1. параметр CURRENT_BASE_PAGE не указан (а его и нет в настройках компонента или его упоминания на сайте);
  2. параметр заполняется полным урлом со всеми гет параметрам, которые окажутся в url;
  3. параметр CURRENT_BASE_PAGE учитывается при расчете использования уже готового кеша.

Особенность параметра

Так же этот параметр имеет особенность и очень важную. Если в компоненте не установить параметру PAGER_BASE_LINK_ENABLE значение Y, то именно строка из CURRENT_BASE_PAGE будет использована как основной путь для построения url пагинации. То есть получается, что в CURRENT_BASE_PAGE нельзя задать просто задать свое значение. Ведь если указать просто строку, скажем Y, то именно от нее будет строиться пагинация, на сайте появятся не рабочие ссылки вида Y?PAGEN_1=2. Если же туда вписать путь страницы без get параметров, то и в ссылки пагинации они тоже пропадут. То есть следует всегда иметь в виду, при ручном указании CURRENT_BASE_PAGE обязательно еще включить PAGER_BASE_LINK_ENABLE.