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

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

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

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

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

Сначала приведём пример стандартной задачи и покажем, что оперативная память сервера быстро расходуется при использовании метода 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 Мб.

Рис. 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 "<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>";

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

Рис. 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 — набор классов для итерации объектов. При их использовании у вас появляются дополнительные возможности при итерировании массивов и объектов.

Источник: https://habr.com/ru/articles/771438/


Интересные статьи

Интересные статьи

Случается, что компании уделяют недостаточное внимание проработке своего ценностного предложения. И совершенно зря, ведь ценностное предложение — это главная причина, почему люди выбирают продукт имен...
В Skyeng есть команда коммуникаций. Она предоставляет инструменты для связи оператора с пользователем. Например, ученику плохо слышно преподавателя на уроке и он хочет пообщаться с поддержкой, чтобы р...
Все началось в 1953 году, когда компания IBM выпустила свой первый коммерческий компьютер. И вот сегодня мы обсуждаем бессерверную архитектуру. За прошедшие годы вычислит...
Всем привет! Это будет очень маленькая статья. Наша задача тут: подключиться к локальному серверу FTP (я выбрала FileZilla) и отправить туда чего-нибудь используя (очевидно) FTP протокол....
В прошлой статье я сказал, что нам пора переходить к потоковым протоколам. Но начав подготовку рассказа о них, я понял, что сам плаваю в одной очень важной теме. Как уже отмечалось, у меня с Лину...