Многопоточные сокеты медленнее Curl. Почему?

Dimanjy

Новичок
Многопоточные сокеты медленнее Curl. Почему?

Был у меня старый примитивный модуль для поисковика, написанный под Curl. В него последовательно передавались адреса сайтов и он отдавал ответ. Решил я ускорить работу поисковика, переписав модуль с использованием многопоточных сокетов. В результате оказалось, что последовательный Curl против многопоточных сокетов на том же списке сайтов дает в 10 раз большую скорость. Испытания проводились на VPS под Fedora Core 2, PHP 4.3.11. На локальной машине под Win оба варианта по скорости примерно одинаковые, но опять же, с некоторым перевесом в сторону Curl.

Вопрос такой: почему многопоточные сокеты не дали ожидаемого эффекта? зависит ли их работа от особенностей организации VPS (Virtuozzo) и как с этим бороться?

Схема реализации на Curl:

PHP:
$hosts = array('site1','site2'); // Массив сайтов
foreach($hosts as $host){
    curl_setopt($ch, CURLOPT_URL, $host);
    $content = curl_exec($ch);
}
Схема реализации на сокетах:

PHP:
$hosts = array('site1','site2'); // Массив сайтов
$sockets = array(); // Массив сокетов
foreach($hosts as $i=>$host){
    $ip = gethostbyname($hosts[$i]);
    $sockets[$i] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    socket_set_nonblock($sockets[$i]);
    socket_connect($sockets[$i],$ip,80);
}

// Рабочий цикл
while(1){
    // Массивы для socket_select
    $arrRead = array(); $arrWrite = array();

     foreach($sockets as $i=>$sh){
        // Заполняем массивы чтения и записи в зависимости
        // от текущего состояния сокета (отправляем GET или уже получаем данные)
        // .....
    }

    $num = socket_select($arrRead, $arrWrite, $arrExcept = NULL, $tv_sec=1);
    if($numc>0){

       // Read sockets
       if(count($arrRead)>0){
		foreach($arrRead as $socket){
		     $str = socket_read($socket,1);
		     if($str==''){ // Все данные получены, закрываем сокет
			socket_shutdown($socket);
			socket_close($socket);
		     }
		}
       }

       // Write sockets
       if(count($arrWrite)>0){
		foreach($arrWrite as $socket){
                    $str = ...; // Берем следующий символ из отправляемого заголовка
		    socket_write($socket,$str,1);
		}
       }

    }
}


}
Помогите, пожалуйста, в поиске причины.
Заранее благодарю!
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Предположение: у тебя bottleneck не в сети, а в ограничении загрузки процессора на твоем vps.
Curl - старая, оптимизированная библиотека для этих задач, написаная на С.
 

Dimanjy

Новичок
Автор оригинала: grigori
Предположение: у тебя bottleneck не в сети, а в ограничении загрузки процессора на твоем vps.
VPS относительно мощный.
Вот данные по загрузке:

Cpu(s): 5.8% us, 0.4% sy, 0.0% ni, 93.7% id

При этом сам процесс тратит 17% CPU.
По Вашему это сильная загрузка? Я просто не знаю...
По серверу, взглянув извне, вроде бы не ощущается. Тот же Apache нормально отдает странички из базы во время работы скрипта.

У меня есть доступ к панели Virtuozzo на VPS, а там есть раздел QoS Alerts - выдает предупреждения о нехватке рессурсов (желтая, красная, черная зона и т.д.) Так вот, во время работы скрипта никаких алертов не выдает: ни по памяти, ни по буферам сокетов, а процессорных алертов я еще даже ни разу не видел.


Curl - старая, оптимизированная библиотека для этих задач, написаная на С.
Так то оно так, но основные принципы-то те же: Curl, вероятно, по тем же алгоритмам с сокетами работает (хотя я исходники не смотрел).
На чем написано, насколько я понимаю, роли не играет, т.к. сетевые процессы намного более медленные, нежели скорость интерпретирования какого-либо скрипта.

Мне бы кто подсказал методику поиска узкого места в работе связки скрипт + сервер!
Т.е. что-то вроде списка для метода исключения - чего попробовать и чего вычеркивать.
С серверами опыта общения мало имею, оттого и как слЯпой котенок - не знаю, куда копать.
 

voodoo

Новичок
а в чем прикол писать/читать по одному байту за цикл?
да еще с учетом того что сокет_селект может секунду висеть (т.е. по байту в секунду отправляем)?
 

Dimanjy

Новичок
Автор оригинала: voodoo
а в чем прикол писать/читать по одному байту за цикл?
да еще с учетом того что сокет_селект может секунду висеть (т.е. по байту в секунду отправляем)?
А почему socket_select должен висеть, если есть готовые к чтению/записи сокеты? А если таковых нет, то он и должен висеть и ждать, пока они не появятся (иначе просто некуда читать/писать).

По поводу чтения/записи по 1 байту точно ответить не могу :), но вот какие соображения у меня (новичка в сокетах) на этот счет:
Если socket_select сработал, значит удаленный хост готов принять/отправить данные через сокет. А вот сколько он их готов принять/отправить - не известно! Но один-то точно примет. Кроме того, таким образом обеспечивается (как я думаю) наиболее равномерная многопоточность, т.е. другие потоки не будут простаивать, пока я отправляю в один из сокетов, например, 512 байт.

Как Вы посоветуете делать? Увеличить размер буфера? Может ли при этом произойти "торможение" других потоков или вообще полный клин сокета, из которого придется вываливаться с ошибками по таймауту?
 

voodoo

Новичок
вообще говоря, socket_select про удаленный хост вообще ничего не знает. он проверяет локальные сокеты. а так-то, может уже и кабель выкопали да на металлолом сдали.

socket_write сам решит сколько можно отправлять. именно поэтому он возвращает int -- кол-во отправленных байтов. на эту величину и режем буффер. Тем более сокет в nonblock

точно так же и socket_read лучше знает сколько можно прочитать, параметр у него -- _максимальное_ кол-во байтов для чтения, а вовсе не обязательное к чтению (по идее, просто для того чтобы в С-программах буффер не переполнялся)


и еще непонятно что за numc в ифе
$num = socket_select();
if($numc>0){
 

Dimanjy

Новичок
Автор оригинала: voodoo
вообще говоря, socket_select про удаленный хост вообще ничего не знает. он проверяет локальные сокеты.

socket_write сам решит сколько можно отправлять. именно поэтому он возвращает int -- кол-во отправленных байтов. на эту величину и режем буффер. Тем более сокет в nonblock

точно так же и socket_read лучше знает сколько можно прочитать, параметр у него -- _максимальное_ кол-во байтов для чтения, а вовсе не обязательное к чтению (по идее, просто для того чтобы в С-программах буффер не переполнялся)
Теперь все проясняется!
Т.е. когда срабатывает select физически данные уже получены с удаленного хоста и лежат где-то в буфере сокетов. И читать мы можем до упора, а признаком завершения чтения будет следующее:

PHP:
$buff = socket_read($socket,$len);
if( strlen($buff) < $len ) {
    // Прочли все, что смогли
}
Про отправку - Вы также абсолютно правы! Мне следовало внимательнее читать документацию.
Большое Вам спасибо! Попробую применить и оттестировать.
 

voodoo

Новичок
ну, на самом-то деле,
if( strlen($buff) < $len )
это вовсе не значит что чтение завершено. может та сторона попозже еще что-то дошлет. или просто сеть медленная.
В общем, это уже на усмотрение "верхнего" протокола (напр. двойной перевод строки в хттп-запросе или в смтп).

вот если socket_read вернул FALSE, то значит читать точно нечего -- связь прервана.
 

Dimanjy

Новичок
По поводу сказанного Вами выше:

Поменял скрипт с учетом Ваших замечаний, и ... невероятно! :) Заработало! Теперь работает в разы быстрее, чем Curl!
Большое Вам человеческое СПАСИБО!

Автор оригинала: voodoo
ну, на самом-то деле,
if( strlen($buff) < $len )
это вовсе не значит что чтение завершено. может та сторона попозже еще что-то дошлет. или просто сеть медленная.
В общем, это уже на усмотрение "верхнего" протокола (напр. двойной перевод строки в хттп-запросе или в смтп).

вот если socket_read вернул FALSE, то значит читать точно нечего -- связь прервана.
А тут у меня снова вопрос:
Почему socket_read может вернуть FALSE, если предварительно этот сокет был выбран через socket_select, который по идее и дает нам гарантию, что в сокете что-то да есть?
Если только данные получаемые с удаленного хоста окажутся вдруг кратными размеру буффера чтения...
Или же socket_select дает нам на чтение и те сокеты, из которых уже все прочитано, кроме FALSE?
 

voodoo

Новичок
когда другая сторона разрывает связь (socket_close), она шлет спец. пакет FIN.
поэтому socket_select и показывает что в сокете что-то есть для чтения. а socket_read не читает, потому что данных, в общем-то, и нет.

Т.е. так сделано специально для того чтобы видеть что нужно прекратить коннект (закрыть сокет на нашей стороне)
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Хехе :) а теперь глянь на
http://ru.php.net/curl_multi_exec

В ext chm формате доки есть пользовательские комментарии с примерами, один из них:
PHP:
$connomains = array(
"http://www.cnn.com/",
"http://www.canada.com/",
"http://www.yahoo.com/"
);
$mh = curl_multi_init();
foreach ($connomains as $i => $url) {
       $conn[$i]=curl_init($url);
       curl_setopt($conn[$i],CURLOPT_RETURNTRANSFER,1);
       curl_multi_add_handle ($mh,$conn[$i]);
}
do { $n=curl_multi_exec($mh,$active); } while ($active);
foreach ($connomains as $i => $url) {
       $res[$i]=curl_multi_getcontent($conn[$i]);
       curl_close($conn[$i]);
}
print_r($res);
 

Dimanjy

Новичок
Автор оригинала: grigori
Хехе :) а теперь глянь на
http://ru.php.net/curl_multi_exec
Ну Вы прям как обидели :)
Знаю я прекрасно про мульти_курл. Ну не лежит у меня душа к нему (пока).
Насколько я знаю, на каждый поток у мульти_курла выделяется своя память, а я ее лучше какому-нибудь memcached отдам.
Кроме того, на сокетах я управляю процессом как хочу, а у курла... ну есть там и коллбеки, конечно, но все равно, как-то закрыто все :)
Короче, это уже дело вкуса.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Конечно. Просто я много довольно сложных задач курлом делал.
Надо класс-обертку к нему выложить и документировать. Лучше чем тут сидеть :)
 
Сверху