Zvládáme zátěž webové aplikace
Martin Major
Zvládáme zátěž webové aplikace Martin Major Když spouštíme webovou aplikaci, měli bychom přemýšlet nad tím, zda budeme schopni zvládnout nápor uživatelů v případě, že naše aplikace bude mít úspěch. Určitě totiž nechceme zažít výpadek právě ve chvíli, kdy o naší aplikaci napíše nějaký hodně navštěvovaný web a my máme konečně šanci získat kritickou masu uživatelů. Na druhou stranu je potřeba si uvědomit, kolik nás bude stát zcela bezvýpadkový provoz a jakou škodu způsobí výpadek. V naprosté většině případů je totiž cena za bezvýpadkový systém daleko vyšší, než cena přiměřeně krátkého výpadku. My potřebujeme najít takový kompromis, který bude ekonomicky nejvýhodnější.
Návštěvnost Když provozujeme webovou aplikaci, je potřeba pečlivě sledovat trendy návštěvnosti. To nám pomůže připravit se na nápor návštěvnosti, zvolit servisní okna atd. Pokud například provozujeme český e-shop, pravděpodobně se budeme muset připravit na největší nápor před Vánocemi, zatímco v noci budeme mít výrazně méně návštěvníků, takže si budeme moci dovolit provést různé servisní úpravy.
Testovací prostředí Abychom minimalizovali množství výpadků, je potřeba do produkce nasazovat pouze pečlivě otestovaný kód, který bude obsahovat co nejméně chyb. Z tohoto důvodu je vhodné zřídit nezávislý webserver (aby chyba v testu nemohla ohrozit produkční kód) a testovací databázi. Na testovací databázi však musíme vyřešit problém s aktuálností dat. Častý přístup je takový, že se na testovací prostředí nasazují zálohy z produkční databáze, takže máme vždy lehce zastaralá data a budou se špatně testovat funkce, počítající s pravidelným přibýváním dat. Z tohoto důvodu se někdy i testovací server připojuje na produkční databázi. To je však potřeba provést až ve chvíli, kdy jsme si jisti, že testovací kód nepoškodí produkční data. Někdy však data z produkce nemůžeme použít žádným způsobem, především když se jedná o citlivá data. V tom případě je potřeba použít náhodně generovaná data. Vlastní testování probíhá buď pomocí automatických testů (např. pomocí Selenium), nebo ručním testováním. Automatické testy jsou výrazně levnější, ale lidští testeři mohou lépe odhalit záludnější chyby. V praxi se osvědčilo psát automatické testy zprvu pouze na základní funkčnost a předpokládané problémové části. A v případě, že tester objeví nějakou chybu, tak spolu s její opravou se naprogramuje i test, aby se chyba už neopakovala. Na druhou stranu není dobré přehnaně testovat úplně vše, protože pak jakákoliv úprava znamená udržovat příliš mnoho testů.
Produkční prostředí V případě, že kód úspěšně projde testovacím prostředím, je možné jej nasadit do produkčního prostředí (více o nasazování v kapitole Deployment). 1
Zvládáme zátěž webové aplikace
Martin Major
To, že kód úspěšně prošel všemi testy, bohužel ještě neznamená, že je zcela bez chyb. Čím více uživatelů používá naší aplikaci, tím větší je pravděpodobnost, že objeví nějakou skrytou chybu. V tom případě je potřeba uložit co nejúplnější informaci o tom, v jakém kontextu a proč chyba vznikla. K tomuto účelu jsou hotová řešení ve většině PHP frameworků (např. v Nette je to „Laděnka“). Zároveň je potřeba dbát na to, aby se uživateli nezobrazily podrobné výpisy, které mohou obsahovat citlivé informace o naší aplikaci. V případě kritické chyby je nejlepší uživateli zobrazit pouze informaci o tom, že v aplikaci došlo k chybě a odeslat e-mail s podrobným popisem chyby správci serveru. V případě nečekaného zvýšení návštěvnosti je potřeba mít k dispozici další servery, nebo možnost zvýšit jejich výkon (v případě virtuálních serverů).
Databáze Většina webových aplikací má největší zátěž na straně databáze. Zároveň je databáze místem, které se nejhůře škáluje. Jednou možností je využít tzv. NoSQL databáze, které řeší škálování již v návrhu. Pokud NoSQL databáze použít nemůžeme či nechceme, nezbývá než optimalizovat použití standardní databáze, rozdělit data na více serverů, případně zkusit použít replikaci. Když se rozhodneme rozdělit data na více serverů, je potřeba nalézt dvě nebo více nezávislých skupin tabulek tak, že se nikdy nepoužívá JOIN mezi tabulkami z různých skupin. Pak můžeme každou skupinu tabulek umístit na jeden server. V praxi však toto rozdělení bývá obtížné, protože například tabulku uživatelů obvykle potřebujeme používat spolu se všemi ostatními tabulkami. V tom případě můžeme replikovat pouze tyto společné tabulky a ostatní mohou zůstat rozdělené na jednotlivých databázových serverech. Replikace funguje tak, že jsou data rozkopírována na více serverů a když potřebujeme data pouze číst, tak je čteme z libovolného serveru. Naopak když potřebuje data měnit, tak musíme operaci provést nad všemi dostupnými servery. To využívá předpokladu, že v aplikacích se výrazně častěji data čtou než mění. Replikace může být dvou druhů: synchronní a asynchronní.
Synchronní replikace Synchronní replikace nám zaručuje, že budeme mít vždy aktuální data na všech databázových serverech. To je obvykle stav, který si přejeme. Není vhodné, aby uživatel uložil změnu svého profilu a po obnovení stránky opět viděl staré údaje. Bohužel je synchronní replikace velmi obtížné dosáhnout. Při každé změně v databázi musíme změny nejprve zreplikovat na všechny servery a teprve po úspěšném dokončení, můžeme data považovat za uložená. V případě kdy se na jednom serveru data nepodaří uložit je potřeba provést rollback na všech serverech. Již zde je zřejmé, že s nárůstem počtu serverů budou všechny operace měnící data trvat déle a zvyšuje se pravděpodobnost selhání. Jsou dva obecné principy, jak provádět synchronní replikaci: pooling a replikace na základě událostí. Pooling znamená, že každý požadavek na změnu předáme jednomu centrálnímu správci a ten provede operaci na všech dostupných serverech a zkontroluje, zda všude proběhla v pořádku. Tento správce by měl také ošetřit různé speciální případy, jako například vkládání přesného aktuálního času, nebo náhodného čísla, které by se v každé databázi uložilo jinak. Tato data by měl vyhodnotit již tento správce. 2
Zvládáme zátěž webové aplikace
Martin Major
Při replikace na základě událostí databáze sama zkontaktuje při změně dat (například pomocí triggerů) ostatní databáze a nechá je zapsat potřebná data. Tato událost se obvykle vyhodnocuje nad konkrétním pozměněným řádkem, takže máme výhodu v tom, že jsou již vyhodnocena náhodná čísla a podobně. Na druhou stranu toto řešení není dobré používat v situaci, kdy se pomocí jednoho dotazu mění velké množství řádků, protože pro každý řádek musíme komunikovat se všemi databázemi. Různé databáze přinášejí různá řešení synchronní replikace, žádné však není jednoduše použitelné za všech okolností a vždy zde nastává mnoho nových problémů. Zároveň je potřeba si uvědomit, že replikace bude VŽDY zpomalovat operace měnící data.
Asynchronní replikace Při asynchronní replikaci nemusí být data zreplikována hned, ale až po určité době. To vypadá na první pohled nepoužitelně, ale opak je často pravdou. Co kdybychom vyhradili databázi, která nám počítá složité dávkové operace na nový databázový server? Hlavní databáze ušetří velké množství výkonu na složitých výpočtech a u dávkových operací nám často nevadí, že běží nad mírně neaktuálními daty. Výhoda asynchronní replikace je v tom, že se mnohem lépe škáluje. Přidání dalšího databázového serveru neznamená zpomalení operací, měnících data. Data totiž můžeme replikovat nezávisle, ve zvolené době. Případně můžeme využít replikaci za pomoci transakčního logu, kdy slave databáze čtou transakční log master databáze a z něj získávají data.
Snížení zátěže databáze Ať už máme nebo nemáme více databází, měli bychom se snažit co nejvíce snížit zátěž každé z nich. Z tohoto důvodu je vhodné na vývojovém prostředí nainstalovat profiler databázových dotazů, ten existuje pro většinu databází. Tím můžeme zjistit, které dotazy zabírají nejvíce času (i v závislosti na počtu volání). Tyto dotazy pak můžeme zkusit nějakým způsobem analyzovat. Na to mají databáze opět nástroje, které zobrazí, jak přesně se dotaz vyhodnocuje, jaké používá indexy atd. Nejdůležitější je samozřejmě mít správně použité indexy – nesprávná volba indexu může o několik řádů zpomalit vyhodnocování dotazů. Druhá nejdůležitější úprava, kterou musíme udělat, je oprostit se od dogmatického dodržování normálních forem. Bylo by velmi neekonomické u každého vypsání článku znovu a znovu počítat počet komentářů, když se mění velmi zřídka. Proto bude lepší si takový údaj uložit přímo k článku a v případě přidání nebo smazání komentáře jej upravit. Je zřejmé, že při takovémto postupu hrozí riziko nekonzistence dat. Abychom jej minimalizovali, je vhodné počítat závislá data v triggerech. Ty nám zaručí, že když se nepodaří upravit počet komentářů, tak se rollbackuje celá operace vložení komentáře. Pokud chceme mít ještě větší jistotu, můžeme si vytvořit kontrolní skripty, které budou data např. každý den v noci kontrolovat. Dále bychom měli kontrolovat všechny provozní vlastnosti databáze – správná konfigurace může podstatně zvýšit výkon a samozřejmě naopak. Jak jsme si již řekli u replikace, je vhodné náročné dávkové úlohy přesunout na vlastní server, nebo je alespoň spouštět v době nejmenšího zatížení databáze.
3
Zvládáme zátěž webové aplikace
Martin Major
Pomoci může také nasazení connection pooleru, který si drží několik připojení k databázi a na požadavek je předává webovému serveru. Po skončení běhu skriptu je pak neukončuje, ale opět je nechá využít dalším skriptem. Tím se ušetří režie s navazováním nových spojení, která může být značná.
Webserver Když začínáme budovat serverovou infrastrukturu, je potřeba rozhodnout zda využít fyzické, či virtuální servery. Pokud zvolíme fyzické servery, je vždy potřeba mít dostupný alespoň jeden rezervní server. Bohužel, hardwarové poruchy nastávají poměrně často a ve chvíli kdy vypadne jeden server je pozdě objednávat nový. Virtuální servery jsou mnohem flexibilnější (můžeme si snadno zřizovat nové a opět je ubírat podle potřeby), ale neznamená to, že můžeme být bez starostí s podrobným nastavováním a plánem pro případ havárie fyzických serverů. Vždy je potřeba mít někoho, kdo velmi dobře rozumí konfiguraci a dokáže vyřešit hardwarové problémy, ale i správně nakonfigurovat všechny parametry operačního systému, webserveru atd. Na rozdíl od databáze je rozložení zátěže mezi více webserverů poměrně lehké. Existují dvě základní možnosti jak dělit zátěž na více serverů: loadbalancer a DNS proxy. Loadbalancer přijímá všechny požadavky uživatelů a přeposílá je na další servery. Ty je zpracují, vrátí vygenerovanou stránku zpět loadbalanceru a ten ji pošle zpět uživateli. Toto řešení je poměrně flexibilní (můžeme snadno přidávat a ubírat servery), ale vyžaduje vždy minimálně jeden server navíc. Pokud tedy potřebujeme zátěž rozložit na tři servery, musíme mít čtyři. Naproti tomu DNS proxy si vystačí se skutečně využitým počtem serverů za cenu mírně nižší flexibility. Při požadavku na překlad doménového jména např. example.com vrací střídavě IP adresy všech serverů a klienti pak zasílají požadavky přímo cílovým serverům. Výkonu webserveru může velmi pomoci oddělení aplikačního serveru a serveru pro statická data. Na aplikačním serveru může běžet „těžký“ webserver (např. Apache), který bude mít k dispozici všechny potřebné zásuvné moduly a bude se starat o běžné požadavky. Na serverech pro statická data poběží pouze nějaký „lehký“ webserver (např. Lighttpd), který bude poskytovat obrázky, styly a ostatní statická data. Při rozdělování na více webserverů musíme vyřešit synchronizaci dat mezi servery. Zdrojové kódy aplikace můžeme buď nahrávat na všechny servery ručně (nebo pomocí deployovacího nástroje), nebo nechat replikaci přímo na serveru. V tom případě lze využít buď jeden síťový filesystém (čímž zvyšujeme riziko, že při selhání jednoho serveru přijdeme o celou aplikaci), nebo pomocí filesystémových hooků nechat data rozkopírovávat přímo filesystémy serverů. Vytvářená data jsou dvojího druhu – buď je potřebujeme sdílená napříč všemi webservery (např. sessions) a pak by měla být na odděleném serveru, nebo mohou být separovaná na každém serveru zvlášť (např. přeložené šablony).
4
Zvládáme zátěž webové aplikace
Martin Major
Kešování There are only two hard things in Computer Science: cache invalidation and naming things. Phil Karlton Správné kešování je klíčem k výkonné aplikaci. Je mnoho možností, jak, kam a co kešovat. Bohužel žádná není univerzální a vždy záleží na konkrétní situaci. •
File cache – hodí se pro dlouho platná data, například přeložené šablony. Webový server má obvykle velkou operační paměť, takže tato data stejně bývají nakešovaná operačním systémem.
•
Memcached – je key-value úložiště, které drží všechna data přímo v operační paměti. Snadno lze vytvořit velkou farmu Memcached serverů a proto se hodí na kešování větších kusů dat, která nepotřebujeme tak rychle (obvykle se přistupuje přes lokální síť). Vhodné využití je například kešování předpřipravených částí vygenerované stránky.
•
Databázová cache – zde velmi záleží na typu databáze. Některé databáze nemají příliš rády časté změny a mazání, proto se příliš nehodí pro ukládání kešovaných dat. Např. u MySQL je velký rozdíl mezi použitím InnoDB, MyIsam, nebo MEMORY tabulky. Dále můžeme využít např. databázi SQLite na RAM disku.
•
OpCode cache – je cache přímo PHP interpretu. Přístup do ní je nejrychlejší, ale může obsahovat pouze omezené množství dat. Jako vhodné využití se nabízí kešování velmi často používaných datových struktur.
•
PHP akcelerátory – dokáží kešovat PHP soubory přeložené do mezikódu, takže se nemusí při každém požadavku opětovně překládat. Dále lze například u akcelerátoru APC zapnout direktiva stat, která zajistí, že se vůbec nekontroluje, zda se soubor na disku nezměnil. Toho lze využít např. při deploymentu, kdy nahrajeme novou verzi a teprve když jsou všechny soubory nahrané tak vyčistíme tuto cache.
Verzování Při vývoji jakékoliv alespoň trochu větší aplikace je verzování nutností. To, jaký verzovací nástroj zvolíme, rozhodne o komfortu, jaký budeme mít po celou dobu vývoje. Proto je dobré rozhodovat pečlivě. Poslední dobou se nejvíce rozmáhá GIT, který nabízí velmi pohodlnou práci s větvemi. Větve umožňují začít pracovat na nové funkci a kdykoliv se vrátit do původního stavu, opravit nalezenou chybu, a opět se vrátit k rozdělané funkci a pokračovat dále. Těžší než verzování zdrojových kódů je verzování databázových scriptů. Jednou možností je psát vždy upgrade a downgrade script, případně v nějakém metajazyku jeden script, který je schopný vygenerovat update script pro oba směry. V tomto případě je vždy při přepnutí do jiné větve potřeba vykonat několik rozdílových scriptů, což může být dost náročné. Dále je problém s daty: pokud například zrušíme sloupeček v databázi a chceme se vrátit zpět, tak už nemůžeme obnovit smazaná data.
5
Zvládáme zátěž webové aplikace
Martin Major
Proto je často jednodušší psát pouze upgrade scripty, ale psát je co nejvíce zpětně kompatibilní. To může znamenat nutnost psát triggery na replikaci dat mezi jednotlivými sloupečky, ale pomůže nám to v situaci, kdy zjistíme, že nová verze nefunguje dle očekávání a můžeme se snadno vrátit k předchozí verzi. Další užitečnou praktikou je export kompletní struktury databáze, ve kterém se pak dobře hledá pomocí diffu verzovacího nástroje.
Deployment Čím složitější se náš projekt stává, tím složitější je nasadit novou verzi na produkční server. Je potřeba provést např.: •
databázové změny – struktury a dat
•
nahrát nové soubory – nepřepsat ale konfigurační soubory apod.
•
statické soubry – nahrát obrázky, styly, scripty,…
•
vymazat cache – šablony, nakešované stránky, APC stat,…
a všechny tyto činnosti je potřeba provést na všech serverech a co nejrychleji, protože ve chvíli kdy máme nahranou pouze polovinu souborů, tak se aplikace může chovat nevyzpytatelně.
Automatický deployment Vzhledem k množství úkolů, které je potřeba provést v co nejkratším čase, je vhodné použít nějaký automatizovaný script. Pokud používáme verzovací systém, je dobré deployovat přímo z něj. Pouze musíme rozhodnout, zda budeme mít pro deploy zvláštní větev, nebo budeme nasazovat přímo master větev. Automatický script by měl připravit všechna data, která může připravit bez výpadku a pak teprve provést co nejkratší switch. V praxi to vypadá např. tak, že se všechny zdrojové kódy z GITu nahrají přes FTP na servery do zvláštní složky a pak se složky pouze přejmenují. Kdyby se zjistilo, že se do produkce dostala nějaká chyba, můžeme opět snadno přepnout na předchozí verzi. Určitě je vhodné o plánovaných delších odstávkách informovat uživatele předem. I pro půlsekundový switch si můžeme nachystat informativní stránku a všechny požadavky během této doby pomocí .htaccessu přesměrovat na tuto stránku.
Zálohování Všichni ví, že by se mělo zálohovat, ne všichni však zálohují tak jak by měli. Čím více má projekt uživatelů, tím bolestivější bude ztráta dat. Proto se opravdu zálohování nesmí podceňovat. Zálohovat by se mělo vždy všechno, co je potřeba pro rozběhnutí projektu někde jinde, kdyby serverovnu zasáhla živelná pohroma. Tedy nejenom data a zdrojové kódy, ale i aktuální verze externích knihoven, nastavení webserveru atd. Zálohování může být při velkých objemech dat velmi zdlouhavé, takže je dobré nenechávat v databázi ani na webserverech zbytečná data, která pouze prodlužují dobu zálohy. V praxi tak bývá možné zálohovat pouze v době s nejnižší zátěží, typicky v noci. Proti fyzické poruče serveru nám
6
Zvládáme zátěž webové aplikace
Martin Major
může pomoci zálohování dat pomocí replikace na jiný server. Auditování dat pomocí triggerů může pomoci proti některým chybám způsobených programátorem.
Závěr Pokud chceme provozovat aplikaci s vysokou zátěží, je potřeba myslet na všechny aspekty a brát v potaz výkon už při plánování nových funkcí. Naštěstí lze obvykle dopředu odhadnout přibližný růst návštěvnosti a tak se můžeme připravit včas. Pokud chystáte malý startup, určitě není potřeba plánovat replikaci databáze a podobně, ale je dobré vědět, čeho se při návrhu vyvarovat a jak moc v předstihu je potřeba řešit konkrétní problémy se zátěží.
7