Пример применения генератора в Битрикс: как не ронять сервер на больших выгрузках
Бытует мнение, что Битрикс прожорлив, и способен поглотить все ресурсы, которые есть на сервере. Такая проблема действительно существует, и компаниям иногда приходится рассматривать покупку другой конфигурации сервера, например, при сильном расширении ассортимента или увеличении количества задач бизнеса, решаемых обращением к БД.
Посмотрим, как можно сэкономить ресурсы сервера, чтобы таких вопросов не возникало.
Зачем это надо
Сначала приведём пример стандартной задачи и покажем, что оперативная память сервера быстро расходуется при использовании метода GetList
. А затем разберёмся, как избежать проблемы.
Итак, у нас есть интернет-магазин на 50 000 товаров. У каждого товара есть 20 пользовательских свойств. Задача: пробежаться по всем товарам и что-то сделать, изменить какие-то свойства или выгрузить каталог.
В данном примере я показываю код в исследовательских целях.
Итак, что обычно делает программист Битрикс, когда надо получить элементы каталога:
$elements = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => $iblockId),
false,
false,
array("ID", "IBLOCK_ID", "NAME")
);
while ($element = $elements->GetNextElement()) {
$el=$element->GetFields();
$el['props’]=$element->GetProperties();
$items[]=$resElement;
}
На выгрузках в несколько тысяч элементов этот код сработает, и мы получим список элементов. Но уже на 20 000 элементах сервер отправляется в даун. Что же происходит, и почему сервер падает?
Чтобы это выяснить, используем дебаг методы Битрикс и метод PHP memory_get_usage(), который позволяет получить количество используемой оперативной памяти.
$debuglable='main';
Bitrix\Main\Diag\Debug::startTimeLabel($debuglable);
echo "<pre>Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."</pre>";
$counter=1;
$elements = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => $iblockId),
false,
[‘nPageSize’ =>$counter],
array("ID", "IBLOCK_ID", "NAME")
);
while ($element = $elements->GetNextElement()) {
$el=$element->GetFields();
$el['props’]=$element->GetProperties();
$items[]=$resElement;
}
echo "<pre>Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."</pre>";
Bitrix\Main\Diag\Debug::endTimeLabel($debuglable);
$lable= Bitrix\Main\Diag\Debug::getTimeLabels();
echo "Выборка из ".$counter." элементов : <pre>";
echo 'Время выполнения скрипта: '. $lable[$debuglable]['time'];
echo "</pre>";
Получим такой результат (рис.1). Зафиксируем, что размер выборки одного элемента составляет около 1,5 Мб.
Увеличим выборку до 10 элементов, изменяя переменную $counter
, для регулирования количества выводимых элементов:
Увеличим выборку до 100 элементов:
Ну и увеличим до 1000 элементов:
Итак, мы видим что при увеличении количества элементов быстро растёт потребление оперативной памяти, что в конечном счете приводит к тому, что объём данных превышает размер оперативной памяти сервера и сервер падает.
Конечно, данную проблему можно решить разбив запрос на несколько, используя параметр nOffset
, запустить цикл, получить результат нескольких запросов и решить вопрос.
Но так мы не решаем проблему, а скорее ее усугубляем. Усложняем код и получаем цикл запросов к БД.
Есть другой путь?
Можно использовать ключевое слово yield
в PHP для создания функции-генератора. Какая польза от yield в PHP?
Возможно, вы уже слышали, но на практике ещё не применяли. Обратимся к справке PHP:
Когда вызывается генератор, он возвращает объект, который можно итерировать. Когда вы итерируете этот объект (например, в цикле foreach), PHP вызывает методы итерации объекта каждый раз, когда вам нужно новое значение, после чего сохраняет состояние генератора и при следующем вызове возвращает следующее значение.
Когда все значения в генераторе закончились, генератор просто завершит работу, ничего не вернув. После этого основной код продолжит работу, как если бы в массиве закончились элементы для перебора.
Вся суть генератора заключается в ключевом слове yield. В самом простом варианте оператор "yield" можно рассматривать как оператор "return", за исключением того, что вместо прекращения работы функции, "yield" только приостанавливает её выполнение и возвращает текущее значение, и при следующем вызове функции она возобновит выполнение с места, на котором прервалась.
Как применить yield в нашем случае и что мы получим:
// Функция-генератор для получения свойств элемента инфоблока
function getProperties($elements)
{
while ($element = $elements->GetNextElement()) {
$resElement=$element->GetFields();
$resElement['PROPS']=$element->GetProperties();
yield $resElement;
}
}
$iblockId=7;
$counter=1;
echo "<pre>Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."</pre>";
// Получение всех элементов инфоблока
$elements = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => $iblockId),
false,
['nTopCount' => $counter],
array("ID", "IBLOCK_ID", "NAME")
);
$propertyes=getProperties($elements);
echo "<pre>Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."</pre>";
Bitrix\Main\Diag\Debug::endTimeLabel($debuglable);
$lable= Bitrix\Main\Diag\Debug::getTimeLabels();
echo "Выборка из ".$counter." элементов : <pre>";
echo 'Время выполнения скрипта: '. $lable[$debuglable]['time'];
echo "</pre>";
При выборке одного элемента результат тот же что и при первом методе:
При выборке 10 элементов потребление памяти не меняется:
На 100 элементах потребление памяти также не растёт:
Ну и проведём финальный эксперимент. Сделаем выборку из 20 000 элементов. Помним, что при первом варианте такая выборка укладывала сервер.
В результате скрипт выполнялся минуту, но потребление памяти выросло совсем немного:
За счет чего мы получили такой результат? Ответ кроется в природе генераторов. При создании массива весь массив помещается в память целиком, а при использовании генератора при итерировании вы каждый раз получаете только один элемент итерируемого массива. Что и позволяет снизить потребление оперативной памяти.
В результате мы получили легкий понятный код. Не переусложнёный пагинацией обращений к БД. И то, что при первом варианте в принципе было невозможно на текущей конфигурации сервера, теперь выполняется, и мы получаем необходимый нам результат.
Где в Битрикс можно применить данный подход?
Во-первых, это всё, что связано с выгрузкой каталога интернет-магазина: формирование прайслистов, фидов и другое.
Во-вторых, обработка данных пользователей:
чистка от регистраций ботов,
изменение формата телефонов,
добавление или удаление каких-то свойств.
В-третьих, это массовое изменение свойств товаров: цен, характеристик и т.п.
А также другие задачи, для выполнения которых требуется выгрузка большого количества элементов.
P.S. Ну и напоследок скажу, что для обработки массивов и объектов PHP предлагает удобные инструменты библиотеки SPL — набор классов для итерации объектов. При их использовании у вас появляются дополнительные возможности при итерировании массивов и объектов.