
В своей работе мы часто сталкиваемся с «1С-Битрикс: Управление сайтом». Мы разделяем боль всех тех, кто пишет о его недостатках, но в то же время понимаем и сторону тех, кто «просто умеет его готовить». Этой статьей мы бы хотели открыть цикл материалов об оптимизации производительности битрикса.
Где бы и когда бы вы ни услышали апологетов битрикса, будь то маркетологи, продающие решения для интернет магазинов, или специалисты битрикса, пытающиеся защищаться от критики из-за низкой скорости работы, они точно будут говорить про кэширование. Приведу небольшую цитату из официальной документации.
Цитатник веб-разработчиков:
Антон Долганин: На данный момент кеширование Битрикса фактически совершенно, и не стоит изобретать своих велосипедов.
И еще одну:
Если в качестве примера брать интернет-магазин, то для каждого товара будет создан файл в кеше, чтобы при следующем обращении покупателя сервер не напрягался с запросами к БД. Это и позволяет запускать магазины уровня Эльдорадо.
Что ж, раз уж Эльдорадо не смогли обойти стороной такой важный вопрос, то и мы начнем именно с него.
Но хватит лирики. Дальше приведем сухие и сжатые строки нашего внутрикорпоративного регламента по «1С-Битрикс: Управление сайтом».
Каждый проект должен разрабатываться с включенным кэшированием «1С-Битрикс: Управление сайтом». Это следует делать для того, чтобы на этапе разработки выяснить все возможные проблемы и ошибки, связанные с кэшированием.
Число запросов к базе данных при включенном кэшировании должно быть сведено к минимуму. Каждая страница должна делать только необходимое и достаточное число запросов к базе данных.
Диагностику числа запросов со страницы можно производить встроенными средствами «1С-Битрикс: Управление сайтом»:
- С помощью тестирования производительности "Монитором производительности",
- С помощью вывода отладочной информации на странице
При работе с кэшированием следует помнить, что весь код внутри кэша исполняться не будет. Предположим, что вам нужно поменять заголовок окна браузера в компоненте 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.phpclass 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;