Het schalen van LunaDb, ons inhouse declaratieve gegevenslaadsysteem

Afbeelding bijdrager Team AsanaArvind Vijayakumar
5 maart 2026
15 min. leestijd
facebookx-twitterlinkedin
Gemini said Two female professionals collaborating at a desk in a bright, modern office. One woman sits while the other leans in, smiling and using the computer mouse, with a male colleague working in the background.

Hier bij Asana hebben we een gegevenslaadsysteem gebouwd met de naam LunaDb, dat de ruggengraat van onze webapp vormt. Ondanks de naam is het geen database. Het is eerder een GraphQL-achtig systeem voor het declaratief ophalen van gegevens - in feite een manier om de nieuwste versie van gegevens en alle toekomstige updates te laden.

We lanceerden LunaDb aanvankelijk in 2015 als een radicale herschrijving van onze backend-infrastructuur¹. Het centrale onderdeel van dit nieuwe systeem was de synchronisatieserver, een monoliet die alles uitvoert, van klantsynchronisatie tot het laden van gegevens en toegangsbeheer. Zonder significante wijzigingen schaalde deze initiële architectuur veel verder dan de aanvankelijk verwachte niveaus, helemaal tot miljoenen wekelijkse actieve gebruikers en miljarden dagelijkse zoekopdrachten. 

Hoewel de prestaties sterk bleven, werd het naarmate het verkeer en de complexiteit van de functies toenamen, steeds moeilijker om LunaDb te bedienen en te verbeteren vanwege de beperkingen van de synchronisatieserver.

Overzicht van onze infrastructuur voor het laden van gegevens

overzicht van onze infrastructuur voor het laden van gegevens

Waarom was het moeilijk om te bedienen?

Verkeersverschuiving is duur

De synchronisatieserver beheerde rechtstreeks permanente websocket-verbindingen met klanten. Elke websocket werd ondersteund door een stateful clientsessie. Wanneer een verbinding werd verbroken, werd al deze status weggegooid en zou de klant zich opnieuw abonneren op alle gegevens die er voor hem toe deden. Wanneer dit bij veel sessies gebeurt, wordt het snel duur. We moesten dus voorzichtig zijn bij het verplaatsen van deze verbindingen, gezien de grote piek in werk door opnieuw verbinden.

verplaatsing van verkeer

verplaatsing van verkeer

Het inzetten van synchronisatieservers betekent het verschuiven van verkeer

Natuurlijk kun je het verschuiven van verkeer niet eeuwig voorkomen. Wanneer je nieuwe code wilt pushen of wilt opschalen/verkleinen, moet je synchronisatieservers buiten gebruik stellen - en dat vereist het verplaatsen van al het verkeer van de beëindigende instanties.

doorlopende updates

doorlopende updates

Synchronisatieservers presteren niet goed bij het opstarten

Tegelijkertijd zouden synchronisatieservers pas performant worden na een niet-triviale hoeveelheid procesopwarming. Het beheer van beide problemen was een vrij fragiel evenwicht en vereiste in het verleden uitgebreid engineeringwerk.

Synchronisatieservers zijn complex en moeilijk te monitoren

Ten slotte, omdat synchronisatieservers veel willekeurige stukjes productcode uitvoerden (via aangepaste server-side functies), waren ze erg kwetsbaar voor prestatieregressies op basis van noisy neighbors die moeilijk toe te schrijven waren². 

het probleem van de luidruchtige buren

het probleem van de luidruchtige buren


Een redelijke vraag om te stellen is: "Waarom zijn synchronisatieservers complex en presteren ze niet goed bij het opstarten?"

Een veelvoorkomende oorzaak van deze problemen is onze productcode aan de serverzijde. De belangrijkste stukjes van de synchronisatieservercode zijn geschreven in Scala.  Ondanks enkele complexiteiten met betrekking tot sessiestatusbeheer en de verschillende aspecten van het Luna-raamwerk, gedraagt deze raamwerk-/platformcode zich meestal zoals we verwachten (er zijn relatief weinig operationele en prestatieproblemen). 

Aan de andere kant zijn deze product server-computed-values (we noemen ze SCV's, maar denk aan aangepaste resolvers) geschreven in Typescript. Beide sets code worden samen uitgevoerd in GraalVM, een polyglot-VM die het gebruik van meerdere talen mogelijk maakt via het Truffle-raamwerk. Omdat ze in Typescript zijn geschreven, worden SCV's in wezen geïnterpreteerd bij het opstarten, wat voorspelbaar resulteert in onaanvaardbare prestaties en CPU-gebruik. GraalVM zal proberen just-in-time compilatie uit te voeren op aangeroepen SCV's. Dit is goed! GraalVM/Truffle kunnen hun prestaties enorm optimaliseren, maar dat is niet gratis. SCV-compilatie kan behoorlijk duur zijn (in CPU, codecache, enz.). 

onze polyglot-VM-instelling

onze polyglot-VM-instelling

Waarom de twee talen?

Ons eerste ontwerp voor SCV's was volledig in Scala. Aan de andere kant zijn onze mutatie- en asynchrone taaksystemen geschreven in Javascript/Typescript. Hoewel op Scala gebaseerde SCV's werkten, werd de duplicatie van bedrijfslogica tussen onze mutatie- en asynchrone taaksystemen en LunaDb, samen met de onbekendheid van productingenieurs met Scala, een behoorlijke rem op de productsnelheid. 

Waarom GraalVM?

We cachen veel gegevens in het proces om het berekenen (en opnieuw berekenen) van abonnementsresultaten te versnellen. Het gebruik van GraalVM geeft ons een eenvoudige manier om deze caches in verschillende talen te delen zonder de zorgen over juistheid of prestaties die kunnen ontstaan door het opsplitsen van de Scala- en TypeScript-delen in afzonderlijke containers.


Waarom was het moeilijk om te verbeteren?

Omdat de server zoveel dingen deed en relatief kwetsbaar was om te bedienen, hadden we de neiging om grotere wijzigingen te vermijden. Niet alleen vanwege de complexiteit van de code, maar ook vanwege de hoge overhead van het veilig uitrollen van nieuwe wijzigingen.

Hoe lossen we dingen op?

Ja, ik stel graag vragen

Gezien de moeilijkheden bij het bedienen en verbeteren van de synchronisatieserver, hebben we de moeilijke beslissing genomen om onze architectuur te veranderen. We besloten voornamelijk om de monolithische synchronisatieserver te vervangen door twee kleinere soorten componenten:

  • een sessiebroker die klantverbindingen en statusresolutie beheert

  • een synchroniseerbare lader die verantwoordelijk is voor het laden van gegevens, d.w.z. het uitvoeren van zoekopdrachten van sessiebrokers

sessiebrokers en synchroniseerbare loaders

sessiebrokers en synchroniseerbare loaders

Waarom helpt dit?

Deze nieuwe architectuur scheidt onmiddellijk het verschuiven van websocketverkeer (d.w.z. het inzetten van sessiebrokers) van het opwarmen van nieuwe processen (d.w.z. het inzetten van synchroniseerbare loaders). Als gevolg daarvan kunnen we verstoringen minimaliseren door syncable loaders afzonderlijk van sessiebrokers te implementeren.

Elk van deze nieuwe componenten is eenvoudiger.

  • Sessiebrokers zijn veel lichter, vereisen geen procesopwarming en voeren geen productcode uit. Daarom hoeven we ze niet zo vaak te implementeren, en als we dat doen, is het vrij eenvoudig.

  • Syncable loaders hebben een eenvoudigere interface (stateless abonnementsverzoeken) die gemakkelijker aan te passen is aan standaard horizontale pod-autoscaling van Kubernetes. Dit maakt het ook sneller en eenvoudiger om ze op te warmen - we kunnen gewoon traffic mirroring gebruiken op de ambient requests die tussen sessiebrokers en syncable loaders flowen

De nieuwe architectuur stelt ons in staat om het productontwikkelingsproces aanzienlijk te vereenvoudigen. Vanaf het begin zijn de onafhankelijke implementatieschema's van productcode aan de serverzijde en het aanroepen van productcode aan de clientzijde een rem op de snelheid en een bron van operationeel, repitief werk geweest (vanwege incompatibiliteit van versies). Aangezien synchroniseerbare loaders nu het enige resterende proces zijn dat productcode uitvoert en het implementeren ervan niet langer storend is, kunnen we ze opnieuw implementeren wanneer we nieuwe productcode pushen.

Deze nieuwe architectuur stelt ons in staat om beter te schalen naar nieuwe functies door verschillende pools van synchroniseerbare loaders in te zetten voor verschillende soorten werkbelasting (zoals verschillende afzonderlijke functies zoals Inbox, Taken, Doelen, enz.). De sessiebroker functioneert als een servicegateway die rechtstreeks kan bepalen hoe gegevensquery's naar verschillende upstream syncable loaders worden doorgestuurd.

Wat zijn de belangrijkste ontwerpuitdagingen?

Geweldig! Deze nieuwe architectuur klinkt veel beter, maar hoe hebben we deze werkelijkheid gemaakt? De synchronisatieserver is in feite een monoliet, omdat deze meerdere functies omvat, en het uit elkaar halen van monolieten is bijna altijd lastig. In ons geval moesten we enkele belangrijke ontwerphindernissen overwinnen. 

PubSub opsplitsen

PubSub, ons systeem voor het implementeren van reactiviteit, is ontworpen rond een enkel proces (de synchronisatieserver) dat verantwoordelijk is voor het laden van nieuwe gegevens en het verzenden ervan naar de klant. We moesten PubSub opnieuw ontwerpen op een manier die de juistheid garandeert voor deze twee nu onafhankelijke procestypen (synchroniseerbare loaders en sessiebrokers). 

Laten we kort ingaan op hoe het is geïmplementeerd in synchronisatieservers. Opmerking: Het kan nuttig zijn om ons vorige bericht over de invalidatiepijplijn te lezen, maar we geven een overzicht van het systeem zonder vereisten.

Op de synchronisatieserver houden we abonnementen per sessie bij. We gebruiken de invalidatiepijplijn om abonnementen voortdurend te monitoren op updates. Bij elk nieuw ongeldigverklaringsbericht laadt de synchronisatieserver alle betrokken abonnementen opnieuw. 

Synchronisatieservers cachen in hoge mate db-object-/queryresultaten, aangepaste resolverresultaten en eerdere abonnementsresultaten om het opnieuw laden van abonnementen te optimaliseren (d.w.z. met behulp van een read-through-patroon). In de cache opgeslagen artefacten worden passief ongeldig gemaakt door dezelfde ongeldigmakingspijplijn die voor abonnementen wordt gebruikt. Telkens wanneer we gegevens in de cache proberen te gebruiken, controleren we de geldigheid ervan en vallen we indien nodig terug op herberekening.

We kunnen een duidelijke afhankelijkheid waarnemen tussen het opnieuw laden van abonnementen en het ongeldig maken van gecachete gegevens. Als we bij het ontvangen van een ongeldigverklaring een abonnement opnieuw laden voordat de gecachte gegevens ongeldig zijn verklaard, berekenen we mogelijk een verouderd resultaat. Wanneer zowel het laden van gegevens als het beheer van abonnementen in hetzelfde proces plaatsvinden, is het eenvoudig om deze afhankelijkheid te garanderen: maak caches gewoon ongeldig voordat je ze opnieuw laadt. 

Race condition kan verouderde gegevens veroorzaken bij het bijwerken

Race condition kan verouderde gegevens veroorzaken bij het bijwerken

In onze voorgestelde nieuwe architectuur zijn sessiebrokers en synchroniseerbare loaders beide onafhankelijke consumenten van de ongeldigverklaringspijplijn. Hoe kunnen we dan afdwingen dat caches ongeldig worden verklaard voordat abonnementen opnieuw worden geladen?

Versiebeheer van aanvraag en reactie

We hadden dit kunnen oplossen door de ongeldigverklaringspijplijn berichten in lockstep te laten afleveren. Of we hadden een mechanisme kunnen bouwen om de volgordegarantie af te dwingen dat er geen ongeldigmakingspijplijnberichten bij de sessiebroker aankomen vóór de synchroniseerbare lader. Beide oplossingen hadden echter niet-ideale afwegingen - cruciaal is dat ze de koppeling van sessiebrokers en synchroniseerbare loaders verhoogden.

In plaats daarvan hebben we dit probleem opgelost door ons gegevenslaadprotocol uit te breiden met aanvraag- en responsversies op basis van hun relatieve voortgang in de ongeldigmakingsstromen. Aangezien de stream een totale volgorde van updates voor de databases vertegenwoordigt, kan onze streamvoortgang worden gebruikt als een globale versieteller.

versies van aanvraag en reactie

aanvraag- en reactieversies

Opnieuw laden van ongeldigverklaringen

Synchronisatieservers laden heel veel gegevens - het grootste deel van alle leesopdrachten voor de site. Onze nieuwe architectuur vereist dat sessiebrokers en syncable-loaders veel gegevens via het netwerk uitwisselen. Voor nieuwe abonnementen is deze netwerkoverhead relatief verwaarloosbaar. Dit is echter bijzonder inefficiënt voor het opnieuw laden van ongeldigverklaringen, omdat we vaak niet de volledige reactie hoeven terug te sturen, maar alleen de bijgewerkte gegevens³. Bepaalde gevallen zijn hier bijzonder slecht. Stel je een geval voor waarin de ene gebruiker 10.000 taken in een project heeft gepaged en een andere gebruiker voortdurend taakbeschrijvingen in dit project wijzigt: bij elke ongeldigverklaring zouden alle taken moeten worden verzonden! Het is duidelijk ideaal om alleen de bijgewerkte gegevens terug te sturen, maar hoe implementeren we dat efficiënt?

ongeldigverklaring herlaadt

ongeldigverklaring herlaadt

Vingerafdrukken

Om de delta van bijgewerkte gegevens te berekenen, moet de synchroniseerbare lader weten welke gegevens de aanvrager al heeft. Maar het doorgeven van de nieuwste gegevens bij het verzoek zou net zo duur zijn als het retourneren van het volledige resultaat.  We moeten de gegevens op een ruimte-efficiëntere manier weergeven.

Nou, hashing is een geweldige manier om ruimte te besparen. Elk stukje gedetailleerde gegevens dat voor ons belangrijk is, wordt een syncable genoemd. We kunnen een 128-bits murmur-hash berekenen van elke geserialiseerde syncable om als vingerafdruk te gebruiken ⁴. Deze vingerafdruk is specifiek een identificatie voor die versie van de syncable.

Overal waar we synchroniseerbare gegevens volgen, kunnen we in plaats daarvan hun vingerafdrukken gebruiken. Als we nu een volledige abonnementsreactie willen bijhouden, kunnen we gewoon een set vingerafdrukken gebruiken zonder de volledige gegevens door te hoeven geven!


Kanttekening: Wat is een syncable en hoe verhoudt die zich tot abonnementen?

Syncables zijn de inhoud van het resultaat van een abonnement. Wanneer we een abonnement laden, worden de resultaten geretourneerd als een set syncables. Meer specifiek kan een syncable een object, een query of een SCV-resultaat zijn.

synchroniseerbaar met abonnementsmapping

synchroniseerbaar met abonnementsmapping

Het is duidelijk dat elk abonnement aan meerdere syncables wordt toegewezen. Syncables kunnen echter door meerdere abonnementen gedeeld worden (wanneer ze overlappende gegevens laden). Daarom is er eigenlijk een veel-op-veel-toewijzing tussen abonnementen en syncables.


We geven de set van deze vingerafdrukken door bij elke aanvraag van de sessiebroker. Op de syncable-lader berekenen we de volledige reactie, berekenen we de set vingerafdrukken, sluiten we alle gegevens uit die de aanvraag overlappen en retourneren we de delta.

Hoe komen we daar?

Gezien de omvang en het kritieke karakter van de wijziging, hebben we de uitrol in ongeveer 4 fasen opgesplitst. 

Fase 1 - Refactoring van de monoliet

  • Opsplitsing van onze sterk gekoppelde sessiebeheer- en gegevenslaadcode in onafhankelijke componenten

Fase 2 - Lokale syncable-loader

  • Gebruik de nieuwe component voor het laden van gegevens om een lokale gRPC-server te maken en migreer het laden van gegevens

fasen 1-2

Fase 3 - Syncable-loader op afstand

  • Een nieuw syncable-loader-binair bestand en een nieuwe implementatie maken

  • Migreer al het laden van sync-servergegevens naar onze nieuwe syncable-loader-implementatie

Fase 4 - Afzonderlijk session-broker-binair bestand

  • Een nieuw session-broker-binair bestand en een nieuwe implementatie maken

  • Al het verkeer van sync-servers naar session-brokers migreren

fasen 3-4

Tegen welke uitdagingen liepen we aan?

Zoveel. Waar te beginnen?

Grote reacties

Een probleem waar we al snel tegenaan liepen, waren grote antwoorden. Aangezien het laden van gegevens op synchronisatieservers allemaal binnen hetzelfde proces plaatsvond, was dit tot nu toe geen groot probleem gebleken⁵. Toen we echter begonnen met het laden van gegevens over een lokale gRPC-grens, begonnen we veel problemen tegen te komen. 

We hadden al die tijd het vermoeden dat sommige reacties groot zouden kunnen zijn, maar toen we dit begonnen te onderzoeken, vonden we echt verrassende resultaten. We hadden duizenden ladingen per dag die regelmatig groter waren dan 100 MiB! We konden fysiek geen reacties van die omvang terugsturen via een unaire gRPC-methode (je begint de maximale framegrootte van http2 te bereiken). Wat te doen?

We hebben een paar manieren overwogen om dit systematisch op te lossen, maar uiteindelijk concludeerden we dat we de hoofdoorzaken moesten aanpakken. We hadden gRPC-server-side streaming kunnen implementeren, maar de hoge serialisatiekosten en verhoogde socket-concurrentie zouden vrij regressief zijn voor latentie en doorvoer. We konden deze reacties gewoon afwijzen, maar de frequentie was te hoog om dit acceptabel te laten zijn. 

We kozen voor een aanpak in drie fasen waarbij we alle grote reacties markeren, elk problematisch geval analyseren en elimineren, en vervolgens een strikte bovengrens voor de reactiegrootte afdwingen.

We markeerden alle ladingen groter dan 1 MB en registreerden gedetailleerde gebeurtenissen over bron, gebruik en gegevensverdeling. Er waren een paar verschillende dure use cases, maar de meest beruchte was waarschijnlijk de bijlage met miniatuurafbeeldings-blobs. Het blijkt dat ze werden gecodeerd als base64-tekenreeksen en werden opgenomen in de gserialiseerde reacties. Ze waren in kleine aantallen acceptabel, maar werden al snel enorm wanneer ze in grote hoeveelheden werden geladen, zoals bij het laden van een rasterweergave die miniaturen van bijlagen voor elke Taak weergeeft.

We waren in staat om dergelijke problemen geleidelijk op te lossen door miniaturen voor enorme antwoorden te beperken, kleinere miniaturen te gebruiken en uiteindelijk binaire gegevens uit het antwoord te verwijderen. Na eenvoudige beperkende maatregelen zoals deze verdween het aantal enorme reacties en konden we vervolgens beperkingen op het niveau van het framework op de grootte van de reactie afdwingen⁶.

Onderwerpconflicten

Een ander vreemd probleem dat we tegenkwamen, waren pubsub-onderwerpconflicten. Het blijkt dat we niet-conforme raamwerkgebruiken hadden die hetzelfde abonnementsonderwerp genereerden, ongeacht het domein. Wanneer pubsub slechts op één procestype plaatsvond, waren de effecten relatief goedaardig. Meestal komt één enkel pubsub-onderwerp overeen met gegevens van één enkel domein. Nu pubsub echter is opgesplitst over sessiebrokers en synchroniseerbare loaders, was het mogelijk dat de twee procestypen het oneens waren over het domein van een bepaald onderwerp. Wanneer dit gebeurde, zagen we een stabiel verhoogd aantal ongeldigverklaringsherladingen als gevolg van deze "domeinmismatch". Gelukkig was de oplossing vrij eenvoudig, maar het is interessant hoe deze bug zo lang in ons raamwerk heeft overleefd zonder te worden gedetecteerd. 

Werkbelastingen opnieuw afstemmen

Sessiebrokers en synchroniseerbare loaders hebben workloads die aanzienlijk verschillen van die van de oorspronkelijke synchronisatieservers. Sessiebrokers zijn alleen verantwoordelijk voor sessiebeheer en synchroniseerbare loaders zijn alleen verantwoordelijk voor het laden van gegevens. 

We wisten niet precies hoe dit hun behoefte aan middelen zou beïnvloeden, dus we begonnen beide met vergelijkbare verzoeken om middelen (cpu/mem) en instellingen voor horizontal pod autoscaler (HPA). 

session-brokers

Zoals we zagen, werd het duidelijk dat sessiebrokers aanzienlijk lichter waren. Ze werkten betrouwbaar op een zeer lage CPU (ze waren zelfs veel meer geheugengebonden⁷). Een paar replica's leken voldoende om het verkeer van een hele infrastructuurcel te bedienen. Toen we echter daadwerkelijk de minReplicas op de HPA verminderden, zagen we dat het opnieuw laden van gegevens en de latentie van het opnieuw laden omhoog schoten. Wat was er aan de hand?

Kortom, we hadden nagelaten om al onze gedeelde instellingen rond cachegrootte en throttlers te overwegen. Met slechts een paar replica's verwerkte elke sessiebroker veel meer sessies per pod (ongeveer 3,5x) dan een typische synchronisatieserver. Omdat ze elk veel meer gegevens zagen, vulden ze hun pubsub-onderwerp ⇔ abonnementscaches volledig en elke verwijdering leidde tot een herladen (uit veiligheidsoverwegingen). Door deze drempel op de juiste manier met ~6x te verhogen, werden de verhoogde cacheverwijderings- en herlaadsnelheden verholpen. Evenzo ontdekten we dat onze hiërarchische herlaadbeperkers slecht waren geconfigureerd voor de nieuwe verkeerssnelheden die binnenkwamen. Het aanpassen van deze throttler-instellingen leidde op dezelfde manier tot een dramatische vermindering van de herlaadlatentie en de end-to-end reactiviteitsvertraging (d.w.z. hoe lang het duurt voordat een web-app zijn eigen schrijfbewerking ziet) van ongeveer 5x - 10x.

syncable-loaders

Aan de andere kant waren synchroniseerbare loaders aanzienlijk zwaarder dan verwacht. Elke server zou meer abonnementen per seconde laden (ongeveer 1,5x) dan een sync-server met gelijkwaardige middelen. In tegenstelling tot sessiebrokers waren ze veel meer CPU-gebonden ⁸.

Interessant is dat een niet onbelangrijk deel van de CPU werd toegeschreven aan een toename van Truffle-deoptimalisaties in verband met onze TS SCV-code. Hoogstwaarschijnlijk werd dit veroorzaakt doordat elke syncable loader toegang had tot meer van onze SCV-code. Hoe dan ook, het vereiste een bescheiden toename van de grootte van onze codecache⁹. 

Procesopwarming

Procesopwarming voor het laden van gegevens is historisch gezien een uitdaging geweest. Gelukkig is het in onze nieuwe architectuur iets eenvoudiger. Onze hoofdinterface voor syncable-loaders is stateless data queries, dus we kunnen ze opwarmen door simpelweg bestaand verkeer tussen session-brokers en syncable-loaders opnieuw af te spelen of te spiegelen.

Aan de andere kant staan we nog steeds voor veel van dezelfde uitdagingen als bij het opwarmen van sync-servers. Het kost vooral veel CPU om processen op te warmen, en dit veroorzaakt allerlei soorten noisy neighbor-problemen (voor ons is de relevante statistiek hier gedeeltelijke stilstand, omdat we de k8s-throttlinglimieten niet bereiken) bij het opstarten. Een mooie verbetering die we hier hebben aangebracht, was het gebruik van in-place pod-resizing om de bronnen van een syncable-loader bij het opstarten te beperken, maar deze na het opstarten te laten uitbreiden. 

Desondanks duurt het opwarmen van elke pod nog steeds enkele minuten. Als we naar JFR-profielen kijken, denken we dat het belangrijkste knelpunt de onvoldoende compilatie van TS SCV-code tijdens het opwarmen is, en we denken dat er daar meer ruimte is. We onderzoeken actief hoe we de relevante paden nauwkeuriger kunnen opwarmen met meer gerichte methoden en hoe we onze TS-interfaces¹⁰ kunnen aanpassen voor een betere compilatie.

Onze sessiebrokers zijn niet verantwoordelijk voor het laden van gegevens en vereisten in de praktijk nooit enige vorm van warming.

Hoe heeft het geholpen?

Onze nieuwe architectuur verminderde onze operationele complexiteit aanzienlijk, versnelde implementaties en codesnelheid en ontsloot toekomstige snelheids- en schaalmogelijkheden. 

Onze nieuwe architectuur schaalt eenvoudig automatisch mee met veranderingen in het totale leesverkeer zonder dat er complex verkeersbeheer nodig is. Elk type verkeersverschuivingsbewerking wordt netjes afgehandeld door simpelweg pods opnieuw op te starten. Moet je alle sessies opstarten? Schakel alle sessiebrokers om. Moeten onze caches worden gewist? Schakel alle synchroniseerbare loaders uit en weer in. Beide bewerkingen zijn veilig met betrekking tot uptime.

In het verleden was de snelheid van productontwikkeling vooral het gevolg van knelpunten bij het implementeren van sync-servers. In onze nieuwe wereld hoeven we alleen maar syncable-loaders te implementeren om productcode te implementeren. Dit hebben we gedaan door syncable-loaders naar hun eigen implementeerbare cel te verplaatsen, die we vaker willen implementeren (en uiteindelijk naast productcode). Het implementeren van synchroniseerbare loaders is al ~40% sneller (~20 minuten sneller) dan synchronisatieservers (we kunnen veilig veel hoger pieken), en we streven naar grotere stijgingen in de toekomst.

Prestatieverbeteringen waren overigens geen Doel van dit werk, maar het is de moeite waard om erover te praten, gezien de mate waarin het bij het ontwerp en de implementatie betrokken was. De latentie van het berekenen van queryresultaten is in ons nieuwe systeem zelfs verbeterd (waarschijnlijk als gevolg van een betere load balancing/tuning/autoscaling). Dienovereenkomstig is de initiële abonnementslatentie merkbaar beter. Aan de andere kant is de end-to-end-mutatie-reflectielatentie (d.w.z. hoe lang het duurt voordat een wijziging naar andere sessies wordt gedistribueerd) ongeveer hetzelfde. Traces tonen aan dat dit waarschijnlijk te wijten is aan de scheefheid in het verbruik van de PubSub-stream tussen sessiebrokers en syncable-loaders (syncable-loaders kunnen een aanvraag niet bedienen totdat ze up-to-date zijn met de aanvraagversie).

Wat gebeurt er hierna?

Ons huidige systeem is veel verbeterd, maar het beperkt nog steeds de productsnelheid door bij elke release rekening te moeten houden met achterwaartse/voorwaartse compatibiliteit. Met al onze verbeteringen in implementatiesnelheid is het nu echter mogelijk om al onze datamodelwijzigingen samen te implementeren, waardoor het niet meer nodig is om na te denken over achterwaartse/voorwaartse compatibiliteit. We zijn actief bezig om dit in de nabije toekomst te bouwen.

Een van onze belangrijkste motivaties voor dit werk waren prestatieregressies die moeilijk toe te schrijven zijn. Dit is met name een gebied waar we als gevolg van dit werk geen significante verbeteringen hebben aangebracht. Het positieve is dat dit nu een van de belangrijkste problemen is die we in de toekomst zullen aanpakken. In tegenstelling tot het grootste deel van het werk dat hier wordt besproken, is het een aanzienlijk meer cross-functional probleem dat betrekking heeft op het productdomein, het gegevensmodel, het raamwerk en de infrastructuur. 

synchroniseerbare lader-werkpools per functie

synchroniseerbare lader-werkpools per functie

We kijken ernaar uit om oplossingen te verkennen voor de platforminfrastructuur (bijv. workerpools per functie), platformraamwerken en platformtools (bijv. blackbox-/whiteboxtesten).


Biografie van de auteur

Arvind Vijayakumar is een engineer in het LunaDb-team, waar hij helpt bij het opbouwen en opschalen van het kernplatform voor het laden van gegevens van Asana - de kritieke infrastructuur die ervoor zorgt dat gebruikers altijd nauwkeurige, reactieve en razendsnelle updates zien in onze webapps en API's.

Team Shout Outs

Dit werk om LunaDb op te schalen is een langdurige teaminzet geweest die de afgelopen jaren heeft geduurd, waarbij veel leden van LunaDb betrokken waren, zowel huidige als voormalige: Brandon Zhang, Alex Matevish, Sean Wentzel, Eric Walton, Spencer Yu, Sophia Yao, George Ong, Tyler Prete, Koushik Ghosh, Natan Dubitski, Vinodh Chandra Murthy en anderen


Voetnoten

  1. Opvallend is dat we het hebben gebouwd voordat Facebook GraphQL open source maakte

  2. Er zijn meer details in het vorige bericht over procesopwarming, maar in het kort vertrouwen we erop dat sync-pods willekeurig kunnen uitbreken als een manier om snel te schalen naar lokale verkeerspieken

  3. We doen ook updates op een granulariteit per object in plaats van per veld (om historische redenen). Dit verhoogt de hoeveelheid gegevens die betrokken zijn bij het opnieuw laden van ongeldige gegevens.

  4. We gebruiken een 128-bits murmur-hash om botsingen te voorkomen.

  5. We gebruiken met name al heel lang websocket-compressie, wat waarschijnlijk de door de klant waargenomen effecten vermindert.

  6. Opvallend is dat we oorspronkelijk ook enkele problemen hadden met de overhead van de netwerkhop. Na wat onderzoek ontdekten we dat het grootste deel van de overhead eigenlijk te wijten was aan ons servicenetwerk (Istio/Envoy) en we hebben hier enkele gerichte aanpassingen gedaan om de prestaties te verbeteren.

  7. Een belangrijke factor in het behouden geheugen was het herschrijven van onze synchroniseerbare opslag aan de serverzijde om alleen hashes van gegevens te behouden. Het was geen eenvoudig probleem en we zullen hier misschien een vervolgbericht over publiceren! Zonder dit zorgden de opgeslagen klantgegevens ervoor dat sessiebrokers aanzienlijk meer geheugen gebruikten.

  8. Hiervoor draaiden alle synchronisatieservers op geheugen-geoptimaliseerde instanties. Synchroniseerbare loaders daarentegen profiteren van knooppunttypes met meer CPU, zoals gemengde instanties.

  9. Een opmerkelijk kenmerk van het op deze manier uitvoeren van TS op GraalVM is dat het behoorlijk wat meer codecache gebruikt dan gebruikelijk is in een meer standaard Scala/Java JVM-applicatie.

  10. Voor zover wij kunnen zien, maakt een hoog niveau van polymorfisme de TS-compilatie duurder. We onderzoeken de mogelijkheid om over te schakelen naar monomorfe interfaces en gebruik te maken van rapporterende polymorfe specialisaties