Формування xml прайсу на php завантажує проц на 100%. Чи можна оптимізувати?

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.


<?php echo '<?yml version="1.0" encoding="UTF-8"?>'; ?>
<?php
$i = 0;
?>
<yml_catalog>
<shop>
<offers>
@foreach ($products as $item)
<?php
$i++;
if ($i % 250 == 0) {
usleep(250000);
}
?>
<offerid="{{$item->product_id}}">


<currencyId>UAH</currencyId>
<article>{{$item->article}}</article>
<barcode>{{$item->barcode}}</barcode>
<vendorBarcode />
<vendorCode />
<categoryId>{{$item->category_id}}</categoryId>
<price>{{$item->price}}</price>
<vendorCode>{{$item->product_id}}</vendorCode>
<vendor>{{$item->brand}}</vendor>
<stock_quantity>{!! \App\Helpers\AppHelper::instance()->myAmount($item->product_id) !!}</stock_quantity>
@if ($lang == 'ua')
<name>{{$item->name_ua}}</name>
<description>
<![CDATA[{{$item->description_ua}}]]>
</description>
{!!$item->param_ua!!}
@else
<name>{{$item->name_ru}}</name>
<description>
<![CDATA[{{$item->description_ru}}]]>
</description>
{!!$item->param_ru!!}
@endif
{!!$item->pictures!!}
</offer>
@endforeach
</offers>
</shop>
</yml_catalog>

Потрібно генерувати xml прайси «на льоту» (потрібно динамічно відображати в ньому залишки товарів, для різих клієнтів з різних складів).

Генерація прайсу на 7000 позицій (2,4 Мб) займає до 15 секунд

Але проблема в тому, що при цьому процесор завантажується на 60%, а коли всі клієнти запускають свої cron-завдання по завантаженню собі прайсів, то все вмирає.

Можливо щось можна оптимізувати? Чи потрібно змінювати бізнес процес?

Параметри FPM

pm = dynamic
pm.max_children = 80
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 150

Параметри серверу

Xeon E5-2630 2.3GHz, 2 Physical core, 2 Logical core

15 Gb RAM





			
	
👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Параметри серверу
Xeon E5-2630 2.3GHz, 2 Physical core, 2 Logical core

E5-2630 6С/12T ark.intel.com/...​z-7-20-gts-intel-qpi.html
тобто, або це не E5-2630
або то віртуальна машина з 2x vCPU

можна кешувати самі запити eloquent
medium.com/...​in-laravel-6-af722c09a6f7

можна генерувати файл по крону і віддавати тільки готовий файл

можна отримати всі дані мінімальною кількістю запитів до бази поза циклом, а вже тоді працювати з цими даними. смикати базу в циклі так собі затія

Закешить на уровне nginx, три строки и пять минут работы.

Мені вбачається ідіотизмом дьоргати 7000 разів ось це

\App\Helpers\AppHelper::instance()->myAmount($item->product_id)
через API замість того, щоб запросити напряму через SQL, чи як ти там на базу дивися.

Так от, подивися одразу ВСЕ, що тобі потрібно, і в базі даних запитуй одразу ВСІ потрібні поля. І вже тоді в одному циклі видавай в потік.

Якщо всі вантажать прайс один і той самий, то в чому проблема зробити вже готовий? Якщо треба, то оновлювати 1 раз на годину. Або сам визнач своїм кроном, як часто його треба оновлювати, і зберігай у файлі. А по запиту тупо віддавай готовий згенерований файл.

PS. Особисто я тебе звільнив би лише за саме іменування myAmount — це видає навіть не джуна, а рівень школяра 8 класу, який не розуміє принципів написання коду так, щоб його можна було розуміти.

PPS. 2.4Мб воно не має важити. Подивися що в тебе на сервері з онлайн-компресією потоку. А ще краще — чи можеш ти віддавати прайс в архіві (zip, rar, 7z, xz). Зазвичай навіть невеликий рівень компресії достатній щоб стиснути структуровані дані разів у 20.

дуже неправильний підхід, нагадує притчу про мікроскоп і молоток,
тут треба самому пройтись профайлером, або тупо dd’шкою і глянути де тупить, це явно щось схоже на блейд, можливо завтики десь в контролері або при заборі даних, якщо завантажується проц то можливо десь рекурсія, або складна серіалізація, можливо в

$item->param_ru

сидить якийсь longtext який треба серіалізувати.
на лице n+1, бо по моїй практиці запити на 25колонок і 5000 рядків при наявності індексів літають на раз-два. я б тут не думав про якусь серйозну оптимізацію з інкрементними змінами чи гарячими кешами, а просто глянув список запитів і пройшовся експлейном, бо там може ще й база помирає, про SimpleXml я мовчу)
щодо фпм, то 80 чілдренів для 16Гб пам’яті як на мене мало, хоча якщо запити жруть багато то ок (але не ок що вони багато жруть)

якщо проблема отут github.com/...​llers/PriceController.php
то тут повне ай-я-яй, n+1 ще й пару раз за виклик

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

Якщо процитувати мого викладача Сухореброго В.Г. — «Погану програму можна написати на будь якій мові програмування, рівно як і добру».

Зменши навантаження — викинь зайве ;)

@else
{{$item->name_ru}}

description_ru}}]]>

{!!$item->param_ru!!}

А шо профайлінг каже? І навіщо там usleep? 7000 позицій, поспати 0.25 сек на кожну 250-ту, то воно 7 секунд тільки спить, ні? Чи лізе у базу

\App\Helpers\AppHelper::instance()->myAmount($item->product_id)

?

Як варіант, зробіть на C++ за допомогою моєї ліби github.com/incoder1/IO. A далі зробіть біндинг stackoverflow.com/...​ngs-and-libraries-for-php

Жесть, у дяди там скорее всего база тормозит или сериализация или просто железо надо докупить, а вы ему такую козу в проект предлагаете загнать)

Я думаю этот сервер сможет обрабатывать сотню таких HTTP запросов каждую секунду и каждый запрос будет выполняться пол секунды, если все нормально оптимизировать. Использовать что-то отличное от PHP это Overengineering

Вы на профайлинг посмотрите, при загрузке в 60-90% CPU база тут причем? Если база притормаживает из за защелок или ещё чего, то загрузки цп как правило нет — просто ожидание ввода-выводп по сети. В Java в Tomcat XML и HTML парсеры наверное просто так перенесли в нативные экстеншены? Можно конечно более детально отпрофилировать и установить бутылочное горлышко более точно.

А с чего вы взяли что у него БД и пых на разных серврерах? Если та волшебная строка действительно лезет в базу, да еще и сама база не ОК (нет индексов например или эти остатки на лету считаются или расчет остатков дедлочится с чем-то) — то оно вполне может генерить загрузку процессора.

База будет давать 90% загрузки по двум причинам. Первая — ее загружают по полной большое колличество клиенских приложений, и уже задержки по сети и на дисках/рейде и т.д. Вторая — крайне тяжеловесные сторед процедуры, триггеры или запросы которые или вообще по данным без индексов и ссылочной целостности или крайне сложные по своей сути с 3+ джоинтами и т.д. а вьюх нет. Вот не видно мне сверх сложных запросов тут, просто датасет большой. Ванную генерация текстового формата: XML,JSON,YAML и т.д. процессор и грузит по полной. Там как правило преобразования чисел к строке и прочие операции по сериализации, крайне не дешовые по CPU + ещё издержки на интерпретацию пыха и т.д.. По этой самой причине и массово переход произошел на Single Page с Angular, React, Vue и т.д. чтобы сервер разгрузить от генерации динамического DOM. В общем тут не на кофейной гуще гадать — а взять профайлер по приличнее, найти узкое место и устранить. В целом много раз видел как к пыху цепляют нативные биндинги — ничего работает все. Проект портят не технологии — а программисты снежинки.

В общем тут не на кофейной гуще гадать — а взять профайлер по приличнее, найти узкое место и устранить.

Это да, без профайлера это просто гимнастика для ума.

Вот не видно мне сверх сложных запросов тут, просто датасет большой.

Коллега, вы слишком верите в человечество. Там могут быть *любые* запросы, вплоть до расчета остатков на лету при каждом обращении к многострадальной `App\Helpers\AppHelper::instance()->myAmount($item->product_id`

PS Кстати, вьюшка по таьлице никак не ускорит фильтрацию по какому-то полю если по нему нет индексов.

Вьюшка не от индексов и не по таблице. Вьюшка по сложному запросу с кучей джоинтов, как кеш. Делаете один раз тяжеловесный запрос, а потом уже из вьюхи забираете (у Тома Кайта «Oracle для профессионалов» прием детально расписан). Гарантировать без профайлера ничего не могу, однако из моей практики — вот такая хрень обычно как раз генерацией текста и получается. А так методов оптимизации — куча, от полной смены алгоритма, перехода на двоичные форматы типа protocol buffers до нативных бустеров.

Тут до этапа материализированных вьюх (это если вообще есть доступ на изменение структуры базы) еще километры более простых оптимизаций.

Там ниже

$products = DB::table(’product_in_pricelists’)->where([’price_id’=>$id])->get();

От подхода «сделаем 7000 запросов к бд» чему угодно станет плохо.

Кстати, забавная вещь — топикстартер пишет что у него остатки считаются по разным складам, при этом он дергает синглтон и передает туда только ИД товара.
Если он инициализирует используемые склады перед вызовом процедуры генерации, то его ожидают чУдные открытия, когда выяснится что списки используемых складов могут меняться прямо в процессе генерации файла.

Ви всі дані з бд згрібаєие в масив об’єктів? Для таких масивів даних, потрібно брати голі дані з бази і поступово виводити. Типу
while($row = mysqld_fetch_row($res))
код циклу.
Виходить що скрипт ще тратить час щоб згенерувати масив об’єктів. А це доволі затратно по ресурсах.

Дані я вигрібаю так:
$products = DB::table(’product_in_pricelists’)->where([’price_id’=>$id])->get();
Це Laravel

Поставьте туда логи с указанием времени в миллисекундах для того, чтобы определить, что тормозит. А не запрос ли в БД это случайно тормозит? Или всё таки тормозит биндинг модели на слишком большую вьюху?

Я не знаю ларавел, але по доках він ніби підтримує lazy loading. Тому ніби нормально тут.

1. Потрібно налаштувати індекс по колонці price_id, якшо ще не налаштували

2. <stock_quantity>{!! \App\Helpers\AppHelper::instance()->myAmount($item->product_id) !!} — у вас тут потенційний N+1. можливо, потрібно прибрати. І бажано також налаштувати індекс по product_id для тієї таблиці, де у вас знаходиться інфа по залишкам

3. Можливо, файл(и) занадто важкий для системи під час запису в нього інформації. Спробуйте записувати в файл по одному продукту за раз, якщо зараз ви кладете відразу все. Приклад тут — stackoverflow.com/...​to-file/33501157#33501157

4. Якшо інформація в таблиці змінюється не частіше 1 разу в день(або на ваш розсуд), можна це перевіряти за допомогою dev.mysql.com/...​.7/en/checksum-table.html
Якшо чексума змінился у порівнянні з попередньою, тоді генерувати новий файл

5. Я так зрозумів, шо прайси від клієнта до клієнта у вас відрізняються, то можливо є сенс налаштувати якесь кешування через Memcache і скидувати кеш для кожного прайсу під час завантаження для нього товарів в базу.

UPD:
Ще бачу, що у вас наче використовується якийсь шаблонізатор для генерування вихідного xml. Хз як вони працюють під капотом, але зазвичай такі шаблонізатори їдять багато памʼяті/процесору через те, що потрібно парсити структуру і підставляти інтерпольовані дані. Якшо так, то спробуйте по можливості переписати це все на звичайний HEREDOC — www.php.net/...​pes.string.syntax.heredoc
або іншим нативним для PHP способом

Очень большие подозрения, что myAmount с помощью lazy loading таки делает 7000 SQL запросов при одном HTTP запросе. Это вполне логично объяснит, почему запрос выполняется 15 секунд.

До речі так, якась каша з пхп в хмл і ще якийсь шаблонізатор. Схожий код останній раз бачив років 14 назад, коли тільки починав цікавитись програмуванням.
Звичайна конкатенація справиться набагато швидше.

Розмістіть xml на гугл диску, замість того щоб генерувати з нуля, завантажуйте лише різницю, себто змінюйте лише ті поля, що змінилися. Тепер ви не генеруєте xml, а лише перекидаєте http запити

Найкраще звичайно оновлювати не частіше ніж раз на хвилину, щоб не навантажувати сервер і гугл

Это 100%, что сгенеренный прайс должен быть закэширован. Я бы поставил автогенерацию кэша на крон, например, раз в минуту и обновлял бы файлик на сервере. А клиентов натравил бы уже на этот файлик.

Ну можно еще сгенеренный xml складывать в какой-нибудь redis/memcached и отдавать клиенту уже оттуда, но будут те же яйца, что и с файликом.

Тюнить php-fpm на таком сервачке я большого смысла не вижу, ну только если нет избытка времени и интереса к внутренностям, но в таком случае порекомендовал бы попрофилировать с помощью blackfire. Авось в коде найдутся какие-нить боттлнеки и отчет начнет генерироваться побыстрее.

Яка різниця коли його генерувать? Генерація всеодно буде грузить проц, ні?

Різниця колосальна. Раз на хвилину не дорівнює декілька разів на секунду. Користувачі забирають вже згенерований xml файл. Навантаження впаде в 2-3 рази, в залежності від того скільки запитів на хвилину прилітає від користувачів.

Раз в минуту будет проц нагружаться на 60%, это предсказуемое поведение. А дальше юзер будет работать со статикой и уже скачивание самой xmlины проц грузить не будет. С сайтом, у которого раз в минуту проц на 60% растет еще можно как-то работать, хотя бы то некоторое время, которое требуется на замену данного сервачка на что-то помощнее.

раз в минуту нагрузку в 60% получать или каждую сек

В серверной Java есть такая вещь, как Visual VM Sampler. Она позволяет удалённо подключаться к виртуальной машине запущенного сервера и смотреть под нагрузкой, какие функции в коде дольше всего выполняются, сколько оперативной памяти съедают и как сильно грузят процессор (в процентах по сравнению с другими). Уверен, в PHP есть похожее. Дальше нужно смотреть где проблема, может быть там какие-то объекты в функции инициализируются по много раз. Ещё мне кажется, что используется или очень неэффективная библиотека для сериализации объекта в XML или она используется неправильно. На эту задачу думаю нужно пол секунды времени. Возможно, есть смысл заменить библиотеку.

Додав код, я не використовую бібліотеку, але пробував різні, результату немає

Первое, что бросается в глаза, используется какая-то задержка. Очень плохо так поступать. Второе, сериализация не используется вообще. Используется биндинг модели на вьюху. Так тоже поступать очень плохо. В данном случае нужно использовать эффективные сериализаторы.

Затримкою я перевіряв чи затримка знизить навантаження на процесор

Кеш? Раз на 30-60 сек генерація файлу, а клієнтам вже готовий xml.

Підписатись на коментарі