V jednom z minulých článků jsem nahlas rozjímal o výhodách a nevýhodách různých webových serverů a přemítal, který z nich si vyberu pro svůj nový virtualhost. Se svou averzí k Apache jsem se nakonec rozhodl pro nginx. Ovšem pod tíhou nových skutečností (a hlavně pod tíhou několika tisíc .htaccess souborů v transferovaných webech) jsem si řekl, že Vinnetouovi nakonec šanci dám. A moc dobře jsem udělal.
Zakopaná sekera
Netušil jsem totiž, že Apache za dobu, co ho nemám rád, vyspěl do takové míry, že se už dá dokonce i používat. Měl jsem ho totiž zafixovaný jako rozežraný moloch se starým zkostnatělým prefork modelem, schopný sežrat půl paměti při nečinnosti a celou paměť, včetně swapu všech okolo stojících počítačů, při zátěži. Jaké bylo moje překvapení, když jsem zjistil, že worker MPM s pamětí hospodaří nejen rozumně, ale navíc je i velmi dobře škálovatelný, takže se dá efektivně použít jak pro malé webíky s pár desítkami přístupů měsíčně, tak i pro vhosta s několika stovkami domén a myriádami nášlapných min s příponou .php. A i hrozba kolaterálních škod způsobené touhle třípísmenkovou hrůzou se dá celkem snadno eliminovat užitím vhodných nástrojů, jako třeba PHP-FPM. A právě u výše jmenovaných, tedy worker MPM a PHP-FPM jsem zůstal i já.
Vidličky a nitě
Když jsem zjistil kolik procesních modelů a modulů dnes Apache nabízí, byl jsem příjemně překvapen. Důvody toho, proč nemám rád klasický prefork jsem naznačil v článku „Závody webserverů“, ale pro úplnost je připomenu i teď. Prefork nepoužívá vlákna. Místo nich používá prachsprosté podprocesy, takže je vhodný pro webové aplikace používající non-thread-safe (čti: „prasácky napsané“) knihovny. Třeba PHP. Vtip je totiž v tom, že všechna rozšíření, všechny knihovny a eventuálně i všechny binárky se kterými se Apache spřáhne, běží v každém z podprocesů a zpracování požadavku se odehrává pouze a výhradně v paměťovém prostoru daného Apache podprocesu. Co se týče bezpečnosti, je to super, protože když si dá cokoliv uvnitř na tlamu, vezme s sebou jen ten jeden jediný podproces a nikdo jiný si toho ani nevšimne. Co už ale není super, je fakt, že paměťová náročnost takového nastavení roste s každým novým rozšířením geometrickou řadou.
Pak tu máme worker model, který staví na klasickém modelu s vlákny, který se v praxi velmi osvědčil u „lehkých“ serverů jako lighttpd nebo nginx. I když v podání Apache se jedná o hybrida, protože se nespawnují přímo jednotlivá vlákna, ale podprocesy s kýblem vláken naráz. V nečinném stavu se v rohu paměťového banku krčí pouze několik podprocesů, které netrpělivě očekávají vaše požadavky a při zátěži se Apache rozparádí tak, že se vlákna střídají rychleji než na tkalcovském stavu. A když špička utichne, využití paměti opět vypadá, jako by se nikdy nic nestalo.
Poslední z trojice modelů pro *nix-based systémy je event model, který funguje podobně jako worker. Dokumentace sice tvrdí, že pro Apache 2.2 je model stále v experimentální fázi, ale i přesto funguje dostatečně spolehlivě na to, aby jej bylo možno bez obav použít i na produkčních systémech. Na rozdíl od workeru eventová vlákna nenakukují do fronty (polling), ale poslušně čekají a reagují na události vyslané systémem. Event model se výborně hodí (resp. je primárně určen) pro servery s vysokým počtem keep-alive spojení, protože má vlastní dedikované vlákno, které dělá v již otevřených spojích pořádek.
Mimo tři výše uvedené sice pro *nixy ještě existuje ITK model, ale ve skutečnosti jde jen o klasický prefork s tím, že každý vhost může běžet pod jiným systémovým uživatelem. Dále existují MPM specificky navržené pro další operační systémy. Jmenovitě - beos, mpm_netware, mpmt_os2 a bohužel i mpm_winnt, který, i když za to tak úplně nemůže, je ostudou všech webserverů. Kdykoliv vidím trio Apache + PHP + MySQL na Windowsovském serveru, přemýšlím, do které kategorie mentálně retardovaných musel architekt takového prostředí patřit. A zvlášť dnes, kdy je deployment webserveru na linuxu záležitostí jednoho yum nebo apt-get příkazu.
Konfigurace modelů
U preforku je konfigurace vcelku snadná.
<IfModule mpm_prefork_module>
StartServers 5
MinSpareServers 5
MaxSpareServers 10
MaxClients 150
MaxRequestsPerChild 0
</IfModule>
- StartServers - Značí počet podprocesů spawnovaných ihned po startu Apache. Očekáváte-li tedy neutichající nápor klientů, StartServers by měl být adekvátně vysoký. Jde-li naopak o nějakou testovací mašinu, kam má přístup pár kódovacích šimpanzů, pár podprocesů by jim mělo stačit.
- MinSpareServers - Udává minimální počet flákajících se podprocesů čekajících na requesty. Pokud přijde klient a pár si jich uzme pro sebe, rodičovský proces začne spawnovat další nečinné procesy, aby MinSpareServers direktivu dodržel. Maximální rychlost vytváření je 1 podproces za vteřinu.
- MaxSpareServers - Jak jste jistě správně odvodili, MaxSpareServers hlídá naopak maximální počet nečinných podprocesů. Jsou-li tedy klienti obslouženi a nemají další požadavky, začnou přibývat nečinná vlákna, a pokud jejich počet začne přesahovat hodnotu stanovenou touto direktivou, rodičovský proces si je začne zabíjet.
- MaxClients - Udává maximální počet současně obsluhovaných klientů (a u preforku tedy i maximální počet podprocesů). Všechny další požadavky nad tento limit se začnou řadit do fronty a zpracovávat až na ně přijde řada.
- MaxRequestsPerChild - Omezuje počet požadavků, které podproces může zpracovat před tím, než je recyklován. V případech, kdy očekáváte nějaké memory leaky nebo jinou neplechu, je vhodné nastavit MaxRequestPerChild na nějakou třícifernou hodnotu dle vašeho uvážení.
U workeru je situace poněkud složitější, ale trocha experimentování a zdravého selského rozumu vás jistě rychle ke vhodným hodnotám navede.
<IfModule mpm_worker_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxClients 150
MaxRequestsPerChild 0
</IfModule>
- StartServers - Podobně jako u preforku i zde StartServers určuje počet podprocesů spawnovaných po startu Apache. Nenechte se však zmást tím, že Apache ve skutečnosti nastartuje o jeden podproces více. Tento totiž nebude obstarávat requesty jako takové, ale bude ve věcech dělat pořádek a přehazovat nově příchozí spojení jednotlivým pracovním vláknům.
- MinSpareThreads - Alternativa k preforkovému MinSpareServers. Tentokrát určuje spodní limit celkového počtu nečinných pracovních vláken.
- MaxSpareThreads - Totéž v bledě modrém.
- ThreadLimit - Ve skutečnosti udává velikost sdílené paměti pro všechna vlákna podprocesu. Efektivně pak nastavuje maximální počet vláken pro podproces, která mohou být direktivou ThreadsPerChild nakonfigurována. Pokud tedy chcete co nejméně rozežraný server, pak je dobré nastavit ThreadLimit a ThreadsPerChild na stejnou hodnotu.
- ThreadsPerChild - Udává, kolik pracovních vláken si s sebou ponese každý nově naspawnovaný podproces.
- MaxClients - Stejně jako u preforku, i zde MaxClients nastavuje maximální možný počet současných spojení.
- MaxRequestPerChild - Opět udává počet požadavků, po kterém je podproces zahozen.
Noticka k worker MPM
Zpočátku se všechny ty údaje a možnosti nastavení worker multi-processing modulu mohou zdát nepřehledné a matoucí, ale zkusím v tom udělat trochu jasno. Jak už jsem se zmínil výše, je třeba si nejprve uvědomit, že worker MPM nespawnuje jednotlivá vlákna, ale rovnou celý nový podproces s pevně daným počtem vláken. Po spuštění Apache tedy bude naspawnováno StartServers procesů z nich každý bude mít ThreadsPerChild vláken. V konfiguraci uvedené výše nás tedy po startu budou netrpělivě očekávat dva podprocesy po pětadvaceti vláknech, celkem tedy 50 vláken připravených plnit rozkazy. Je-li na serveru nával, Apache vytvoří další podproces s ThreadsPerChild vlákny. V našem případě tedy budeme mít 3 podprocesy a 75 vláken, pak 4 podprocesy s celkem 100 vlákny až dokud Apache nedosáhne ServerLimit podprocesů. Výchozí ServerLimit, který v konfiguraci výše není vidět, je 16, takže Apache může při návalu může naflákat teoreticky až 400 pracovních vláken, ale protože máme nastaven MaxClients na 150, počet vláken se bude plazit někde okolo tohoto čísla. A když špička opadne, začne podprocesy zabíjet tak, aby byl celkový počet vláken menší nebo roven MaxSpareThreads.