Processzkezelőkkel szemtől szemben: HTTPD MPM és PHP FPM változatok linux környezetben

cikk borítókép

Webszerverek beállításánál az MPM konfigurálása egy nem elhanyagolható kérdés. Nem az üzemeltetett weblapokhoz optimalizált értékekkel ugyanis előfordulhat, hogy az oldal belassul vagy a látogatók hibaüzenetekkel találkoznak, mert a szerver nem képes több kérést kiszolgálni egyszerre, vagy éppen erőforrást pazarolsz a webszerverre, miközben máshol nagyobb szükség lenne rá. Az Apache HTTPD-nél ismert MPM-hez hasonló a PHP-nél az FPM, ami szintén a processzek kezelését végzi a PHP értelmező esetén, amikor az nem a HTTPD moduljaként működik, hanem külső szolgáltatásként. Mivel a "rimelek.hu" egy időben rendszeresen megállt, akkor megelégeltem tudatlanságom és benéztem a színfalak mögé. Ebben a cikkben összegzem a tanultakat.

MPM és FPM feladata

Gyakran, amikor elindítasz egy programot, a háttérben az több újabb processzt indít magának. Mint, amikor a nagyfőnök kirakja a melósokat a helyszínen és vezényel, miközben a többiek dolgoznak. Valami ilyesmiről van szó az MPM és FPM esetén is. Van egy fő processz, ami viszont további gyerek processzeket indít, amik külön-külön figyelik a bejövő kéréseket és ezeket akár párhuzamosan fel is dolgozzák. De, hogy hány ilyen folyamatosan figyelő processz legyen az induláskor, illetve mi történjen, ha mind dolgozik vagy épp túl sok "lógatja a lábát", azért egy külön modul felelős.

És ez az Apache webszervernél az MPM, PHP-nél pedig az FPM. Bonyolítja a dolgot, hogy ezt a fajta vezénylést többféle algoritmus alapján is el lehet végezni. Nem mindig ugyanaz az optimális választás. Sajnos a választáshoz még emberi agytekervényekre van szükség és figyelembe kell venni azt is, milyen egyéb programok dolgoznak még a gépen, illetve, hogy milyen terhelésre számítasz. De azt is érdemes végiggondolni, hogy egy-egy feladat milyen sokáig fut általában. Egy nagyon lassú feladatokkal teli oldal alatt hamarabb elfogyhatnak a processzek, mivel lassabban szabadulnak fel.

Ezen kívül egy processz még ráadásul több szálat is indíthat, amiknek száma szintén meghatározható. És ezt mind össze kell hangolni, hogy ne legyenek egymásnak ellentmondó állítások.

MPM

Jelenleg, HTTPD 2.4-ben 3 különböző MPM létezik linuxra. Ezekkel foglalkozom, mert általában linuxon fut a HTTPD éles környezetben. Főleg, ha Docker konténereket használsz. Ezek nem teljesen különböznek, vannak közös tulajdonságaik, de egy-egy direktívát esetleg másképpen kell értelmezni némelyiknél.

Event MPM

Ez a legújabb lehetőség, ami a 2.2-es HTTPD-ben is létezett már, de még "experimental"-ként, azaz nem éles használatra szánva. A 2.4-esben viszont már bátran bevethető.

Ennél az MPM-nél egy szál egy kéréshez tartozik, utána felszabadulhat és akár egy egész más HTTP kapcsolatból érkező kérést is kiszolgálhat. A beállítható direktívák a szálakat veszik alapul. Azaz a szálak kezelésén keresztül limitálható az aktuálisan futtatandó processzek száma.

Direktívák

StartServers
Ennyi darab gyerek processt indít el a fő HTTPD processz már a webszerver elindításakor. Ez alapértelmezetten 3
ServerLimit
Maximum ennyi processzt indíthat el. Az alapértelmezés 16 és le van korlátozva húszezerre. Ehhez már azért tényleg hatalmas terhelés kell, így ez keveseknél fog valaha is gondot jelenteni.
ThreadsPerChild

Minden elindult gyerek processz elindít magának ennyi darab szálat, ami a kéréseket ki tudja szolgálni. Alapértelmezetten 25. A ThreadLimit általános direktíva limitálja, hogy ennek maximum mennyit lehet megadni. Ez pedig 64 alapbeállítás szerint.

Ami azért érdekes, mert azt is írja a dokumentáció, hogy a ThreadLimit ne legyen jóval nagyobb, mint a ThreadsPerChild, mert extra, nem használt osztott memória lesz lefoglalva. De talán nem ennyi a jóval nagyobb. Ha tudod a helyes választ, kérlek, oszd meg velünk kommentben.

Tehát, ha az alapértelmezés szerint 3 darab process indul el az Apache indításakor, ami mind elindít 25 darab szálat, akkor 75 darab szál fog elindulni. Ami a következőknél lesz érdekes.

MinSpareThreads
Ennyi darab szálat kell legalább fenntartani arra, hogy azok azonnal kéréseket tudjanak fogadni. Amint nő a kérések száma, újabb szálak lesznek előkészítve, hogy legyen ennyi. Mivel egy processz által indított szálak száma adott, ez befolyásolja, hogy induljon-e el újabb processz, amíg nem értük el a ServerLimit-et. Az alapértelmezés 75, ami pont megfelel annak, ahány szál elindul már a kezdetekkor, mivel 3 process egyenként 25 szálat elindít.
MaxSpareThreads
Ahogy nő a kérések száma, úgy nő a szálak száma. Ha csökken a kérések száma, a szálak megmaradnak egészen addig, amíg az itt megadott értéknél nem több a szabad, nem dolgozó szálak száma. Amint több lesz, megpróbál bezárni annyi processzt, hogy a nem dolgozó szálak száma ez alá csökkenjen. Alapértelmezésként ennek értéke 250, ami azt jelenti, hogy akár 10 felszabadítható processz is lehet, amik ugye egyenként 25 szálat indítottak el.
MaxRequestWorkers
Ennyi darab kérést fogadhat az összes processz együtt nézve. Alapértelmezetten 400, ami pont megfelel a 16 darab maximálisan indítható szerver processz és a 25, processzenként indítható szál szorzatának.
ListenBackLog
Globális direktíva. Ha nagyon sok lenne a kérés egyszerre, akkor az itt megadott értéknek megfelelő számú kérés még bekerülhet egy várakozó sorba, ami, ha nem jár le a beállított timeout, akkor még feldolgozásra kerülhet, amint lesz szabad szál. Ennek értéke alapértelmezésben 511.
MaxConnectionsPerChild
Ez megint egy globális MPM direktíva. Alapértelmezésben 0, azaz korlátlan, de ennyi darab kapcsolatot építhet ki egy darab gyerek processz, mielőtt az ki lesz gyilkolva. Tehát nem az egyidőben kiépíthető kapcsolatokra vonatkozik. De ha memóriakezelési gondok lennének, akkor ezzel egy idő után lelőhető egy processz és felszabadul a lefoglalt memória.
AsyncRequestWorkerFactor
Ez egy trükkösebb direktíva, mivel a dokumentáció szerint is kiterjedt tesztelésre van szükség a 2-es alapérték megváltoztatása előtt, de egyfajta szorzó, ami befolyásolja, hány kapcsolatot fogadjon még el egy processz annak függvényében, hány várakozó szál van éppen. Talán megér egy külön cikket is.

Worker MPM

Ez az Event MPM elődje. Van azonban pár különbség. Például, hogy egy szál egy kiépített HTTP kapcsolaton keresztül végig foglalt marad. A szál a kapcsolathoz tartozik. Ezen kívül nincs AsyncRequestWorkerFactor direktíva.

Prefork MPM

A Prefork MPM nem szálakban gondolkodik, hanem processzekben. Tehát a MaxSpareThreads helyett MaxSpareServers van és MinSpareThreads helyett MinSpareServers. Mivel itt nem szálak vannak, hanem processzek, a ServerLimit is 256-ra van állítva alapból, ugyanis a MaxRequestWorkers 250, tehát ennyi processzt mindenképp el kell tudni indítani. De ráhagy egy kicsit. Ahogy látható, a preforknál tehát alapból több processz indulhat.

MPM tanulságok

Amit a fentiek szerint észben kell tartani:

  • Ha a StartServers értéke nagyobb, mint a ServerLimit (ami nincs a konfigurációs fájlban alapból, de a default 16), akkor nem tud elindulni a szerver.
  • Ha a StartServers értéke kisebb, mint a MinSpareThreads / ThreadsPerChild, akkor már az elején sem tud elég szál várakozni, ezért már ekkor elindulnak új processzek, hogy legyen elég várakozó szál.
  • Ha a StartServers * ThreadsPerChild értéke nagyobb, mint a MaxRequestWorkers, akkor már az elején több szálat próbál elindítani, mint ahány kérés teljesíthető, ezért minimum hibaüzenettel fog indulni a szerver, hogy elérte a MaxRequestWorkers limitet.
  • Ha a MaxRequestWorkers értéke kisebb, mint a ServerLimit * ThreadsPerChild, akkor sem tud a ServerLimit-nek megfelelő számú processz elindulni.
  • A MaxRequestWorkers értéke nem lehet nagyobb, mint a ServerLimit * ThreadsPerChild, mivel nem is tud elég processz elindulni hozzá.
  • Ha a MinSpareThreads értékét 75-ről 76-ra módosítom, de a ThreadsPerChild 25 marad, akkor is el fog indítani 100 szálat, mivel szüksége lesz még egy gyerekre, ami 25 szálat fog indítani, nem 1-et. Tehát ezt a ThreadsPerChild szorzatára érdemes beállítani. Érdekes kérdés, mi történik, ha a Min értéke 76, de a max értéke 90. Mert így 100 szál fog várakozni, de 90 várakozhat szerinte maximum. De a maximum érték akkor értelmes, ha az legalább ThreadsPeChild-dal nagyobb, mint a minimum.
  • Az egy processz által kihasznált memória behatárolja, hogy hány processzt szabad engedni attól függően, mennyi áll rendelkezésre. a HTTPD processzek kevesebb memóriát esznek, ha a PHP FPM-mel indul, mert az FPM-nek saját processzei és limitjei vannak. Apache modulként a PHP memóriahasználata is hozzáadódna. Eddigi tapasztalataim szerint ez akár 4-5 -szörös különbséget is jelent a modulként futó PHP terhére.

Arra, hogy mennyi memóriát használ egy processz, az alábbi parancsot találtam:

ps aux | grep 'httpd' | awk '{print $6/1024 " MB";}'

Forrás: http://drupalwxt.github.io/performance/apache-fpm/

De ebbe belekerülne a grep processze is, illetve más, amiben épp szerepel a httpd is. Ezért érdemes előre megnézni, hogy néznek ki a httpd sorok a listában és pontosabb szűrést készíteni. Például:

ps aux | grep 'httpd -DFOREGROUND' | grep www-data | grep -v grep | awk '{print $6/1024 " MB";}'

Mivel a legelső processz a root processz, azt a második esetben szintén kiszűrtem. A többi érdekesebb. Docker konténereknél számításba kellene venni, hogy egy node-on hány konténert indítunk és így mennyi jut egy Apache-nak. Ezért fontos, hogy ne legyen alapértelmezésen a beállítás. Illetve ennek tudatában a konténer memórialimitjét is be lehet állítani.

PHP FPM

A FPM-nek hasonló beállításai vannak, mint a HTTPD-nek. Itt is van három mód: static, dynamic, ondemand

static
Ez megfelel a prefork-nak. Tehát előre elindul valahány PHP gyerek process ( pm.max_children ) és ezek élnek is folyamatosan
dynamic
Itt a pm.max_children a max elindítható processzek számát adja, de az Apachehoz hasonló "pm.start_servers", "pm.min_spare_servers" és "pm.max_spare_servers" befolyásolja, hány processz induljon el az elején és mennyi legyen fenntartva készenlétben. A felesleges processzek az idő függvényében nem záródnak be.
ondemand
A dynamic-hoz hasonló, viszont nincs minimum processz szám. Ha nincs kérés, nincs processz sem. Ha van kérés, elindulnak processzek, de "pm.process_idle_timeout" letelte után is bezáródnak, nem csak a szabad processzek számától függően. Így egy kevésbé forgalmas oldalnál memóriát lehet spórolni, de a timeout-on túl érkező kérések lassabban lesznek kiszolgálva valamivel, mivel akkor indul el neki a processz is. De pont egy kevésbé látogatott oldalnál ez talán nem lényeg. Én ezért használom általában ezt, mivel, ha sok a látogató, akkor lehet, nem is nagyon lesz a timeoutot elérő processz, hogy bezáródjon. Ha meg kevés, akkor valószínűleg kevésbé is fontos, hogy villámgyorsan induljon el az oldal minden alkalommal.

A memóriakihasználás becsléséhez:

ps aux | grep 'php-fpm: pool' | grep -v grep | awk '{print $6/1024 " MB";}'

Persze ezeket úgy érdemes mérni, hogy közben generálni kell lehetőleg nagyobb terhelést is erőforrásigényesebb oldalak megnyitásával. Másképpen nem igazán kapsz valós képet.

Az Apache HTTPD és PHP kapcsolata

Az FPM-hez kapcsolódhat a webszerver unix socketen keresztül vagy tcp socketen. TCP socket elvileg valamivel lassabb az adatok oda-vissza konvertálása miatt, viszont Dockernél csak így lehet több példányban indítani ugyanazt a konténert (terheléselosztás klaszterben). TCP socketnél limitálni kell, honnan lehet megszólítani a szervert. Dockernél a PHP konténer akár az Apache konténer hálózatát is újrahasznosíthatja, így a 127.0.0.1-re lehet limitálni és csak az az egy konténer fogja elérni akkor is, ha a virtuális hálózat IP címeit használva egyébként elérnénk a portot más esetben bárhonnét. Vagy mehet saját hálózattal, de akkor az Apache IP címére kell korlátozni, amit nem tudunk előre általában.

További érdekes kérdések

  • Korábban a rimelek.hu-nál belefutottam, hogy az FPM-et elérő proxy modulban bekapcsoltam az "enablereuse" opciót. Ez életben tartotta az összes PHP processzt, elfogytak és Gateway error jött nagyon gyakran. Biztos van, amikor van értelme, de nem biztos, hogy érdemes erőltetni.
  • Általában nincs szükség sokkal több indítható PHP processzre, mint amennyi kapcsolatra képes a webszerver, mert sosem lesz kihasználva. Igaz, előfordulhat, hogy CSS, Javascript vagy akár képfájlokat is PHP szkript generál. Mivel ezek akár egy kapcsolaton keresztül is átjöhetnek, ilyenkor nyilván a PHP kérések száma több lehet, mint a webszerver felé érkező kérések száma.
  • Ha jóval kevesebb, mint amennyi kapcsolatot tud az Apache, és a kérések nagy része nem statikus, hanem PHP szkript, és a PHP processzek lassúak, mert hosszú feladatokat végeznek, akkor nem lesz, aki kiszolgálja az Apache kéréseket és az fcgi (ami a PHP fpm-hez kapcsolódik) server sokszor éri el a timeoutot.
  • Docker konténernél, ha egy alkalmazáshoz saját Apache és saját PHP FPM tartozik, akkor ezeket jobban össze lehet hangolni.

Összefoglalás

A fentiekből látható, hogy annyira nem triviális egy szerver üzemeltetése és Docker ide vagy oda, éles rendszerek esetén fontos, hogy a rendszergazda tisztában legyen a szerver működésével. A cikkben nem volt szándékomban egy dokumentációt másolni, ezért nem is fogsz mindent megtalálni benne. Javaslom a hivatalos dokumentáció tanulmányozását is.

Mivel nem teszteltem le az összes lehetséges konfigurációt, akadhatnak a cikkben tévedések is. Ha találsz ilyent, kérlek jelezd akár hozzászólásban, hogy mielőbb javíthassam!

Források

Megosztás/Mentés