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

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.

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

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 34

Přehled komentářů

napalm Webserver
Martin Re: Webserver
Dundee5 Re: Webserver
jos Re: Webserver
cita Re: Webserver
napalm Re: Webserver
jlx Re: Webserver
Jerry12 Re: Webserver
Ferda Mravenec Re: Webserver
Aleš Roubíček Ad malé odpovědi
Martin Re: Ad malé odpovědi
knapek.mojeid.cz Google SPDY
Aleš Roubíček Re: Google SPDY
Martin Re: Google SPDY
mekele Re: Jak zrychlit server - několik praktických postřehů
Martin Re: Jak zrychlit server - několik praktických postřehů
Petr JS Async
Martin Re: JS Async
Petr Re: JS Async
jiridobry Rychlost odezvy na onclick
Martin Re: Rychlost odezvy na onclick
Franta Re: Rychlost odezvy na onclick
Tin Re: Rychlost odezvy na onclick
langpa Re: Rychlost odezvy na onclick
Franta Re: Rychlost odezvy na onclick
v6ak Re: Rychlost odezvy na onclick
jiridobry Re: Rychlost odezvy na onclick
jiridobry Re: Rychlost odezvy na onclick
Oldis co treba vygenerovat nektere stranky jako staticke?
Aleš Roubíček Re: co treba vygenerovat nektere stranky jako staticke?
Martin Malý Re: co treba vygenerovat nektere stranky jako staticke?
Pilgrim JSMin
www.google.com keep alive
Patrik Ján Percona server
Zdroj: https://www.zdrojak.cz/?p=3522