290  /  397

Примеры улучшения производительности

Просмотров: 773

Рекомендации по производительности на примерах

Рассмотрим примеры, как можно улучшить производительность проекта.

Кешируем правильно

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

Но работа кеша может быть организована не совсем корректно, что приводит к проблемам производительности.

Комплексный пример: класс кеширования пользователя

Как неправильно:

// Класс кеширования пользователя с некорретным кешированием
class User
{
	private $userData;
	function __construct($userId = false)
	{
		if (!$userId)
		{
			global $USER;
			$userId = $USER->GetID();
		}

		$cntStartCacheId = __CLASS__.'::'.__FUNCTION__.'|'.SITE_ID.'|'.$userId;
		$cache = new \CXxxCache($cntStartCacheId.'sid0',3600,'user_data');
		$this->userData = $cache->Init();

		if (null == $this->userData)
		{
			$this->putUserData(array("ID"=>$userId));
			$this->putUserData(\CUser::GetByID($userId)->Fetch());
			$this->putUserData(array("DEPARTMENT" => $this->getDepartment()));

			$cache->registerTag('USER_NAME_'.$userId);
			$cache->set($this->userData);
		}
	}
}
Что не так в примере выше:
1. Общая папка для кеша пользователей
Такой пример кеша практически не работает на проекте. На портале, где много пользователей, мы постоянно будем терять кеш Большое количество хитов попадает в том момент, когда кеш уже скинут. Происходит это из-за того, что кеш пользователей уникальный на каждого человека и его хотелось бы скидывать отдельно. В примере указана общая папка для кеша, а не отдельная для пользователя. Следовательно, когда мы хотим скинуть кеш для пользователя, мы скидываем его его для всех. , что негативно отразиться на его работе.
2. Время кеширования
Кеширование сущности в примере на 1 час - неэффективно. Сохранение на малое время может привести к массовой генерации кеша Простой пример: сотрудники ушли на обед и вернулись через час, а кеш уже у всех сброшен. . Такая проблема актуальна для больших компаний и проектов.
3. Заполнение пользователя методом GetByID
В кеш попадает большой объем данных о пользователе, а именно много ненужной информации (пароль, подтверждение пароля, настройки синхронизации и т.д.).
4. Использование Fetch
При использовании Fetch данные не экранируются и никак не проверяются, что приводит к ошибкам безопасности.

Как правильно:

GetID();
		}

		$cntStartCacheId = __CLASS__.'::'.__FUNCTION__.'|'.SITE_ID.'|'.$userId;
		$cache = new \CXxxCache(
			$cntStartCacheId.'sid0',
			// увеличили время кеширования
			604800,
			// путь для ключей кеша сделали зависимым от $userID
			'user_data/'.substr(md5($userId),2,2).'/'.$userId 
		);

		$this->userData = $cache->Init();

		if (null == $this->userData)
		{
			$this->putUserData(array("ID"=>$userId));

			// Выбираем только нужные поля
			$this->putUserData(\CUser::GetList(...)->GetNext(true, false));
			$this->putUserData(array("DEPARTMENT" => $this->getDepartment()));

			$cache->registerTag('USER_NAME_'.$userId);
			$cache->set($this->userData);
		}
	}
}
  • Увеличили время кеширования до недели;
  • Изменили папку хранения кеша (разложили персонализированный кеш по подпапкам);
  • Изменили сбор информации по пользователю с GetByID на GetList Достаточно часто в проектах многие используют вызов CIBlockElement::GetById. Простой, удобный метод, когда надо вытащить какое-то поле для элемента инфоблока. Но этот метод тянет все поля и все свойства элемента. В случае инфоблока с большим количеством свойств и большого числа посетителей на сайте этот простой запрос приводит к снижению производительности.

    Подробнее...
    ;
  • Заменили Fetch на GetNext(true, false).

Время кеширования (cache key and ttl)

Пример на компоненте, который отображает дни рождения. Установлено большое время кеширования и добавлен дополнительный параметр, который сам компонент никак не отрабатывает и его не обрабатывает шаблон. Но т.к. для параметры подставлен date, то в 0 часов получим новый ключ у кеша данного компонента.

IncludeComponent(
	"bitrix:intranet.structure.birthday.nearest",
	"widget",
	Array(
		"CACHE_TYPE" => "A",
		"CACHE_TIME" => "86450",
		"DATE_FORMAT" => "j F",
		"DETAIL_URL" => "#SITE_DIR#company/personal/user/#USER_ID#/",
		"DEPARTMENT" => "0",
		.....
		"CACHE_DATE" => date('dmy')
	)
);

Пример: как в API правильно подставлять ключи

В таком случае часто проставляют лишние параметры, что приводит к увеличению кеша. (например date без параметров приводит к обновлению кеша каждую секунду).
<?php

// Пример добавление в ключ кеша метки времени для корректного переключения кеша. Метка может быть и не из времени.

$cache = Bitrix\Main\Data\Cache::createInstance();
if ($cache->initCache(86450, '/some_key/'.date('myd').'/', '/some_dir/'))
{
	$var = $cache->getVars();
}
else
{
	// Получение данных
	$cache->startDataCache();
	$cache->endDataCache($var);
}

Отключаем сброс ключей

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

  1. Отключите кеширование элементов (сбрасывание тегированного кеша) перед импортом;
  2. Включите его по окончании процесса импорта элементов;
  3. Сбросьте те теги инфоблока, которые сбрасывались в данном случае.
В таком вариант кеш сбросится один раз после полной загрузки, а не после загрузки каждого элемента.

Индексация фасетного индекса также может быть отложена (т.е. отключена перед импортом и включена по окончании).

<?php
// Отключение сброса тегированного кеша инфоблоков и пересчета фасетного индекса, во время импорта.

Bitrix\Iblock\PropertyIndex\Manager::enableDeferredIndexing();
Bitrix\Catalog\Product\Sku::enableDeferredCalculation();

\CAllIBlock::disableTagCache($iblockID);

// Импорт элементов 

\CAllIBlock::enableTagCache($iblockID);
\CAllIBlock::clearIblockTagCache($iblockID);

Bitrix\Catalog\Product\Sku::disableDeferredCalculation();
Bitrix\Catalog\Product\Sku::calculate();

Bitrix\Iblock\PropertyIndex\Manager::disableDeferredIndexing();
Bitrix\Iblock\PropertyIndex\Manager::runDeferredIndexing($iblockID);

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


Итоги рекомендаций по кешированию

ХорошоПлохо
  • Уникальные ключи, в отдельных папках;
  • Только нужные поля в кеше;
  • Максимальное время хранения ключей;
  • Конечные списки в одном ключе;
  • Разумное количество тегов в ключе;
  • Сброс ключей по пути.
  • Закрывать кешем долгие вычисления или страницы;
  • Часто сбрасывать ключи, особенно в большом количестве.

Внимание! Не забывайте, что управляемый кеш инфоблоков очищается только при вызове CIBlockElement::Update. При изменении, например, свойств с помощью CIBlockElement::SetPropertyValueCode очистки не произойдет. Делаем вручную после изменения:
if(defined('BX_COMP_MANAGED_CACHE'))
   $GLOBALS['CACHE_MANAGER']->ClearByTag('iblock_id_'.$arParams['IBLOCK_ID']);

Снижаем нагрузку с помощью API

Фильтры по like

Часто встречаемая проблема на проектах это фильтры по like.

Пример в старом API:

<?php

// Различные вызовы АПИ и запросы которые они генерируют

Bitrix\Main\Loader::includeModule('iblock');
$rs = CIBlockElement::GetList(
	[], 
	['CODE' => 'xxx'], // правильно вариант данного фильтра ['=CODE' => 'xxx'], 
	false, 
	false, 
	['ID', 'CODE', 'NAME']
);

/*
SELECT BE.ID as ID,BE.CODE as CODE,BE.NAME as NAME
FROM
	b_iblock B
	INNER JOIN b_lang L ON B.LID=L.LID
	INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
WHERE
	1=1 AND ( ((((BE.CODE LIKE 'xxx')))))
		AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL))
	)
*/

Пример в ORM:

<?php

Bitrix\Iblock\ElementTable::getList([
	'select' => ['ID', 'NAME', 'CODE'],
	'filter' => ['CODE' => 'xxx'] // правильный вариант фильтра 'filter' => ['=CODE' => 'xxx']
]);

/*
SELECT
	`iblock_element`.`ID` AS `ID`,
	`iblock_element`.`NAME` AS `NAME`,
	`iblock_element`.`CODE` AS `CODE`
FROM `b_iblock_element` `iblock_element`
WHERE UPPER(`iblock_element`.`CODE`) like upper('xxx')
*/

Для ORM есть решение, это новый фильтр В обновлении main 17.5.2 в ORM появился новый фильтр.

Подробнее...
.

<?php

// С новым фильтром не получится допустить ошибку
Bitrix\Iblock\ElementTable::query()
	->setSelect(['ID', 'NAME', 'CODE'])
	->where('CODE','xxx')->exec();

/*
SELECT
	`iblock_element`.`ID` AS `ID`,
	`iblock_element`.`NAME` AS `NAME`,
	`iblock_element`.`CODE` AS `CODE`
FROM `b_iblock_element` `iblock_element`
WHERE `iblock_element`.`CODE` = 'xxx'
*/

В старых фильтрах необходимо контролировать like.


Подготовка собственных индексов

В штатном продукте не предусмотрены все индексы, т.к. все проекты разные. Для улучшения производительности не забывайте предусмотреть создание своих индексов. В этом поможет инструмент Монитор производительности Заочно нельзя сказать какие индексы необходимо создавать, надо всегда рассматривать конкретную ситуацию. Индексы нужны для конкретных выборок на конкретных проектах. В зависимости от архитектуры и логики проекта медленные запросы получаются у каждого свои, и для них нужны свои индексы, часто составные.
Страницы Анализ индексов и Список индексов - инструмент анализа и рекомендаций по созданию индексов.

Подробнее в курсе Администратор. Базовый
.


Убираем count

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

Проблема очень критична для Rest. Чтобы не использовать Count в случае если вам не нужно количество элементов Например вам нужно просто 10 последних записей. или вы делаете импорт всех записей по фильтру, передавайте параметр start= -1. Подробнее почитать об этом и посмотреть пример можно в документации по REST.


Предотвращаем срыв конвейера

Высоконагруженные проекты как правило реализованы с использованием кластера. В кластере есть одна часто встречаемая проблема - срыв конвейера. Конвейер это поток запросов, которые идут последовательно и ядро распределяет их по серверам: на один из слейвов, либо на мастер. Если идет запрос на изменение данных, то конвейер в рамках хита перестает работать и все запросы идут на master, при этом слейвы перестают обслуживать этот хит, оставаясь не занятыми.

Рекомендации по предотвращению срыва конвейера

  • Не должно быть файлов after_connect*.php в ядре продукта. Эти файлы содержат настройки подключения к базе данных для случаев, когда мы устанавливаем проект на какой-то хостинг;
  • Не использовать SET (в начале хита полностью срывает конвейер);
  • Минимизировать изменения в начале страницы (для отлавливания таких мест в коде поможет модуль Монитор производительности);
  • В ряде случаев изменения можно экранировать и они не сломают конвейер.

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

<?php
// Выполнение всех запросов на master сервере, без срыва конвейера

Bitrix\Main\Application::getInstance()->getConnectionPool()->useMasterOnly(true);

// Какие либо обновления и изменения

Bitrix\Main\Application::getInstance()->getConnectionPool()->useMasterOnly(false);

Например, таким образом сейчас обернута работа с тегированным кешем.

Вебинар Мастер-класс по производительности

Рассмотренную в уроке тему подробнее смотрите в вебинаре:

Мастер-класс по производительности. Highload проекты, как их сделать и поддерживать? Техноволна 6 от 08 апреля 2020.



6
Курсы разработаны в компании «1С-Битрикс»

Если вы нашли неточность в тексте, непонятное объяснение, пожалуйста, сообщите нам об этом в комментариях.
Развернуть комментарии