Пример применения генератора в Битрикс: как не ронять сервер на больших выгрузках

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

Посмотрим, как можно сэкономить ресурсы сервера, чтобы таких вопросов не возникало.

Зачем это надо 

Сначала приведём пример стандартной задачи и покажем, что оперативная память сервера быстро расходуется при использовании метода 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  "
Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."
»; $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 »
Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."
»; Bitrix\Main\Diag\Debug: endTimeLabel ($debuglable); $lable= Bitrix\Main\Diag\Debug: getTimeLabels (); echo «Выборка из ».$counter.» элементов :
";
echo 'Время выполнения скрипта: '. $lable[$debuglable]['time'];
echo "
»;

Получим такой результат (рис. 1). Зафиксируем, что размер выборки одного элемента составляет около 1,5 Мб.

Рис. 1. Выборка из 1 элемента.

Рис. 1. Выборка из 1 элемента.

Увеличим выборку до 10 элементов, изменяя переменную $counter, для регулирования количества выводимых элементов:

Рис. 2. Выборка из 10 элементов.

Рис. 2. Выборка из 10 элементов.

Увеличим выборку до 100 элементов:

Рис. 3. Выборка из 100 элементов.

Рис. 3. Выборка из 100 элементов.

Ну и увеличим до 1000 элементов:

Рис. 4. Выборка из 1000 элементов.

Рис. 4. Выборка из 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 "
Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."
»; // Получение всех элементов инфоблока $elements = CIBlockElement: GetList ( array (), array («IBLOCK_ID» => $iblockId), false, ['nTopCount' => $counter], array («ID», «IBLOCK_ID», «NAME») ); $propertyes=getProperties ($elements); echo »
Количество используемой оперативной памяти: ". round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL."
»; Bitrix\Main\Diag\Debug: endTimeLabel ($debuglable); $lable= Bitrix\Main\Diag\Debug: getTimeLabels (); echo «Выборка из ».$counter.» элементов :
";
echo 'Время выполнения скрипта: '. $lable[$debuglable]['time'];
echo "
»;

При выборке одного элемента результат тот же что и при первом методе:

Рис. 5. Использование генератора: выборка из 1 элемента.

Рис. 5. Использование генератора: выборка из 1 элемента.

При выборке 10 элементов потребление памяти не меняется:

Рис. 6. Использование генератора: выборка из 10 элементов.

Рис. 6. Использование генератора: выборка из 10 элементов.

На 100 элементах потребление памяти также не растёт:

Рис. 7. Использование генератора: выборка из 100 элементов.

Рис. 7. Использование генератора: выборка из 100 элементов.

Ну и проведём финальный эксперимент. Сделаем выборку из 20 000 элементов. Помним, что при первом варианте такая выборка укладывала сервер.

В результате скрипт выполнялся минуту, но потребление памяти выросло совсем немного:

Рис. 8. Использование генератора: выборка из 20 000 элементов.

Рис. 8. Использование генератора: выборка из 20 000 элементов.

За счет чего мы получили такой результат? Ответ кроется в природе генераторов. При создании массива весь массив помещается в память целиком, а при использовании генератора при итерировании вы каждый раз получаете только один элемент итерируемого массива. Что и позволяет снизить потребление оперативной памяти.

В результате мы получили легкий понятный код. Не переусложнёный пагинацией обращений к БД. И то, что при первом варианте в принципе было невозможно на текущей конфигурации сервера, теперь выполняется, и мы получаем необходимый нам результат.

Где в Битрикс можно применить данный подход?

Во-первых, это всё, что связано с выгрузкой каталога интернет-магазина: формирование прайслистов, фидов и другое.

Во-вторых, обработка данных пользователей:

  • чистка от регистраций ботов,

  • изменение формата телефонов,

  • добавление или удаление каких-то свойств.


В-третьих, это массовое изменение свойств товаров: цен, характеристик и т.п. 

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

P.S. Ну и напоследок скажу, что для обработки массивов и объектов PHP предлагает удобные инструменты библиотеки SPL — набор классов для итерации объектов. При их использовании у вас появляются дополнительные возможности при итерировании массивов и объектов.

© Habrahabr.ru