SBD: We hebben een eigen bestandssysteem

Ik heb onaardige dingen te zeggen over mensen die hun eigen pasta maken. Hier zal ik hen alleen gebruiken als voorbeeld van hoe je niet moet denken wanneer je infrastructuur bouwt. Het is onredelijk makkelijk om te geloven dat iets beter wordt naarmate je er harder aan werkt. Maar infrastructuur gaat over de zak spaghetti van één dollar: een simpel ding dat goed werkt (en waarbij je niet een week lang meel uit de hoekjes van je keuken hoeft te schrapen).

Toen het werk aan Sprites begon, was het een nieuw soort infrastructuur voor een nieuw soort product. Omdat we het juiste bestandssysteem niet konden vinden in het "pastagesprek", hebben we er zelf een gemaakt. Het was slim, en dat is precies waarom we er niet van hielden. Dit bericht introduceert de vervanger: het Sprites Block Device (SBD). Het is de zak spaghetti die je zo uit het schap pakt, en het enige verschil dat je zult merken is een betere betrouwbaarheid.

Altijd al object storage

Onze visie op één kernaspect van de opslag van Sprites is sinds versie 1 niet veranderd: de root van de schijf van een Sprite is object storage, niet de NVMe in welke server de Sprite toevallig ook draait. Dat is wat een Sprite in staat stelt om in slaap te vallen, wakker te worden op een andere server en zijn bestanden nog steeds te vinden waar hij ze heeft achtergelaten. De duurzame status van een Sprite is, grosso modo, een locatie in een bucket.

Het probleem met object stores is dat ze geen deel van een object kunnen wijzigen. Om één byte te veranderen, moet je het hele object vervangen. Dat past slecht bij Linux-programma's, die constant een paar bytes in het midden van een bestand herschrijven. Iets moet die vertaling verzorgen.

Bij de lancering was dat "iets" een zwaar aangepaste versie van JuiceFS. In de standaardvorm splitst JuiceFS opslag in data (als chunks in de bucket) en metadata (een kaart van welke chunks welke bestanden vormen) in een database. Dat is echter niet hoe wij het gebruikten. Onze aangepaste versie draaide op een herschreven SQLite-metadata-backend, die duurzaam werd gehouden met Litestream. Het bewaarde precies één bestand: een disk image van 100 GB, gemount op een loop device met ext4 erin. Linux deed alle normale ext4-zaken; JuiceFS hield simpelweg de image vast.

Het werkte prima. Sprites werden hiermee uitgebracht, mensen maakten er veel van en checkpoints waren snel omdat checkpointing voornamelijk bestond uit het verschuiven van metadata. Maar naarmate het gebruik groeide, ontstonden er scheuren.

Manifesten die liegen

Een bestandssysteem met een metadatadatabase heeft twee onscheidbare componenten: de chunks in de bucket en de database die uitlegt hoe die chunks in elkaar passen. Wanneer ze overeenstemmen, heb je bestanden. Wanneer ze dat niet doen, heb je een probleem.

Elke laag die we daarop bouwden, moest die structuur overnemen en aanvullen. Een checkpoint onder JuiceFS was een copy-on-write kloon van de disk image, waardoor de metadatadatabase de chunks voor elk checkpoint én voor de actieve schijf bijhield. Een tweede SQLite-database, checkpoints.db, lijstte de checkpoints zelf op en welk image-bestand op het loop device gemount moest worden voor elk checkpoint.

Een Sprite-schijf bestond dus uit chunks, plus een metadatadatabase die aangaf welke chunks elk image vormden, plus een checkpoint-database die aangaf welk image welk checkpoint was. Elk van deze onderdelen werd afzonderlijk duurzaam gemaakt, wat betekende dat één onderdeel correct kon zijn terwijl de andere dat niet waren.

Wat kon er misgaan?

Een verhaal dat wel of niet gebaseerd is op praktijkervaring: op een dag stopt een Sprite met booten. JuiceFS vraagt object storage om een chunk en krijgt een 404-foutmelding. Omdat we snelheid wilden, hadden we JuiceFS in writeback-modus gedraaid. Hierbij wordt een chunk eerst in de metadata geregistreerd en pas daarna worden de bytes geüpload. Als de machine op enig moment tussen deze twee stappen wordt beëindigd, wijst de metadata naar een chunk die niet bestaat.

Het resultaat is dat het ext4-bestandssysteem dat daarop draait een directory-block vindt die de checksum-controle niet haalt en overschakelt naar read-only. Het verplaatsen van de Sprite naar een nieuwe machine helpt niet, en het herstellen van een checkpoint ook niet, omdat JuiceFS-checkpoints klonen waren. Ze deelden dezelfde chunks, dus elk checkpoint had hetzelfde gat.

De ultieme test

Het waren niet zozeer de technische details van dit soort problemen, maar hun sluipende aard die leidde tot de meest intense periode van dogfooding in de geschiedenis van Fly.io. En als je computers voor agents gaat testen, is het beter als de agents het testen doen.

We bouwden een testopstelling die vloten van Sprites herhaaldelijk door cycli van schrijven, checkpointen, slapen, wakker worden en herstellen leidde, waarbij elke byte die terugkwam werd geverifieerd. We zetten dit in op 400 JuiceFS-Sprites voor ongeveer 5.000 cycli. Een niet-verwaarloosbaar aantal van hen verloor permanent het vermogen om checkpoints te maken.

Een tijdlang bleven we onze methode bevragen. Er kwam meer reconciliatie, betere reparatietools en steeds slimmere manieren om drie verschillende registers in overeenstemming te houden. Maar spaghetti die wordt geleverd met een reparatieset laat altijd een vreemde smaak in je mond achter, hoe je het ook kookt.

Gewoon een schijf

Hier komt het Sprites Block Device (SBD). SBD neemt een bucket en presenteert deze aan de Linux-kernel als een block device via NBD. De Sprite formateert dit apparaat als ext4 en mount het, op precies dezelfde manier als je een schijf op elke willekeurige Linux-box zou mounten.

Onder SBD leeft de metadata in de bucket, samen met de blocks die het beschrijft. Er is dus geen aparte database die synchroon gehouden moet worden. Een daemon binnen de Sprite, sbd serve, beantwoordt de block-reads en writes van de kernel vanuit zijn eigen lokale cache, uploadt "dirty blocks" op de achtergrond naar de object storage en voert een flush uit op aanvraag wanneer er een garantie nodig is dat de bytes daadwerkelijk zijn verzonden.

Ext4 doet nog steeds het zware werk. We hebben alleen alles daaronder veel eenvoudiger gemaakt: één daemon en één bucket, waar voorheen een bestandssysteem, een database en een disk image in overeenstemming gehouden moesten worden.

Simpelere checkpoints zijn betere checkpoints

Checkpoints zijn van groot belang voor infrastructuur die is ontworpen als een veilige omgeving voor agents. Onder JuiceFS was een checkpoint een copy-on-write kloon van de disk image, geregistreerd in checkpoints.db. Onder SBD is een checkpoint een block-snapshot van het hele volume, met een versienummer, geregistreerd in de eigen checkpoint-catalogus van SBD. Het is het ultieme comfort van één enkele bron van waarheid.

Herstel (restore) is even eenvoudig. Elk checkpoint weet tot welke versie van het volume het behoort. Een restore unmount het bestandssysteem, spoelt het volume ter plaatse terug en mount het opnieuw. Dit kost ongeveer net zoveel tijd als een herstart.

Natuurlijk kunnen er nog steeds dingen misgaan. Het verschil is dat een Sprite nu stopt en dit meldt, in plaats van door te gaan met een schijf waarvoor hij niet kan voucheren. SBD controleert elke block die hij terugleest uit de object storage tegen wat hij heeft weggeschreven. Als SBD zelf crasht, start de hele Sprite opnieuw, omdat een schone herstart de enige manier is om terug te keren naar een schijf die je kunt vertrouwen.

De concluste

Door voortdurende intensieve tests, een bijna misbruikelijke testopstelling en input van enkele pionierende designpartners, is gebleken dat SBD orders van grootte betrouwbaarder is dan ons zelfgemaakte "pasta-bestandssysteem".

SBD zal daarom voortaan het bestandssysteem voor Sprites zijn. Ext4, op een block device, op een bucket. Het enige dat je bestanden bijhoudt, is de bestandssysteemcode die elke Linux-box al draait.