Jak se loví milisekundy (nejen v #nettefw)

Posted 06. 07. 2014 / By Petr Soukup / Vývoj

Jedna z věcí, kterou hodně řešíme, je rychlost načítání eshopů. Je to poměrně komplexní problém, který je nutné řešit na několika frontách. Jednou z nich je generování stránky v PHP a na té vám předvedu, jaké prkotiny se musí řešit.

Interně máme nastaveno, že generování jedné stránky nesmí trvat déle jak 100 ms. Některé stránky z toho mají vyjímku (například export celého katalogu) a u jiných se naopak čeká, že budou hodně pod touto hranicí (například hlavní strana eshopu). Na první pohled to možná nevypadá, ale 100 ms není moc času a tak se loví každá milisekunda. Jak takový lov probíhá vám předvedu na definování routování. Není to samozřejmě nic objevného, ale je to ideální příklad pro článek, protože je to dostatečně výstižné a zároveň jednoduché.

 

 

Routujeme

Definice routování probíhá při každém načtení stránky a říká, pod jakou URL se skrývá jaká akce. Standardně toto řeší webserver a úplně nejvýkonnější řešení by bylo, nechat to na něm. Největší killer feature rout ale je, že kromě z překladu z URL na akci je lze použít i obráceně (přeložit akci na URL) a to už by s webserverem nešlo.

Definice rout vypadá v kódu nějak takhle:

<?php
use Nette\Application\Routers\RouteList,
    Nette\Application\Routers\Route;

$router = new RouteList;

$router[] = new Route('/Slevy/[<category>]',
    'Homepage:default');
$router[] = new Route('/Kosik', 'Cart:default');
// ... a tak dále

Velké aplikace

Routování je věc, kterou při vývoji děláte už poslepu. Co se ale stane, když máte třeba 200 definovaných rout? Jejich definování najednou trvá 3-5 ms. Není to bezdůvodně (Nette za vás definováním routy dělá spoustu práce) a není to nějak tragicky dlouho, ale definováním rout aplikace vlastně ještě nic neudělala. Pokud je v aplikaci třeba deset dalších podobných "nic", najednou jste ztratili 30-50 ms. Pokud se máme vejít do limitu 100 ms, tak už najednou začíná být dost těsno.

Když se ale podíváme, co se těch 3-5 ms dělá, zjistíme, že se při KAŽDÉM načtení stránky dělají identické úkony s identickým výsledkem. Řešení je tedy jasné - cache.

Cachujeme

Cachování je jeden z nejnáročnějších problémů při programování. Cachovat je totiž snadné, ale cachovat správně už tak snadné není. Musíme totiž řešit několik problémů

  1. invalidace cache - při používání cache musíme zjistit, zda je aktuální. Ověřování aktuálnosti ale může být ve výsledku mnohem nákladnější, než kdyby tam žádná cache nebyla
  2. rychlost cache - pokud je režie kolem načtení z cache (hledání cache, přístupy na disk atd.) nákladnější, než řešení bez cache, je cache k ničemu
  3. konzistentní cache - typicky v cloudu se musí řešit, že na každé instanci cachovaný záznam mohl vzniknout jindy
V případě cachování routování máme situaci jednoduchou - routy závisí jen a pouze na tom, co je uvedeno v souboru s jejich definicí. Pro invalidaci tak můžeme použít čas změny souboru a nebo ještě lépe hash commitu (v ostré verzi). Tím se nám vyřešil rovnou i bod 3, protože instance se stejnými zdrojáky budou mít vždy stejnou cache.

Invalidační vsuvka

Proč je invalidace takový problém? Představte si, že máte misku rýže a máte za úkol říct, kolik zrnek rýže v ní je. Zrnka tedy spočítáte. Pak misku někomu půjčíte, on ji vrátí a máte říct, kolik v ní je zrnek rýže. Nechcete ale pokaždé všechna zrnka počítat, když nevíte, jestli se vůbec něco změnilo. Kromě počtu zrnek si tak po spočítání zapíšete váhu misky. Když se vám vrátí miska a váží stejně, nemusíte nic počítat. Jenže co když někdo odebral jedno velké zrnko a přidal dvě malá? Kromě váhy tak budete měřit ještě objem. A tak dále ... Při hledání spolehlivého řešení tak vlastně hledáte kompromis mezi dostatečně přesným určením, zda se něco změnilo a náklady na přepočítání všech zrnek.

Reálný příklad

Pro nasazení cachování rout stačí provést jen drobné úpravy. Do definice rout doplním jeden řádek, který bude vracet seznam rout. Do initu pak jednu podmínku a cachování do APC. Pro článek zjednodušuji a v reálu budete chtít cachovat spíš přes nějakou chytrou třídu než přes přímé volání (PHP 5.5 navíc APC nemá).

router.php

php use Nette\Application\Routers\RouteList, Nette\Application\Routers\Route; $router = new RouteList; $router[] = new Route(...); // ... 200 dalších rout return $router;

bootstrap.php

php <?php // automaticky doplněno při deploy nebo jen mtime('router.php') define('VERSION','9adddef86'); $cachekey = 'router_' . VERSION; if(apc_exists($cachekey)){ $router = apc_fech($cachekey); }else{ $router = require "./router.php"); apc_store($cachekey, $router, 60*60); } unset($cachekey); Jednoduché, že? O cachování se pak nemusíte nijak starat. Pokud se změní definice routování, změní se i $cachekey. Do cache se uloží nová verze bez zbytečných prodlev a ta původní se z cache po maximálně hodině sama vymaže. Výhodou navíc je, že jsme kromě celého definování rout ušetřili i jedno hledání na disku. Výsledkem je, že místo původních 3-5 ms routování zabere méně jak 1 ms. Tradááá! Můžeme se posunout k lovení dalších milisekund :)

Šlo by to lépe?

Výše uvedeným jsem ušetřil čas při definování rout. Když se ale router použije pro překlad URL na presenter, tak se stejně musí všechny routy (z cache) projít a najít správná. Tam už se ale optimalizuje hůře. Láká mě Router naučit, aby z definovaných rout vygeneroval pravidla pro nginx a logika by se přesunula do něj (nginx je rychlejší než PHP), ale to už je úplně jiný level a náklady implementace (zatím) převyšují ušetřenou milisekundu.  

Další díly:

  1. Jak se loví milisekundy (nejen v #nettefw)
  2. Optimalizujeme pro rychlost: Obrázky
  3. Optimalizujeme pro rychlost: HTTPS
  4. Efektivní minifikace Javascriptu
  5. Optimalizujeme: critical+asynchronní CSS


O blogu
Blog o provozování eshopů a technologickém zázemí.
Aktuálně řeším hlavně cloud, bezpečnost a optimalizaci rychlosti.

Rozjíždím službu pro propojení eshopů s dodavateli.