Блог компании CREATIVE

Производительность 1С-Битрикс: кэширование

В своей работе мы часто сталкиваемся с «1С-Битрикс: Управление сайтом». Мы разделяем боль всех тех, кто пишет о его недостатках, но в то же время понимаем и сторону тех, кто «просто умеет его готовить». Этой статьей мы бы хотели открыть цикл материалов об оптимизации производительности битрикса.

Где бы и когда бы вы ни услышали апологетов битрикса, будь то маркетологи, продающие решения для интернет магазинов, или специалисты битрикса, пытающиеся защищаться от критики из-за низкой скорости работы, они точно будут говорить про кэширование. Приведу небольшую цитату из официальной документации.

Цитатник веб-разработчиков:
Антон Долганин: На данный момент кеширование Битрикса фактически совершенно, и не стоит изобретать своих велосипедов.

И еще одну:
Если в качестве примера брать интернет-магазин, то для каждого товара будет создан файл в кеше, чтобы при следующем обращении покупателя сервер не напрягался с запросами к БД. Это и позволяет запускать магазины уровня Эльдорадо.

Что ж, раз уж Эльдорадо не смогли обойти стороной такой важный вопрос, то и мы начнем именно с него.

Но хватит лирики. Дальше приведем сухие и сжатые строки нашего внутрикорпоративного регламента по «1С-Битрикс: Управление сайтом».

Каждый проект должен разрабатываться с включенным кэшированием «1С-Битрикс: Управление сайтом». Это следует делать для того, чтобы на этапе разработки выяснить все возможные проблемы и ошибки, связанные с кэшированием.

Число запросов к базе данных при включенном кэшировании должно быть сведено к минимуму. Каждая страница должна делать только необходимое и достаточное число запросов к базе данных.

Диагностику числа запросов со страницы можно производить встроенными средствами «1С-Битрикс: Управление сайтом»:

  1. С помощью тестирования производительности "Монитором производительности",
  2. С помощью вывода отладочной информации на странице

При работе с кэшированием следует помнить, что весь код внутри кэша исполняться не будет. Предположим, что вам нужно поменять заголовок окна браузера в компоненте bitrix:catalog.element. Мы знаем, что в bitrix:catalog.element настроено и включено кэширование, а, значит, весь код в файлах template.php и result_modifer.php будет выполнен только один раз во время создания кэша. Соответственно, строка $APPLICATION->SetTitle(‘title’); внутри result_modifer.php при включенном кэше работать не будет. В таких случаях нужно использовать component_epilog.php.


Важным моментом при использовании кэша является правильное создание идентификаторов кэша. По умолчанию в каждом компоненте «1С-Битрикс: Управление сайтом» для создания идентификатора кэша используется массив входящих данных $arParams, поэтому, если мы будем в один из параметров передавать timestamp, то мы рискуем получить огромное количество файлов кэша, которое займет все доступное дисковое пространство. Это произойдет из-за того, что идентификатор кэша будет изменяться каждую секунду из-за изменения параметра, в котором передается timestamp. Поэтому нужно быть очень внимательным с такими опциями, как «Кэшировать при установленном фильтре» или при самостоятельном использовании кэша.

Отдельно нужно отметить то, что для компонента bitrix:menu по умолчанию кэш создается для каждой страницы сайта, на которой имеется данное меню. Если меню есть в шаблоне, то мы рискуем получить по одному файлу кэша меню для каждой страницы сайта. Если в шаблоне два меню, то по два. Если три, то по три и т.д. Это поведение можно и нужно отключить с помощью опции CACHE_SELECTED_ITEMS, установленной в N.

$APPLICATION->IncludeComponent(
"bitrix:menu",
"footer_menu",
Array(
"ROOT_MENU_TYPE" => "top", // Тип меню для первого уровня
"MENU_CACHE_TYPE" => "Y", // Тип кеширования
"MENU_CACHE_TIME" => "360000", // Время кеширования (сек.)
"MENU_CACHE_USE_GROUPS" => "N", // Учитывать права доступа
"MENU_CACHE_GET_VARS" => "", // Значимые переменные запроса
"MAX_LEVEL" => "1", // Уровень вложенности меню
"CHILD_MENU_TYPE" => "left", // Тип меню для остальных уровней
"USE_EXT" => "N", // Подключать файлы с именами вида .тип_меню.menu_ext.php
"DELAY" => "N", // Откладывать выполнение шаблона меню
"ALLOW_MULTI_SELECT" => "N", // Разрешить несколько активных пунктов одновременно
"CACHE_SELECTED_ITEMS" => "N",
),
false
);

Нужно понимать, что при включении этой опции, выбор активного пункта меню («подсветка» текущей страницы) работать перестанет, поэтому этот вопрос нужно будет решать иными средствами.

При использовании в качестве ключа кэша одного или нескольких параметров, приходящих от пользователя, нужно помнить, что при невалидном параметре кэш создавать не следует. В противном случае возникнет проблема схожая с пунктом 5. Кроме того, входящие параметры от пользователя всегда нужно обрабатывать так, чтобы не создавался отдельный кэш для null, '' и 0. Предположим, что мы создаем кэш:

$obCache = new CPHPCache();
if ($obCache->InitCache(300000000000, $_GET['id'], '/')) {
$vars = $obCache->GetVars();
} elseif ($obCache->StartDataCache()) {
$res = CIBlockElement::GetById($_GET['id']);
$obCache->EndDataCache($res->Fetch());
}

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

$obCache = new CPHPCache();
//обработаем параметр и приведем его к int
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($obCache->InitCache(300000000000, $id, '/')) {
$vars = $obCache->GetVars();
} elseif ($obCache->StartDataCache()) {
$res = CIBlockElement::GetById($id);
$vars = $res->Fetch();
if ($vars) {
//если нашли объект, то записываем его в кэш
$obCache->EndDataCache($vars);
} else {
//если не нашли, то создание кэша нужно отменить
$obCache->AbortDataCache();
}
}


Следует создавать как можно меньше файлов кэша, которые покроют как можно большее число запросов. Например, существует инфоблок с событиями организации. На странице нужно выводить только те события, которые проходят в данный момент или пройдут в будущем и скрывать прошедшие. В таком случае нам следует исключить из идентификатора кэша параметр фильтрации
'>=ACTIVE_FROM' => ConvertTimeStamp(time(), 'FULL'), а прошедшие события отфильтровать на стороне php. Пример:

$obCache = new CPHPCache();
//создаем кэш на один день
if ($obCache->InitCache(86400, 'today_events', '/')) {
$events = $obCache->GetVars();
} elseif ($obCache->StartDataCache()) {
$res = CIBlockElement::GetList(
[],
//не будем запрашивать весь список, запросим только активные события на момент создания кэша
['IBLOCK_ID' => 1, 'ACTIVE' => 'Y', '>=ACTIVE_FROM' => ConvertTimeStamp(time(), 'FULL')],
false,
false,
['ID', 'NAME', 'PREVIEW_TEXT', 'DATE_ACTIVE_FROM']
);
$events = [];
while ($ob = $res->GetNext()) {
$events[] = $ob;
}
$obCache->EndDataCache($events);
}
//кэш готов, весь день мы будем получать один и тот же список событий
//теперь нужно отфильтровать прошедшие события
$newEvents = [];
$now = time();
foreach ($events as $event) {
if (strtotime($event['DATE_ACTIVE_FROM']) < $now) continue;
$newEvents[] = $event;
}

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

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

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

//init.php
class CitiesRepo
{
//в эту переменную мы сложим данные запроса
protected static $_data = null;

public static function getList()
{
//только если мы еще ничего не запрашивали
//обратите внимание на строгое равенство
if (self::$_data === null) {
//на случай, если не получим ничего из базы, зададим пустой массив, чтобы не запрашивать повторно
self::$_data = [];
$obCache = new CPHPCache();
if ($obCache->InitCache(86400, 'CitiesRepo', '/')) {
//сначала пытаемся получить данные из кэша
self::$_data = $obCache->GetVars();
} elseif ($obCache->StartDataCache()) {
//если не удалось, то запрашиваем базу
$res = CIBlockElement::GetList(
[],
['IBLOCK_ID' => 1, 'ACTIVE' => 'Y'],
false,
false,
['ID', 'NAME', 'CODE']
);
while ($ob = $res->GetNext()) {
self::$_data[] = $ob;
}
$obCache->EndDataCache(self::$_data);
}
}
return self::$_data;
}
}

//какой-то из многочисленных template.php
$cities = CitiesRepo::getList(); //подгрузит данные из кэша или из базы

//какой-то иной из многочисленных template.php
$cities = CitiesRepo::getList(); //не совершит практически никаких операций, просто вернет массив, который уже в памяти

С другой стороны, если помимо запроса из базы данных, нам нужно будет провести довольно сложную обработку данных, то, наоборот, следует усложнить идентификатор кэша и создать побольше файлов, в которых будет храниться готовый к выводу в браузер пользователя текст. Предположим, что нам нужно вывести в json большой массив с городами, чтобы передать его через ajax. В таком случае есть смысл закэшировать именно строку с готовым к отправке json:

$obCache = new CPHPCache();
//предположим, что у нас есть фильтр по областям
//обязательно обработаем входящую переменную с идентификатором области и добавим ее в идентификатор кэша
$cId = 'cities_json';
$stateId = isset($_GET['stateId']) ? intval($_GET['stateId']) : 0;
if ($stateId) $cId .= $stateId;
//кэшируем вывод
if (!$obCache->InitCache(86400, $stateId, '/') && $obCache->StartDataCache()) {
//в кэше у нас уже есть готовая строка, поэтому InitCache выкинет ее сразу же на вывод, данных из кэша получать не нужно
//если готовой строки нет, то запрашиваем базу
$filter = ['IBLOCK_ID' => 1, 'ACTIVE' => 'Y'];
//добавляем фильтр по области
if ($stateId) $filter['PROPERTY_STATE'] = $stateId;
$res = CIBlockElement::GetList(
[],
$filter,
false,
false,
['ID', 'NAME', 'CODE']
);
$cities = [];
//получаем несколько тысяч записей, каждый раз преобразовывать их в json будет накладно
while ($ob = $res->GetNext()) {
$cities[] = $ob;
}
//просто сохраняем уже готовый текст
//данный текст уйдет в буфер вывода, а затем будет сохранен в кэш
echo json_encode($cities);
$obCache->EndDataCache();
}

Большой прирост к производительности может дать переключение кэша с файлов на какое-либо более быстрое хранилище, например, memcache. Однако всегда следует помнить о возможностях сервера, на котором будет располагаться проект. Memcache требователен к оперативной памяти. Если ее будет мало, то мы рискуем получить постоянное вытеснение одних записей кэша другими, а, значит, постоянные запросы к базе данных.
Разработка