За последние 24 часа нас посетил 17191 программист и 1650 роботов. Сейчас ищут 1028 программистов ...

Как примирить curl_multi_select и curl_multi_info_read?

Тема в разделе "PHP для новичков", создана пользователем yup, 7 ноя 2024.

  1. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Возникла у меня задача, в рамках которой нужно периодически опрашивать достаточно большое число веб-страниц, причём делать это быстро и в то же время не сильно грузя процессор, да и всю систему в целом, так как работать должно на довольно слабой машине, на которой и без того много всякого крутится.

    Подобные задачи на PHP раньше решать не приходилось, поэтому полез читать документацию по curl_* (и, в частности, по curl_multi_*).

    Почитал, написал код. Не работает. Помучился-поэкспериметировал сначала сам - не помогло. Поизучал Интернет, ужаснулся увиденному, но некий работающий вариант из кусочков вселенского мусора знания собрал (URL-ы здесь просто для примера, чтобы можно было скопировать код и сразу запустить, но всё остальное: "Шаг влево, шаг вправо - пуля в голову, и не обязательно серебряная"):
    PHP:
    1. <?php
    2. $targets = array(
    3.   'https://ya.ru/',
    4.   'https://google.ru',
    5.   'https://msfn.org/',
    6. );
    7.  
    8. $MultiCURL = curl_multi_init();
    9. if (!curl_multi_setopt($MultiCURL, CURLMOPT_MAX_TOTAL_CONNECTIONS, 5))
    10.   die('Failed when setting max connections parameter');
    11.  
    12. $options = array(
    13.   CURLOPT_FOLLOWLOCATION => true
    14. , CURLOPT_RETURNTRANSFER => true
    15. , CURLOPT_CONNECTTIMEOUT => 10
    16. , CURLOPT_TIMEOUT => 15
    17. );
    18. if (defined('CURLSSLOPT_NATIVE_CA') && version_compare(curl_version()['version'], '7.71', '>=')) {
    19.   $options[CURLOPT_SSL_OPTIONS] = CURLSSLOPT_NATIVE_CA;
    20. } else {
    21.   $options[CURLOPT_CAINFO] = 'cacert.pem'; // список сертификатов брать с https://curl.se/ca/cacert.pem
    22. }
    23.  
    24. foreach ($targets as $target) {
    25.   if (($ch = curl_init()) === false)
    26.     die('Failed init for ' . $target);
    27.   if (!curl_setopt_array($ch, $options))
    28.     die('Failed setting options for' . $target);
    29.   if (!curl_setopt($ch, CURLOPT_URL, trim($target)))
    30.     die('Failed setting proxy address' . $target);
    31.   $rc = curl_multi_add_handle($MultiCURL, $ch);
    32.   if ($rc)
    33.     die($curl_multi_status[$rc] . ' when adding ' . $target);
    34. }
    35.  
    36. while (curl_multi_exec($MultiCURL, $running) == CURLM_CALL_MULTI_PERFORM);
    37.  
    38. while ($running > 0) {
    39.   $sel = curl_multi_select($MultiCURL, 10);
    40.  
    41.   while (curl_multi_exec($MultiCURL, $running) == CURLM_CALL_MULTI_PERFORM);
    42.  
    43.   if ($sel < 1) continue;
    44.  
    45.   while (($info = curl_multi_info_read($MultiCURL)) != false) {
    46.     $ch = $info['handle'];
    47.     $info = curl_getinfo($ch);
    48.     $httpCode = $info['http_code'];
    49.     $text = "\r\nURL: ${info['url']} | HTTP code: $httpCode";
    50.     $text .= "\r\nTotal time: ${info['total_time']}\r\n";
    51.     echo "$text\r\n";
    52.     if ($httpCode == 200) {
    53.       //...
    54.     }
    55.     curl_multi_remove_handle($MultiCURL, $ch);
    56.     curl_close($ch);
    57.   }
    58. }
    59. ?>
    Всё, вроде бы, хорошо, работает, и даже довольно шустро, только вот процессор во время этой своей работы грузит неслабо. А это противоречит условиям задачи, да и вообще не соответствует логике алгоритма и обещаниям документации.

    Начал разбираться и обнаружил ужасы.

    Во-первых, вызов curl_multi_select(), вместо того, чтобы долго висеть в ожидании поступления ответов на запросы, завершается моментально и возвращает -1, что означает ошибку в общении со стеком операционки. И хотя почти вся остальная часть цикла при этом пропускается, процессор всё равно грузится.
    И только после изрядного количества оборотов цикла -1 меняется на положительное число.

    Во-вторых, даже когда curl_multi_select() говорит, что данные пришли, curl_multi_info_read() возвращает false ("Нет никаких ответов").
    И ещё сколько-то оборотов цикла должны прокрутиться, прежде чем curl_multi_info_read() соизволит-таки заметить пришедшие ответы. А значит, ещё бессмысленная нагрузка на процессор.

    В-третьих, даже если curl_multi_info_read() увидела несколько (3-4-5) пришедших ответов, вовсе не факт, что их все удастся вычитать во внутреннем цикле. Нередко вычитываются только часть из пришедших, а остальные - на следующих проходах внешнего цикла.

    Соответственно, вопросы:

    1. Как сделать, чтобы curl_multi_select() висела до прихода ответов (как того обещает документация)?

    2. Как сделать, чтобы curl_multi_info_read() вычитывала всё, что пришло, не откладывая часть "на потом"?

    Частично я проблему бессмысленных циклов сгладил, вставив в цикле задержку на полсекунды (usleep(500000)) перед вызовом curl_multi_select(). Но это же костыль, а хочется, чтобы было по-человечески.
     
    #1 yup, 7 ноя 2024
    Последнее редактирование: 7 ноя 2024
  2. don.bidon

    don.bidon Активный пользователь

    С нами с:
    28 мар 2021
    Сообщения:
    914
    Симпатии:
    143
    может, попробовать wget2 и парсить скачанное потом?
     
  3. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Насколько я понял из описания, это "wget c многопоточным выкачиванием сайта". А в моём случае с каждого "сайта" нужно выкачивать ровно одну "страничку". То есть, нужно будет кучу раз запускать wget. А это очевидный проигрыш даже по сравнению с кучей обычных вызовов curl_exec().
    --- Добавлено ---
    И ещё одна часть моих проблем (о которой я не упоминал раньше, потому что она с вопросами не связана) - линии связи с "серверами" (на самом деле это датчики) я должен считать ненадёжными. Поэтому мне нужно получать как можно более низкоуровневую информацию о сеансе связи (в идеале вообще оперативную, то есть даже для ещё открытых сокетов), чтобы в случае чего оперативно извещать человека: "У вас ус отклеился датчик отвалился, срочно принимайте меры." От внешних утилит-"качалок" такого не дождёшься.
     
  4. Vladimir Kheifets

    Vladimir Kheifets Новичок

    С нами с:
    23 сен 2023
    Сообщения:
    425
    Симпатии:
    79
    Адрес:
    Бавария, Германия
    Добрый день!
    Правильно ли понял, что на обоих серверах - на сервере 1 где датчики и на сервере 2 где обработчики сигналов, установлены Ваши скрипты?
     
  5. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Нет.

    1. Датчики не на сервере, они - каждый сам по себе сервер. Именно отсюда вся проблема: приходится же опрашивать хренову тучу IP-адресов.
    2. Я занимаюсь только опросом. А к самим датчикам настолько не имею никакого отношения, что у меня нет ни их образцов, ни даже удалённого доступа. Поэтому вынужден имитировать их на своём собственном сервере. Но это элементарно просто - от них приходит даже не HTML-документ, а всего лишь несколько обычных текстовых строк.

    Но чисто философски моё творение и можно будет расценивать как сервер, к которому подключены датчики, поскольку информацию от них я складываю в SQL БД, и дальше к ней доступается уже кто-то другой (визуализатор).
     
  6. Vladimir Kheifets

    Vladimir Kheifets Новичок

    С нами с:
    23 сен 2023
    Сообщения:
    425
    Симпатии:
    79
    Адрес:
    Бавария, Германия
    Чисто философски, если Вы имитирует датчики, т,е. как-то имитируете смену их показаний, то почему Вы не можете при смене покаказаний отправлять request на скрипт визуализатор на другом сервере?
     
  7. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Смену показаний я не имитирую - нет смысла. Мне нужно получить оттуда несколько строк вида "имя_параметра=значение_параметра", разобрать их и обновить записи в БД по принципу: IP-адрес это ключ, по которому выбирается запись, а имена обновляемых в ней полей определяются именами параметров, которые я получил.

    Опрос производится регулярно, по таймеру, следить за изменениями значений и извещать при изменениях не требуется.
    Поэтому сейчас мои усилия направлены на имитацию разных возможных проблем со связью (каналы ненадёжные).
     
  8. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Да, кстати: после несколькодневных экспериментов выяснилось, что английский вариант документации можно правильно понять, только если точно знаешь, как его следует понимать :) И у переводчиков на русский язык такого понимания не было.

    В общем, тот вариант работы работы с curl_multi, который я привёл в самом начале, и есть единственно возможный.
    (Ещё иногда бывают нужны дополнительные нелепые телодвижения, но о них писать не буду - всё равно никто не поверит.)
     
  9. Vladimir Kheifets

    Vladimir Kheifets Новичок

    С нами с:
    23 сен 2023
    Сообщения:
    425
    Симпатии:
    79
    Адрес:
    Бавария, Германия
    Ранее Вы написали
    Обычно, когда в распределённых системах решается какая-то реальная задача,
    в начале определяется источник и структура данных, а также регламент доступа к ним.
    У Вас этого нет.
    Вы поэкспериментировали curl_multi_select и curl_multi_info_read
    и оценили их работособособность и ресурсоёмкость.
    Ваше оценка этих функций может быть полезна для принятия решений
    о целесообразности их использования.
    Удачи!
     
  10. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    Да, прежде чем использовать использовать новый для себя инструмент в серьёзной работе, нужно его как следует изучить.
    (Как говорилось в анекдоте 80-х годов: "Хлопцi, вчiть матчасть, бо дуже б'ють, коли питають.")

    Вчера, например, обнаружил, что curl загаживает TCP/IP-стек операционки - после каждого полученного по HTTP ответа сервера там остаётся недозакрытое соединение (хотя я честно вызываю curl_close()). И висит оно там, отъедая ценный ресурс (порт), минуту-две-пять - в зависимости от операционной системы.

    И страдает этим не только PHP-шный модуль, но и сама утилита curl, и вообще всё, что использует библиотеку libcurl.

    Причём известно об этой проблеме минимум лет двадцать, и авторы libcurl о ней знают, но всех жалующихся посылают на три буквы: WAD.

    Хотя способ борьбы с этим я нашёл, прямо средствами curl. Не со всеми серверами, правда, помогает, но в данном случае все и не надо.
     
  11. l_2001

    l_2001 Активный пользователь

    С нами с:
    9 дек 2014
    Сообщения:
    82
    Симпатии:
    3
    У меня в 4-е потока льются данные через curl и я никаких тормозов не нашёл... проверьте тайминги при отработке, надеюсь тесты Вам доступны! так-же работает и wget... единственное, я бы порекомендовал, запускать всё это дело через jenkis, он корректно умеет закрывать и открывать рабочие сессии...
     
  12. yup

    yup Новичок

    С нами с:
    7 ноя 2024
    Сообщения:
    7
    Симпатии:
    0
    У меня сейчас при тестированиях в процессе написания параллельно запускается 30 потоков, а в реальной работе предвидятся сотни. (Скорее всего, ограничение на число одновременно работающих поставлю, остальные в очереди подождут.)

    Разве ж я на низкую скорость curl жаловался? Вовсе нет. Работает быстро, просто очень хотелось загрузку процессора снизить за счёт висения на select и уменьшения числа холостых циклов info_read. Да не судьба.

    Да, практика показала, что искусственное внесение некоторых ручных задержек (usleep) в цикл не только снижает загрузку процессора, но и повышает скорость получения данных(!) - как раз за счёт уменьшения холостых циклов.
    Плохо то, что оптимальную величину задержки на той машине, где всё это работать будет, придётся подбирать экспериментально.

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

    Я при отработке подобных проблем и с curl уже наткнулся на неприятные особенности, но для него хотя бы придумал, как в таких случаях выкручиваться, а что будет в случае wget - даже представлять не хочется.
     
  13. don.bidon

    don.bidon Активный пользователь

    С нами с:
    28 мар 2021
    Сообщения:
    914
    Симпатии:
    143
    Запустил 3 wget-а параллельно, камень амдюк топовый, но 2011-го года, QuadCore AMD Phenom II X4 Black Edition 975, 3600 MHz, лил на hdd 7200rpm, 2-4% проца, я хз, что там с накладными расходами.