Sådan optimerer du hastigheden på PHP med memory caching

I artikelserien "Hastighedsoptimering af PHP" går jeg i dybden med, hvordan man bygger hurtige og skalerbare PHP applikationer. Det er ikke en masse tips til små optimeringer, men håndgribelige metoder til at undgå flaskehalse og suboptimal kode.

Artiklerne omhandler udelukkende god eksekveringstid på dine PHP scripts. Vil du forbedre din hjemmesides overall loadtid, så start på PageSpeed Insights, eller se i din browsers Netværks-fane for nogle lavthængende frugter - det er ikke lige det, denne artikelserie handler om.

Artikler i serien:

Problemet

Hver gang PHP skal lave beregninger eller vente på et tredjepartsprogram (kald til database, API kald over netværk etc.) tager det tid.

Dette problem kan blive forværret ved unødvendige gentagelser i koden.

Tunge beregninger, lang ventetid på tredjepartsprogrammer eller mange gentagelser kan udgøre en flaskehals i eksekveringen af din applikation. Du kan finde flaskehalse i din kode igennem eksempelvis en cachegrind analyse.

Hvad kan der gøres ved en sådan flaskehals, spørger du? Jeg har svaret 🙂

Caching på funktionsniveau to the rescue

En ofte anvendt løsning på hastighedsproblemer er caching.

Mange beregninger eller kald til tredjepartsprogrammer ændrer ikke resultat fra minut til minut, og at lade alle brugere foretage de samme beregninger igen og igen er håbløs spild af resurser på din server og tid for brugerne.

I stedet kan resultatet af en beregning gemmes ved første eksekvering, og det gemte resultat kan serveres på de efterfølgende eksekveringer direkte fra hukommelsen eller en fil.

En god fremgangsmåde er at cache returværdien af en eller flere funktioner. Denne struktur gør, at man bevarer overblikket over, hvor caching er implementeret.

Afhængig af hvilken cache service der benyttes, vil en cached værdi blive gemt med et navn, fx “tung_funktion_user_3466”, og med en værdi svarende til returværdien af den cachede funktion.

Eksempel på funktion uden cache

<?php

/**
 * Tung funktion
 *
 * Funktion, der udfører en tung beregning for bruger $userId
 *
 * @param int $userId
 * @param array $array
 * @return int Beregnet tal for bruger
 */
public function tungFunktion($userId, $array)
{
     $calculatedValue = 0;

     foreach($array => $item) {
          // Beregning, der ændrer $calculatedValue,
          // foretages her.
     }

     return $calculatedValue;
}

Eksempel på funktion med cache

<?php

/**
 * Tung funktion
 *
 * Funktion, der udfører en tung beregning for bruger $userId
 *
 * @param int $userId
 * @param array $array
 * @return int Beregnet tal for bruger
 */
public function tungFunktion($userId, $array)
{
     if ($cached = $this->cache->get('tung_funktion_user_' . $userId))
          return $cached;

     $calculatedValue = 0;

     foreach($array => $item) {
          // Beregning, der ændrer $calculatedValue,
          // foretages her.
     }

     $this->cache->set('tung_funktion_user_' . $userId, $calculatedValue);

     return $calculatedValue;
}

Funktionerne $this->cache->get() og $this->cache->set() henviser til den pågældende cache drivers funktioner til hhv. at hente og gemme i cachen – men disse funktioner kan hedde hvad som helst, afhængig af valg af framework, cache driver osv.

Som det ses, gør implementering af caching, at funktionens resursekrævende kode slet ikke eksekveres, men simpelthen springes over.

Hvilken caching skal jeg vælge?

Først og fremmest bør du tage stilling til, om du vil bruge en filcache eller in-memory cache. Da normal filcache næsten altid vil tabe på hastighed til in-memory cache, bør man som udgangspunkt vælge in-memory caching, hvis du har mulighed for det.

Jeg vil anbefale at benytte Redis, som er en solid in-memory caching service, der er rig på funktioner og har mange forskellige datatyper. Det er sikkert, at Redis vil overtage mere og mere marked fra det ellers etablerede Memcached.

Hvor Memcached er at finde i alle de kendte frameworks, er Redis stadig kun tilgængeligt i nogle få, men let at implementere med eksempelvis denne composer pakke.

P.S. Filcaching kan også opsættes til at benytte en RAM disk, ligesom det bør bemærkes, at MySQL også vil forsøge at cache resultater af forespørgsler i RAM. Der kan være situationer, hvor det er hurtigere ikke at benytte en caching service, selvom det må anses for at høre til sjældenhederne.

Sådan vælger du, hvilke funktioner, du skal cache

Du skal udvælge funktioner, som kaldes ofte og ændrer resultat sjældent.

Eksempler

  • Funktion, der henter basisinformation om et produkt fra database
  • Funktion, der henter stamdata for en bruger
  • Funktion, der henter noget statistik

Alle funktioner kan caches, men vælger du funktioner, hvor resultatet ikke får lov at ligge i cachen særligt lang tid af gangen, inden cachen skal slettes (invalideres) og værdien beregnes på ny, så giver caching på funktionen ikke mening, og det bliver blot en ekstra kilde til eventuelle fejl.

Fejl 1: afhængighed af cache

Caching skal ses som et ekstra lag, der skal kunne fjernes, uden at funktionaliteten i din applikation går i stykker. Der skal ikke være nogen funktioner, der er afhængige af en cached værdi.

Ligeledes skal du ikke bruge caching til at komme udenom dårligt performende kode, så en sådan suboptimal kode bør rettes i stedet. Ellers ender du med at bruge caching som et plastrer, der holder hele din kode sammen 🙂

Din applikation kan i drift alligevel være afhængig af caching, pga. det høje load, som din server ellers vil blive udsat for. Det er helt berettiget, da det jo er en af grundene til, at man ønsker at implementere caching i første omgang. Hvis caching systemet går ned i drift, bliver det en opgave for server administratoren at holde skibet flydende.

Fejl 2: manglende cache invalidering

There are only two hard things in Computer Science: cache invalidation and naming things.

– Phil Karlton

Det gamle citat lyver ikke.

Når gemning og hentning af cache værdier er implementeret, er du kun halvvejs færdig med implementering af caching.

Nu ligger den store opgave foran dig med at invalidere cachen, når værdierne ikke længere er gældende.

I praksis gøres dette ved at slette cache værdierne på given tidspunkter. Har du eksempelvis cached oplysningerne om en bruger, skal disse værdier slettes, når brugeren retter i sin profil.

Det kan være svært at nå hele vejen rundt, men det er ikke desto mindre meget vigtigt, at du gør en indsats for det.

Nu er du klar

Dette var de grundlæggende principper i caching af værdier på funktionsniveau. Nu er det tid til at undersøge mulighederne i dit yndlings frameworkog komme i gang med at få optimeret hastigheden på din applikation 🙂

Har jeg fat i noget? Eller er der fejl? Lav en pull request til artiklen på GitHub, hvis der er noget, du mener, skal være anderledes.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *