Přejít k navigační liště

Zdroják » Různé » Jak zrychlit server – několik praktických postřehů

Jak zrychlit server – několik praktických postřehů

Články Různé

Teoretických rad na téma „jak zrychlit server“ nalezneme všude dost. Horší to bývá s praktickými články, kde by lidé popisovali, co pro zrychlení udělali, proč a s jakým výsledkem. Jednu takovou „případovou zrychlovací studii“ známého českého serveru vám nabízíme.

Nálepky:

Disclaimer: Článek vyšel původně na blogu Webtrhu pod názvem Jak zrychlit Web(trh). Jeho autorem je Martin Schlemmer, provozovatel serveru Webtrh, a článek publikujeme s jeho výslovným souhlasem.

Posledních několik dní jsem, povzbuzený přestěhováním na nový server, zrychloval načítání Webtrhu.

Reakční doba

Uživatelův dojem z aplikace se mění podle reakční doby:

  • Pod 0,1 s vnímáme reakci jako okamžitou. Aplikace reaguje na naše požadavky, přímo ji ovládáme.
  • Pod 1 s ztrácíme pocit okamžitosti, ale prodleva nepřerušuje naši práci.
  • Prodleva pod 10 s přerušuje uživatelovu práci, ale ten ještě dokáže udržet pozornost na úkol.
  • U reakce trvající déle než 10 s se ztrácí lidská pozornost.

Cíl byl jasný: Reagovat pod desetinu sekundy.

Jaká kritéria optimalizovat

1. Co nejméně požadavků

Každý požadavek je drahý. Může obsahovat řadu zdržujících činností od překladu DNS, zahájení a ukončení TCP [neřkuli SSL] spojení po zbytečné chybové (4×x) a přesměrovávací (30×) stavy.
Je vhodné požadavky spojit, šetřit spojení – udržovat je otevřené pomocí Keep-Alive – a opakované požadavky úplně odstranit správným cachováním.

2. Rychlé odpovědi

Požadavky musí být vyřízené co nejdřív. Server je musí zpracovat rychle, výstup odeslat co nejdřív a odpověď musí cestovat co nejrychleji.

3. Malé odpovědi

Všechny odpovědi je důležité gzipovat a kód – HTML, JS, CSS – předtím minifikovat, pokud je to možné. U kódu se objem přenášených dat po minifikaci a gzipování zmenší většinou na méně než 20 %.

4. Feedback co nejdřív

Často můžeme dát uživateli feedback ještě před odesláním požadavku. Reakční doba je pak rozložená na okamžitý feedback a opožděné zobrazení odpovědi. Neurychlí se tím samotné provádění úkolu, ale zlepší se dojem z používání aplikace.

MySQL

Vlastně jsem na začátku neříkal úplně pravdu. Optimalizovat jsem začal už v zimě, kdy jsem se zakousl do užitečné knihy High Performance MySQL.

Autoři měli naprostou pravdu, když tvrdili, že správně navržené indexy a napsané dotazy dokáží urychlit zpracování dat o řády. Hlavní stránku Webtrhu jsem přepsáním několika dotazů a úpravou indexů zrychlil z několika sekund na desetiny.

Optimalizace nastavení SQL serveru a cachování výkon vylepšila přesně podle jejich předpovědi už „jen“ v desítkách procent.

Pokud má aplikace problémy v DB, je dobré začít právě tam. Optimalizace DB poskytne nejvíc muziky, protože nejvíc ovlivňuje Time To First Byte.

Server

Všechny odpovědi by se měly gzipovat a měly by mít správné hlavičky pro cachování. V Apache jednoduše pomocí mod_expires a mod_deflate:

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css
 text/xml application/x-javascript text/javascript application/javascript
</IfModule>

<IfModule mod_expires.c>
  ExpiresActive on
  ExpiresDefault                          "access plus 1 month"
  ExpiresByType text/html                 "access plus 0 seconds"
  ExpiresByType text/xml                  "access plus 0 seconds"
  ExpiresByType application/xml           "access plus 0 seconds"
  ExpiresByType application/json          "access plus 0 seconds"
  ExpiresByType application/rss+xml       "access plus 1 hour"
</IfModule> 

Víc komentované konfigurace je v ukázkovém .htaccess HTML5Boilerplate.

Až budeme cachované soubory měnit, klienty přinutíme stáhnout si novou verzi souboru změnou jména. Stačí upravit query za otazníkem:  external.js?20110721

Nezapomeňte zkontrolovat zapnuté Keep-Alive (Apache má defaultně zapnutý) pro udržování otevřených spojení.

Výstup bychom měli začít odesílat co nejdřív. Pokud se v requestu provádí nějaká údržba, je dobré postupovat takto:

  1. Vytvořit výstup
  2. Odeslat výstup
  3. Provést údržbu (zvýšení čítačů, přepočet statistik apod.)

Nebo ještě čistěji – přesunout údržbu do pravidelně volaných procesů.

PHP v továrním nastavení odešle výstup až na konci běhu skriptu. Dá se přinutit k okamžitému odeslání pomocí flush(); (jednorázově), ob_implicit_flush(); (po dobu běhu skriptu) nebo příslušným konfiguračním příznakem.

Této techniky jsem se musel zříci, protože ve vBulletinu se HTML výstup generuje až úplně nakonec. Můžeme ji ale využít třeba u AJAX požadavků.

Nainstaloval jsem taky APC, opcode cache pro PHP, nakonec s touto konfigurací:

apc.shm_size="128" ; velikost cache v MB.
  ; Pokud APC nepoužívá mmap, nastavit podle tohoto
apc.stat="0" ; APC přestává při každém požadavku ověřovat, jestli se soubor nezměnil.
  ; Pro staging server zapnout
apc.ttl="86400" ; životnost opcode cache
apc.user_ttl="86400" ; životnost user cache (proměnných)
apc.lazy_functions="1"
apc.lazy_classes="1"
 ; Dvě nové featury přidané vývojáři Facebooku, lazy loading funkcí a tříd 

HTML

Požadavky jsou drahé, takže pokud je to možné, spojte všechny JS soubory do jednoho, všechny CSS do jednoho, a všechny obrázky do jednoho image spritu.

HTML je možné minifikovat pomocí CSSMin nebo YUI Compressoru, ale pozor na signifikantní whitespace. Na Webtrhu minifikace začala žrát odstavce (oddělené novými řádky), kdykoliv jsem editoval příspěvek.

Javascript

Javascript blokuje render a načítání kvůli document.write. Proto se dřív doporučovalo: Přesuňte JS až na konec stránky. Dnes existuje lepší metoda.

Přestaňte používat document.write a načítejte JS asynchronně pomocí knihovny jako LABjs nebo Require.js. Pro Webtrh jsem zvolil LABjs, nedělá nic jiného a gzipovaná má jen 2,2 kB.
Javascript se pak načítá mimo ostatní requesty na stránce a neblokuje je.

Kód musí být připravený pro minifikaci. Minifikátory jsou různě agresivní, od extra agresivního Dean Edwards Packeru přes hodně doporučovaný YUI Compressor až po mírumilovný JSMin od Douglase Crockforda.

Kód zabalený Packerem nefungoval správně a jelikož rozdíl mezi minifikátory je zanedbatelný, nevěnoval jsem se tomu a zvolil JSMin.

Překvapivé překážky při přeskládávání JS

(aneb V nouzi pomůže aliterace.)

Při spojování souborů do jediného se vyskytla chyba v syntaxi, protože poslední deklarace v jednom ze spojovaných souborů nebyla ukončená středníkem. První deklarace z následujícího souboru se tedy stala součástí předchozí deklarace a syntax error byl na světě:
  callMe()callMeToo();

Jakmile začnete načítat všechny skripty na stránce asynchronně a zároveň máte ve stránce inline skripty, začnou okamžitě v konzoli vyskakovat chyby. Inline skripty se totiž najednou rozběhnou PŘED načtením zdrojů.

LABjs poskytuje řešení ve formě metody wait():

$LAB
.script('external.js')
.wait( function() {
  // na předchozím souboru závislé inline skripty
});

To s sebou ale přineslo několik neočekávaných záludností, které mě přiměly k emotivním commitům v Gitu.

Problém č. 1

Pokud je external.js načtený z cache a je dlouhý, inline skripty se spustí dřív, než se interpretuje kód z externího souboru.

Řešení: setTimeout(executeInlines, 1);

Problém č. 2

Inline skripty ztratí globální scope, na kterém závisí. Zároveň jsou deklarované jako privátní proměnné a nelze je uchopit výčtem for(var prop in this) {}

Čisté řešení: Zbavit kód závislosti na globálním scope. V ideálním případě bych skripty přepsal a zbavil chybné závislosti na globálním stavu. Protože ale legacy kód má několik tisíc řádků, použil jsem regex/eval hack zmíněný dál. Šlo o záměrné rozhodnutí ponechat technický dluh, abych se nezdržel na několik dnů až týdnů.

Rychlé řešení: Projít inline kód regexpem a vybrat všechny názvy funkcí a lokálních proměnných. Předat je JS proměnné a zkopírovat eval()em do globálního scope. Brutální, ale funguje.

Problém č. 3

Protože se asynchronní skript načítá nezávisle, je nutné explicitně stanovit závislosti všech událostí.

Tohle zní nevinně, ale zabralo mi to celý den. vBulletin definuje dvě vlastní události, které se musí odpálit v určitém pořadí. Nikde to ale není explicitně napsané, natožpak nakódované.
Protože na sebe skripty a DOM přestaly čekat, události se spouštěly v náhodném pořadí.

Řešení: Identifikovat závislost a explicitně ji uvést do kódu:
Událost č. 2 musí počkat na událost č. 1.

Uživatelské rozhraní

Pokud má akce jasný výsledek (jeden stav), můžeme zareagovat ještě před odesláním požadavku. Rozhraní se pak chová responzivněji, ačkoliv se reakční doba nezkrátila.

Okamžitý feedback jsem aplikoval v hlavním navigačním menu Webtrhu. Menu zareaguje na kliknutí ihned, ještě předtím, než se začne načítat nová stránka. Vyzkoušejte to.

Co dál?

Zrychlování stránky se odehrává na mnoha úrovních a je to hodně zajímavá a uspokojující činnost.
Nepodařilo se mi zatím ale dosáhnout původního cíle a zrychlit stránky pod desetinu sekundy.

Jak zrychlit načítání dál? Použít pro statický obsah doménu bez cookies. Zvětšit initial TCP congestion window. Dál optimalizovat databázi. Agresivněji cachovat části stránek, alespoň pro nepřihlášené návštěvníky.

Napadají vás další možnosti? Podělte se v komentářích.

Další čtení k tématu

Komentáře

Subscribe
Upozornit na
guest
34 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
napalm

Použít místo Apache nginx a memcached jako cache nějakých statických dotazů, PHP objektů a session.

Martin

Pokud běží na jediném serveru, APC funguje stejně jako memcached. (Je to ta část, které se říká user cache:
http://stackoverflow.com/questions/1965290/difference-between-user-and-system-caches-using-php-apc/
)

Díky za tip na Nginx. Stejný tip se objevil v prvním komentáři u mě na blogu, takže populárním hlasováním Nginx bude další, co vyzkoušíme. :)

Dundee5

Nginx je sice méně žravý a pro statický obsah i rychlejší něž Apache, ale pro spojení Apache + PHP je nejvýkonnější volbou stále Apache + mod_php.

jos

nejvýkonnější je apache + mod_fcgid a ještě je to stabilnější

cita

parkrat jsem s timto experimentoval a pod velkou zatezi(400 php requestu/s, staticky obsah je servirovan poolem nginxu) to rozhodne rychlejsi a stabilnejsi nez apache+mod_php nebylo.. Mate to jak podlozit? Nejake spesl nastaveni?

btw jako dobra technika pro „zrychleni webu“, napr. eshopu a jinych mene aktualizovanych webu se mi osvedcilo cachovani celych vygenerovanych stranek a jejich invalidace pri zmene dat. Vetsinou to znamena dost prace, ale vysledek stoji za to.

napalm

Já jsem APC krátce zkoušel a nepodařilo se mi ho zprovoznit, respektive jsem ho zprovoznil, ale výsledkem byly chyby při provádění skriptů, takže jsem ho opět odinstaloval.

jlx

Z moji zkusenosti:

– nejvetsi vyhoda Nginxu je, ze spotreba pameti je mnohem mensi nez u Apache a hlavne, i pro velke zateze je konstantni.
– Nginx dobre funguje v kombinaci s PHP-FPM
– APC pouzivat urcite, ale jenom jako bytecode cache. APC user cache je spise nestabilni..
– jako „user cache“ funguje asi nejlepe memcached.
– ostatni zbylou pamet pridelit cache MySQL (je jich vice druhu), cim vice tim lepe..

Jerry12

Zajimava alternativa nginxu je cherokee.

Vrele doporucuju se kouknout i kdyby jen pro zajimavost. Me se zdala treba webova konfigurace jako totalni silenost, ale realita je vazne takova, ze rozchodit webserver s nekolika virtualhostama, 2 konfiguracema PHPcka a uWSGI Pythonem mi trvalo asi 3 hodiny vcetne nastaveni debianu, tak abych mel Cherokee 1.3 z testingu. To musite uznat je bez predchozi zkusenosti zazrak. Nastaveni reverzni proxiny je otazka asi 5 minut.

Ferda Mravenec

Cherokee jsem nasadil na produkci jednoho webu, ktery pres PHP generuje nekolik desitek milionu stranek mesicne, pres den cca 9000 UIP, ale s velkym poctem opakovanych navstev, a zatim se mi jevi velmi stabilni. Administrace pres web je naprosto luxusni, vim, ze mnohy „linuxar“ nad tim ohrne ret :), ale nutno si priznat, ze nez donekonecna lezt pres SSHacko na server, editovat config a restartovat …, tak tady to mam hotove hned. Rozchodit php-fpm opet bez problemu, nastaveni pro wordpress a jina open source zverstva taktez na dve kliknuti, takze pokud to nekdo chce zkusit, vrele doporucuju … Na jinem serveru pouzivame lighttpd a taktez spokojenost, oproti Apachi se nam vyrazne zlepsila stabilita i odezvy systemu pri vetsi zatezi (napriklad sken googlem ci jinymi roboty, stahovani webu pres ruzne downloadery atd)

Aleš Roubíček

Minifikovat před gzipem nemá smysl. gzip má rád opakující se vzory. Minifikovaný JavaScript může být po kompresi gzipem ve výsledku větší než neminifikovaný.

Dobré, je si to vyzkoušet. Zajímaostí může být, že Google Closure Compiler optimalizuje JS tak, aby gzip byl co nejúčinější.

Martin

Opřel jsem se o srovnání minifikátorů:
http://compressorrater.thruhere.net/

Stále ukazuje rozdíl cca deseti procentních bodů mezi minifikovaným a přirozeným gzipovaným kódem.

O vychytávce Closure Compileru jsem nevěděl, díky. Používáte ho někde naživo?

knapek.mojeid.cz

Napadlo mě co zkusit Google SPDY protokol, ale nasazení by asi zabralo spoustu úsilí a přínos by viděli pouze (zatím) uživatelé Google Chrome.

Při prohlížení HTML5 databáze v prohlížeči jsem si všiml, že si tam stránky Facebooku ukládají docela dost dat (taková lepší cache?, mám uživatelovu chace na jeho pc přímo pod kontrolou) ale to se asi bude hodit jenom Facebooku a jemu podobným stránkám co načítají téměř veškerý obsah AJAXem.

Aleš Roubíček

Pomocí local storage se povedlo Hotmailu dosáhnout velice zásadního zrychlení pohybu v aplikaci. Doporučuji vyzkoušet.

Martin

Díky oběma za další tip. Local storage zní určitě užitečně, hlavně protože se narozdíl od cookies neposílá s požadavky.

Co si tam Facebook ukládá? (Nemám účet, podíval bych se).

Tady je knihovna, která abstrahuje od nekompatibilních a starých prohlížečů
https://github.com/marcuswestin/store.js

mekele

Postrehy jsou to pekne a rad jsem si je precetl. Nevim jak to behalo predtim, kazdopadne po prokliknuti nekolika stranek to beha opravdu fajn.

Martin

Děkuji, to mě těší. (Zatím nedosaženou) Metou každopádně zůstává, aby se stránka načítala na lusknutí prstů, pod desetinu sekundy. :)

Mrzí mě (pro čtenáře i pro sebe), že jsem si změny pořádně neměřil, abych věděl, jaký přínos která měla.

Pro srovnání tedy aspoň výsledky externího měření rychlosti z února 2011
http://www.webpagetest.org/result/110219_F8_Y1R/
a ze dneška
http://www.webpagetest.org/result/110727_8B_1587S/

Test probíhající z Frankfurtu ukazuje dvoj- až trojnásobné zrychlení.
Můj Firebug ukazuje zhruba něco podobného (s nižšími čísly, protože server je v Praze).

Petr

Neřeší celé to asynchronní načítání HTML atributy defer a async?

Martin

Async je to, co Labjs využívá a co nahrazuje pro prohlížeče, které to neumí.

Kromě toho taky umožňuje explicitně specifikovat závislosti – což je u asynchronně načítaných skriptů nutné (píšu v textu).

Viz metoda $LAB.wait()
http://labjs.com/documentation.php

Implementace defer je prý nespolehlivá.
http://hacks.mozilla.org/2009/06/defer/

Petr

Paráda, díky za info, o chybném chování jsem nevěděl.

jiridobry

V pár aplikacích jsem viděl vtipně použít místo „onclick“ „onmousedown“. Je to extra jedna dvě desetinky vteřiny.

Martin

Tak to je hardcore technika. :)
Máte pravdu, nakonec se počítá každá setina.

Na druhou stranu je dobré začít s nízko visícím ovocem, tedy tam, kde za jednotku času své práce získáte největší zrychlení.

To podle mě je databáze, cachování na serveru a cachovací hlavičky pro cachování u klienta.

Pokud se někdo dopracuje k onmousedown, klobouk dolů. :)

Franta

Jenže pak se aplikace chová jinak, než je uživatel zvyklý — normálně totiž zmáčkne tlačítko myši (třeba nad odkazem) a může si to ještě rozmyslet (přesunout kurzor jinam a až pak pustit).

Tin

Navíc uživatel nejčastěji používá copy a paste. Tedy když se o to pokusí, odejde na jinou stránku.

Pavel Lang

To mi nepříjde jako optimalizace, spíš jako nepochopení významu. onmousedown se má používat ve speciálních případech. Na klikání (tj. stisk a uvolnění tlačítka) reaguje onclick a to v souladu s chováním OS.

onmousedown by se mělo používat na akce, jejichž spuštění modifikuje pouze zobrazení, ale neprovádí žádný „zásah do dat“.
Třeba přepnutí záložky (tabu), zobrazení menu, tam se použití onmousedown přímo vybízí, kdežto jakékoliv tlačítko typu uložit, odeslat by mělo reagovat až na celý klik.

Franta

+1 přesně tak

v6ak

Zajímavé, ale mění to trošku ovládání, byť ne až tak závratně. Ale nevím, jak se s tím vypořádá které dotykové zařízení – jestli nějaké nespustí nějakou akci již při doteku, když uživatel chtěl rolovat nebo zobrazit kontextové menu.

Obecně bych se snažil takovýmto změnám pokud možno vyhýbat, pokud neladíme pro konkrétní zařízení například do firemního prostředí, protože nepatrné zrychlení by mohlo bý’t vykoupeno nepříjemnými problémy, zvláště (ale nejen) u uživatelů se specifickým prostředím a/nebo specifickými návyky.

jiridobry

Používá to například gmail u „hvězdičkování“ mailů, tedy tam, kde většinou nemůže dojít k nějakému závratnému omylu při „ukliknutí“.

jiridobry

Nebo ještě třeba při otevírání mailů.

Oldis

Hodne staticke nemenne stranky vygenerovat komplet a pak je predhazovat, nasledne je updateovat jen kdyz dojde ke zmene informaci nanich, typicky zrovna vstupni stranky fora, stranky pod polozkami horniho menu. je treba je updateovat jen kvuli statistikam na prave strane.

Aleš Roubíček

vítejte v roce 1998

Martin Malý

Viď? Tak starý princip, na který pak na nějakých deset let vývojářský mainstream radostně zapomněl, opojen skriptovacími jazyky a databázemi, a teď ho znovuobjevuje.

Pilgrim

Já už léta používám na minimalizaci JavaScriptu web http://fmarcia.info/jsmin/test.html a když je kód dobře napsaný (středníky za objektem var foo = {}; apod.), tak funguje dobře :-)

www.google.com

Mám web, kde je na titulní stránce hodně obrázků. Řekl bych typická situace pro uplatnění KeepAlive.

Jakmile ho ale zapnu, načítání se vyhoupne třeba k deseti vteřinám a tak polovina obrázků se vůbec nenačte. V Chrome vidím, že tyto chybějící mají dlouhou dobu načítání, jejich mimetyp je zobrazován application/octet-stream (jinak je správně) a v podrobnostech nejsou vidět hlavičky odpovědi.

Jakmile KA vypnu, vejdu se do dvou sekund (žádná sláva, vím) a vše se načítá v pořádku, jsou správně mimetypy i hlavičky.

Vůbec už netuším kde hledat vysvětlení. V apache2.conf klidně nastavím timeout i keepalive timeout na vysoké hodnoty, apache restartuji (gracefull) a nemá to na tenhle problém efekt.

Patrik Ján

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.