2. Jaký je plán?
- řekneme si co je Redis
- co umí kdyby/redis
- zprovozníme ho
- profit
3. Co je Redis?
- nízkoúrovňová databáze
- s datovými strukturami
- se scriptováním v LUA
- serializuje se na disk
- jednovláknový
Redis umí datové struktury od jednoduchých key:value, přes seznamy a hashe po seřazené sety. Je možné mu poslat script v LUA který evalne přímo nad
databázi, tedy si ušetříte hromadu komunikace. Další killer feature oproti memcache je i serializace na disk, což z něj dělá perfektního kanditáta pro session.
4. .. a umí ještě
- databáze (forma namespace)
- transakce (optimistic locking)
- pub/sub
- replikace
5. .. a hlavně
Je ve všem stejně rychlý
nebo rychlejší
než memcached
6. První dva bottlenecky v Nette?
session a journal
Výchozí session file storage je super, ale od určité návštěvnostni zvládá s přehledem zabíjet stránku.
Journal v Nette je velice jednoduchá databáze, do které se ukládá jaké mají data v cachi tagy a priority pro mazání. Journal není tak úplně bottleneck pokud
nepoužíváte tagy, ovšem pokud je používáte, tak narazíte na problémy při větší zátěži, nebo když do něj chce zapisovat hodně dat.
7. Na co je Redis super?
- session
- cache
- je to přece databáze!
Redis se dá použít nejenom na cachování a sessions, ale je to i plnohodnotná databáze a tedy do ní můžete přešunout i jiné části aplikace a zefektivnit ji tím.
9. Instalace: Kdyby/Redis
$ composer require kdyby/redis:~3.0
app/config/config.neon
extensions:
redis: KdybyRedisDIRedisExtension
Samozřejmostí je instalace Kdyby/Redis přes composer a compiler extension. Řada 3.0 je pro Nette 2.1 a dev.
11. Session
Výchozí PHP session storage
stojí za úplný ho... uby!
Největší dvě úzká hrdla jsou čištění starých session a limit počtu souborů na složku ve většine filesystémů. Obojí jde řešit, ale přoč tím ztrácet čas když máme
možnost použít Redis.
12. Session: nativní storage
session.save_handler = redis
session.save_path
= "tcp://127.0.0.1:6379"
zapíná se pomocí:
redis:
session: on
Nativní storage využívá interního mechanismu v PHP na ukládání session a handleru který dodává phpredis (ten php extension). Důležité ovšem je, že tato
forma ukládání session neblokuje a možná ani nikdy nebude, protože Redis sám o sobě nepodporuje zamykání.
13. Session: emulovaný storage
SessionHandlerInterface
zapíná se pomocí:
redis:
session: {native: off}
Kdyby/Redis ale obsahuje i vlastní implementaci, která využívá jiný PHP mechanismus na ukládání sessions. Proč psát vlastní? Protože tato implementace
dokáže session zamykat, jako standardní filesystemová. Díky tomu se nám nebudou přepisovat a ztrácet data. Obě implementace jsou kompatibilní a lze je
libovolně přepínat bez strachu ze ztráty dat.
15. Cache: CachingIStorage
redis:
storage: on
journal: on
Kdyby/Redis obsahuje oddělené implementace journalu a cache a je možné obě dvě používat samostatně, ale ve výsledku dává smysl hlavně použití obou
zároveň.
16. Cache: rychlost
class BenchmarkFileStorage
implements NetteCachingIStorage
{
function __construct(NetteCachingIStorage $storage);
function read($key) {
try { $time = microtime(TRUE);
return $this->storage->read($key);
} finally {$this->calls[]=microtime(TRUE)-$time;}
}
Večer před posobotou jsem se rozhodl změřit rozdíl rychlosti FS storage oproti Redisu abych mohl ohromit slečny v publiku a napsal jsem si vlastní storage
dekorátor, kterým jsem plánoval měřit dobu strávenou v cachi.
18. Cache: rychlost
Filesystem (SSD disk!)
První pageload 2607 calls / 452.5 ms / 0.17 ms
Obyčejný pageload 188 calls / 26.8 ms / 0.14 ms
Redis
První pageload 2607 calls / 776.9 ms / 0.30 ms
Obyčejný pageload 188 calls / 44.4 ms / 0.23 ms
První načtení stránky generovalo hromadu cache a tedy i hromadu požadavků na Redis. Ovšem toto nejsou požadavky na Redis, to jsou jednotlivá volání
metod storage, celkový čas strávený jejich voláním a průměr na jeden method call.
19. Cache: jaktože je pomalejší?!
- redis neumí zámky jako fs
- zápis storage dělá více requestů
- journál dělá více requestů
Jsem stejně v šoku jako vy :) Jenže ono to není tak úplně jednoduché. Zaprvé, časy z předchozího slajdu jsou pro jeden jediný request na localhostu a tedy to
není úplně vypovídající. Filesystem totiž neškáluje zdaleka tak dobře jako Redis a s nárůstem zátěže se rychlost strovná a Redis postupně získá náskok,
protože ten s množstvím requestů téměř vůbec nezpomaluje a horní limit je někde kolem půl milionu GETů za vteřinu.
Redis neumí zamykat a tedy musím zámky emulovat. Emulování zámků se dělá pomocí http://redis.io/commands/setnx a lepší řešení jsem ani po hodinách
studia nenašel. Když se zapisuje do storage, musí se tedy nejdříve získat zámek, potom se zapíše a ještě když ukládáme nějaké tagy ke klíčům tak to dělá
další requesty kvůli journalu. Strávili jsme s @matej_21 několik bezesných nocí optimalizací journalu i storage až na dřeň plošných obvodů a pokud si myslíte
že to zvládnete lépe, budu strašně rád když se zapojíte.
20. Cache: na produkci
18 ms / rq
Newrelic nám tvrdí že těch ~ 200 požadavů na Redis (tohle je jiné číslo než počet method calls storage) trvá průměrně 18ms na http request. Not bad huh?
21. Cache: na produkci
FS cacheRedis Redis
Zkoušel jsem Redis storage vypnout, ale bohužel v nočních hodninách už je ten rozdíl neznatelný. V poledne bych si naopak něco takového nedovolil.
22. Cache: jak probíhá zápis
- získám zámek (loop (setnx;usleep))
- zapíšu
- uvolním zámek
Zápis do cache funguje tak, že nejprve získám zámek cyklickým voláním SETNX na klíč se suffixem ‘:lock’ oproti klíči který chci zamknout. Platnost zámku jde
samozřejmě nastavit na libovolný počet vteřin. Následně zapíšu data a zámek uvolním.
23. Journál: jak probíhá zápis
- smažu předcházející data pro klíč
- zapíšu (rq ~= klíče*tagy + priorita)
- uvolním zámek
Do journalu se zapisuje trošku složitěji. Nejprve musíme smazat stará data pro daný klíč, tedy odmazat tagy a následně z tagů odmazat klíč, případně ještě
další request na priority. Potom to samé udělám naopak, tedy spáruji jednotlivé tagy s klíčem a klíč s tagy, případně zapíšu prioritu.
24. Journál: jak to dělá filejournal?
- zamkne journal
- celý journal načte do paměti
- změní co potřebuje
- zapíše journal
Oproti tomu filejournal zamkne celý soubor ve kterém jsou všechna metadata, načte ho do paměti, pomění co potřebuje a opět zapíše. Jinak to totiž udělat ani
nejde, pokud chceme mít atomický zápis. Tento způsob je samozřejmě skvělý, ale pouze do určitého počtu http požadavků.
25. Journál: jak to dělá filejournal?
- FileJournal::NODE_SIZE
- není možné mít pro jeden klíč víc než
X tagů a pro jeden tag víc než X klíčů
Bohužel tady narážíme na limity filejournalu, tedy že se skládá z jakýchsi datových bloků, do kterých se ukládají data a byť je rozumně naddimenzovaný, i tak
se může stát že máte moc tagů pro nějaký klíč, nebo moc klíčů pro nějaký tag a zkrátka nebude možné data uložit. Redis journal je omezen pouze velikostí
ramky.
26. Cache: smazání klíče
- smaže klíče
Tady je to naštěstí velice jednoduché, stačí jeden request a data jsou fuč :)
27. Cache: expirace klíče
Filestorage ho musí načíst,
ověřit a “ručně” smazat.
V Redisu automaticky expiruje,
jako by tam nikdy nebyl :)
Další skvělá feature redis cache storage je automatická expirace klíčů na úrovni databáze. Filestorage je po sobě musí klízet když zjistí že data mají expirovat,
v Redisu prostě zmizí.
28. Cache: mazání podle tagu
- získá seznam klíčů pro tag (journal)
- smazat tagy z klíčů (journal)
- smazat klíče z tagů (journal)
- vrátí seznam klíčů na smazání
- smazat nalezené klíče (cache)
Tohle je asi největší špek jaký jsme v celém journalu řešili. Normální filestorage a journal fungují tak, že storage dá journalu tag (případně jiné pravidlo jak
cache mazat) a zeptá se jaké klíče ten tag mají a které teda má mazat. Journal to přechroustá a vrátí list klíčů. Cache následně maže data.
29. Cache: mazání podle tagu
počet requestů odpovídá velikosti dat
10 000 rq?
100 000 rq?
1 000 000 rq?
Co byste řekli, kolik musí udělat requestů algoritmus popsaný na minulém slide? Pokud byste se nebáli tipnout i ta šílená čísla s hodně nulami nebyli byste
daleko od pravdy, protože pokud máte u jednoho tagu tisíce klíčů cache, může to dát redisu pořádně pokouřit.
30. Cache: mazání podle tagu
1 request
= eval lua scriptu
Ovšem s tím se odmítám spokojit! Jak jsme si řekli, Redis umí scriptovat v Lua a proto jsem napsal mazání pomocí tagů journalu jako Lua script, který se
jednoduše pošle databázi s argumentem (jméno tagu, priorita), databáze evalne script a ten najde všechny klíče, promaže všechna metadata a pokud
použiváte redis cache storage i journal zároveň, rovnou zkusí klíče z databáze smazat. Ušetří se tedy vracení seznamu klíčů a mazání “z venku”.
32. Optimalizace: unixové sockety
“The typical latency of a 1 GBits/s
network is about 200 us, while the
latency with a Unix domain socket can
be as low as 30 us.”
Lokální síť mezi dvěma VPS je samozřejmě brutálně rychlá a když posíláte jenom pár requestů tak ten rozdíl nejspíš nepocítíte, ale když sypete do Redisu na
každý http request stovky příkazů tak už se vyplatí zamyslet se nad použitím lokálních instací redisu na cachování pro každý http server (myslím tím VPS, která
je určená ke zpracování http requestů a běží na ní PHP).
33. Optimalizace
- zvlášť instance na session a na cache
- vypnout serializaci cache na disk
- vypnout zamykání cache pro zápis
Kdyby/Redis je sice chytrý a cache i session prefixuje a tedy se nikdy nepomýchají, ale výborné je rozdělit je do dvou různých databází. Jak jsme si řekli, data v
redisu se mohou dělit na databáze a tedy při deployi můžete jednoduše zavolat `redis-cli -n 0 flushdb` a smazat tak databázi s cachí a session zůstane
nedotčena.
Pokud to chcete povýšit, místo použití databází můžete nastartovat dvě různé instance redisu a jednu použít na session a jednu na cache. Díky tomu budete
moci vypnout ukládání cache na disk a tedy zase trochu ulehčíte Redisu ve špičkách.
Když jsme měli problémy s výkonem, protože jsme hodně experimentovali, zkoušeli jsme i vypnout zamykání cache pro write, ale výsledkem bylo jenom to, že
se cache generovala klidně 100x dokud ji ten první request, co ji generovat začal, konečně nezapsal a tedy se výkon logicky zhoršil.
34. Optimalizace: cache nestíhá :(
Vypnutí generování cache, nebo naopak když generujete cache strašně moc v jeden moment může vypadat i takto. Naprostá většina toho nárustu je
spůsobená sleepy v cyklech a opakovné dotazování Redisu jestli už se zámek uvolnil. Stroj tedy ve skutečnosti není zase o tolik vytíženější, jenom mu déle
trvá zpracovat http requesty.
36. Optimalizace: sharding
Více instancí => více vláken!
+ persistentní spojení
No a když už vám návštěvnost naroste takovým způsobem že už nic nepomáhá a komunikace s Redisem se ukáže jako úzké hrdlo, je na řadě zvážit sharding.
Protože jak jsme si řekli na začátku, Redis je jednovláknový a tedy tím že nastartujete více instancí (třeb 16, nebo klidně 64) získáte více vláken :)
37. Optimalizace: sharding
- https://github.com/twitter/twemproxy
- kdyby/redis (maybe?)
Asi nejpoužívanější nástroj na shardování Redisu je twemproxy od Twitteru, jenže ta je navržená tak, že by asi úplně dobře nefungovala při invalidaci cache
pomocí našeho LUA scriptu, tedy tu bohužel nemůžu pro kombinaci s Kdyby/Redis doporučit.
Ale nenechám vás na holičkách, mám napsané vlastní klientské shardování. Zatím ovšem není v masteru, ale v samostatné větvi a budu rád když ji se mnou
zkusíte otestovat, ale opatrně! :)
38. A závěr?
A vyplatí se teda (Kdyby/)Redis?
Jak jsme si ukázali, naše konkrétní implementace storage umí bohužel i nemile překvapit.
39. Vyplatí se pro
Session: vždy
Journal: vždy
Ovšem i kdyby byla klidně ještě o polovinu pomalejší, pořád se ve 100% případů vyplatí použít pro session.
A pokud tagujete cache tak v ten moment se journal a cache vyplatí i vám.