выполнить код скрипта в нескольких потоках параллельно

DiMA

php.spb.ru
Команда форума
выполнить код скрипта в нескольких потоках параллельно

Имеется пхп-код, состоящий из пяти долгих запросов (разные таблицы, а возможно и mysql-хосты), закачки с хттп чего-нибудь и запись логов, которая не возвращает никаких результатов (итого 7 частей).

Какие есть готовые решения, чтобы параллельно в 1 главный и вспомогательных 7 потоков выполнить эту работу? Возможно, потоки будут вообще на другом хосте (прием-передача через сериализованный конфиг-запрос и результат).
 

Krishna

Продался Java
готовые решения это как? типа распределённая пхп ОС? )

-~{}~ 11.04.09 01:25:

форк из главного процесса дочерних в цикле (не надо гемороиться с передачей параметров)

можно делать на одном хосте управляющий процесс-интерфейс (и лучше его тогда делать в веб-варианте)

ну а на втором хосте главный CLI процесс, с которого будут форкаться остальные и который будет запускаться этим, вебовским, как вариант - через веб "минизагрузчик"на втором хосте


в общем, не очень понятна суть проблемы
 

DiMA

php.spb.ru
Команда форума
Ну, все очень просто... пхп (основной) засовывает N конфигов-запросов в очередь, которая разгребается первым дорвавшимся до записи свободным фоновым потоком. Последний исполняет нужную функцию (запрос к другой базе или иную работу) и складирует куда-то результат (о наиболее оптимальном месте/методе вернуть данные - тоже вопрос) . Я так делал с вычислениями - пяток фоновых потоков через очередь на MySQL конкурируют между собой, захватывают и выполняют первый попавшийся из поступивших запросов. Тока не оптимально на MySQL очередь заводить и на другой хост не засунуть. Куча накладных расходов.

-~{}~ 11.04.09 01:33:

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

Krishna

Продался Java
Тока не оптимально на MySQL очередь заводить и на другой хост не засунуть.
Как раз таки MySQL позволяет работать на разных хостах. Я на деюсь речь не идёт о копеешных вирт. хостингах?

Ещё раз, я не понял - тебе надо 7 раз фоновый процесс выполнить и для ускорения хочешь разбить на потоки? Или нет?

-~{}~ 11.04.09 01:40:

что-то мне не нравится форк в своей сути
Предрассудки? )
Просто самый простой способ запустить ещё один процесс.
Ты когда в bash команду с & в конце набираешь - происходит fork баша и только потом запуск. Не смущает? :)
 

DiMA

php.spb.ru
Команда форума
Речь идет о более быстрой генерации веб-странички, где есть независимые между собой блоки и фильная сборка данных (на корпоративных серверах, естественно). Еще форк не приемлем, т.к. в случае моего примера на запуск фонового потока тратится 200-300 метров памяти и 30 секунд времени, после чего они начинают отрабатывать запросы. Т.е. запросы нужно слить в очедь на исполнение соседям. В будущей задаче 300 метров памяти никто ждать не будет, но потоки лучше заранее наплодить. Это стабильность. Можно прогнозировать нагрузку, недопускать переполнения памяти и своппинг...

На другой хост так просто не не засунуть. Основной пхп пишет свои запросы в одну табличку одной базы. Фоновые потоки будут по сети интенсивно читать эту таблицу, ожидая захватить новый поступивший запрос. После чего основной пхп интенсивно будет отслеживать с других хостов результат в базе от фоновых потоков. Когда это происходит на одном хосте - левой нагрузки я не замечаю. Заморачиваться и изобретать систему передачи этих запросов через сокет данные (аля SOAP) или изобретать очереди т.д. не хочется, т.к. я не знаю об эффективности методов.

Мне надо выполнить параллельно 7 операций: первый тип из 5 операций - обращения к чужой базе, второй тип - скачать веб-документ, третий тип - записать что-то в лог (без ответа). Еще можно кучу типов операций придумать.

Мне нужно просто слить несколько пхп-функций в другое место. Собственно в чем вопрос - как наиболее эффективно и просто это сделать?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
я делаю подобное именно форком дочек демона
правда, решение не абстрактной очереди, а конкретно распаралеливание краулеров

у форка 2 минуса - проблемы очистки мусора и затраты на сам форк, собсно
1ю проблему я решил, 2я мне была не важна, не занимался
можно решить классически группой чайлдов, если надо ...

в чем хранить - зависит от объема данных
в общем случае - memcache

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

а вообще, да, многопоточность - единственное, чего мне не хватает в PHP
 

DiMA

php.spb.ru
Команда форума
если тебе не понятно, вот пример :)

<?
долгий запрос к некой базе
долгий запрос к некой базе
долгий запрос к некой базе
долгий запрос к некой базе
долгий запрос к некой базе
скачать документ
записать лог

ждать, пока не поступят ВСЕ данные

вывести на экран сборную солянку по полученным данным
?>

И все параллельно :) Лог - в том числе (т.е. не ожидая остальных).
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
знаешь, а ведь ты можешь просто запустить паралельный пул fcgi-процессов как демон для обработки своей группы задач
fpm позаботится об управлении процессами и форк не нужен
 

DiMA

php.spb.ru
Команда форума
grigori
пока не понимаю

каким методом пхп-скрипт веб-сервера будет отдавать запрос на запущенный где-то другой пхп-процесс и с него читать результат?

как свободные процессы будут конкурировать между собой за право выполнить поступивший с веба запрос (и тут же блокировать всем остальным) ?

можно ли на другой хост переместить и чтобы не было паразитного трафика при IDLE?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: DiMA
grigori
каким методом пхп-скрипт веб-сервера будет отдавать запрос на запущенный где-то другой пхп-процесс и с него читать результат?
1я мысль - очередь можно сделать в mysql, таблице типа memory

результат писать в файл/memcached, и в поле строки таблицы этой задачи писать имя файла/ключ

как свободные процессы будут конкурировать между собой за право выполнить поступивший с веба запрос (и тут же блокировать всем остальным) ?
ставить флаг (писать в поле свой pid)

можно ли на другой хост переместить и чтобы не было паразитного трафика при IDLE?
если процессы выполняются на одной машине, постановщика можно усыплять и будить
если на разных - слушать порт udp

-~{}~ 11.04.09 01:24:

я не знаю, как заставить fcgi-воркера сразу приступать к выполнению одного заданного скрипта вместо ожидания cgi-запроса от веб-сервера
но можно придумать или спросить у а-найта и товарищей в hi-load
 

DiMA

php.spb.ru
Команда форума
Т.е. ты предлагаешь, чтобы у меня все осталось без изменений?? Фоновые процессы все дружно тупо ломяться в MySQL с запросом
SELECT count(*) FROM comproxy
WHERE status=0 AND (forThread IS NULL OR forThread=$this->thread)
каждые 50 мс? Причем, если кол-во не нуль, нужно залочить всю таблицу, повторно выполнить запрос на чтение НОВОГО запроса, поставить туда status=1 (+номер потока, начало времени обработки), разлочить и начать выполнять. По окончанию в нужную ячейку вернуть результат (или в мемкеш).

PHP:
        if (time()>=$cronTouth) {
		    @touch("live-$this->thread.tmp");
			$cronTouth=time()+10;
        }

        $qty=$db->value("
        SELECT count(*) FROM comproxy 
        WHERE status=0 AND (forThread IS NULL OR forThread=$this->thread)";

        if (!$qty) continue; // ушли на usleep и повтор цикла

        $q=$db->query("LOCK TABLES comproxy write");
        $q=$db->query("UPDATE comproxy SET status=-2 WHERE status=0 AND timeOut<now()");
        $line=$db->line("SELECT * FROM comproxy WHERE status=0 LIMIT 1");
        if (!$line) {
            $q=$db->query("UNLOCK TABLES");
        	continue;
        }
        echo "\r\n<< begin $line[id] >>\r\n";
		$q=$db->query("UPDATE comproxy SET status=1, timeBegin=now(), runThread=$this->thread WHERE id=$line[id]");
		$q=$db->query("UNLOCK TABLES");

		$cmd=unserialize($line['queryFull']); // начало выполнение запроса
Я задал этот вопрос потому, что считаю текущий метод работы кривым. Его спасает то, что фоновых потоков всего 3, из-за их тяжести (300 метров ОЗУ) и мусорные запросы внутри одного хоста (между пхп и базой) не дают никакой нагрузки на сеть. А завтра это будут обычные пхп-скрипты. И их будет сотня. И на другом хосте. MySQL ляжет от тупых запросов.

Идеальный вариант, кроме того, что он ЭФФЕКТИВНЫЙ и МАЛОЗАТРАТНЫЙ:
1. фоновые потоки просыпаются только при наличии нового запроса
2. основной поток спит до получения ответа

> если процессы выполняются на одной машине, постановщика можно усыплять и будить

Приведи плиз пример такой связи между двумя независимыми процессами.

> если на разных - слушать порт udp

Мне кажется не надежным метод самому (т.е. веб-скрипту) открывать порты и через него ждать событий. Хотя, этот вариант очевидный и не порождает мусорного трафика.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Автор оригинала: DiMA
Т.е. ты предлагаешь, чтобы у меня все осталось без изменений??
Я предлагаю детально рассмотреть задачу и перечислить варианты, а выбирать будешь ты :)
До этого поста я просто не знал деталей задачи.

Фоновые процессы все дружно тупо ломяться в MySQL
Причем, если кол-во не нуль, нужно залочить всю таблицу, повторно выполнить запрос на чтение НОВОГО запроса, поставить туда status=1 (+номер потока, начало времени обработки), разлочить и начать выполнять. По окончанию в нужную ячейку вернуть результат (или в мемкеш).
А завтра это будут обычные пхп-скрипты. И их будет сотня. И на другом хосте. MySQL ляжет от тупых запросов.
Если это узкое место - напиши протенький демон-роутер запросов.
Он висит на порту на машине с базой, держит соединения со всеми ждущими воркерами, проверяет базу каждые 50 мс.
запрос "select id from comproxy where taken=false limit 1" по таблице memory базу не нагрузит
При получении запроса отдает детали 1му воркеру из FIFO-стека. Воркер сам пишет результат в таблицу.


Идеальный вариант, кроме того, что он ЭФФЕКТИВНЫЙ и МАЛОЗАТРАТНЫЙ:
1. фоновые потоки просыпаются только при наличии нового запроса
2. основной поток спит до получения ответа
в PHP 2 вида внешних событий - сигналы и пакеты на порт/сокет
периодическая проверка - эмуляция
при всем богатстве выбора ...

> если процессы выполняются на одной машине, постановщика можно усыплять и будить

Приведи плиз пример такой связи между двумя независимыми процессами.
http://php.net/manual/en/function.pcntl-signal.php

http://www.phpclasses.org/browse/package/3065.html
это у меня работает месяцами

> если на разных - слушать порт udp

Мне кажется не надежным метод самому (т.е. веб-скрипту) открывать порты и через него ждать событий. Хотя, этот вариант очевидный и не порождает мусорного трафика.
в локалке - вполне ...
можно и комбинировать - ждать, слушть порт и проверять состояние запроса в базе через N секунд (вдруг пакет не дошел)

вообще, тут самое сложное - прописывать обработку ошибок

-~{}~ 11.04.09 02:44:

если кол-во не нуль, нужно залочить всю таблицу, повторно выполнить запрос на чтение НОВОГО запроса, поставить туда status=1
другой вариант:
PHP:
while(true){
    usleep(mt_rand(20,80));
    SELECT id FROM comproxy where pid='' limit 1;
    update comproxy set pid=CONCAT(pid,';',posix_getpid()) where id=$id,
    select pid from comproxy where id=$id;
    if ($res != ';'.posix_getpid()){//collision
        if (strpos($processes, posix_getpid()) !=1) //I am not the 1st
            continue;
    }
    break;//the girl is mine!
}
 

DiMA

php.spb.ru
Команда форума
Все таки, народ, у кого есть опыт параллельных вычислений на пхп?
 

Макс

Старожил PHPClub
есть 3 основных способа распараллеливания работы:
- процессы (fork) - в веб-скрипте я бы форкаться не стал (при форке детки получают еще и копию сокета по которому пришел запрос и черт знает что с ним станет например после завершения одного из child-процесса) а в php-cli fork работает нормально.
- threads - в пхп недоступно
- мультиплексирование (socket_select, poll и прочее) - скачать документ можно через socket_select, а вот с mysql - либо самому патчить mysql-extension, либо использовать использовать php5.3 + mysqlnd + mysqli_poll()

Ну и 4-ый вариант - придумать схему, которая бы обходила ограничения - например через очереди.
В твое варианте можно было бы сделать в веб-скрипте : делаешь 7 инсертов в таблицу и потом раз в X мс делать селект из тбалицы.
В background-е висит php-cli скрипт (всего один) который постоянно делает:
PHP:
while (true) {
SELECT * FROM QueueCommands WHERE status=New LIMIT 50;
fork на каждую запись и обработка команды в child-е 
UPDATE QueueCommands SET status=Started WHERE id IN(список выбранных ID)
обработка child-ов (wait/waitpid -  и прочее)
usleep(1000);
}
каждая команда будет отрабатывать в отдельно форке и писать ответ в базу:
UPDATE QueueCommands SET status=Finished, reply=$fetched_data WHERE id=$id
Поскольку скрипт-обработчик очереди всего один - не нужно никаких локов на таблицу.
Описал лишь общую схему - можно придумать всякие мелкие оптимизации.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
>при форке детки получают еще и копию сокета по которому пришел запрос и черт знает что с ним станет например после завершения одного из child-процесса

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

DiMA, к сожалению, готового решения для твоей задачи нет

еще вариант: один cli-демон и пул fcgi
cli-демон слушает сокет
веб-скрипт открывает сокет с cli-демоном и отдает ему задачи
cli-демон открывает для каждой задачи соединение на fcgi-пул и ведет очередь в памяти
воркеры отрабатывают как обычно и возвращают результат cli-демону, который отдает веб-процессу

все отлично масштабируется, никакой базы, может разделяться по сетке, никакого оверхеда
но надо научить cli-демон быть fcgi фронтом (может, написать его на python / tcl)
 

Макс

Старожил PHPClub
grigori
скорее всего он отправит туда сигнал конца работы
если открыть соединение с базой и т.п. до форка, то при смерти чайлд его закрывает (проверял)
все сетевые соединения (mysql например) лучше перед fork-ом закрывать, а после fork-а открывать заново (если нужно). Потому что child наследует это соединение, а при умирании процесса ПХП закрывает все соединения, в том числе и унаследованые у родительского процесса.
 

DiMA

php.spb.ru
Команда форума
и какое же преимущество в скорости транзакции перед локом?
 

fixxxer

К.О.
Партнер клуба
очередь.
записываем идентификатор задания и необходимые данные (ниже их содержит $e) в табличку очереди. в строке ставим статус QUEUED, также там есть автоматическое поле updated_ts timestamp. определяем количество воркеров константой MAX_WORKERS, значение подбирается экспериментально. скрипт обработки выгребает очередную запись из очереди, вида

псевдокод, в первом приближении
PHP:
// устанавливаем обработчик сингала sigchild и все такое чтобы следить за завершением работы деток и количеством запущенных воркеров
for ( ; ; ) {
   if (number_of_workers > MAX_WORKERS || $queue_is_empty) usleep(SLEEP_TIME);
   $e = get_row(select * from queue where status='QUEUED' order by updated_ts limit 1);
   if (empty($e)) { $queue_is_empty = true; continue; }
   query(update queue set status='LOCKED' where queue_id=$e['queue_id']);
   fork();
   if (i_am_child()) {
      try {
          process_element($e); // выполняем длительную операцию
      } catch (Exception) {
           query(update queue set status='QUEUED', error_counter=error_counter+1 queue_id=$e['queue_id']); //сдвигаем в конец очереди
           // надо бы обработку если error_counter превышает лимит
           exit;
      }
      query(delete from queue where  queue_id=$e['queue_id']);
      exit;
   }
   $queue_us_empty=false;
}
чтобы не форкаться каждый раз, можно реализовать апачевую схему с префорком и связью с воркерами через сокеты - если есть свободный воркер передаем ему команду, иначе ждем. но это несколько сложнее)

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

это все если скажем те 5 запросов независимы. если задачи связанные, то либо выполняем все кучкой, либо выполнение одной подзадачи может добавлять в очередь следующую.
 

Alexandre

PHPПенсионер
на мыльные (долгоиграющие) скрипты я использовал сл методику:

запускаю параллельно несколько процессов сишником bgscript (код ниже) скрипт script.php
запуск типа: $res = system('path/to/bgscript');
результат работы скрипта пишу в файл tmp.tmp
а результаты отслеживаю аяксом из файла tmp.tmp
PHP:
#include <cstdlib>
#include <errno.h>
#include <string>
int main(int argc, char *argv[]){

        pid_t pid = fork();

        if ( pid == -1 ) {
            printf( "fork abort ");
        }
        if ( pid == 0 ){
            system( '/usr/bin/php /full/path/script.php  ' );
        }
        if ( pid > 0 ){
            printf( "Ok");
        }
}
модернизации прир необходимости:
- имя пхп скрипта передаю в качестве параметра (если скриптов несколько)
- имя временного файла вместо tmp.tmp, если нужна уникальность - можно генерить в сишнике и передавать в скрипт пхп параметром
соответственно отдавать в вызывающий пхп скрипт вместо printf( "Ok"); пишем printf( "%s", tmp_filename);

PS готовил на эту тему минидоклад на Hi++
может оформиить в статью? но наш любимый журнал загнулся к сожалению :(

PSS
вообще из готовых решений есть SRM
но я этим не пользовался

был еще какой-то реализован пулл коннекций на sf.net в качестве пхп модуля, но не знаю подойдет-ли данная задача для этих решений
 
Сверху