Het artikel analyseert de architectuur van Atproto vanuit het perspectief van distributed backend engineering. Het beschrijft eerst de evolutie van traditionele webbackends: van sterke consistentie in SQL-databases naar 'eventual consistency' via NoSQL, eventlogs (zoals Kafka) en View-servers.
Om dit model te decentraliseren, maakt Atproto gebruik van de volgende componenten:
- User Data Repositories: Een uniform datamodel waarbij elke gebruiker een eigen repository heeft met collecties van cryptografisch ondertekende JSON-records.
- Decentrale Services: Interne backend-services worden omgezet in publieke API's die door iedereen gehost en geconsumeerd kunnen worden.
- Appviews: Een combinatie van een appserver en View-server die data verzamelt uit het netwerk.
De datastroom is circulair: schrijfacties in de repositories worden via eventlogs verspreid naar de View-servers, waardoor een open applicatienetwerk ontstaat. Door standaardpraktijken voor high-scale systemen te combineren met p2p-technologie, streeft Atproto naar een systeem dat zowel schaalbaar en open is als gebruiksvriendelijk.
Atproto voor engineers van gedistribueerde systemen
Het schalen van de traditionele webbackend
De klassieke, ideale webarchitectuur bestaat uit "één grote SQL-database" achter onze appserver. De app communiceert met de database en handelt verzoeken van de frontend af.
Naarmate onze applicatie groeit, lopen we tegen prestatielimieten aan, dus voegen we caches toe aan de stack. Vervolgens schalen we onze database horizontaal door middel van sharding en replica's.
Dit werkt redelijk goed, maar we bouwen een sociaal netwerk met honderden miljoenen gebruikers; zelfs dit model stoot op grenzen. Het probleem is dat onze SQL-database "sterk consistent" (strongly consistent) is, wat betekent dat de status uniform in sync wordt gehouden over het hele systeem. Het handhaven van sterke consistentie brengt prestatiekosten met zich mee die uiteindelijk our bottleneck worden.
Als we ons systeem kunnen versoepelen naar "eventuele consistentie" (eventual consistency), kunnen we veel verder schalen. We beginnen door over te stappen op een NoSQL-cluster.
Dit is beter voor het schalen, maar zonder SQL wordt het moeilijker om onze queries te bouwen. Het blijkt dat SQL-databases veel nuttige functies hebben, zoals JOIN- en aggregatiequeries. In feite is onze NoSQL-database niets meer dan een key-value store. Het ontwikkelen van nieuwe features wordt hierdoor een worsteling.
Om dit op te lossen moeten we programma's schrijven die vooraf berekende weergaven (precomputed views) van onze dataset genereren. Deze weergaven fungeren in feite als gecachte queries. We dupliceren zelfs de canonieke gegevens in deze weergaven om ze zeer snel te maken. We noemen deze servers de View-servers.
Nu merken we dat het lastig is om onze View-servers synchroon te houden met de canonieke gegevens in het NoSQL-cluster. Soms crashen View-servers en missen ze updates. We moeten ervoor zorgen dat onze views betrouwbaar up-to-date blijven.
Om dit op te lossen introduceren we een eventlog (zoals Kafka). Die log registreert en verspreidt alle wijzigingen in het NoSQL-cluster. Onze View-servers luisteren naar — en spelen deze log af — om er zeker van te zijn dat ze nooit een update missen, zelfs niet wanneer ze opnieuw moeten opstarten.
We zijn nu gekomen bij een stream-processing-architectuur. Hoewel er veel meer details besproken kunnen worden, is dit voor nu voldoende.
Het goede nieuws is dat deze architectuur goed schaalt. We hebben sterke consistentie opgegeven en soms lopen onze read-queries achter op de meest actuele versie van de data, maar de service verliest geen schrijfacties (writes) en komt niet in een incorrecte status terecht.
In zekere zin hebben we een database custom-built door hem "binnenstebuiten" te keren. We hebben de canonieke opslag vereenvoudigd tot een NoSQL-cluster en vervolgens onze eigen query-engine gebouwd met de View-servers. Het is minder handig om mee te ontwikkelen, maar het schaalt.
Onze high-scale backend decentraliseren
Het doel van AT Protocol is om applicaties met elkaar te verbinden, zodat hun backends status delen, inclusief gebruikersaccounts en content.
Hoe kunnen we dat doen? Als we naar onze architectuur kijken, zien we dat het grootste deel van het systeem is geïsoleerd van de buitenwereld, waarbij alleen de Appserver een publieke interface biedt. Ons doel is om deze isolatie te doorbreken, zodat anderen kunnen deelnemen aan ons NoSQL-cluster, onze eventlog, onze View-servers, enzovoort.
Dit is hoe het eruit komt te zien: elke interne service wordt nu een externe service. Ze hebben publieke API's die iedereen kan consumeren. Bovendien kan iedereen zijn eigen instanties van deze services creëren.
Ons doel is om ervoor te zorgen dat iedereen kan bijdragen aan deze gedecentraliseerde backend. Dat betekent dat we niet slechts één NoSQL-cluster of één View-server willen, maar vele van deze servers die samenwerken.
Hoe zorgen we ervoor dat al deze services samenwerken?
Het uniformeren van het datamodel
We stellen een gedeeld datamodel vast, genaamd de "user data repository".
- Elke datarepository bevat JSON-documenten, die we "records" noemen.
- Om organisatorische redenen groeperen we deze records in "collections".
Vervolgens passen we onze NoSQL-services aan zodat ze allemaal dit datarepository-model gebruiken. De data-repo-services blijven in feite NoSQL-stores, maar ze zijn nu op een zeer specifieke manier georganiseerd:
- Elke gebruiker heeft een datarepository.
- Elke repository heeft collecties.
- Elke collectie is een geordende K/V store van JSON-documenten.
Omdat de datarepositories door iedereen kunnen worden gehost, moeten we ze voorzien van URL's. Tegelijkertijd maken we een volledig URL-schema voor onze records.
Daarnaast is het, aangezien we deze records over het internet gaan synchroniseren, verstandig om ze cryptografisch te ondertekenen, zodat we weten dat ze authentiek zijn.
De datastroom in kaart brengen
Nu we de high-scale gedecentraliseerde backend hebben opgezet, kijken we hoe een applicatie daadwerkelijk werkt op atproto.
Voor een nieuwe app hebben we twee dingen nodig: een appserver (die onze API en frontend host) en een View-server (die data uit het netwerk voor ons verzamelt). We bundelen deze servers vaak en noemen dit een "Appview".
Het proces verloopt als volgt:
- Een gebruiker logt in op onze app via OAuth. Tijdens dit proces geven ze aan welke server hun datarepository host en geven ze ons toestemming om deze te lezen en te beschrijven.
- We kunnen nu JSON-documenten lezen en schrijven in de repo van de gebruiker. Als zij al data hebben uit andere apps (zoals een profiel), kunnen we die data ook lezen.
Wanneer we een JSON-document schrijven, gebeurt het volgende:
- Het document wordt vastgelegd (committed) in de repository.
- Dit activeert een schrijfactie in de eventlogs die naar de repository luisteren.
- Vanuit daar wordt het event verzonden naar alle View-services die meeluisteren — inclusief de onze.
Waarom luisteren we naar de eventstream als we zelf degene zijn die de write uitvoeren? Omdat we niet de enige zijn die schrijfacties uitvoeren! Er zijn talloze gebruikersrepo's die events genereren en vele apps die erin schrijven.
Er ontstaat dus een circulaire datastroom binnen onze gedecentraliseerde backend: writes worden vastgelegd in de datarepos, vervolgens via de eventlogs uitgezonden naar de View-servers, waar ze door onze applicaties kunnen worden gelezen. De hoop is dat dit netwerk blijft schalen, niet alleen om capaciteit toe te voegen, maar om een grotere variëteit aan applicaties te creëren die delen in dit open applicatienetwerk.
Het bouwen van praktische open systemen
Het AT Protocol voegt p2p-technologie samen met praktijken voor high-scale systemen. De oprichtende engineers waren kernontwikkelaars bij IPFS en Dat, en Martin Kleppmann — de auteur van Designing Data-Intensive Applications — is een actieve technische adviseur.
Voordat Bluesky werd gestart, stelden we de harde eis vast: "geen stappen terug". We wilden dat het netwerk net zo gebruiksvriendelijk en wereldwijd zou aanvoelen als elke sociale app voor hen, terwijl het toch een open netwerk bleef. Dit is waarom de schaallimieten van federatie-modellen en blockchains ons opvielen bij onze analyse. Onze oplossing was om standaardpraktijken voor high-scale backends te nemen en daar technieken uit peer-to-peer systemen aan toe te voegen om een open netwerk te creëren.
Atproto voor engineers van gedistribueerde systemen
Het schalen van de traditionele webbackend
De klassieke, ideale webarchitectuur bestaat uit "één grote SQL-database" achter onze appserver. De app communiceert met de database en handelt verzoeken van de frontend af.
Naarmate onze applicatie groeit, lopen we tegen prestatielimieten aan, dus voegen we caches toe aan de stack. Vervolgens schalen we onze database horizontaal door middel van sharding en replica's.
Dit werkt redelijk goed, maar we bouwen een sociaal netwerk met honderden miljoenen gebruikers; zelfs dit model stoot op grenzen. Het probleem is dat onze SQL-database "sterk consistent" (strongly consistent) is, wat betekent dat de status uniform in sync wordt gehouden over het hele systeem. Het handhaven van sterke consistentie brengt prestatiekosten met zich mee die uiteindelijk our bottleneck worden.
Als we ons systeem kunnen versoepelen naar "eventuele consistentie" (eventual consistency), kunnen we veel verder schalen. We beginnen door over te stappen op een NoSQL-cluster.
Dit is beter voor het schalen, maar zonder SQL wordt het moeilijker om onze queries te bouwen. Het blijkt dat SQL-databases veel nuttige functies hebben, zoals JOIN- en aggregatiequeries. In feite is onze NoSQL-database niets meer dan een key-value store. Het ontwikkelen van nieuwe features wordt hierdoor een worsteling.
Om dit op te lossen moeten we programma's schrijven die vooraf berekende weergaven (precomputed views) van onze dataset genereren. Deze weergaven fungeren in feite als gecachte queries. We dupliceren zelfs de canonieke gegevens in deze weergaven om ze zeer snel te maken. We noemen deze servers de View-servers.
Nu merken we dat het lastig is om onze View-servers synchroon te houden met de canonieke gegevens in het NoSQL-cluster. Soms crashen View-servers en missen ze updates. We moeten ervoor zorgen dat onze views betrouwbaar up-to-date blijven.
Om dit op te lossen introduceren we een eventlog (zoals Kafka). Die log registreert en verspreidt alle wijzigingen in het NoSQL-cluster. Onze View-servers luisteren naar — en spelen deze log af — om er zeker van te zijn dat ze nooit een update missen, zelfs niet wanneer ze opnieuw moeten opstarten.
We zijn nu gekomen bij een stream-processing-architectuur. Hoewel er veel meer details besproken kunnen worden, is dit voor nu voldoende.
Het goede nieuws is dat deze architectuur goed schaalt. We hebben sterke consistentie opgegeven en soms lopen onze read-queries achter op de meest actuele versie van de data, maar de service verliest geen schrijfacties (writes) en komt niet in een incorrecte status terecht.
In zekere zin hebben we een database custom-built door hem "binnenstebuiten" te keren. We hebben de canonieke opslag vereenvoudigd tot een NoSQL-cluster en vervolgens onze eigen query-engine gebouwd met de View-servers. Het is minder handig om mee te ontwikkelen, maar het schaalt.
Onze high-scale backend decentraliseren
Het doel van AT Protocol is om applicaties met elkaar te verbinden, zodat hun backends status delen, inclusief gebruikersaccounts en content.
Hoe kunnen we dat doen? Als we naar onze architectuur kijken, zien we dat het grootste deel van het systeem is geïsoleerd van de buitenwereld, waarbij alleen de Appserver een publieke interface biedt. Ons doel is om deze isolatie te doorbreken, zodat anderen kunnen deelnemen aan ons NoSQL-cluster, onze eventlog, onze View-servers, enzovoort.
Dit is hoe het eruit komt te zien: elke interne service wordt nu een externe service. Ze hebben publieke API's die iedereen kan consumeren. Bovendien kan iedereen zijn eigen instanties van deze services creëren.
Ons doel is om ervoor te zorgen dat iedereen kan bijdragen aan deze gedecentraliseerde backend. Dat betekent dat we niet slechts één NoSQL-cluster of één View-server willen, maar vele van deze servers die samenwerken.
Hoe zorgen we ervoor dat al deze services samenwerken?
Het uniformeren van het datamodel
We stellen een gedeeld datamodel vast, genaamd de "user data repository".
- Elke datarepository bevat JSON-documenten, die we "records" noemen.
- Om organisatorische redenen groeperen we deze records in "collections".
Vervolgens passen we onze NoSQL-services aan zodat ze allemaal dit datarepository-model gebruiken. De data-repo-services blijven in feite NoSQL-stores, maar ze zijn nu op een zeer specifieke manier georganiseerd:
- Elke gebruiker heeft een datarepository.
- Elke repository heeft collecties.
- Elke collectie is een geordende K/V store van JSON-documenten.
Omdat de datarepositories door iedereen kunnen worden gehost, moeten we ze voorzien van URL's. Tegelijkertijd maken we een volledig URL-schema voor onze records.
Daarnaast is het, aangezien we deze records over het internet gaan synchroniseren, verstandig om ze cryptografisch te ondertekenen, zodat we weten dat ze authentiek zijn.
De datastroom in kaart brengen
Nu we de high-scale gedecentraliseerde backend hebben opgezet, kijken we hoe een applicatie daadwerkelijk werkt op atproto.
Voor een nieuwe app hebben we twee dingen nodig: een appserver (die onze API en frontend host) en een View-server (die data uit het netwerk voor ons verzamelt). We bundelen deze servers vaak en noemen dit een "Appview".
Het proces verloopt als volgt:
- Een gebruiker logt in op onze app via OAuth. Tijdens dit proces geven ze aan welke server hun datarepository host en geven ze ons toestemming om deze te lezen en te beschrijven.
- We kunnen nu JSON-documenten lezen en schrijven in de repo van de gebruiker. Als zij al data hebben uit andere apps (zoals een profiel), kunnen we die data ook lezen.
Wanneer we een JSON-document schrijven, gebeurt het volgende:
- Het document wordt vastgelegd (committed) in de repository.
- Dit activeert een schrijfactie in de eventlogs die naar de repository luisteren.
- Vanuit daar wordt het event verzonden naar alle View-services die meeluisteren — inclusief de onze.
Waarom luisteren we naar de eventstream als we zelf degene zijn die de write uitvoeren? Omdat we niet de enige zijn die schrijfacties uitvoeren! Er zijn talloze gebruikersrepo's die events genereren en vele apps die erin schrijven.
Er ontstaat dus een circulaire datastroom binnen onze gedecentraliseerde backend: writes worden vastgelegd in de datarepos, vervolgens via de eventlogs uitgezonden naar de View-servers, waar ze door onze applicaties kunnen worden gelezen. De hoop is dat dit netwerk blijft schalen, niet alleen om capaciteit toe te voegen, maar om een grotere variëteit aan applicaties te creëren die delen in dit open applicatienetwerk.
Het bouwen van praktische open systemen
Het AT Protocol voegt p2p-technologie samen met praktijken voor high-scale systemen. De oprichtende engineers waren kernontwikkelaars bij IPFS en Dat, en Martin Kleppmann — de auteur van Designing Data-Intensive Applications — is een actieve technische adviseur.
Voordat Bluesky werd gestart, stelden we de harde eis vast: "geen stappen terug". We wilden dat het netwerk net zo gebruiksvriendelijk en wereldwijd zou aanvoelen als elke sociale app voor hen, terwijl het toch een open netwerk bleef. Dit is waarom de schaallimieten van federatie-modellen en blockchains ons opvielen bij onze analyse. Onze oplossing was om standaardpraktijken voor high-scale backends te nemen en daar technieken uit peer-to-peer systemen aan toe te voegen om een open netwerk te creëren.