Paralelní zpracování
Školení, která pořádám
Článek vyšel v rámci PHP okénka na serveru Root.cz.
Řekněme, že bychom chtěli vytvořit kód zjišťující, kolik jednotlivé vyhledávače vrací výsledků při hledání různých slov. Když pominu omáčku okolo, mohl by kód vypadat nějak takhle:
<?php
$vyhledavace = array( // nazev => array(url, pattern), ...
'Seznam.cz' => array('http://search.seznam.cz/search.cgi?w=', '<b>([ 0-9]+)</b> nalezen'),
'Centrum.cz' => array('http://search.centrum.cz/index.php?q=', 'Nalezeno <strong>([ 0-9]+)</strong>'),
'Atlas.cz' => array('http://search.atlas.cz/default.aspx?q=', '<b>([ 0-9]+)</b></span> nalezen'),
'Jyxo.cz' => array('http://jyxo.cz/s?s=', 'Jyxo nalezlo <b>([0-9]+)</b>'),
'Google.com' => array('http://www.google.com/search?q=', 'of about <b>([,0-9]+)</b>'),
);
foreach ($vyhledavace as $nazev => $vyhledavac) {
$file = file_get_contents($vyhledavac[0] . urlencode($_GET["slovo"]));
echo "$nazev: " . (preg_match("~$vyhledavac[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n";
}
?>
K dokonalosti kódu ještě leccos chybí (různé vyhledávače např. používají různé kódování a je jim potřeba posílat některé hlavičky), ale o tom teď psát nechci. Při spuštění si nejde nevšimnout toho, že skript většinu času jenom čeká na stažení dat a je proto zbytečně pomalý. Rozdělením toku do vláken PHP nedisponuje, ale díky rozšíření PCNTL je možné tok kódu rozdělit do nezávislých procesů. Princip je stejný jako v jazyce C – funkce pcntl_fork vytvoří nový proces, rodičovi vrátí číslo procesu potomka a potomkovi nulu. Kód tedy upravíme tak, že rodič vytvoří pro každý vyhledávač samostatný proces a dotaz do vyhledávače bude klást tento potomek. Tím dosáhneme paralelního zpracování.
<?php
foreach ($vyhledavace as $nazev => $vyhledavac) {
$pid = pcntl_fork();
if (!$pid || $pid == -1) { // potomek nebo nelze forknout
$file = file_get_contents($vyhledavac[0] . urlencode($_GET["slovo"]));
echo "$nazev: " . (preg_match("~$vyhledavac[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n";
if (!$pid) { // pro potomka práce skončila
exit;
}
}
}
while (pcntl_wait($status) > 0) {
// počkáme na dokončení všech dětí, ať se z nich nestanou zombie
}
?>
Výsledky z jednotlivých vyhledávačů se budou vypisovat v tom pořadí, v jakém budou získány. Pokud bychom místo toho chtěli data z dětí předávat rodičovi, hodila by se funkce msg_send, ale o tom zase až někdy příště.
Rozšíření PCNTL je za normální okolností k dispozici pouze pro systémy typu Unix, na Windows můžete použít CygPHP – speciální kompilaci, která obsahuje mimo jiné i toto rozšíření. Rozšíření je navíc k dispozici pouze v prostředích CGI a CLI, takže se dá použít hlavně při spouštění skriptů z příkazové řádky – při použití na webu běží PHP obvykle jako modul webového serveru.
Využití knihovny CURL
Řešení s přidáváním procesů je univerzálně použitelné, ale poměrně nehospodárné. Pokud nám jde pouze o paralelní načítání dat ze sítě, existují i jiné prostředky. Alternativní způsob řešení podobného problému s využitím asynchronního připojení popisuje na svém blogu Wez Furlong. Jeho řešení je hodně nízkoúrovňové, já proto ukážu, jak se tento problém dá řešit s využitím rozšíření CURL a funkcí curl_multi_*
, které byly přidány v PHP 5:
<?php
$curl_multi = curl_multi_init();
foreach ($vyhledavace as &$val) {
$val[2] = curl_init($val[0] . urlencode($_GET["slovo"]));
curl_setopt($val[2], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($curl_multi, $val[2]);
}
unset($val);
?>
V první části se s využitím funkce curl_multi_add_handle inicializuje multi stack. Cyklus foreach využívá novou vlastnost PHP 5 – proměnná $val
je předávaná referencí, takže změny v ní se projeví přímo v poli. Po skončení cyklu je vhodné tuto proměnnou zrušit, protože jinak se do ní přiřazené hodnoty promítnou i do posledního prvku pole. Identifikátor připojení ještě budeme potřebovat, proto se ukládá jako třetí prvek jednotlivých polí v proměnné $vyhledavace
.
<?php
do {
curl_multi_select($curl_multi);
while (CURLM_CALL_MULTI_PERFORM == curl_multi_exec($curl_multi, $running)) {
// provést znovu
}
if ($running && strtoupper(substr(PHP_OS, 0, 3)) == "WIN") {
sleep(1); // pod Windows curl_multi_select() nečeká
}
} while ($running);
?>
V druhé části se funkcí curl_multi_select čeká na dostupnost dat a funkcí curl_multi_exec se tato data přijímají. Na Windows funkce curl_multi_select bohužel skončí okamžitě, takže čekání zajistíme alespoň funkcí sleep.
Po stažení všech dat se již jen vypíšou výsledky:
<?php
foreach ($vyhledavace as $nazev => $val) {
$file = curl_multi_getcontent($val[2]);
echo "$nazev: " . (preg_match("~$val[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n";
}
?>
O něco pohodlnější způsob nabízí třída HttpRequestPool.
Problém paralelního zpracování není v PHP potřeba řešit příliš často – některé úlohy stačí rozdělit na více menších skriptů a jejich paralelní zpracování zajistí přímo webový server. Když na to ale přijde, jsou k dispozici přinejmenším tři možné způsoby. Ideální samozřejmě je, když úloha při čekání na data provádí nějakou výpočetně náročnější operaci – např. kontroluje pravopis nebo hledá syntaktické chyby v dosud stažených souborech.
Přijďte si o tomto tématu popovídat na školení Výkonnost webových aplikací.
Diskuse
korny:
nevim proc ale mam vzdy jenom jeden vystup...
a to za seznamu :-(
<?php
$vyhledavace = array( // nazev => array(url, pattern), ...
'Seznam.cz' => array('http://search.seznam.cz/search.cgi?w=', '<b>([ 0-9]+)</b> nalezen'),
'Centrum.cz' => array('http://search.centrum.cz/index.php?q=', 'Nalezeno <strong>([ 0-9]+)</strong>'),
'Atlas.cz' => array('http://search.atlas.cz/default.aspx?q=', '<b>([ 0-9]+)</b></span> nalezen'),
'Jyxo.cz' => array('http://jyxo.cz/s?s=', 'Jyxo nalezlo <b>([0-9]+)</b>'),
'Google.com' => array('http://www.google.com/search?q=', 'of about <b>([,0-9]+)</b>'),
);
foreach ($vyhledavace as $nazev => $vyhledavac) {
$pid = pcntl_fork();
if (!$pid || $pid == -1) { // potomek nebo nelze forknout
$file = file_get_contents($vyhledavac[0] . urlencode($_GET["slovo"]));
echo "$nazev: " . (preg_match("~$vyhledavac[1]~", $file, $matches) ? $matches[1] : "nenalezeno") . "\n";
if (!$pid) { // pro potomka práce skončila
exit;
}
}
}
while (pcntl_wait($status) > 0) {
// počkáme na dokončení všech dětí, ať se z nich nestanou zombie
}
?>
korny:
Apache/2.0.54 (Mandriva Linux/PREFORK-13mdk)
PHP Version 5.0.4
pcntl support enabled
prijde mi ze to zpracuje pouze tu nejrychlejsi(seznam) a ostatni pak nic..
muj vystup: Seznam.cz: nenalezeno
Chápu, že článek už je starší, nicméně nic novějšího o cURL nikde není... rád bych opravil z vlastní zkušenosti
<?php
if ($running && strtoupper(substr(PHP_OS, 0, 3)) == "WIN") {
sleep(1); // pod Windows curl_multi_select() nečeká
}
?>
bych opravil na
<?php
if ($running && strtoupper(substr(PHP_OS, 0, 3)) != "WIN") {
sleep(1); // pod Linuxem curl_multi_select() nečeká
}
?>
přesně tak se mi to chová na WIN XP SP2, multi_select čeká, ale pod Linuxem nikoli. Na Linux serveru libcurl/7.12.1 OpenSSL/0.9.7a zlib/1.2.1.2 libidn/0.5.6, na Win lokále libcurl/7.16.0 OpenSSL/0.9.8d zlib/1.2.3.
Navíc mám neřešitelný problém s cURL, kdy na windows funguje vše bez problémů, ale na Linuxu mi při multi selectu se některé spojení jakoby zaseknou a ačkoli stránky už jsou kompletně vykonány tak multi select stále čeká. Problém je reprodukovatelný cca. v 1 ze 4 shodných volání. Kdyby někdo tušil, kde je problém, tak budu vděčný za radu...
Stěžejní kód zde:
<?php
do{
$returncode = curl_multi_exec($service_handle,$running);
} while ($returncode == CURLM_CALL_MULTI_PERFORM);
//get OS name
$notwinos = (strtoupper(substr(PHP_OS,0,3)) != 'WIN');
//still running and ok performed
while ($running && $returncode == CURLM_OK){
//get number of descriptors
if(curl_multi_select($service_handle,1) != -1){
if($notwinos){
//sleep
sleep(1);
}
do{
$returncode = curl_multi_exec($service_handle,$running);
} while ($returncode == CURLM_CALL_MULTI_PERFORM);
}
}
?>
Asi to tedy nebude tak docela operačním systémem, protože mě na Windows XP SP2 se standardní distribucí PHP 5.2.5 curl_multi_select() skutečně nečeká.
Xavy:
nevyřešil jste nějak již ten problém, kdy vám pod linuxem nechtelo jít curl paralelní zpracování stejně jako pod windows ? Mám ten samý problém, u mě na apachi na windowsu mi to chodí nádherně, rychle, paralelně. Ovšem na linuxu se to chová podivně. Někdy to sice jde aspon blbě, ale většinou vůbec.
Nemyslím si, že by kód běžel o vteřinu déle – pokud funkce curl_multi_select() zjistí nějakou událost, tak nečeká. Timeout je jen pro případ, že se ještě nic nestalo.
Lukáš Břečka:
Ahojte,
mám jeden krátký dotaz.
Má smysl snažit se o paralelní zpracování pomocí PCNTL na serveru s 1 CPU s jedním jádrem?
dle mého názoru to smysl nemá.
díky moc
Podle toho u jaké úlohy. Pokud úloha většinu času na něco čeká, tak se to vyplatit může.
Lukáš Břečka:
většinou se čeká na provedení insertu nebo selectu v DB.
Zkusím to tedy pomocí PCTNL. Třeba to pomůže.
Díky
Vyplatit se to může. Ale ono hlavně nezáleží ani tak na počtu jader, protože k dobře zatíženému serveru přistupuje v praxi asi více uživatelů zároveň.
Chtěl bych se zeptat pomocí jaké funkce se pošle můj požadavek a jak zjistím, že můj požadavek byl vyřízený a mohu stáhnout data? díky
Zdravím,
oceňuji článek, vzhledem k tomu, že chci implementovat MultiRequest do svého wrapperu na cURL, ale ještě jsem se k tomu nedostal. Budu se snažit poskládat z článku a komentářů funkční řešení :)
kdyby někdo chtěl kouknout na současnou verzi: http://github.com/HosipLan/cURL-wrapper :)
Jirka:
Ahoj, mám jeden dotaz: Představ si, že skript v příkladu funguje trochu jinak - hodnotu počtu výsledku ukládá někam do databáze pravidelně (třeba se může jednat o nějaké online statistiky).
Ať už skript(nemusí jít o PHP) - proces získávání n (ať je to třeba 10 nebo 500) výsledků z několika serverů poběží konkurečně(react.php,fork,curl_multi,ruby eventmachine) nebo ne, co je lepší?
- Po získání každého výsledku ho zapsat do databáze(celkem se provede n updatů)
- Nebo si hodnoty ukládat do pole a na konci udělat jeden velký update?
Samozřejmě se asi jedná o hledisko zátěž databáze na jedné straně a odolnost vůči výpadkům na druhé straně. (Nebo jsem na něco ještě zapomněl?)
aký je tvůj názor na to?
v6ak :
V podstatě sis odpověděl. Záleží na tom, co chceme. Pokud máme vytížený DB server a malou pravděpodobnost, že se něco pokazí v průběhu, bude se hodit ukládat vše zároveň. Pokud se DB server fláká a chceme nějak řešit, že v průběhu fetchování se něco pokazí, pak asi využijeme průběžné ukládání.
Diskuse je zrušena z důvodu spamu.