Application scope v PHP

Po čase zase jeden článek o programování v PHP - spíš takové nadhození jednoho problému než jeho řešení...

Stručné intro: Aplikace na webovém serveru nemá, pokud je napsána třeba v Javě nebo "něčem podobném", problém se svým vlastním stavem. Tedy že o sobě v každém okamžiku ví, co je, co dělá a čím je. Nemusí přemýšlet, jestli je to ona sama nebo svoje sestra, protože v systému je jen ona. Byť má spuštěno mnoho obslužných vláken, byť běží jako víc procesů, vždycky má nějaký základ ("kontejner"), kde se všechny její kousky sbíhají a kde mohou komunikovat.

Aplikace v PHP tohle neví. Na serveru s aplikací v PHP plní úlohu toho kontejneru HTTP server (Apache třeba), který si podle potřeby volá PHP interpret a předává mu potřebné parametry, tedy hlavně cestu ke skriptu, který je potřeba provést. Aplikace v PHP tedy není monolit s jednou hlavou a mnoha chapadélky - jsou to jen ta chapadélka. A nic víc programátor nevidí.

Ono to většinu času nevadí. Renomovaní odborníci na Rootu odhadují, že 94.7135% aplikací v PHP nikdy nepotřebuje mít něco, co by bylo v celé aplikaci, nehledě na to kolik žádostí je právě obsluhováno, jednou a jen jednou (a pokud možno furt - PHP je taková kvantová technologie: běží jen když se někdo dívá).

K čemu že taková věc je? No, třeba - počet přihlášených uživatelů. Pokud chcete spočítat přihlášené uživatele, musí se jednotlivá vlákna shodnout na jednom místě, kde si budou dělat čárky za každý login...

Leckoho napadne: Vždyť máme společný bod - databázi! Ta si řeší konkurenční přístupy, tam to půjde! Udělá si tabulku, do ní si poznačí, kdo že je přihlášen, každé vlákno, které provede "login.php", tam cosi zapíše, podle nějakého pravidla se zase odhlášení (či "odpadlí") odepíší, a když budeme chtít vědět, kolik máme přihlášených, tak to prostě spočítáme.

Drsnější udělají to, že spočítají počet session souborů v adresáři /temp (pokud tedy session dostanou jen přihlášení). Nemusí se starat o expiraci a podobná zvěrstva a mají to bajvoko přesně.

Tak to by šlo. Sice to není kdovíjak pohodlné, ale nešť. Co dál?

Dál třeba fronty. Příklad: Uděláte službu, kam půjde nahrát fotografie. Služba si ji uloží a udělá s ní něco... blíže nespecifikovaného... což ovšem trvá třeba minutu. Nebo pět. Nemůžeme nechat uživatele čekat minutu koukat na stahovanou stránku, protože ji zavře. Musíme to udělat jako na YouTube, kde po uploadu může uživatel normálně pracovat, a video se zatím NA POZADÍ nějak zpracovává.

Takže jde o to, aby uživatel mohl něco nahrát, uploadovací skript vzít data, dát je do fronty "ZPRACOVAT" - a jede se dál, až to zpracují, tak to zpracují. Jak to udělat?

Tak, šlo by, že by skript vytvořil nový asynchronní proces na úrovni OS, s nízkou prioritou, a nečekal na jeho dokončení. To je cesta, ale přináší to některé problémy - pokud je zpracování náročné nebo pokud nějak zapisuje do společného prostoru, tak to není nejšťastnější řešení. Procesy se budou hromadit, budou si navzájem překážet, zamykat si přístup...

Lepší bude udělat frontu - něco jako v automyčce. Přijede auto a obsluha ho pošle do fronty. Jeden nezávislý proces ("myčka") pak dělá stále dokola to, že bere toho kdo je na řadě, udělá potřebné operace, vypustí ho ven a bere dalšího. Pokud ve frontě nikdo není, tak čeká.

A jsme zase u toho společného prostoru. Jasně, společná fronta se dá simulovat databází, to není problém, a pak nějak spustíme jedno vlákno, kde bude běhat ten proces stále dokola a dokola a... Aha, a v PHP to uděláme jak?

Ještě odbočím: Asynchronní komunikaci mezi vlákny a "společný datový prostor pro celou aplikaci" lze nasimulovat nejen pomocí databáze, ale i třeba dnes velmi populárním memcache - tento způsob má tu nespornou výhodu, že je rychlý.

Vrátím se k tomu jednomu univerzálnímu společnému obslužnému procesu v PHP. K němu se váže několik otázek:

1. Jak ho spustit?

2. Jak ho udržet naživu?

3. Jak ho spustit jen jednou?

---

Přiznám se rovnou, že správné odpovědi neznám. Kdysi jsem ale tuhle úlohu řešil, tak mám pár poznatků.

Řešil jsem na Bloguje přidávání příspěvků pomocí ICQ. Pokud jste někdy viděli protokol ICQ zblízka, dáte mi za pravdu, že se s ním moc kouzlit nedá. Poslat po ICQ zprávu ještě jde, ale přijímat...?

Nakonec jsem tedy objevil, jak se přihlásit a přijmout zprávu, co čeká ve frontě. První nápad byl, že tedy udělám skript, který přesně tohle provede, stáhne zprávy, uloží je do databáze, a šmytec. Když jej budu volat dostatečně často, třeba každou minutu, tak to bude to pravé ořechové.

Což fungovalo perfektně - prvních pět minut. Pak server oznámil "too many login attempts" a zablokoval přístup na další hodinu. Tudy cesta nevedla a musel jsem jít opravdu cestou jednoho jediného skriptu, který se jednou přihlásí a pak bude cyklicky kontrolovat zprávy a pracovat jak pilná včelka...

Bohužel, cesta nějakého Pythonu, vlastního programu či "nice ./blabla.pl &" nešla jednoduše použít, tak nezbylo, než to opravdu celé smontovat v PHP.

Udělat nekonečnou smyčku není problém. Problém je udržet ji při životě a neumřít tím server. Na udržení při životě jsem použil nastavení velkého timeoutu (a to přímo v ní), na to, aby server taky dělal něco jiného a ne jen rotoval uvnitř nekonečné smyčky jsem použil, tuším, sleep(). Problém jak skript spustit právě jednou jsem neřešil, použil jsem metodu "zavolám to sám wgetem".

A vše fungovalo, krásně a perfektně. Hurá. Po pěti hodinách to celé ovšem zabředlo. Proces žil, ale neběžel, spíš se jen tak klepal... Nakonec jsem sáhnul k metodě "Pán jeskyně" - k CRONem spouštěnému procesu, co každé dvě hodiny výše jmenovaný PHP proces natvrdo zabil a vytvořil nový.

---

Ne, to rozhodně není hezký způsob, jak to v PHP řešit. Podle Jakuba Vrány takové řešení, které by umožňovalo vytvářet podobné "PHP daemony" na obecném webserveru (tedy i "Troška lepší sdílený hosting") čistě, snad ani neexistuje (jestli jsem Jakuba dobře pochopil, jestli ne, tak se omlouvám). Čisté řešení je "napsat to v něčem jiném, co na hostingu je".

A teď se zeptám (a povolím komentáře): Řešili jste už někdy v PHP něco, co by bylo nejlépe řešitelné pomocí neustále běžícího procesu, "singletonu" (proces, který běží vždy jen v jedné kopii) a pomocí společného aplikačního prostoru? A jak jste to řešili?

---

Komentáře k věci:

Démonické PHP v pasti - popis zajímavého UNIXového hacku, který řeší "neustálý běh".

Komentáře

Setkal jsem se v praxi s

Setkal jsem se v praxi s konverzí videa na serveru, kde se to ale řešilo skrz rouru v UNIX-like systému (PHP připsalo do roury a nezávislý proces ji zpracovával).

Napadá mě, jestli by k řešení přímo tohoto problému s vyzvedáváním zpráv nešlo využít permanentní spojení (http://www.php.net/manual/en/function.pfsockopen.php)? V ten okamžik by nebyl problém s "Too many login attempts", protože by to spojení bylo "na pozadí" otevřené a v okamžiku, kdy by skript požádal o spojení, tak by dostal již toto otevřené (permanentní) spojení. Není to vyžadovaný "singleton", ale myslím, že smysl to má.

IMHO řešením "singletonu" v některých případech by bylo to, že by se celá aplikace serializovala a v pravidelných intervalech (buď volané CR0Nem nebo by se po dokončení každého (či některých) dotazů na server kontrolovalo, zda-li už je čas) deserializovala a zavolala vyžádaná činnost. Problémy by tam samozřejmě byly (třeba s přesným časováním), ale čistější řešení mě nenapadá.

PHP není jen pro web

PHP je naštěstí (bohužel?) taky úplně obyčejný skriptovací jazyk, jehož intepretr není závislý na přítomnosti webového serveru a není omezen jen na použití na webu. Není žádný problém napsat v PHP daemon stejně jako v Pythonu, Perlu, Ruby, či jakémkoliv jiném intepretovaném jazyce. Stačí vygooglit "PHP daemon" :-) Pěkně jednoduše to popisují zde: http://kevin.vanzonneveld.net/techblog/article/create_daemons_in_php/ i s připravenou knihovnou pro psaní daemonů v PHP.

Předpokládám, že se ozve nějaký komentátor, který bude argumentovat tím, že na většině webhostingů je to prakticky nepoužitelné (např. nespustitelné kvůli nemožnosti připojit se na SSH). Naštěstí existují i zahraniční webhostingy a případně levné VPS :-)

S tím souhlasím, já si v PHP

S tím souhlasím, já si v PHP píšu CLI skripty pro Win, když jsem líný hledat nějakou jednorázovou utilitu anebo pouštět C# - JENŽE! :) Vážně by se mi líbilo, kdyby pro něco takového existovala podpora a ne hack.

Omezení technologie

Je to omezení technologie, pokud potřebuješ něco víc musíš si vybrat technologii, která to umožňuje. FB je taky udělaný v PHP, ale to slouží hlavně pro ksicht aplikace. Zbytek jsou věci v C (nebo z PHP do C přeložené) v Erlangu a kdo v čem ještě. :) PHP nikdy nebylo koncipováno na webové aplikace.

Řeším obdobné věci poměrně

Řeším obdobné věci poměrně často. Potřebuju například sledovat výstupy z doménových security logů nebo podle zadaných parametrů průběžně opingávat servery v síti a ukládat výsledek. Používám http://cz.php.net/manual/en/book.win32service.php, protože funguju v homogenním Windows prostředí a mám k dispozici vlastní server. Fronta požadavků je v databázi, služba na základě klíčového slova u požadavku ví co má provést a jak naložit s výsledkem. Paměťová náročnost v mezích únosnosti a nárůst vytížení procesorů neznatelný. Takových služeb mi na stejném serveru běží osm nebo devět k všeobecné spokojenosti.
Nicméně chápu, že to je realizovatelné pouze za předpokladu existence vlastního serveru...

Na vlastním stroji to není

Na vlastním stroji to není problém, ale už třeba u "managed serveru" budou možná prskat... :) Ale já to pro jistotu v článku zvýrazním, že mi jde o řešení, použitelné na Normálním Nebo Jen Trochu Lepším hostingu. :)

Řešil, bohužel nic lepšího

Řešil, bohužel nic lepšího než CRON + PHP jsem nevymyslel a myslím že v PHP ani nejde...

Singleton PHP

V minulosti jsem toto vyřešil nečistým, ale funkčním způsobem (inspirováno dle způsobu řízení služeb v Linuxu):
1) Singleton se spustil a zapsal PID procesu do souboru. Do druhého souboru jsem uložil aktuální timestamp. Pokud již soubor PID existoval, tak předtím ověřil, zda proces s daným PID žije a pokud ano, nespustil se.
2) Singleton běžel v nekonečné smyčce, vždy si počkal 5 sekund a pak juknul do fronty a po vykonání práce zapsal aktuální timestamp do souboru.
3) Po 60*60 sekundách singleton smazal soubor s PID a timespampem a spustil novou instanci seme sama a sám se ukončil.
4) Každých 15 minut se pustil kontrolní script, který ověřil, že timestamp není starší než 5 minut a že proces podle PID žije. Pokud ne, zmaže dané souboru (zabije případný zamrzlý singleton) a spustí novou instanci singletonu.

Fungovalo to úspěšně rok a půl a pak to někdo vyhodil i se serverem.

Nezjišťoval jsem ale jestli operační systém neposkytuje nějaké nástroje, které by toto zastoupily. Čisté řešení asi neexistuje, ale dá se s tím pracovat.

Proč za každou cenu PHP?

Já mám sice PHP rád, ale jsou situace kdy si člověk musí přiznat, že každý jazyk se hodí na něco jiného, a že jsou i aplikace, na které se můj oblíbený jazyk nemusí hodit. PHP prostě není určené na takovéhle věci. Proč to dělat zrovna v PHP? Proč ne třeba v JavaScriptu nebo Assembleru?

Tak nevím, jestli jsem to

Tak nevím, jestli jsem to nenapsal dostatečně srozumitelně nebo jestli jsem neporozuměl vašemu komentáři... Vy vidíte něco o "zakaždéceně"? To, že to mohu napsat v Perlu, Pythonu nebo v C je jasné asi každému, ale tak otázka nestojí. Otázka nezní "Jak to řešit", ale "lze to vyřešit v PHP?"

Cron vládne všem, cron je přivede,...

Spouštění démonků (a celkem jedno, jestli je to něco v php, v bashi případně binárka ala imagemagick) na vlastním stroji z cronu se mi taktéž osvědčilo jako univerzálně vhodné řešení na všechno.

A když už ten stroj běží, tak se jím dají volat i spřátelené skripty na "normálních" hostinzích (omezení třeba dle IP, tokenu, čehokoli) skrze http.

Fork a sdilena pamet...

Kdesi (tusim na builderu) se kdysi objevil takovyto nastrel reseni, kdy potrebujem spustit narocnou ulohu, ale nemuzem nechat uzivatele cekat. Komunikace se zbytkem aplikace (kde by si mezitim uzivatel pracoval) by mohly uspesne resit funkce pro praci se sdilenou pameti (shm_*()) ale otazkou je, zda je ma "prumerny lepsi" hosting povolene.

---
<?
ignore_user_abort(1);
set_time_limit(0);
/* fork na jinou stranku */
Header("Location: /nekam_jinam.html");
Header("Connection: close\n\n");
/* donutime jej poslat hlavicku.. */
echo "neco";
flush();

/*** fork proveden */
/* tady ty narocne vypocty :) */
for($i=0; $i<3000000; $i++) {/* */};
/* kontrola ze vse provedeno.. */
mail("a@b.cz","test","TEST, i=$i");
?>

To je to, co jsem popsal v

To je to, co jsem popsal v článku zde: "šlo by, že by skript vytvořil nový asynchronní proces na úrovni OS, s nízkou prioritou, a nečekal na jeho dokončení. To je cesta, ale přináší to některé problémy - pokud je zpracování náročné nebo pokud nějak zapisuje do společného prostoru, tak to není nejšťastnější řešení. Procesy se budou hromadit, budou si navzájem překážet, zamykat si přístup..." - akorát se liší v tom, že ten nový proces bude fork se stejnou prioritou. Sejde se, při dostatečné náročnosti výpočtu, takových procesů deset, a už to bude sakra znát.

Asi to bude možno trochu

Asi to bude možno trochu (dosť) overhead na problém, ktorý riešite, ale v praxi som videl práve u projektu videoportálu, kde sa po uploadovaní videa má konvertovať video v pozadí riešenie pomocou Gearman - http://gearman.org/.
Na servri beží Gearman daemon, ktorý plní v podstate len funkciu queue, beží 1-N dalších workerov (napr. PHP skriptov).
Web app v prípade, že je treba spustiť task cez Gearman API pošle parametre tasku do queue, queue vyberie voľného workera a predá parametre.
Má to závislosť na prostredí, t.j. musíte mať možnost spustiť si zo shellu daemon+workerov,
a nesplňuje Váš požiadavok na spoločný space, na druhú stranu je s tým možné jednoducho v backende manipulovať bez veľkých zásahov do app, tasky sa daju distribuovať na fyzicky oddelené stroje, ich beh nie je závislý na web app atď..

Řešil

Řešil, kombinace CRON + PHP proces to řeší k všeobecné spokojenosti (alespoň u nás).

K problemu - ne reseni v PHP

Bitblbee + python => takhle lze mit aplikaci ktera kompletne komunikuje pres ICQ a dokonce si muze pres XML stromy "povidat" s drouhou stranou - nejen davat prispevky do databaze a tak dale.

Volby prohlížení komentářů

Vyberte si, jak chcete zobrazovat komentáře a klikněte na „Uložit změny“.