ugrás a tartalomhoz

A háttéreljárások hátulütőiről

janoszen · 2010. Aug. 16. (H), 11.13

A minap belefutottam a gyakorlatban egy érdekes problémába a PHP-ban írt háttérben futó folyamatai kapcsán. Ennek kapcsán úgy döntöttem, összeszedem azokat a baklövéseket, amiket egy fejlesztő elkövethet aszinkron folyamatok írásakor.

Élek a gyanúperrel, hogy a fejlesztői társadalom nagy százaléka – tisztelet természetesen a kivételnek – a háttérfolyamatait úgy valósítaná meg, hogy crontabból elindítana egy PHP fájlt, ami aztán mindenféle varázslatos úton-módon megcsinálná a megcsinálandót a saját logikája szerint. Lássuk a buktatókat:

  • Egymásra futás: amennyiben a fejlesztő nem ügyel arra, hogy egyszerre több ilyen PHP script is futhasson, könnyen előfordulhat az, hogy egyre növekvő számú szál kezdi el ugyanazt feldolgozni, amíg végül az oprendszer meg nem adja magát. Ez azért alattomos, mert a fejlesztői környezetben jóval egy perc alatt lefutnak a feladatok, azonban ahogy kezd nőni az adatmennyiség és végrehajtandó feladatok száma, úgy kerül a végrehajtás ideje egyre közelebb a halálos egy perchez, ami a crontab minimális felbontása és ahol el tud kezdődni az egymásra futás.
  • Éhezés: egy PHP script átlagos módszerekkel egy szálon fut. Ez azt is jelenti, hogy alapvetően a feladatok egymás után hajtódnak végre, tehát ha nem alkalmazunk valamiféle prioritást, előfordulhat az a kellemetlen eset, hogy egy viszonylag rövid és gyors futást igénylő folyamatot feltartóztat egy hosszabb lefutású valami.
  • Monitorozás: mint fejlesztők, hajlamosak vagyunk abból kiindulni, hogy az alattunk működő rendszer bármilyen körülmények között teszi a dolgát, nincsenek tranziens vagy permanens hibák. Mivel a háttérfolyamatainkra azonban nem vigyáz semmilyen webszerver, nincs, ami helyrehozza azt, ami lerohadt, érdemes egy kicsit foglalkozni a monitorozással is.

A kérdés adott, milyen megoldásokkal lehet ezt kikerülni?

Egymásra futás

Az egymásra futás megoldása viszonylag egyszerű, ki kell olvasni a saját process ID-nkat a posix_getpid() függvénnyel, majd le kell írni egy fájlba. (Ha a fájlban már van valami, akkor természetszerűleg nem szabad így tenni.) A dolog egyetlen hátulütője az, hogy ha a PHP egy fatal errorral eltávozik az örök bitmezőkre, nincs ki eltakarítsa a pidfájlt, a háttérfolyamatok leállnak. Pontosan ezért érdemes a PHP futást egy shell scriptbe burkolni, ami ezt megoldja:

#!/bin/bash
PIDFILE="/var/www/example.com/mybackgroundprocess.pid"

if [ -f $PIDFILE ]; then
	exit 1
fi

echo $$ >$PIDFILE
/usr/bin/php /var/www/example.com/cron.php
rm $PIDFILE

Ez nem is volt olyan bonyolult. Természetesen ettől még lepusztulhat az egész, de ez a monitorozás témakörébe tartozik.

Éhezés

Az éhezés egy izgalmas témakör, mert a tesztkörnyezetben ritkán jön elő. Tegyük fel, hogy van három fajta feladatunk, amit prioritási sorrendben futtatunk, egyet egyszerre. A teszt környezetben semmi gond, mindegyiknek viszonylag ritkán akad teendője, ezért szépen sorban lefutnak, mindegyik három percenként. Az éles rendszerben viszont kezdenek jönni a gondok, a futás lassú lesz, mivel mondjuk az elsőnek annyira besűrűsödtek a dolgai, hogy a többi a szó legszorosabb értelmében éhezik, egyszerűen nem jut el odáig, hogy fusson.

Fejlesztőként szeretünk tökéletes kontrollt alkalmazni a rendszer felett, ezért sokan döntenek úgy, hogy PHP-ból vezérlik a feladatok időzítését. A konkrét folyamatok vannak, akkor viszont érdemesebb lehet a crontabbal elvégeztetni az időzítést és minden háttér folyamatnak különálló cron bejegyzést és indítófájlt készíteni.

Ezzel ellentétben, ha olyan feladatok vannak, amik előre ki nem számítható időközökben jönnek létre egy feladat sorban, érdemes feladatonként kezelni (megjelölni és ellenőrizni azt, hogy egy feladat futása folyamatban van-e) és a cronjobokat hagyni egymásra futni. Ha felsűrűsödnek a teendők, az egymásra futó folyamatok révén egyszerre több szálon hajtódnak végre. A feladat szintű zárolás pedig segít abban, hogy ne legyen géprohasztó az egész. Természetesen ez egy meglehetősen nyers megoldás, sokkal kifinomultabb eszközök állnak rendelkezésre, ha lehetőségünk van saját daemont futtatni például Gearmannel vagy más messageing vagy task queue rendszerekkel.

Monitorozás

Ha saját virtuális vagy fizikai szerver tulajdonosok vagyunk, minden bizonnyal beüzemeltünk már valamiféle monitorozást (ha nem, akkor ejnyebejnye). Linux esetén jó esély van rá, hogy ez a szoftver a Nagios lesz, ami azért jó, mert baromi egyszerű hozzá plugint írni.

 
<?php
const NAGIOS_OK=0;
const NAGIOS_WARNING=1;
const NAGIOS_CRITICAL=2;
const NAGIOS_UNKNOWN=3;
$status = NAGIOS_UNKNOWN;
 
// Service check itt
 
exit($status);
 

Innentől már csak annyi a feladatunk, hogy kitaláljuk, mire milyen eredményt szeretnénk kapni. Ezek után konfiguráltassuk be a rendszergazdával a service checket, ami aztán akár még SMS-t is küldhet, ha probléma van.

További olvasmányok

Ha szeretnénk egy kicsit jobban elmélyedni a témában és megérteni az időzítő algoritmusokat, érdemes elolvasni Andrew S. Tannenbaum és Andrew S. Woodhull Operációs Rendszerek (tervezés és implemencáció) című könyvéből a megfelelő részeket. A könyv egyébként igen-igen hasznos bármely fejlesztő számára, úgyhogy aki nem fél egy kis elméleti alapozást szerezni, annak mindenképpen ajánlanám elolvasásra.

Amennyiben crontab helyett a daemonok kerülnek elő a megvalósításnál, érdemes elolvasni a Weblaboron is megjelent Nagy terhelésű rendszerek fejlesztése 2 cikket (shameless self promo).

 
1

Operációs rendszerek

Poetro · 2010. Aug. 16. (H), 12.39
Az Operációs rendszerek könyv elolvasását én is minden programozónak javaslom, főleg, ha több szálú alkalmazásokat akarnak írni, vagy meg akarják érteni a működésüket.
2

Egymásra futás

kayapo · 2010. Aug. 18. (Sze), 19.49
Éreztem, hogy nem tökéletes az a bash script, mivel ha magát a shelscriptet lövöd ki - így a pidfile ottmarad - soha többet nem fut le a háttérfolyamat.

Inkább így:
#!/bin/bash
PIDFILE="/var/www/example.com/mybackgroundprocess.pid"

# letezik-e a pidfile
if [ -f "${PIDFILE}" ]; then
  # ha igen olvassuk fel
  PID=`cat "${PIDFILE}"`
  # nezzuk meg, hogy fut-e a folyamat
  if [ "`kill -0 ${PID}`" == "0" ]; then
    # ha fut a folamat lepjunk ki
    exit 1
  fi
fi

echo $$ > "${PIDFILE}"
/usr/bin/php /var/www/example.com/cron.php
rm $PIDFILE

3

ertesitest kuldhetnel ha ott

Tyrael · 2010. Aug. 18. (Sze), 21.29
ertesitest kuldhetnel ha ott a pidfile, de nem fut a process, valoszinuleg hibara utal.

Tyrael
4

Soha nem értettem az ilyen

kuka · 2010. Aug. 19. (Cs), 09.41
Soha nem értettem az ilyen PID-es szemfényvesztés lényegét.
  • egyim.php elindul
  • a PID-jét, mondjuk 12345, beírja az enyim.pid állományba
  • enyim.php futása idő előtt megszakad, az enyim.pid megmarad
  • egyéb folyamatok indulnak, köztük egy XMMS, a PID-je mondjuk 12345
  • enyim.php megint elindul
  • megtalálja az elődje PID-jét az enyim.pid állományban
  • ha az ottmaradt PID-el fut folyamat, méghozzá ugyanaz a futtatható mint az ő és a felhasználó is stimmel, akkor kilép
A vastagított feltételt még sehol sem láttam. (Igaz PHP esetén nem ilyen sima ügy.) Vagy csak én tulajdonítok ennek fontosságot?
5

igazabol ahogy te is irod,

Tyrael · 2010. Aug. 19. (Cs), 11.01
igazabol ahogy te is irod, nem trivialis a lekovetese, de mar maga a teny, hogy ott a script pid-je, azaz potencialis egymasrafutas tortent volna ha elindul a script, a rendszer nem megfelelo mukodesere utal.
vagy joval lassabban fut a script, mint ahogy mi szamitunk ra, vagy pedig abortalva lett a futasa.
ilyenkor a legbiztosabb a manualis ellenorzes, kozbeavatkozas.

ha robosztusabb rendszer kell akkor erdemesebb lehet elgondolkozni valami daemonizalt alkalmazason.
a monitor alkalmazasunk(nagios) figyelheti hogy fut-e a daemon alkalmazasunk, a daemon alkalmazasunk meg kiexecelheti/kiforkolhatja magabol a workereket, akiknek a futasat tudja figyelni, kontrolalni, szukseg eseten kiloheti, ujraindithatja, vagy leallithatja oket.

Tyrael
7

Daemonizálás nyet

Ifju · 2010. Aug. 20. (P), 15.11
Véleményem szerint php-val a daemonizálást érdemes elkerülni, ha csak lehetséges.

Ajánlom figyelmetekbe a supervisor python kliens-szerver alkalmazást, amellyel előtérben futó alkalmazásokat lehet menedzselni, képes figyelni arra, hogyha leállt egy felügyelt folyamat, akkor az újra elinduljon, illetve többszöri sikertelen újraindítás után képes riasztást küldeni.

A kliens részével lehetőségünk van a supervisord által felügyelt processek kimeneteire ránézni, illetve leállítani-újraindítani egyes folyamatokat. Rendelkezik webes frontenddel is, bár az a része messze nem tökéletes.

Ezen kívül ha rootként fut a supervisord, akkor lehetőségünk van arra, hogy a különböző folyamatokat nem privilégizált user nevében futtassuk.

Jelen pillanatban most egy migráció kapcsán 4 szerveren több mint 130 processt felügyeltetek ezzel az eszközzel, emellett 4 gearman tölti be a messagebus szerepét.
6

Lock

janoszen · 2010. Aug. 19. (Cs), 21.04
Igazából tök mindegy, mi van a pid fájlban, lehetne egy csont üres fájl is. A pidet azért szokás beleírni, hogy tudd, mit kell kikillelni anélkül, hogy nézegetnéd a processz listát. Ez egy nagyon egyszerű és buta lock, ha ennél több kell, írj daemont (mint ahogy Tyrael is mondta). A futó processz nevét nézegetést tessék elfelejteni, Linux alatt bármilyen processz bármilyen nevet állíthat be magának és ezt a mindenféle shellbotok aktívan ki is szokták használni (az ördög meg nem nagyon szokott aludni), ergó erre inkább ne építsen senki, inkább a monitorozásra.

A monitorozás meg egy másik kérdés, azt úgysem fogod megúszni és kezelni kell mindenféle helyzetet. Sajnos a nagyobb hiányosságok inkább ezen a ponton találhatóak. Sajnos kevés PHP fejlesztő kezéből kikerülő jól monitorozható szoftvert láttam eddig életemben. Olyat, ami kiköhögött magából egy OK stringet egy Móricka-teszt után persze rengeteget, az ilyenkor kötelező önérzettel együtt. Hogy metrikákat mondjon a saját állapotáról, Muninnal grafikont rajzoljon belőle az ember csak álmodni merek, pedig mindenkinek a saját jól felfogott érdeke, hogy ilyet írjon.