Caching is de laatste tijd een populaire term en wordt vaak in de algemene zin gebruikt. Deze techniek kan echter op verschillende niveau’s plaatsvinden, zoals op applicatie- of file system-niveau. Wanneer er gesproken wordt over caching voor een website, dan vindt dit plaats op applicatie-niveau. Daar gaan we het nu niet over hebben. Aan caching binnen applicaties kun je namelijk al een volledig artikel wijden, wie weet iets voor later 😉 We duiken daarom nu een stukje dieper in het file system-niveau, en dan specifiek file system caching!
File system caching wordt gebruikt om het opvragen en wegschrijven van bestanden te versnellen door veel opgevraagde bestanden of ‘blocks’ (tijdelijk) op een ander, veel sneller medium te plaatsen. Bij Antagonist maken we gebruik van een cachingmechanisme dat anders is dan traditionele systemen. Het is niet alleen anders, want anders is per definitie niet beter. De caching die wij gebruiken, is vele malen sneller. Sneller is wel beter!
Dit artikel is behoorlijke ’technische hardcore’. Om ervoor te zorgen dat het zo goed mogelijk is te volgen, heb ik het zo gestructureerd mogelijk opgezet. Handig om te weten, is dat er een aantal termen zijn die ik willekeurig door elkaar gebruik, maar waarmee ik hetzelfde bedoel:
- blocks, files en bestanden;
- lezen, leestransacties en reads;
- schrijven, schrijftransacties en writes;
- disk, harddisk, schijf en harde schijf;
- werkgeheugen en RAM.
Read en write caching
Het cachen van bestanden (file system caching) kan worden onderverdeeld in twee categorieën, namelijk: lezen (read caching) en schrijven (write caching).
Read caching wordt gebruikt om leestransacties te versnellen. Wanneer een bestand of block in de cache zit, hoeft dat niet van de onderliggende gegevensdrager opgehaald te worden. Dit is handig, want gegevensdragers zoals harde schijven zijn relatief traag.
Een gemiddelde WordPress-website heeft enkele duizenden bestanden. Je kunt je voorstellen dat dit erg veel tijd kost wanneer deze bestanden elke keer van de schijf gelezen moeten worden. Zonder enige vorm van caching zal het al snel enkele seconden kosten om al deze bestanden van schijf te lezen. Bezoekers van je website zijn ongeduldig, ze hebben geen zin om lang te wachten. In het geval van een webshop kost elke seconde vertraging je meerdere percentages aan conversie.
Write caching wordt ingezet om schrijftransacties te versnellen door het eerst tijdelijk op een sneller medium op te slaan, net als bij read caching. Write caching zit alleen wat complexer in elkaar: door foute instellingen kun je onbedoeld voor ernstige datacorruptie zorgen (wanneer bijvoorbeeld de stroom uitvalt of de server een ‘kernel panic’ krijgt).
De cache kan opgeslagen worden in het RAM (het werkgeheugen) of op een SSD. Wij doen beide, afhankelijk van hoe actief de data opgevraagd wordt. We gaan verderop in het artikel hier nader op in. Eerst gaan we bekijken wat het verschil is tussen traditionele systemen en onze implementatie.
Traditioneel vs. Antagonist
Traditionele systemen maken vaak gebruik van een RAID-controller. Deze controller beschikt over een ‘write-back cache’ om schrijftransacties tijdelijk op te vangen, voordat deze naar een harde schijf worden geschreven. Deze write-back cache is een fysieke chip op de controller. Vroeger waren RAID-controllers hip en cool, wij zijn er echter allergisch voor geworden. Een aantal oorzaken van deze ‘allergie’ zijn:
- Er is weinig zekerheid dat weggeschreven data ook goed op de onderliggende disks staat. We hebben in het verleden controllers gehad met een defecte write-back cache, waardoor je langzaam maar zeker je data corrupt maakt.
- Bij stroomuitval verlies je de data die aanwezig is in de write-back cache. Uiteraard gebruik je altijd een ‘Battery Backup Unit’ (toch?) om tijdens stroomonderbrekingen de cache tijdelijk van stroom te kunnen voorzien. Deze kleine batterijen moeten echter altijd in optimale staat zijn en dit zijn ze vaak niet (zonder dat je dit weet). Ze gaan trouwens ook vaak stuk.
- Er valt weinig te tweaken en tunen aan een RAID-controller. Een cache vergroten of verkleinen zit er bijvoorbeeld niet in.
- Het is een zogeheten ‘Single Point Of Failure’ (SPOF). Cache of volledige controller defect? Dikke pech, je moet per direct naar het datacenter om de boel te vervangen.
Voor het gebruik van RAID-controllers kan ik nog veel meer nadelen (en twee voordelen) opnoemen, maar deze hebben niet direct met caching te maken. Zodoende laat ik dat voor nu achterwege.
Vanwege onze allergie zijn RAID-controllers verbannen uit ons nieuwe platform. In plaats daarvan maken we gebruik van een file system dat alle data van begin tot eind afhandelt en dus direct met de harde schijven communiceert. Tijdens dit proces wordt de data gecontroleerd op correctheid. Data die weggeschreven moet worden, zogeheten ‘dirty data’, wordt altijd op minimaal twee gegevensdragers opgeslagen. We zijn er dus van verzekerd dat data altijd correct en consistent is. Corruptie is nagenoeg onmogelijk, ook bij het stevig inzetten van verschillende cachingmechanismen.
Ons platform is dus retesnel, omdat we (onder andere) zorgeloos stevig kunnen cachen. De voordelen van onze storage zijn verder overduidelijk geworden door de vuurdoop tijdens de stroomstoring vorig jaar. Wij waren met ons nieuwe platform als eerste partij weer volledig operationeel in het datacenter, zonder enige vorm van dataverlies.
Read caching
Het versnellen van leesoperaties doen we op twee niveau’s. Deze niveau’s hebben beiden een eigen mechanisme. De eerste laag caching vindt plaats in het RAM. Dit werkgeheugen is minimaal 300 keer zo snel als een SSD, het is dus belangrijk dat hier zo efficiënt mogelijk gebruik van wordt gemaakt. De tweede laag gebeurt op SSD, voor de data die net niet meer binnen het RAM kan worden gecachet.
Level 1
Het cachen van data in RAM is niets nieuws. Bijna elk besturingssysteem gebruikt beschikbaar geheugen voor het versnellen van leestransacties. Traditionele file systems maken gebruik van een ‘MRU-mechanisme’. MRU staat voor ‘Most Recently Used’. Dit houdt in dat een bestand in de cache wordt opgenomen, wanneer een bestand net gelezen of geschreven is.
De theorie hierachter is dat bestanden die net zijn weggeschreven of opgevraagd een grotere kans hebben in een korte tijd opnieuw te worden opgevraagd. Dit is in veel gevallen ook zo. Echter, het nadeel is wanneer je bijvoorbeeld een bestand van 2 GB inleest, dat hiervoor 2 GB aan andere data uit de cache wordt gehaald, zelfs als deze data constant wordt opgevraagd. Deze data moet dus vervolgens opnieuw van disk gelezen worden.
Om het probleem te illustreren, heb ik een simpel testje opgezet. We maken een kopie van bestand en lezen daarna de kopie uit. Je ziet dat het bestand direct uit de cache wordt gehaald, zonder dat het ooit eerder is gelezen. De disk waar deze file van wordt gelezen, kan ongeveer 450 MB per seconde lezen en niet 4.5 GB/s. Het is duidelijk dat deze file uit RAM wordt geserveerd. Lekker snel hè!
Nu ga ik je laten zien wat er gebeurt wanneer ik een nieuw bestand schrijf, in dit geval een kopie van het bestand dat we eerder hebben gelezen. Vervolgens gaan we het originele bestand inlezen.
Huh? Heeft het wegschrijven van een bestand nou een ander bestand uit de cache gegooid? Jazeker! In veel gevallen is dit gedrag niet gewenst. Onze read cache zit daarom anders in elkaar. Laten we nu nog een keer hetzelfde doen, maar dan met een veel beter cache-algoritme:
Zoals je kunt zien, wordt het bestand dat net is weggeschreven niet in de cache opgenomen, en het oorspronkelijke bestand staat er nog wel in. Stukken beter.
Alleen het niet cachen van bestanden die net weggeschreven zijn, levert voor veel workloads al een behoorlijke optimalisatie op. We gaan echter verder dan dit. Voor het cachen van reads gebruiken we een automatisch optimaliserend algoritme dat balanceert tussen ‘Most Recently Used’ (MRU) en ‘Most Frequently Used’ (MFU). In de MFU-cache komen de blocks die vaak worden opgevraagd: hoe vaker ze zijn benaderd, hoe langer ze in de cache blijven. In de MRU-cache komen blocks die net opgevraagd of weggeschreven zijn. Door een intelligent mechanisme wordt altijd de juiste balans gevonden tussen de hoeveelheid geheugen die aan de MFU en de MRU wordt toegewezen.
Door het gebruik van dit caching algoritme halen we meer dan 95% van de opgevraagde data uit RAM, dit is veel meer dan de traditionele methode. Het cachen vanuit RAM is slechts stap één. We gaan ook hier nog een stap verder. Data die wel af en toe wordt opgevraagd, maar niet frequent genoeg om in RAM te blijven, komen van SSD, de level 2 cache.
Level 2
Zoals ik vertelde, hebben we naast het cachen in RAM een extra niveau om het opvragen van data nog sneller te maken: caching op SSD. Een SSD is trager dan RAM, maar wel zo’n 100 keer sneller dan een harde schijf. Voor het bepalen wat er op SSD gezet moet worden, loopt er op de server elke seconde een proces dat kijkt wat voor data bijna uit de level 1 cache valt. Dit wordt vervolgens ingelezen en naar SSD weggeschreven. In het RAM van de server wordt vervolgens opgeslagen dat dit specifieke stukje data niet alleen op harde schijf, maar ook op SSD staat. Het hoeft dus niet van de (tragere) schijf gelezen te worden. Hoe deze caches samenwerken, heb ik hieronder schematisch weergegeven
Write caching
Naast het versnellen van reads, kunnen we writes ook erg efficiënt cachen. Wanneer je een bestand wegschrijft, dan hoeft je applicatie niet te wachten totdat het op harde schijf is weggeschreven. Data die nog naar schijf geschreven moeten worden, heet ‘dirty data’. Om goed te kunnen begrijpen hoe we writes cachen, is het belangrijk om te weten dat dirty data onder te verdelen is in twee groepen: ‘sync’ (synchroon) en ‘async’ (asynchroon).
Bij ‘async data’ moet je denken aan bestanden waarbij het geen grote ramp is wanneer deze verloren gaan als een systeem onverwacht uitvalt. Een paar voorbeelden van async data zijn:
- tijdelijke bestanden van MySQL (e.g. voor een grote ‘sort’ op disk);
- backups;
- logfiles.
Sync dirty data mag daarentegen absoluut niet verloren gaan. Wanneer dit wel gebeurt, verlies je informatie die bijvoorbeeld belangrijk is voor het goed functioneren van je applicatie. Een paar voorbeelden van sync dirty data:
- e-mail files;
- MySQL tables;
- InnoDB transactie logs.
Alle writes gegenereerd op ons platform worden tijdelijk in het RAM opgeslagen. Vanuit het RAM worden ze eens per periode (enkele seconden) naar schijf geschreven. We slaan alle sync writes niet alleen op in het RAM maar ook op schrijf-geoptimaliseerde SSD’s, omdat de inhoud van het RAM verloren gaat wanneer een server onverwacht uitvalt. Wanneer een systeem na een onverwachte crash weer opstart, wordt deze data van SSD gelezen, gecontroleerd op correctheid en naar de harde schijven geschreven. Deze SSD’s hebben we dubbel uitgevoerd, voor de uitzonderlijke situatie dat een van de SSD’s defect raakt tijdens een crash.
Tot slot
We zijn aangekomen bij het einde van deze rondleiding achter de schermen bij Antagonist. Hopelijk heb je niet een al te grote informatie overload en kan je cache het verwerken 😉
Zoals je hebt kunnen zien, passen we verschillende cachingmechanismes en -algoritmes toe. Elk heeft z’n eigen specifieke toepassing om de meeste performance aan jouw website te geven. Een belangrijk aspect bij het toepassen van caching, is het kunnen garanderen van dataconsistentie.
Hierover kan ik nog véél meer vertellen. Dataconsistentie heeft namelijk één van de hoogste prioriteiten binnen Antagonist. Echter, in dit artikel wilde ik ons unieke en supersnelle caching-systeem nader toelichten. Wie weet in een toekomstig artikel!
Handig om te weten: alle pakketten die wij aanbieden, beschikken over de technieken die in dit artikel zijn beschreven. Wil jij ook zijn verzekerd van maximale performance? Neem dan gerust een kijkje in onze winkel!
P.S. Wil je op de hoogte blijven van alle artikelen, updates, tips en trucs die verschijnen op ons blog? Dat kan! Rechts bovenin via RSS, e-mail, het liken op Facebook, het +1’en op Google+ of het volgen op Twitter.