Minder laadtijd met cache - how to?

Door Sam de Poorter, back-end developer
5 oktober 2015 - 3555 x bekeken - Categorie├źn: Tech

Cache boost de performance en schaalbaarheid van je applicatie enorm. Het is belangrijk om de juiste vormen van cache toe te passen en vaak is dat een combinatie van meerdere technieken. Wat er mogelijk is en hoe je dit toepast, licht ik in dit artikel verder toe. 

"Insanity: doing the same thing over and over again and expecting different results."

 

De bovenstaande quote van Einstein beschrijft waar een perfecte vorm van cache om draait. Zolang het resultaat hetzelfde is, is het onnodig en traag om opnieuw te blijven berekenen. Als het resultaat anders is, wil je dit wel, omdat het betekent dat je site niet 100 procent up-to-date is. Sommige vormen van cache sluiten hier perfect bij aan door de cache te legen zodra er relevante veranderingen zijn. Andere vormen en situaties laten dat helaas niet of minder toe. Een concessie die velen voor lief nemen, gezien dat juist de vormen zijn die de grote winst in performance met zich meebrengen. In dit artikel ga ik in op verschillende niveaus en vormen van cache. Vooral bij de onderdelen waar jij als developer invloed op kan uitoefenen, staan we even stil bij de aandachtspunten. Het belangrijkste voor nu: zorg dat je basisperformance al in orde is voordat je ook maar een enkele vorm van cache overweegt. 

Server-niveau

OPcache (bytecode)

OPcache is native in PHP (5.5+) beschikbaar en cached Operation Code. Operation code is het resultaat van het compilen en parsen van de PHP-scripts zelf, zie figuur 1.

Figuur 1, Toepassing van APC-cache, wat in essentie op hetzelfde neerkomt als OPcache. Bron: inmotionhosting.

OPcache biedt de optie om files te 'watchen', om de cache te vernieuwen als files zijn aangepast. Een betere optie is om het watchen uit te schakelen en de cache te legen binnen je publicatieproces. Dit maakt het watchen overbodig en zorgt er ook voor dat je altijd up-to-date bent. 

Mocht je nog niet beschikken over PHP 5.5+, dan heb je aan Alternative PHP Cache (zie figuur 1) voor nu een prima alternatief.

Pluspunten

Minpunten

 + Native binnen PHP (lager dan PHP 5.5? APC!)
 + Geen risico aan verbonden
 + Breed inzetbaar, in elke situatie bevorderlijk
 - Je moet zelf maatregelen nemen om de cache te legen in je publicatieproces. Je zal een workaround moeten toepassen als je gebruik maakt van symlinks.

 

Database-niveau

 

Query Cache

De Query Cache is een look-up table (in memory) van query's en de resultaten die daarvoor zijn opgehaald. Zodra een gecachede query opnieuw wordt opgevraagd, kan de opgeslagen resultset direct worden teruggeven. Query cache is standaard opgenomen binnen MySql en activeer je met enkele instellingen. Dit doet vaak wonderen voor je performance, maar vertrouw hier niet blind op. De Query Cache is ingericht om alleen de resultset terug te geven als deze gelijk is gebleven. Om dit te garanderen wordt de cache grondig en selectief geleegd bij updates aan je data. Als jij je productentabel bijwerkt, worden al je query-resultaten die hieraan gerelateerd zijn (dus ook join query's) verwijderd uit de Query Cache.

Over het algemeen is de Query Cache zeker bevorderlijk voor je applicatie, maar ga er niet blind van uit. Er zijn situaties waarbij de Query Cache juist voor vertraging zorgt. Dit is bijna alleen bij applicaties die write-heavy (cache vaak geleegd) zijn of waar veel diversiteit aan data (weinig cache-hits) is.

Voor jou als developer zijn er extra aandachtspunten om de Query Cache zo effectief mogelijk in te zetten:

  • Dependend subquery's (afhankelijk van de 'outer query') zijn niet te cachen, joins daarentegen wel;
  • Query Cache van PDO- en niet PDO-statements worden apart bijgehouden in een tabel. Deze kunnen geen gebruik maken van de Query Cache van de ander, writes daarentegen legen beide caches;
  • Query's die een waarschuwing genereren, worden niet gecached;
  • Look-ups wordt gedaan aan de hand van de hashwaarde van de query. Een extra spatie of inline-comment zorgt ervoor dat de hash anders is, waardoor de Query-cache niet hit;
  • Probeer updates te batchen om je Query-Cache niet onnodig te legen. Aanvullend voordeel is dat je database maar één keer de indexen hoeft bij te werken.
  • 'Dynamische' query's worden niet in de cache opgenomen. Dit zijn query's die gebruik maken van functies als NOW(), CURDATE() en meer. Zie hiervoor figuur 2.


Figuur 2, functieaanroepen die voorkomen dat een query in de cache opgenomen wordt. Bron: dev.mysql.com

Het is verstandig om forse data-tabellen onder te verdelen in meerdere kleine tabellen. Bij een update wordt alleen de cache verwijderd voor deze (kleinere) tabel. Dat voorkomt dat bij elke write, de gehele Query Cache verwijderd wordt voor je 'hoofdtabel'.
 

Overkoepelend-niveau

 

Gateway cache (Cache HTTP reverse proxy)

Software als Varnish en Squid gebruiken (een vorm van) full-page-cache. In de ideale opzet vormen ze een extra schakel tussen de webserver en de bezoeker. Als de opgevraagde pagina in de cache opgenomen is, wordt deze direct geserveerd aan de bezoeker. Dus zonder dat de request überhaupt op de webserver terecht komt waar je applicatie draait. Hierdoor krijg je een performance die vergelijkbaar is met statische HTML pagina's, wat in de praktijk neerkomt op laadtijden van 0,1 tot 0,3 seconden. Als een verzoek niet verwerkt kan worden uit de cache, wordt deze alsnog opgevraagd op de webserver en opgenomen in de cache (mits toepasbaar).

Pluspunten

Minpunten

 + Niet te matchen performance
 + Flink verbeterde schaalbaarheid
 + Goed te outsourcen

 - Lastig debuggen
 - Vaak extra kosten (server of outsourcen)
 - Lastig om logica toe te passen (selectief cache te flushen)
 - Vaak geen meerwaarde voor HTTPS verkeer.

 

Code-niveau

 

Full-page-cache

Gateway-caches vervallen als optie zodra je complexere logica wil toepassen op je cache, gepersonaliseerde content wil weergeven of natuurlijk als het te prijzig is. Dat betekent niet dat je opties daar ophouden. Als je geen framework tot je beschikking hebt die je hierin kan ontlasten, heb je nog genoeg keuzes in libraries en componenten. In tegenstelling tot de Gateway-cache betekent dit wel dat je vaak je framework moet booten, alvorens je het bestand uit de cache kan serveren. Afhankelijk van de laadtijd van je framework, is een laadtijd van 0,5 seconden nog prima haalbaar.

In figuur 3 zie je hoe full-page-cache is toegepast op een webshop. De content is afhankelijk van het type regio waar de gebruiker vandaan komt, dat opgeslagen is in een cookie. Omdat gateway-proxy's hier moeilijk mee omgaan, hebben we ervoor gekozen een eigen vorm toe te passen (zie figuur 3).

Figuur 3, voorbeeld van full-page-cache

De opzet is simpel. Maak een cache-key aan de hand van alle variabelen en controleer of hier een cache-bestand voor bestaat. Als dit niet het geval is, wordt de request afgehandeld en de response in de cache opgeslagen. Betere frameworks als Symfony spelen hier enorm goed op in met hun eigen cache-componenten. Wat jou als developer ontzorgt in denkwerk, tijd en risico. Als we Symfony-cache als voorbeeld pakken zien we:

  • dat je makkelijk onderscheid kan maken tussen shared (voor elke gebruiker) en private cache (alleen voor de huidige gebruiker);
  • onderscheid in cache-configuratie per omgeving in je OTAP;
  • dat het cache component goed rekening houdt met http-headers. No-cache headers, age-headers, private en shared headers worden allen correct afgehandeld.
  • dat Symfony controleert of een pagina geen POST-request is, of een formulier bevat met een CSRF-token. Als dit wel zo is, wordt de pagina niet gecached.
  • dat Symfony placeholders (in de vorm van 'E-tags') aanbiedt. Dit geeft bijvoorbeeld de mogelijkheid om alleen het menu in de full-page-cache te verversen zonder dat overige cache-bestanden verwijderd of aangepast moeten worden.

Blok-cache

Een website is vaak opgebouwd uit wat wij noemen 'blokken'. Denk aan een navigatieblok, gerelateerde items, twitterberichten of een footer. Veel van deze blokken lenen zich om te cachen. Het menu is bijna altijd geschikt om te cachen. Deze is op elke pagina opgenomen en is vaak traag om te genereren.

Menu
We gaan als voorbeeld uit van een menu in een webshop. Het menu wordt gegenereerd aan de hand van merken en (sub)categorieën. Bij elk merk wordt gecontroleerd of er actieve producten aan zijn gekoppeld. Bij categorieën gebeurt dit ook, maar daarnaast mag de categorie ook naar voren komen als een subcategorie daarvan actieve producten heeft. Het genereren van dit menu neemt 1.5 seconden in beslag.

Gecached als HTML, is 0,1 seconden nodig om dit blok op te halen (file_get_contents is voldoende). Dat is per request een winst van 1,4 seconden. Dit scheelt ook veel voor de belasting van je database.

Aandachtspunten voor menu-cache:

  • De beste performance heb je, zodra je het menu volledig als HTML bestand kan cachen. Dit is geen optie op het moment dat je css-classes (active-states, hidden) moet toekennen aan menu items. Dan plaats je het een stap eerder, dus het cachen van je data die je gebruikt om je menu te genereren. 
  • Als de applicatie submenu’s heeft, hoef je niet per se selectief te cachen. Het is beter om het gehele menu te cachen, maar alleen selectief bepaalde onderdelen te tonen of door te geven. Op deze manier heb je één cache-bestand voor je gehele site, maar nog steeds alleen je relevante submenu's op de juiste plek.

Buiten een header of footer is blok-cache op veel meer componenten toe te passen. Als je hier efficiënt mee omgaat, bespaar je op veel pagina's tijd.

Twitter-berichten 
Je bent al verplicht om je twitter berichten via een cronjob binnen te halen (om niet tegen het limiet aan te lopen). Dit is ook een perfect moment om de cache te verversen aangezien je net nieuwe data binnen hebt gekregen. Het maakt functioneel voor gebruikers geen verschil, alleen qua laadtijd.

Items voor lijstweergaven (bijvoorbeeld laatste nieuwsberichten)
Als je verschillende aantallen in je views hebt (laatste 5,10 of 25), is het  verstandig hiervan een enkel cache-item te maken. Je neemt hierin het maximaal aantal items (in dit geval 25) op dat je nodig hebt. Maar je gebruikt alleen de eerste 'x' items die nodig zijn voor je view. 

Afhankelijk van je applicatie zijn er ongetwijfeld nog meer toepassingsgebieden te vinden.

Attribuut-cache

In figuur 4 zie je een uitwerking voor het ophalen van Id's van producten binnen een webshop. In dit praktijkvoorbeeld zijn de volgende zaken belangrijk:

  • De query haalt alle producten op die gereserveerd zijn voor andere gebruikers, of geen voorraad hebben;
  • Je wilt in de webshop geen producten tonen die vallen in de bovengenoemde categorie;
  • De query is complex en daardoor langzaam (250ms);
  • Je hebt de resultaten vaker nodig per pagina om producten te ontsluiten (voor alternatieve producten, up-sell producten en gerelateerde producten).

Figuur 4, voorbeeld van attribuut cache

Met deze opzet hoeft de query maar één keer uitgevoerd te worden. In dit praktijkvoorbeeld scheelt het 0,75 seconde per pagina, omdat hij een enkele keer wordt uitgevoerd en daarna drie keer uit de 'cache' gelezen kan worden. Met een opzet in Memcached beperk je dit verder tot éénmalig 250ms per bijvoorbeeld een kwartier, los van het aantal bezoekers (zie onder).

 

Tools

 

Memcached

Memcached is een (simpele) key-value storage. Omdat het volledig in-memory werkt, is het bizar snel. Daarnaast staat Memcached los van sessies of individuele requests. Wat je in memory cachet, is voor iedere bezoeker en bij elke request al beschikbaar. Bij Attribuut-cachen (zie hierboven) moet je per request minimaal één keer deze data ophalen. Dit kan bij een hoog bezoekersaantal makkelijk duizenden cache-hits betekenen.

Opzet Resultaat
Zonder Memcache, zonder Attribuut cache 4x per paginaview, per gebruiker;
Zonder Memcache, met Attribuut cache 1x per paginaview, per gebruiker;
Met Memcache, met attribuut cache 1x in totaal tot cache-verversing (in de praktijk 15 min)

Bij een bezoekersaantal van 25.000 per uur heb je met enkel gebruik van Attribuut-cache normaal in een uur hetzelfde aantal aan calls nodig. In combinatie met Memcached wordt dit beperkt tot vier calls per uur.

Memcached is makkelijk te implementeren en geschikt voor een simpele set aan data (key-value). Als je complexere data wil opslaan is Redis een perfecte keuze. 

Pluspunten

Minpunten

 + Makkelijk te implementeren
 + Bloedsnel (in-memory)
 + Beschikbaar voor de hele server, los van requests, bezoekers of zelfs van individuele websites.

 - Niet geschikt voor complexe data (Redis als alternatief)
 - Gebruikt memory van je server
 - De scope is serverbreed, andere applicaties hebben ook hun invloed op deze cache

 

Aandachtspunten

 

Combineren van cache-vormen

De meeste cache-vormen spelen goed samen. In bijna elke situatie wil je Opcache en Query-cache toepassen. Dit zorgt voor een performance-winst voor al je applicaties, zonder al te veel inspanning en risico. De overige vormen brengen meer arbeid met zich mee, ook afhankelijk van hoe grondig je het wilt implementeren. Maar dit zijn juist de vormen waar je een enorm verschil mee maakt. Ook hierin wil je voor het beste resultaat meerdere vormen combineren. Al maak je gebruik van full-page-cache, dan is het alsnog verstandig om daarnaast blok-cache toe te passen. Pagina's waar geen full-page-cache voor aanwezig is, worden alsnog gegeneerd door gebruik te maken van blok-cache.

Als je gebruik maakt van blok-cache en full-page-cache krijgen bezoekers die geen full-page hit hebben toch een snellere response.

Cache warming / virtual visitor

Het hoofddoel is om je site snel te maken, bloedsnel. In de ideale situatie krijgt elke bezoeker een gecachede versie voorgeschoteld. Voor enkele applicaties zetten wij hiervoor een 'virtual visitor' in. Dit is een cronjob die met gebruik van Wget alle pagina's bezoekt die opgenomen mogen worden in de cache. Als je dit slim toepast zal een gebruiker nooit het cache-bestand moeten genereren en altijd de snelle versie geserveerd krijgen. Paar kleine aandachtspunten in combinatie met full-page-cache:

  • Zorg dat je bij een bezoek de cache opvraagt en als deze er niet is deze aanmaken en opslaan;
  • Zorg dat je de virtual visitor forceert een nieuw cache-bestand aan te maken. Gebruik hiervoor desnoods een Cache Buster (zie hieronder);
  • Zorg dat je virtual visitor op 75 procent van de duur van je cache-bestand de pagina's vernieuwt. In elk geval ruim voordat bezoekers een kans krijgen dit te doen;
  • Op deze manier is je cache niet volledig afhankelijk van je cronjob. Worst-case scenario zal de eerste bezoeker van de pagina het cache-bestand aanmaken.

Naast een virtual visitor, kan je ook aanvullende cronjobs inrichten om je blok-cache en Memcached variabelen te verversen.

Al valt het niet geheel onder de noemer cache, we passen dit bij E-sites soms ook toe op thumbnails. Als je de afmetingen weet, ben je in staat van een nieuwe gekoppelde foto de bijhorende thumbnails in alle nodige formaten al te genereren. Ook dit hoeft niet aan de gebruiker overgelaten te worden.

Cache buster

Soms wil jij (of de klant, die is er ook nog ;-)) er zeker van zijn dat ze niet tegen oude cache aankijken. Of alleen de cache voor de pagina/het component legen waar ze net een aanpassing hebben doorgevoerd. Forceer hiervoor dat er niks uit de cache kan worden gehaald als er bijvoorbeeld in de url ?cacheBuster={credentials} is opgenomen. Het resultaat is dat het bestand niet uit de cache wordt gehaald en dat het nieuwe bestand opnieuw wordt opgenomen in de cache. Zo biedt je nog steeds de flexibiliteit die de klant hierin wenst.

Conclusie

Zorg ervoor dat je cache niet gebruikt als redmiddel bij een slecht database-model of een slecht gecodeerde applicatie. Verdiep je in meerdere vormen van cache en bekijk wat voor jou applicatie toepasbaar is. Bijna altijd is dit een combinatie. Ze ondersteunen elkaar. Soms moet je concessies doen in je real-time data, maar als developer ben je vaak in staat dit te beperken of te voorkomen. Dat gezegd hebbende, is cache één van de beste investeringen die voor een niet te matchen performance en schaalbaarheid zorgt!

 

E-sites zoekt developers!

Technologie ontwikkelt zich razendsnel. En wij dus ook. We omarmen nieuwe technologie, experimenteren en investeren. In goede tools en apparatuur. En in jou. Join us! Bekijk onze vacatures.

UX Impact event

Door Carlo Vingerling

Vorige week werd het UX Impact event gehouden in het Tobacco Theater in Amsterdam. Carlo Vingerling, interaction designer bij E-sites, was erbij. - Lees meer

Lees verder