Nő a terhelés az oldalon, lassul az oldalletöltés, felhasználók türelmetlenek és továbbállnak, ha sokat kell várniuk. Ha csak kicsivel is lassul az oldal átlagos betöltési ideje már érezhetően csökken a látogatások száma. Google is szereti (SEO), ha gyorsan jön be az oldal.
Arról nem is beszélve, hogy sok erős hardver megvétele/fenntartása drága. Kézenfekvő a szerveroldali optimalizáció, azaz minél kisebb, gyengébb hardveren, minél jobb eredményt érj el teljesítmény tekintetében. Minél több felhasználót, egyszerre történő lekérést szolgálj ki, minél gyorsabban.
Aztán azt se szeretnéd, hogy ha hirtelen nő meg a terhelés, akkor elérhetetlen legyen az oldal.
Fontos még azt megjegyeznem, hogy itt a lassú oldalletöltés alatt most csak az oldalgenerálás idejének problémájával foglalkozom. Teljesen más téma, ha az oldal lejön gyorsan, de még betölt több tucat CSS és JavaScript fájlt és ezek miatt lassú a kliens oldalon.
Megoldás?
Cache. Ez a sokszor hallott szó – amelyre nem tudnék magyar fordítást adni – kihúz téged a pácból. Persze csak akkor, ha a kódod már optimális más szemszögből. Bár ha nem hallottál a cache-ről, akkor nem hiszem, hogy így van.
Mit takar a cache-elés?
A lényeg, hogy a nagy műveletigényes adatkinyeréseket valamilyen módon gyorsítod. Ha az adatokat csak lassan lehet kinyerni, akkor az eredményt célszerű eltárolni egy gyorsabban elérhető helyre és onnan kiszolgálni. (Azaz a kész kinyert/összeállított adatot – a végeredményt – kell elmenteni és csak azt újra felhasználni.) Időnként persze ezt frissíteni kell a frissebb adatokkal.
Cache szintek
Maga a cache-elés több szinten is megvalósítható. Ebben a cikkben a cache három szintjével fogok foglalkozni:
- php opcode cache/mysql query cache
- reverse proxy
- php kód
Egy nagy terhelésű portálon mindegyiket célszerű alkalmazni. Ha mind a három szintet használod, akkor már sok meglepetés nem érhet. (Ez persze ebben a szakmában nem igaz ☺)
Könnyen gyorsat
Kezdem is a legkönnyebb és leggyorsabb eredményt szolgáltató megoldással. Ehhez csak fel kell telepíteni egy opcode cache kiterjesztést PHP-hoz (ajánlom az apc nevűt) és hihetetlen jó eredményeket kapsz. Ez annyit tesz, hogy a php fájlból generált opcode-ot, azaz már értelmezett, bináris kódot eltárolja a cache, és ha újra ugyanazt a fájlt kell futtatni, nem kezdi el újra értelmezni (hacsak nem változott), hanem kiszolgálja az opcode cache-ből. Ezzel már nagymértékű, általános feldolgozási időt nyersz. Gyakorlatilag pár perc munkával látványos eredményt lehet elérni. Ez a legjobb befektetett energia/eredmény arányú megoldás.
Ehhez hasonló hatékonyságú a MySQL query cache. Ez pusztán annyit tesz, ha ugyanolyan mysql lekérés (select) érkezik a szerverhez, amiről már kapott eredményt egyszer, akkor egy ún. query cache-ből szolgálja ki. Így 200-300x gyorsabb a feldolgozás. Persze csak akkor, ha nem változik gyakran az adat, ugyanis ha bármely érintett táblában volt módosulás, akkor már újrafuttatja a lekérést.
Ehhez ellenőrizd le, hogy be van-e kapcsolva az opció a MySQL konfigurációs fájlban:
query_cache_size = X
query_cache_type = Y
ahol X, a cache mérete byte-okban (max. pár 10 MB elég) és Y = 1, ha alapértelmezetten be akarod kapcsolni és Y = 2, ha alapértelmezetten nem, de kérésre működjön (query-ben meg kell adni direktben, hogy ezt most cache-elje).
Ez a két egyszerű módszer sokat dobhat a teljesítményen, de elég korlátoltak és csodákra nem képesek, ha bonyolultabb a rendszer, az adott portál. Viszont legalább a fölösleges overheadet elveszik. Mindenképen nyersz vele, de annál kevesebbet, minél bonyolultabb megoldásról van szó. Mivel ilyen kevés befektetést igényelnek, ezért kezdd ezekkel!
Reverse proxy megoldások
Sokan ettől várják a csodát. Aztán az vagy jön, vagy rosszul jön, vagy sehogy. Ehhez kicsit gondolkodni is kell. Ennek a lényege, hogy bizonyos url-eket cache-elsz, mégpedig úgy, hogy a reverse proxy szerver eltárolja magának az egyszer már lekért url-t. Nem kéri el újra az app szervertől, azaz nem kell a php-nek újra előállítania azt az oldalt az adott url-en, hanem villámgyorsan kiszolgálja a proxy szerver. Tipikusan használt reverse proxy szerverek: varnish, nginx. Bár utóbbi nem erre van dedikálva, de erre is jó és használják is sikerrel. Ezekkel hihetetlen gyorsan lehet statikus tartalmakat kiszolgálni, hisz nem is kell mindig eljutni a php-ig. Legkönnyebben http header paraméterekkel lehet szabályozni, hogy adott URL-t meddig cache-elje a proxy. Fontos, hogy be legyen állítva a következő http header:
Cache-Control: public, max-age=X
ahol X egy szám, mely megadja másodpercben meddig legyen cache-ben az url. Persze itt nem állnak meg a lehetőségek, például varnish-nak saját konfigurációs nyelve van, ahol aztán tényleg mindent testreszabhatsz. Ehhez persze elengedhetetlen, hogy a keretrendszered támogassa ezt a http headert és megfelelően kezelje.
Miért nem jön a csoda?
Hát például azért, mert rengeteg dinamikus tartalmad van. Ezzel az a baj, hogy ezt eltárolva elveszik a dinamizmus vagy pedig keveset tud cache-elni a reverse proxy. Aztán azért nem jön, mert nem jól használod. Például a hibásan működő keretrendszer mindig cookie-kat állítgat be fölöslegesen és a varnish ilyenkor alapértelmezetten nem cache-el. Ha cookie beállítást észlel, azaz a hozzátartozó http header jelen van. A jó hír, hogy van megoldás. Legalábbis részben, de itt már nagyon ügyeskedni kell. Ugyanis a Varnish tud olyat, hogy az oldal bizonyos elemeit a háttérszerverektől veszi (php-val le tudod generálni), míg a nagyobb részét a saját cache-éből.
Ezt nevezik Edge Side Includes, azaz röviden ESI-nek. Ilyenkor tényleg meg lehet azt tenni, hogy például az oldal login információkat megjelenítő része dinamikus, így teszem azt a megjött üzeneteket, és egyéb login információkat dinamikusan meg tudod jeleníteni, de az oldal többi részét a varnish villámgyorsan kiszolgálja. Ehhez csak picit kell módosítani a html kódot, amit generálsz:
<!–esi <esi:include src="[http://valami.valahol/?esi=esilogininfo](http://valami.valahol/?esi=esilogininfo)"> –>
Ebben a példában, ha működik az ESI funkció, akkor betölti a login információkat a http://valami.valahol/?esi=esilogininfo url-ről dinamikusan. Ha valamiért nem működne az ESI rendszer, akkor sima html comment lesz, így ha valamit elkonfigurálsz, vagy más gond van, akkor se rontja el az oldalt ez az esi:include tag.
Persze ehhez meg tényleg profi keretrendszer kell, hogy ezt lekezelje és ezeket automatikusan generálja az igényeidnek megfelelően. Így lehet maximalizálni a reverse proxy-k hatékonyságát a dinamizmust megőrizve, hisz az okos keretrendszer csak azt a pici részt generálja le, nem kell neki az egész oldalt.
Reverse proxynak van más előnye is, ilyen például a horizontális skálázás könnyű megvalósíthatósága. Azaz nagyon könnyen tudsz több szervert beállítani és a reverse proxy okosan kihasználja a több back-end szerver kapacitását, szétosztva a kéréseket köztük.
Ezekről még sokat lehetne beszélni, például be lehet állítani, hogy a reverse proxy miben tárolja a cache-t: fájlokban, memóriában, stb. Így ha sok a RAM, nagyon nagy sebességet érhetsz el egy memcached alapú tárolással.
PHP kód szintű cache
Ez a legtöbb munkát igénylő rész: amikor programozni kell a cache-elést, hiszen szeretnél dinamikus tartalmat kiszolgálni, okosan és gyorsan. Ha még használsz is reverse proxykat, akkor is a dinamizmus miatt (akár ESI kérés miatt) gyakran eljut a kérés a back-end app szerverig és akkor a PHP-nak kell előállítania valami tartalmat, lehetőleg gyorsan. Ha meg nincs reverse proxy, akkor itt kell nagyon ügyesnek lenni.
Miket kell leggyakrabban cache-elni?
- SQL lekéréseket
- Távoli lekéréseket (másik szervertől, másik hálózaton)
- bármit, ami lassan fut le
A legtipikusabb megoldás
Egyszerűen annyit kell tenni, hogy az időigényes kérés eredményét lemented fájlba és ha a fájl létrehozási ideje X percnél régebbi, akkor újra lekéred és újra lemented. A köztes időben meg csak simán a fájlból szolgálod ki. Egyszerű, mint a pofon. Azért ennek is vannak buktatói. Például, ahogy a fájlt létrehozzuk. Itt figyelni kell arra, hogy jöhet több kérés egyszerre. Ha még nincs kész a fájl, de már jön a következő kérés, akkor még nem tud mit kiszolgálni. Ezért célszerű egy másik, átmeneti fájlba tárolni az eredményt, hogy amíg a fájl készül, addig se legyen gond a többi kéréssel és a végén, egy pillanat alatt átnevezni az átmeneti fájlt a végleges nevére. A többi lekérés meg ne kezdje el a cache fájl elkészítését, ha már létezik az átmeneti fájl – hisz akkor már készül –, addig szolgálja ki a régiből. Ha ezekre az alapszabályokra odafigyelsz, akkor nem lehet gond és nagyon sok esetben hatékony és jól működő fájl alapú cache-t kapsz. (Persze I/O terhelés nőhet, de ahhoz képest elenyésző mértékben, mint amit nyersz ezzel, ha jól van minden konfigurálva)
Fejlettebb tipikus megoldás
Tovább finomíthatod a cache-elést, ha nem fájlba cache-elsz, hanem memóriába. Lehet akár memcached alapú a cache-ed, de akár redis is. Mindkettő memóriában tárolja az adatokat. A probléma az, hogy néha napján akármennyire nem akarod, de újra kell indítani a szervert, vagy bármi más probléma adódik és elveszik a memória tartalma, akkor hirtelen üresek a cache-ek. Így ezek a memória alapú cache rendszerek csak okosan használva működnek. Igaz, lehet lustán is okos az ember: a redis tud olyat, hogy kiírja a háttértárra is a tartalmát. Így azért lassabb lesz, de mégis perzisztens. Nem kell félni, hogy elveszik az adat és eltűnik a cache-ed tartalma. Sőt a redis-ben még könnyebb kulcs alapján elérni az adatokat, így még picit hatékonyabb is. De összességében a memcached gyorsabb, csak résen kell lenni, hogyan oldod meg a „nincs adat a cache-ben” szituációkat.
Pusztán, hogy memória alapú rendszert használsz, további nagy mértékű teljesítmény-növekedést érhetsz el és nem bonyolult a használatuk sem. Persze egy jó keretrendszer egy közös API felületen nyújtja az összes cache megoldást, sőt, tud köztük automatikusan is váltani, de ne szaladjunk ennyire előre.
Maradj a háttérben
Másik megközelítés, hogy a nagyon időigényes lekéréseket és hosszú műveleteket a háttérben végzed el. Ha már van cron, akkor használd. Ezzel tudod időzíteni hogy mi, mikor fusson le. Így egyáltalán nem terheled a front-endet és sose futsz bele túlterhelésbe. Legalábbis ezen tekintetben. Persze ezért nagy árat fizetsz: a dinamizmus csorbát szenved. Hisz adott időben (időközönként) fut le és addig nincs új eredmény. Ez nyilván sokszor elfogadható, de van, amikor nem. Most az utóbbi esetről beszélek.
Lehetséges olyan megoldásokat alkalmazni, hogy a háttér végrehajtás vezérelhető. A legegyszerűbben ez úgy érhető el, hogy létrehozol egy percenként futó cront, ami nem csinál mást, csak megnézi, kell-e valamit csinálni. Így egy perc finomságú vezérelhető időzítőt kapsz. Persze lehet máshogy is megoldani, de ez a módszer működik szinte bárhol. Sőt, mivel a cron úgymond parancssorból indítja a PHP-t, ezért akár több szálon futó feldolgozást is csinálhatsz – párhuzamosítva a cache-eléseket.
Felmerül azonban egy icike-picike probléma: az oldalon lehetnek lapozások és keresések, vagy más paraméterfüggő lekérések, akkor ezt nehéz cache-elni. De nem lehetetlen: követni kell, mik a tipikus paraméterek, ezt egy okos rendszer tudja követni és azokat tartja cache-ben, illetve azokra készíti el. Lapozásra könnyű, tudja, hogy lapozhatnak 20 oldalt. Ki fog mondjuk ellapozni a 100. oldalra? Nagyon kevesen. Így előre le lehet gyártani a cache-t 20 oldalig, a tipikus szűrésekre is. A legáltalánosabb felhasználás gyors és hatékony (úgyis ez az esetek fő része), a maradék meg marad előtérben, mert itt nem tudod cache-elni.
Összefoglalva: kapsz egy olyan háttér cache-elő rendszert, amely egy perces időközönként vezérelhető, már elég elfogadható és a dinamizmus terén sem a legrosszabb. Ellenben megadja azt a biztonság-érzést, hogy ezekkel az időigényes műveletekkel sosem fogják túlterhelni az oldalt, mivel semmi sem fut a lekéréskor közülük. Hátránya, hogy nem tudsz mindenre előre cache-t gyártani és nem elég dinamikus.
Terhelés függő cache-ek
Az előző megoldás jól hangzik, de nem tudsz vele sok mindent megcsinálni. Ha ennél nagyobb dinamizmus kell, sokkal gyorsabban változnak az adatok, akkor más megoldást kell keresni.
Erre lehet jó módszer a terhelés függő cache-ek, ami egy felturbózott fájl vagy memória alapú cache. Arról van szó, hogy, ha kicsi a terhelés, akkor előállítja a tartalmat, ha nő, akkor átáll cache használatra. Érdemes beleprogramozni valamiféle prioritás kezelést, hogy mi az, amit elsőnek ki lehet lőni, ha gond van (átállítani cache-elésre) és mi az, amit a legtovább dinamikusan lehet hagyni. Amikor már nagyon nagy a terhelés a szerveren, akkor szinte minden cache-ből jön és ritkán frissíti a cache-t. Ennek a megoldásnak a titka a jó beállítás, mit mikortól, mennyi ideig cache-eljen. A finomhangolás időigényes és nehéz, viszont utána már a lehető legjobban megtartják az oldal dinamizmusát és nem lesz gond akkor se, ha hirtelen nagyobb lesz a terhelés az oldalon.
Gondolni kell még cache csoportokra is. Nem csak a terhelés függő cache-eknél, hanem általában is érdemes azzal foglalkoznod, hogy mi az, ami függ mástól. Pontosabban, melyek azok a cache-ek, amiket egyszerre kell más adatokkal újracache-elni. A keretrendszernek ezt le kell kezelnie.
Nagyon fontos a mai világban, hogy minél frissebb adatokkal szolgáljunk, minél inkább fent tudd tartani a frissítések gyakoriságát, ha olyan jellegű az oldal. A terhelés függő cache-elés ebben sokat segít.
Ultimate megoldás – the best of both worlds
Összevonva a kettő megoldást – a háttérben cache-elést és a terhelésfüggő cache-t – egy rendszerbe. Ebben az esetben megtartod a dinamizmust, de ha nő a terhelés, akkor nem csak simán átállsz a cache használatra (mindig van kész cache, ha hirtelen kell átállni!), hanem átteszed a háttérbe a cache tartalmának az előállítását. Így megmarad a dinamizmus is, még extrém terhelésnél sem kell lemondani róla. Minden előnyét élvezed az összes technikának.
Egyedüli hátránya, hogy ilyen összetett cache rendszert összehozni nem könnyű. Az erre felkészített keretrendszerek már tényleg minden körülmény között megállják a helyüket és megérik az árukat, ami a nagy terheléses környezeteket illeti. Persze ehhez össze kell még házasítani egy reverse proxy rendszerrel is és jól kell kezelje az ESI beszúrásokat és minden más előnyét is amit adhat a reverse proxy.
Végszó
Sok tapasztalatot szereztünk a saját keretrendszerünk (feracotta) fejlesztése és használata közben, a fentieket mind megvalósítottuk benne. Könnyen rá lehetett jönni, hogy egy rendszer teljesítményét mindig a leggyengébb eleme határozza meg, ezért nem elég csak egy dologra odafigyelni. Különösen nagy terheléses környezetekben bukik ki gyorsan, ha valami valahol hibádzik. A cache rendszer is csak egy eleme az egésznek. Kiélezett körülmények között mindennek tökéletesnek kell lennie. Például a cache nem oldja meg, ha sok adatot kell írni és nem oldja meg az extrém igényeket. Ilyen esetben sok minden mást is finomra kell hangolni (egymással is) és olyan megoldásokat bevetni, amely már túlmutat a cache-eken. Csak hogy CDN (content delivery network) megoldásokat emlegessem például, ahol földrajzilag közelebbi szerverről történik a statikus tartalom kiszolgálása. Vagy említhetném azt, ha sok adatot kell kezelni, írni, kiszolgálni, akkor a séma mentes adatbázisok használatát, vagy multi-master megoldásokat. Főleg, ahogy bejön a HA szócska is (high availability), azaz a nagy rendelkezésre állás. Ez nagyon sok mindent megváltoztat, de ez már egy másik cikk(sorozat) témája.
Ha nincs extrém körülmény, akkor pusztán a jó cache stratégia jelent megoldást a terheléses problémákra.
+1 bónusz megoldás
Erre ugyan nem tértem ki, de muszáj megemlítenem, hogy a php session adatokat szintén célszerű memcached-ben tárolni. Nemcsak, hogy gyorsabb lesz a rendszer, hanem ha elosztott rendszerekről van szó, akkor a közös session kezelést nagyon könnyű így megoldani.
Hozzászólások