Ebben a cikkben olyan praktikákat mutatok be, amellyel nem csak hatékonyabb lesz a cachelésünk, de még a szerverköltségeken is spórolhatunk. Olvass tovább!
Mit cacheljünk?
Ez nyilván nagyban függ az adott weboldal felépítésétől és tartalmától, azonban általánosságban mégis elmondható, hogy mindig arra törekszünk, hogy a teljesítmény- vagy időigényes tartalom megjelenéseket szeretjük cachebe elrakni. Azaz minél kevesebb adatbázis lekérdezéssel és tartalom generálással kelljen küzdeni a szervereinknek. Meg kell próbálnunk ezeket a kódrészeket valamilyen cachelési technikával kiváltani. Általános elv az, hogy ha a lekérés paraméterei nem változnak, akkor arra az adott lekérésre mindig ugyanaz lesz a válasz, így elegendő ha egy cachelt statikus tartalmat adunk válaszul, nem kell minden alkalommal végrehajtanunk a konkrét kérést. Két fő irányunk lehet, hogy mely kéréseket cacheljük:- Adatbázis lekérés: Az ilyen típusú lekérések általában megadott paraméterek alapján történnek, melyre válaszul egy tömböt fogunk kapni. Amennyiben a lekérés paraméteri nem változnak úgy mindig ugyanazt a tömböt kapjuk válaszul az adatbázistól. Tehát ezt a tömböt el is menthetjük úgy, hogy az adott cache indexébe szerepelnek a paraméterek, így amikor ezzel a paraméterekkel érkezik a kérés akkor társíthatunk hozzá cachet, így nem is kell az adatbázis felé elindítani a kérést. Viszont gondolnunk kell arra, hogy amikor a lekérésben szereplő adatok változnak, akkor invalidálnunk kell az adott cacheket.
- Tartalom generálás: A weboldalak felépítése általában modulokból és fő tartalomból áll. Ezeket a tartalmakat valamilyen kóddal generáljuk. Programozási nyelvtől függetlenül legtöbbször végül egy html tartalmat fogunk kapni. Bizony ezen a generálási időn is tudunk spórolni, ha az adott tartalmunk nem változik vagy csak bizonyos paraméterek változása esetén módosul. Amikor ugyanazokkal a paraméterekkel kell megjelenítenünk például egy modult, akkor nem kell generálnunk, mert van egy elmentett statikus tartalmunk, amit azonnal válaszul adhatunk.
Hogyan cacheljünk?
A cél az, hogy minél több előre eltárolt tartalmunk legyen. Na de hol és hogyan tároljuk ezeket a statikus tartalmakat? Talán ez a legfontosabb kérdés ugyanis maga a cache elérése és tárolása meghatározza, hogy egyáltalán van-e értelme cachelni. A cachelt tartalmat gyorsabban kellene elérnünk, mintha az eredeti kérést szolgálnánk ki. Rengeteg módszer van cache tárolásra, nézzünk meg pár példát.- Fájl cache: Legegyszerűbb és legprimitívebb módszer. Egyszerűen egy fájlba elmentjük a statikus tartalmat és aztán onnan olvassuk fel. Előnye, hogy nem kell hozzá külön szerver, viszont számolnunk kell a fájlrendszer elérési idejével, illetve a tárhely méretével.
- Adatbázis cache: Egy lassú lekérdezés eredményét lementhetjük akár MySQL-be is, amit már sokkal gyorsabban vissza tud adni. Azonban vannak erre alkalmasabb adatbázisok is. Ilyenek a key-value adatbázisok. Ezek közül a leggyakrabban használtak a MemCached, a Redis és az Aerospike. Előnyük, hogy sokkal gyorsabbak, mint a fájl cache, mert memóriában tárolják az adatokat. Viszont méretben is jóval korlátozottabbak.
- Memória cache: Ez a leggyorsabb cache. A kódon belül a programozási nyelv eszközeit használva (pl.: változók) tárolunk adatokat. Ennek az elérése villámgyors, viszont pl.: PHP-ben nem használható, mert a script futása csak a request idejéig tart.
Hogyan cacheljünk hatékonyan?
El is érkeztünk a kulcs kérdéshez. Van egy nagyon gyors elérésű cache tárolónk, teljesen elégedettek vagyunk vele. Viszont a ShopRenternél közel 3000 áruház cachét kell tárolnunk. Egy 16GB memóriával rendelkező Redis szervert használunk. Bizony kezdtük elérni a memória limitet. Ekkor jött az ötlet, mi lenne ha tömörítenénk a cachet? A PHP-ban vannak beépített tömörítő függvények. Mivel futási időben fogunk be- és kitömöríteni, nagyon nem mindegy, hogy ez mennyi időt vesz igénybe, hiszen ha túl sok a tömörítési idő akkor nem nyerünk semmit a cacheléssel. A tömörítési algoritmusoknál megválaszthatjuk a tömörítés szintjét. Minél magasabb a szint annál kisebb tömörített fájlt kapunk, viszont annál több ideig dolgozik az algoritmus. Ezért egy pár tesztet érdemes futtatni mielőtt kiválasztjuk a kívánt tömörítési algoritmust és a tömörítés szintjét. Két népszerűt hasonlítsunk össze a bzcompress és gzcompress-t.500 be- és kitömörítés idejét láthatjuk 30 KB-os fájlok esetén. Az időt másodperceben mértük. A vízszintes tengelyen a gzcompress majd a bzcompress idei szerepelnek. A piros görbe a becsomagolást, a zöld pedig a kicsomagolást jelzi. Átlagban a gzcompress 500ms alatt tömörít és 200ms kell a kitömörítéshez. Míg a bzcompress majdnem 3000ms alatt tömörít és 600ms alatt kitömörít. Tehát a gzcompress betömörítés esetén hatszor gyorsabb, kitömörítés esetén pedig háromszor gyorsabb. Futási időben eddig a gzcompress bizonyul jobbnak.
A fenti diagramon a vízszintes tengelyen ismét előbb a gzcompress méréseit láthatjuk, aztán a bzcompresst. Százalékosan mértük, hogy mennyivel lesz kisebb a tömörített fájl, az eredeti fájlhoz képest. Láthatjuk, hogy gzcompress level 6 felett már nem sokkal fog csökkenni a fájl mérete. A bzcompress esetén pedig mintha nem is számítana, hogy milyen szinten tömörítünk. Így a futási időt és az elérhető fájlméretet figyelembe véve a gzcompress-re esett a választásunk.
A gzcompress neve félrevezető lehet, a ZLIB tömörítési szabványt használja, nem pedig a gzip-et. A készítők a 6 szintű tömörítést ajánlják, de gyakorlatilag ez olvasható ki a diagrammokból is, így mi is ezt választottuk. Az előzetes számítások szerint a tömörítési arány miatt 84%-kal kisebb cache méreteink lesznek. Azaz a jelenlegi 13GB memória helyett csupán 2GB memóriát fogunk felhasználni. http://php.net/manual/en/function.gzcompress.php
Új cachelés élesítésének tapasztalatai
Miután élesítettük a fejlesztésünket, azt tapasztaltuk, hogy nemhogy a Redis memóriánk felszabadult, de network-ben is rengeteget nyertünk.Közel negyedére esett vissza a hálózati forgalom a szervereinken. Nyilván köszönhetően annak, hogy kisebb méretű cache-t kell letölteni, tehát azt a picit, amit órajelben elvesztünk a tömörítés miatt gyakorlatilag kamatostul megtérül a hálózati forgalomban. Igazából, nemhogy megtérül, bőven nyerünk vele. A fenti ábra elég szignifikánsan szemlélteti.
Amikor élesítettük a fejlesztést, a fenti ábrán látható, hogy a szabad memória szinte nulla volt, így swapolt a Redis, ami eléggé csökkenti a hatékonyságát. Amint élesítettük a fejlesztést, a képen az látható, hogy 16GB szabad RAM elkezdett csökkeni. Sajnos arról már nincs diagram, de 13+ GB-ról 6 GB-ra esett a redis memória használat. Ez körülbelül 54%-os hatékonyság. Nem akarunk telhetetlenek lenni, de hova tűnt az a körülbelül 30%-os különbség, amit várnánk a tömörítési aránynak megfelelően? Egyrészt a cache-k egy része lehet olyan tartalommal bír, amit már annyira nem lehet tömöríteni, másrészt a redis ugye key -> value alapú, amelyből a kulcsot is ugyanúgy tárolni kell, mint magát az értéket. Ezzel is érdemes számolni, ha több millió kulcsunk van.
Figyeljünk a kulcsokra
A key -> value alapú tárolásnál figyeljünk arra, hogy a kulcsot is ugyanúgy tárolnunk kell, nem csak az értéket. Erre legfőképp akkor kell figyelnünk, ha milliós nagyságrendű kulcsunk van. Nálunk mivel az összes bolt összes cache adatát egy szerver tárolja így több millió kulcsunk lesz. Egy példán keresztül nézzük meg, miért is kell erre figyelmet fordítani.Például így épül fel a kulcs nevünk: shopnév/frontend/modulnév/termékazonosító
egy ilyen azonosító 41 karakter. Amihez 41 byte szükséges a tároláshoz. Tegyük fel van körülbelül 50 millió kulcsunk. 50,000,000 X 41 = 2050,000,000 byte (több mint 2 milliárd byte) 2050,000,000 ÷ (1024×1024×1024) = 1,9 GB.
Tehát erre is különösen érdemes figyelni, ha több millió kulcsokat akarunk tárolni, hogy a kulcsok neveit minél inkább rövidítsük, mert sokkal több memóriát foglal a tárolásuk, mint elsőre gondolnánk.