Git op elke schaal
Twintig jaar later is Git een industriestandaard geworden, maar de waarheid is dat de gedistribueerde aard ervan vaker een belemmering is dan een voordeel. Het gemiddelde open-source softwareproject werkt niet met een gedecentraliseerde workflow. Het gemiddelde bedrijf zeker niet. Ze maken gebruik van de vele voordelen van het gedistribueerde model (zoals offline kunnen werken, pushes uitstellen, etc.), maar ze leunen zwaar op een centrale host. En het blijkt dat het hosten van een Git-repository een ongelooflijk lastige opgave is.
Wat maakt Git zo lastig?
De uitdaging bij het hosten van Git-repositories op grote schaal zit inherent in het ontwerp van Git zelf: een gedistribueerd versiebeheersysteem betekent dat alle instanties van een repository identiek zijn. Er is niets speciaals aan de repository op een Git-server dat niet ook geldt voor een repository op de laptop van een ontwikkelaar. Hoewel het op het eerste gezicht lijkt alsof dit het hosten van Git-repositories eenvoudig maakt (zet simpelweg een HTTP-daemon voor een kopie van een repository op schijf en je hebt een Git-server!), zijn er veel complexe uitdagingen op het gebied van schaalbaarheid en betrouwbaarheid die het tegendeel bewijzen.
In een normale Git-repository worden je code en metadata (bestanden, commits, trees) gecomprimeerd en opgeslagen in packfiles — een simpel binair serialisatieformaat dat handig is voor gebruik op een lokale machine, maar niet ideaal om op schaal op een server te beheren. Packfiles zijn de fundamentele bouwsteen van Git-opslag en Git-netwerking. Wanneer je gegevens pusht of ophaalt (fetch) uit een repository, worden deze overgedragen als een packfile.
Dit is hoe Git is ontworpen, maar het zou fair zijn om te denken dat het niet per se zo hoeft te zijn. Je hebt immers geen controle over de Git-client (althans, niet zonder je gebruikers te irriteren en veel wrijving te veroorzaken), maar binnen de muren van je eigen server kun je doen wat je wilt. Niets verplicht je om packfiles te gebruiken — Linus komt niet langs om dat te controleren. De enige beperking is dat je packfiles over het netwerk moet ontvangen en verzenden voor alle Git-operaties.
Door de jaren heen merkten bedrijven die Git-repositories op grote schaal probeerden te hosten dat dit ontwerp op basis van packfiles een grote beperking vormde voor zowel de beschikbaarheid als de schaalbaarheid. Packfiles zijn grote binaire bestanden die op een bestandssysteem moeten bestaan zodat Git erbij kan. De simpele aanpak van een HTTP-server voor een repository op schijf heeft een zeer laag plafond. Ideaal gezien zou je willen dat de repository op veel schijven en veel machines staat (dit stelt je in staat om veel Git-operaties parallel uit te voeren en houdt je repository beschikbaar als een server crasht). Maar hoe doe je dat?
Er zijn grofweg drie mogelijke benaderingen om dit te bereiken, in volgorde van toenemende complexiteit: het bestandssysteem distribueren, de packfiles distribueren, of Git zelf distribueren.
Git zonder packfiles
Git is een content-addressable datastore. Alle objecten in een Git-repository (blobs, trees, commits, etc.) worden geïndexeerd via de SHA-1 van hun inhoud. Dit is iets dat intuïtief zeer goed mapst naar een gedistribueerde key-value store (de sleutel is de SHA-1; de waarde is het eigenlijke object), en zou een schone manier kunnen bieden om de opslag van een repository op te schalen. Maar dit werkt in de praktijk niet.
Het probleem is als volgt: de feitelijke lay-out van een Git-repository is een directed acyclic graph (DAG). Je kunt elk object opzoeken via zijn SHA, maar om zelfs de meest triviale operatie in de repo uit te voeren, moet je de DAG stap voor stap doorlopen.
Als je bijvoorbeeld een operatie wilt uitvoeren zoals het opsommen van recente wijzigingen in een repository, moet je de commits verwerken. Wanneer je een commit verwerkt, krijg je een verwijzing naar de wortel van de bijbehorende tree. Vanuit die tree krijg je verwijzingen naar elk bestand en elke sub-tree. Van de oorspronkelijke commit krijg je een verwijzing naar de ouder (de commit die eraan voorafging in de geschiedenis). Cruciaal is dat je bij elke stap van deze wandeling de waarde van de volgende verwijzing niet weet totdat je de vorige hebt opgehaald. Als elke ophaalactie een round trip naar een gedistribueerde store vereist, worden zaken zeer snel zeer kostbaar.
Deze aanpak om Git op objectniveau te distribueren is vaker geprobeerd, maar faalt vaak op schaal. De meest veelbelovende implementatie werd geprobeerd door Shawn Pearce bij Google. Zijn aanpak was om de objecten op te slaan in een gedistribueerde hash-tabel (DHT). Dit was alleen mogelijk dank aan JGit, een aangepaste Git-implementatie in Java. Hoewel het systeem werkte en de resultaten voldoende waren voor normale Git-operaties, zorgden de beperkingen van het Git-protocol (die opnieuw vereisen dat packfiles over het netwerk worden verzonden, ongeacht hoe je de data op de server opslaat) ervoor dat de prestaties van git clone zo slecht waren dat het ontwerp volledig werd verworpen.
GitHub en bestandssystemen
Enkele jaren nadat Git uit zijn Linux-kernel-bubbel was ontsnapt, werd in San Francisco GitHub opgericht. GitHub begon in 2008 als een sociaal codeerplatform met de profetische tagline: "Git repository hosting: no longer a pain in the ass." Er was in 2008 een brede consensus dat er, ondanks (of juist vanwege) het gedistribueerde ontwerp van Git, een gecentraliseerde manier nodig was om Git-repositories te hosten om ze gebruiksvriendelijk te maken, en dat dit proces zeer pijnlijk was.
Het platform begon als (en is grotendeels nog steeds) een Rails-monoliet. De allereerste versies draaiden op één krachtige machine, met een Ruby-server en kopieën van de repositories op de schijf ernaast. Het schalen van een Rails-app is eenvoudig: implementeer meer instanties ervan. Maar in dit specifieke geval, omdat Git in het spel is, stuitten ze op de terugkerende vraag: als de Rails-app toegang moet hebben tot de Git-repositories op schijf, hoe implementeer je dan meer kopieën daarvan?
De vroege systeemengineers bij GitHub probeerden de simpelste aanpak: het distribueren van het bestandssysteem (in plaats van de packfiles of Git zelf). Het idee was dat ze de Rails-app ongewijzigd konden laten en zich konden concentreren op het leveren van nieuwe functies. Dit werkte niet.
Het team probeerde vele benaderingen voor een gedistribueerd bestandssysteem voor Git-data. Het gebruik van NFS om alle repositories op een centrale server op te slaan, werd snel verworpen. De standaardimplementatie van Git maakt veel aannames over bestandssysteem-semantiek (locking, tearing, reading, syncing...) die zorgen voor goede prestaties op een lokaal bestandssysteem van een trage laptop, maar geen rekening houden met hoe deze zich gedragen over een netwerkbestandssysteem. Het was traag en foutgevoelig.
Verdere pogingen werden gedaan met technologieën die het bestandssysteem op blokniveau repliceerden, zoals GFS en later DRBD. Ze liepen allemaal tegen een muur aan. Ze waren dagelijks verschrikkelijk om te beheren en boden geen goede prestaties. Alles komt neer op het ontwerp van packfiles op schijf.
Er is geen correlatie tussen de lay-out van objecten in de DAG en de manier waarop ze in een packfile zijn geplaatst. De belangrijkste heuristiek bij het genereren van packfiles is het minimaliseren van de grootte; objecten worden willekeurig in het pack geplaatst, ze worden gecomprimeerd, en cruciaal: ze worden zelden volledig opgeslagen. De meeste objecten worden opgeslagen als een delta bovenop een ander object in hetzelfde packfile. Het lezen van een individueel object, na het volgen van de vele logische sprongen in de grafiekstructuur, houdt dus ook fysieke sprongen in het op-schijf-formaat in.
Dit soort willekeurige wandelingen over gigabytes aan data, die voor elke Git-operatie moeten plaatsvinden, werkt simpelweg niet goed met een netwerkbestandssysteem. De enige manier waarop dit werkt zonder tot stilstand te komen, is als je het hele bestand lokaal kunt cachen. Maar met honderdduizenden repositories in hetzelfde bestandssysteem is caching geen optie.
Uiteindelijk gaven de engineers bij GitHub het op met het distribueren van het bestandssysteem. Ze ontwikkelden een RPC-systeem zodat repositories op toegewezen bestandsservers konden staan, en pasten de Rails-app aan om alle operaties op afstand uit te voeren. Dit zorgde voor horizontale schaalbaarheid, maar loste de beschikbaarheid en de prestaties voor de drukste repositories niet op, aangezien elke repository nog steeds op slechts één machine stond.
Spokes en consistentie
Spokes werd rond 2013 bij GitHub ontwikkeld en is sindsdien een industriestandaard geworden. De meeste Git-hostingservices gebruiken een variant van de Spokes-benadering (replicatie op applicatieniveau voor Git-repositories). Spokes maakte drie fundamentele keuzes die optimaal bleken:
- Het distribueert niet Git zelf, maar werkt op het niveau van de packfile.
- Het slaat alle data op als feitelijke Git-repositories op lokale NVMe-schijven.
- Het repliceert de Git-data, maar houdt alle kopieën consistent synchroon.
Vanwege de willekeurige leespatronen over packfiles is het opslaan van normale Git-repositories op NVMe-schijven essentieel om basisoperaties snel te houden. Bovendien blijven clones efficiënt omdat de data niet getransformeerd hoeft te worden naar wat de Git-client verwacht.
Het consistent synchroon houden van alle kopieën is cruciaal. De Git-client gaat namelijk zeer slecht om met eventual consistency. Als een lokale Git-client een commit pusht en deze vervolgens direct na een fetch niet kan lezen, is dat problematisch. Als een CI-pipeline over honderd runners draait en drie daarvan de commit die ze moeten testen niet vinden na het clonen, is dat een slechte gebruikerservaring.
Spokes is een consensus-gebaseerd gedistribueerd systeem. Het slaat meerdere kopieën van een Git-repository op verschillende servers op. Wanneer er nieuwe data wordt gepusht, verspreidt een orchestrator deze push naar elke instantie. Deze "fan-out" wordt gesynchroniseerd met een klassiek consensus-algoritme genaamd 3PC (three-phase commit), zodat een push alleen wordt geaccepteerd als een meerderheid van de nodes deze bevestigt.
3PC is een algoritme dat ervoor zorgt dat alle nodes in een systeem het eens zijn over het committen of terugdraaien van een transactie via drie round-trips. Het lijkt op een Two-phase commit, maar introduceert een extra "pre-commit"-fase zodat het systeem kan herstellen als de coördinator offline gaat tijdens een transactie.
Een Git-push bestaat uit twee componenten: een packfile en een referentietransactie. De packfile bevat de objecten (blobs, trees, commits). De transactie publiceert de wijzigingen door één of meer referenties (bijv. de branch) bij te werken naar de nieuwe commits. Omdat een gepushte commit niet zichtbaar is totdat de referentie is bijgewerkt, kan Spokes de packfiles gelijktijdig naar alle hosts sturen en vervolgens 3PC gebruiken voor de referentietransactie, die veel kleiner en sneller te synchroniseren is.
Hoewel Spokes al 13 jaar goed werkt, zijn er in 2026 beperkingen zichtbaar. Een kritiek punt is de beperkte horizontale schaalbaarheid van 3PC. Voorheen waren drie replica's per repository optimaal, maar voor moderne enterprise-monorepos is dit onvoldoende om het verkeer (vooral van CI) te verwerken. Bij het toevoegen van meer replica's verslechtert de push-throughput, omdat de latentie van elke stap wordt bepaald door de traagste server in het cluster.
Aan de andere kant worstelt Spokes met enorme aantallen kleine repositories. Het vereist nog steeds drie replica's voor elke repository, zelfs voor die welke nauwelijks worden aangeraakt, wat leidt tot inefficiënt ruimtegebruik. Bovendien is Spokes operationeel zwaar; repositories moeten als "huisdieren" (pets) worden behandeld in plaats van als "vee" (cattle). Je moet precies weten waar elke repository staat, wat een afhankelijkheid van een externe database voor routingsstabellen creëert. Corruptie op schijf is fataal: als twee van de drie kopieën corrupt zijn, kan het systeem geen pushes meer accepteren omdat er geen quorum is.
Continuity
Continuity is het Git-opslagsysteem dat we bij Cursor hebben ontwikkeld. Het doel is om te leren van wat Spokes goed deed en de bekende problemen op te lossen.
Continuity is een simpel systeem. De kern is een write-ahead log (WAL), die we opslaan in S3-compatibele object storage. In productie draaien we direct op S3, maar het ontwerp is cloud-agnostisch.
Wanneer een repository een push ontvangt, slaan we deze op als een WAL-entry in S3. Een push wordt pas bevestigd als deze volledig is gepersisteerd. De push is pas zichtbaar zodra we de referentietransactie succesvol voorbereiden op een lokale kopie van de repository en een verwijzing naar de WAL-entry vastleggen in het WAL-indexbestand. Dit dwingt alle pushes om lineariseerbaar te zijn.
Om de throughput te maximaliseren, gebruiken we batching om het aantal S3-schrijfacties te beperken. Omdat we de referentietransactie alleen hoeven te synchroniseren met één lokale repository in plaats van een quorum van replica's, kan het systeem pushes verwerken zo snel als de schijf toelaat. De lokale kopie is een normale Git-repository op een snelle NVMe-schijf, waardoor we kunnen profiteren van alle bestaande open-source optimalisaties van Git.
Consensus
In tegenstelling tot Spokes hoeft Continuity niet bij te houden waar elke repository zich bevindt. We behandelen repositories op schijf als een warm cache, maar de source of truth is altijd de WAL in S3. Het systeem is stateless en heeft geen routingsstabellen of relationele databases nodig. Als een repository ontbreekt op de lokale schijf van een host, materialiseren we deze simpelweg opnieuw vanuit de WAL. We gebruiken rendezvous hashing om een repository-ID te mappen aan de nodes waar we verwachten dat deze staat, maar als dit uit sync raakt, is dat geen probleem; de repository wordt dan gewoon op de volgende node gematerialiseerd.
Er is geen complexe consensus of verkiezing van een primaire server nodig. Elke server kan de primaire zijn. Alle updates aan de WAL worden gesynchroniseerd met een atomaire compare-and-swap (CAS) operatie op S3. In de praktijk kiezen we via rendezvous hashing meestal dezelfde server als primaire om CAS-retries te minimaliseren, maar het systeem blijft correct werken bij failovers of netwerkproblemen.
Replicatie
De WAL in S3 maakt vrijwel onbeperkte replicatie mogelijk. We gebruiken optimistische replicatie door UDP-gossip-pakketten door het cluster te sturen. Deze pakketten bevatten de metadata die nodig is voor replica's om direct bij S3 bij te werken.
Hoewel UDP onbetrouwbaar is, maakt dat niet uit. Elke replica kent de ETag van de laatste versie van de WAL-index. Bij een leesoperatie doen we een conditional GET naar S3 met de verwachte ETag. Een 304-respons betekent dat we up-to-date zijn en de fetch of clone direct kunnen serveren. Een 200-respons levert de nieuwste WAL-index op, waarna de replica bijwerkt voordat de read wordt geserveerd.
Dit betekent dat alle reads op alle replica's volledig consistent zijn, omdat ze worden geverifieerd tegen de source of truth (S3). Dit biedt enorme voordelen:
- Een grote monorepo kan over honderden replica's worden verspreid om CI-load op te vangen.
- Miljoenen kleine repositories kunnen met één replica per stuk worden bediend, omdat S3 de beschikbaarheid garandeert.
- Inactieve repositories kunnen van de schijf worden verwijderd (garbage collection) en later opnieuw worden gematerialiseerd uit de WAL.
Compactie
WAL-systemen vereisen periodieke compactie om te voorkomen dat het log ongebreideld groeit. Ook normale Git-repositories hebben dit nodig: elke push of fetch creëert een nieuwe packfile. Als er te veel packfiles zijn, moet Git voor elk object door alle indexen lopen, wat de prestaties degradeert.
In systemen zoals Spokes is repacking een probleem voor de beschikbaarheid, omdat het CPU-intensief is en op alle replica's moet gebeuren. In Continuity amortiseren we deze kosten. Alleen de primaire server voert compacties uit. Het resultaat wordt toegepast op zowel de on-disk repository als de WAL. Replica's hoeven niet zelf te repacken; ze downloaden simpelweg de reeds gecompacteerde packs vanuit S3, waarbij bandbreedte wordt geruild voor CPU.
Schaalbaarheid
Replicatie en compactie bepalen hoe een Git-opslagsysteem zich onder belasting gedraagt. Het WAL-first ontwerp van Continuity biedt volledig consistente horizontale schaalbaarheid: de throughput voor read-only operaties groeit lineair met het aantal replica's.
In synthetische stresstests met tot 100 replica's zagen we een consistente lineaire schaling voor reads, zonder regressies in de push-throughput. De push-throughput hangt af van de latentie van S3. Met S3 Standard kunnen we tot 120 pushes/s aan. Met S3 Express One Zone, dat een veel lagere latentie heeft voor PUT-operaties, kunnen we meer dan 300 pushes/s verwerken, waarbij de bottleneck verschuift naar de snelheid waarmee Git data op schijf kan compacteren.
WAL als waarheid
S3 is een krachtig bouwsteen voor grote datasystemen. Hoewel andere systemen (zoals Azure DevOps) packfiles als blobs opslaan, gebruiken zij vaak een relationele database voor referenties. Wij geloven dat de consistentie van Git-data belangrijker is dan elke andere overweging, wat ons leidde naar een WAL-gebaseerd systeem zonder externe databases.
Een WAL-model biedt volledige provenance data voor alle pushes en repacks. We kunnen elke replica terugspoelen of vooruitspoelen. Wanneer we een bug in Git tegenkomen, kunnen we exact pinpointen wat er is gebeurd en dit ongedaan maken. Omdat alle Git-operaties worden uitgevoerd op een normale Git-repository op schijf met standaard tooling, introduceren we zelf nauwelijks nieuwe bugs.
Origin
Het hosten van iemands broncode is van cruciaal belang. Een bedrijf kan volledig tot stilstand komen als ontwikkelaars niet kunnen pushen of pullen. De productiviteitskosten van vijf minuten downtime in een CI-systeem zijn enorm.
Agents hebben de manier waarop we met software werken fundamenteel veranderd, wat de druk op versiebeheer heeft vergroot: meer code, meer PR's, meer CI-runs. We hebben deze uitdagingen intern bij Cursor ervaren en een platform gebouwd dat deze problemen oplost.
Origin is geen experiment; het is het resultaat van decennia aan ervaring met het bouwen van dergelijke systemen. We streven naar maximale betrouwbaarheid, prestaties en schaalbaarheid, en we zetten ons in om dit platform te laten evolueren samen met het landschap van versiebeheer.
Groetjes,