Skalning av LunaDb, vårt interna deklarativa dataladdningssystem

Bild på medarbetare i Asana-teametArvind Vijayakumar
5 mars 2026
14 min. läsning
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.

Här på Asana har vi byggt ett dataladdningssystem som heter LunaDb och som fungerar som ryggraden i vår webbapp. Trots namnet är det inte en databas. Det är snarare ett GraphQL-liknande system för att deklarativt hämta data – i princip ett sätt att läsa in den senaste versionen av data och alla framtida uppdateringar.

Vi lanserade ursprungligen LunaDb 2015 som en radikal omskrivning av vår backend-infrastruktur¹. Den centrala komponenten i det nya systemet var synkroniseringsservern, en monolit som utför allt från klientsynkronisering till datainläsning och åtkomstkontroll. Utan några större ändringar skalades den här ursprungliga arkitekturen långt bortom de nivåer som vi förväntade oss i början, hela vägen till miljontals aktiva användare varje vecka och miljarder dagliga frågor. 

Även om prestandan förblev stark, i takt med att trafiken och funktionskomplexiteten växte, blev det allt svårare att driva och förbättra LunaDb på grund av synkroniseringsserverns begränsningar.

Översikt över vår infrastruktur för datainläsning

översikt över vår infrastruktur för datainläsning

Varför var det svårt att använda?

Trafikförskjutning är dyrt

Synkroniseringsservern hanterade direkt beständiga WebSocket-anslutningar till klienter. Varje websocket stöddes av en stateful-klientsession. När en anslutning bröts slängdes hela detta tillstånd och klienten skulle prenumerera på nytt på alla data som var viktiga för den. När det här händer med många sessioner blir det snabbt dyrt. Så vi var tvungna att vara försiktiga när vi flyttade de här anslutningarna, med tanke på den stora ökningen av arbete som återanslutningar medför.

trafikomställning

trafikförskjutning

Implementering av synkroniseringsservrar innebär att flytta trafik

Naturligtvis kan du inte för alltid förhindra att trafiken flyttas. När du vill skicka ny kod eller skala upp/ner måste du ta synkroniseringsservrar ur drift, och det kräver att all trafik flyttas bort från de avslutande instanserna.

löpande uppdateringar

löpande uppdateringar

Synkroniseringsservrarna presterar inte bra vid start

Samtidigt skulle synkroniseringsservrarna bara bli effektiva efter en icke-trivial mängd processuppvärmning. Att hantera båda dessa problem har varit en ganska känslig balansgång och har tidigare krävt omfattande tekniskt arbete.

Synkroniseringsservrar är komplexa och svåra att övervaka

Slutligen, eftersom synkroniseringsservrar körde många godtyckliga delar av produktkod (via anpassade funktioner på serversidan), var de mycket sårbara för prestandaregressioner baserade på noisy neighbor som var svåra att tillskriva². 

problemet med den högljudda grannen

problemet med den högljudda grannen


En rimlig fråga att ställa är: ”Varför är synkroniseringsservrar komplexa och ineffektiva vid start?”

En vanlig orsak till dessa problem är vår produktkod på serversidan. De viktigaste delarna av synkroniseringsserverns kod är skrivna i Scala.  Trots viss komplexitet relaterad till hantering av sessionstillstånd och de olika aspekterna av Luna-ramverket beter sig denna ramverks-/plattformskod mestadels som vi förväntar oss (det finns relativt få drifts- och prestandaproblem). 

Å andra sidan är dessa produktvärden som beräknas av servern (vi kallar dem SCV:er, men tänk anpassade resolvers) skrivna i Typescript. Båda koduppsättningarna körs tillsammans i GraalVM, en polyglot-VM som gör det möjligt att använda flera språk via sitt Truffle-ramverk. Eftersom de är skrivna i Typescript tolkas SCV:er i huvudsak vid start, vilket förutsägbart resulterar i oacceptabel prestanda och CPU-användning. GraalVM kommer att försöka utföra just-in-time-kompilering på anropade SCV:er. Det här är bra! GraalVM/Truffle kan optimera prestandan avsevärt – men det är inte gratis. SCV-kompilering kan vara ganska dyr (i CPU, kodcache osv.). 

vår flerspråkiga VM-konfigurering

vår flerspråkiga VM-konfigurering

Varför de två språken?

Vår första design för SCV:er var helt och hållet i Scala. Å andra sidan är våra mutations- och asynkrona jobbsystem skrivna i Javascript/Typescript. Även om Scala-baserade SCV:er fungerade, blev dupliceringen av affärslogik mellan våra mutations- och asynkrona jobbsystem och LunaDb, tillsammans med produktteknikernas bristande förtrogenhet med Scala, en ganska stor broms för produktutvecklingens hastighet. 

Varför GraalVM?

Vi cachelagrar mycket data i processen för att påskynda beräkningen (och omberäkningen) av prenumerationsresultat. Genom att använda GraalVM får vi ett enkelt sätt att dela dessa cachar mellan olika språk utan de problem med korrekthet eller prestanda som kan uppstå om vi delar upp Scala- och TypeScript-delarna i separata behållare.


Varför var det svårt att förbättra?

Eftersom servern gjorde så många saker och var relativt känslig att använda tenderade vi att undvika större ändringar. Inte bara på grund av kodens komplexitet, utan också på grund av de höga omkostnaderna för att på ett säkert sätt införa nya ändringar.

Hur löser vi saker?

Ja, jag gillar att ställa frågor

Med tanke på svårigheterna med att driva och förbättra synkroniseringsservern fattade vi det svåra beslutet att ändra vår arkitektur. I huvudsak bestämde vi oss för att ersätta den monolitiska synkroniseringsservern med två mindre typer av komponenter:

  • en sessionsförmedlare som hanterar klientanslutningar och statuslösning

  • en synkroniserbar laddare som ansvarar för dataladdning, dvs. att uppfylla förfrågningar från sessionsförmedlare

session-brokers och syncable-loaders

session-brokers och syncable-loaders

Varför hjälper det här?

Omedelbart separerar den här nya arkitekturen skiftande websocket-trafik (dvs. distribution av sessionsförmedlare) från uppvärmning av nya processer (dvs. distribution av synkroniserbara laddare). Som ett resultat kan vi minimera störningar genom att distribuera synkroniserbara laddare separat från sessionsförmedlare.

Var och en av dessa nya komponenter är enklare.

  • Sessionsförmedlare är mycket lättare, kräver ingen processuppvärmning och kör ingen produktkod. Därför behöver vi inte distribuera dem lika ofta – och när vi gör det är det ganska enkelt.

  • Synkroniserbara laddare har ett enklare gränssnitt (tillståndslösa prenumerationsförfrågningar) som är lättare att anpassa till standardmässig horisontell pod-autoskalning i Kubernetes. Detta gör det också snabbare och enklare att värma dem – vi kan helt enkelt använda trafikspegling på de omgivande förfrågningarna som flödar mellan sessionsförmedlare och synkroniserbara laddare

Den nya arkitekturen gör att vi kan förenkla produktutvecklingsprocessen avsevärt. Från början har de oberoende implementeringsschemana för produktskoden på serversidan och anropen av produktskoden på klientsidan varit en broms för hastigheten och en källa till operativt slit (på grund av inkompatibilitet mellan versioner). Eftersom synkroniserbara laddare nu är den enda återstående processen som kör produktkod och distributionen av dem inte längre är störande kan vi distribuera dem på nytt när vi skickar ny produktkod.

Den här nya arkitekturen gör att vi bättre kan skala upp till nya funktioner genom att distribuera olika pooler av synkroniserbara laddare för olika typer av arbetsbelastning (till exempel olika specifika funktioner såsom inkorgen, uppgifter, mål osv.). Sessionsförmedlaren fungerar som en tjänstegateway som direkt kan styra hur datafrågor dirigeras till olika uppströms synkroniserbara laddare.

Vilka är de viktigaste designutmaningarna?

Bra! Den här nya arkitekturen låter mycket bättre, men hur förverkligade vi den? Synkroniseringsservern är i princip en monolit i det att den omfattar flera funktioner – och att bryta upp monoliterna är nästan alltid knepigt. I vårt fall var vi tvungna att övervinna några viktiga designhinder. 

Dela upp PubSub

PubSub, vårt system för att implementera reaktivitet, utformades kring en enda process (synkroniseringsservern) som ansvarar för att ladda nya data och skicka dem till klienten. Vi var tvungna att omforma PubSub på ett sätt som garanterar korrekthet mellan dessa två nu oberoende processtyper (synkroniserbara laddare och sessionsförmedlare). 

Låt oss kort gå in på hur det implementeras i synkroniseringsservrar. Obs! Det kan vara bra att läsa vårt tidigare inlägg om ogiltigförklaringspipelinen, men vi kommer att ge en översikt över systemet som inte kräver några förkunskaper.

På synkroniseringsservern spårar vi prenumerationer per session. Vi använder ogiltigförklaringspipelinen för att kontinuerligt övervaka prenumerationer för uppdateringar. Vid varje nytt ogiltighetsmeddelande kommer synkroniseringsservern att ladda om alla berörda prenumerationer. 

Synkroniseringsservrar cachelagrar i stor utsträckning db-objekt-/frågeresultat, anpassade lösningsresultat och tidigare prenumerationsresultat för att optimera prenumerationsomladdningar (dvs. med hjälp av ett read-through-mönster). Cachelagrade artefakter ogiltigförklaras passivt av samma ogiltigförklaringspipeline som används för abonnemang. När vi försöker använda cachade data kommer vi att kontrollera deras giltighet och återgå till omberäkning om det behövs.

Vi kan observera ett tydligt beroende mellan omladdning av prenumerationer och ogiltigförklaring av cachade data. Om vi, efter att ha mottagit en ogiltigförklaring, laddar om en prenumeration innan de cachade uppgifterna har ogiltigförklarats, kommer vi potentiellt att beräkna ett inaktuellt resultat. När både dataladdning och prenumerationshantering sker i samma process är det enkelt att garantera detta beroende – ogiltigförklara helt enkelt cacherna innan du laddar om. 

Låsning kan leda till inaktuell information vid uppdatering

Låsning kan leda till inaktuell information vid uppdatering

I vår föreslagna nya arkitektur är sessionsförmedlare och synkroniserbara laddare båda oberoende konsumenter av ogiltigförklaringspipelinen. Hur kan vi då genomdriva att cachar ogiltigförklaras innan abonnemang laddas om?

Versionshantering av begäran och svar

Vi kunde ha löst detta genom att få ogiltigförklaringspipelinen att leverera meddelanden i takt. Eller så kunde vi ha byggt en mekanism för att genomdriva ordningsgarantin att inga meddelanden från ogiltigförklaringspipelinen anländer till sessionsförmedlaren före den synkroniserbara laddaren. Båda de här lösningarna hade dock oönskade kompromisser – framför allt ökade de kopplingen mellan sessionsförmedlare och synkbara laddare.

Istället löste vi detta problem genom att utöka vårt dataladdningsprotokoll med begäran- och svarsversioner baserat på deras relativa framsteg i ogiltigförklaringsströmmarna. Eftersom flödet representerar en total ordning av uppdateringar till databaserna kan vårt flödesförlopp användas som en global versionsräknare.

versioner av förfrågan och svar

versioner av förfrågan och svar

Omgivningar för ogiltigförklaring

Synkroniseringsservrar laddar massor av data – majoriteten av alla läsningar för webbplatsen. Vår nya arkitektur kräver att sessionsförmedlare och synkroniserbara laddare utbyter mycket data över nätverket. För nya prenumerationer är denna nätverksoverhead relativt försumbar. Detta är dock särskilt ineffektivt för omladdningar vid ogiltigförklaring, eftersom vi ofta inte behöver returnera hela svaret – bara de uppdaterade uppgifterna³. Vissa fall är särskilt dåliga här. Föreställ dig ett fall där en användare har bläddrat ner 10 000 uppgifter i ett projekt och en annan användare ständigt ändrar uppgiftsbeskrivningar i det här projektet: alla uppgifter skulle behöva skickas över vid varje ogiltigförklaring! Det är uppenbart att det är bäst att bara skicka tillbaka de uppdaterade uppgifterna, men hur implementerar vi det på ett effektivt sätt?

ogiltigförklarande omladdningar

ogiltigförklarande omladdningar

Fingeravtryck

För att den synkroniserbara laddaren ska kunna beräkna deltan för uppdaterade data måste den veta vilka data den som gör begäran redan har. Men att skicka den senaste informationen med begäran skulle vara lika kostsamt som att returnera hela resultatet.  Vi måste representera datan på ett mer utrymmesbesparande sätt.

Hashing är ett utmärkt sätt att spara utrymme. Varje bit av detaljerade data som vi bryr oss om kallas för en syncable. Vi kan beräkna en 128-bitars murmur-hash av varje serialiserad syncable att använda som ett fingeravtryck ⁴. Specifikt är detta fingeravtryck en identifierare för den versionen av syncable.

Var vi än spårar synkroniserbara data kan vi i stället använda deras fingeravtryck. Nu, när vi vill spåra ett fullständigt prenumerationssvar, kan vi helt enkelt använda en uppsättning fingeravtryck utan att behöva skicka runt alla uppgifter!


Sidokommentar: Vad är en synkbar och hur relaterar den till prenumerationer?

Syncables är innehållet i resultatet av ett abonnemang. När vi laddar en prenumeration returneras resultaten som en uppsättning synkbara objekt. Mer specifikt kan en synkbar vara ett objekt, en fråga eller ett SCV-resultat.

kan synkroniseras med prenumerationskartläggning

kan synkroniseras med prenumerationskartläggning

Det är uppenbart att varje prenumeration är kopplad till flera synkbara objekt. Synkbara objekt kan dock delas av flera prenumerationer (när de laddar överlappande data). Därför finns det faktiskt en många-till-många-mappning mellan prenumerationer och synkbara objekt.


Vi skickar uppsättningen av dessa fingeravtryck med varje förfrågan från sessionsförmedlaren. På den synkbara laddaren beräknar vi det fullständiga svaret, beräknar dess uppsättning fingeravtryck, utesluter alla data som överlappar begäran och returnerar deltan.

Hur når vi dit?

Med tanke på ändringens storlek och hur kritisk den är delar vi upp lanseringen i ungefär fyra steg. 

Fas 1 – Refaktorering av monolitten

  • Dela upp vår starkt kopplade kod för sessionshantering och datainläsning i oberoende komponenter

Fas 2 – Lokal syncable-loader

  • Använda den nya dataladdningskomponenten för att skapa en lokal gRPC-server och migrera dataladdning

fas 1–2

Fas 3 – Fjärrsynkroniserbar laddare

  • Skapa en ny binär fil för syncable-loader och implementering

  • Migrera all datainläsning från sync-server till vår nya implementering av syncable-loader

Fas 4 – Separat session-broker-binär

  • Skapa en ny session-broker-binärfil och implementering

  • Migrera all trafik från sync-servers till session-brokers

fas 3–4

Vilka utmaningar stötte vi på?

Så många. Var ska man börja?

Stora svar

Ett problem som vi snabbt stötte på var stora svarstorlekar. Eftersom datainläsning på synkroniseringsservrar skedde inom samma process hade detta inte visat sig vara ett stort problem fram till nu⁵. Men när vi började ladda data över en lokal gRPC-gräns började vi stöta på många problem. 

Vi hade hela tiden misstänkt att vissa svar kunde vara stora, men när vi började undersöka detta fann vi verkligen häpnadsväckande resultat. Vi hade tusentals laddningar per dag som regelbundet översteg 100 MiB! Vi kunde fysiskt inte returnera så stora svar via en unär gRPC-metod (du börjar nå http2:s maximala ramstorlek). Vad kan vi göra?

Vi övervägde några olika sätt att åtgärda detta systematiskt, men kom till slut fram till att vi behövde ta itu med grundorsakerna. Vi kunde ha implementerat gRPC-streaming på serversidan – men de höga serialiseringskostnaderna och den ökade socket-konflikten skulle vara relativt regressiva för latens och genomströmning. Vi kunde bara avvisa dessa svar – men frekvensen var för hög för att detta skulle vara acceptabelt. 

Vi bestämde oss för en metod i tre faser där vi markerar alla stora svar, analyserar och eliminerar varje problematiskt fall och sedan inför en strikt övre gräns för svarsstorlek.

Vi markerade alla laddningar som var större än 1 MB och loggade detaljerade händelser om källa, användning och datauppdelning. Det fanns några olika dyra användningsfall, men det mest ökända var förmodligen miniatyrbilder av bilagor. Det visade sig att de kodades som base64-strängar och ingick i de serialiserade svaren. De var acceptabla i små mängder, men blev snabbt enorma när de laddades i stor skala – till exempel när en rutnätsbaserad vy laddades som renderade miniatyrbilder av bilagor för varje uppgift.

Vi kunde gradvis åtgärda problem som dessa genom att begränsa miniatyrbilder för enorma svar, använda mindre miniatyrbilder och så småningom flytta binärdata bort från svaret. Efter enkla begränsningar som dessa försvann antalet enorma svar, och vi kunde därefter genomdriva begränsningar av svarsstorlek på ramverksnivå⁶.

Ämneskollisioner

Ett annat konstigt problem vi stötte på var pubsub-ämneskollisioner. Det visade sig att vi hade icke-kompatibla ramverksanvändningar som genererade samma prenumerationsämne oavsett domän. När pubsub bara inträffade på en enda processtyp var effekterna relativt godartade. Vanligtvis motsvarar ett enda pubsub-ämne data från en enda domän. Men eftersom pubsub nu var uppdelat mellan session-brokers och syncable-loaders var det möjligt för de två processtyperna att vara oense om en viss ämnesdomän. När det här hände såg vi en stabil förhöjd frekvens av ogiltigförklaringsomladdningar på grund av denna ”domänkonflikt”. Tack och lov var lösningen ganska enkel – men det är intressant hur den här buggen överlevde i vårt ramverk så länge utan att upptäckas. 

Justera arbetsbelastningarna på nytt

Session-brokers och syncable loaders har arbetsbelastningar som skiljer sig avsevärt från de ursprungliga synkroniseringsservrarnas. Sessionsförmedlare ansvarar endast för sessionshantering och synkbara inläsare ansvarar endast för datainläsning. 

Vi var inte riktigt säkra på hur detta skulle påverka deras resursbehov, så vi startade båda med liknande resursbegäranden (cpu/minne) och inställningar för horizontal pod autoscaler (HPA). 

session-brokers

När vi observerade blev det tydligt att sessionsförmedlare var betydligt lättare. De fungerade tillförlitligt med mycket låg CPU (om något var de mycket mer minnesbundna⁷). Några replikor verkade tillräckliga för att betjäna trafiken i en hel infrastrukturcell. Men när vi faktiskt minskade minReplicas på HPA observerade vi att datainläsningar och inläsningslatens sköt i höjden. Vad var det som hände?

Kort sagt, vi hade förbisett alla våra gemensamma inställningar för cache-storlek och begränsare. Med bara några få replikor hanterade varje sessionsförmedlare mycket fler sessioner per pod (ungefär 3,5 gånger) än en typisk synkroniseringsserver. Eftersom de var och en såg mycket mer data fyllde de sina pubsub-ämne ⇔ prenumerationscacher helt och varje borttagning utlöste en omladdning (av säkerhetsskäl). Att öka denna tröskel på lämpligt sätt med ~6x löste de förhöjda frekvenserna för cache-rensning och omladdning. På samma sätt upptäckte vi att våra hierarkiska begränsare för omladdning var felkonfigurerade för den nya trafikhastigheten som kom in. Att justera dessa begränsningsinställningar ledde på samma sätt till en dramatisk minskning av omladdningslatens och end-to-end-reaktivitetsfördröjning (dvs. hur lång tid det tar för en webbapp att se sin egen skrivning) med cirka 5–10 gånger.

syncable-loaders

Å andra sidan var synkroniserbara laddare betydligt tyngre än väntat. Varje server läste in fler prenumerationer per sekund (cirka 1,5 gånger) än en synkroniseringsserver med motsvarande resurser. Till skillnad från sessionsförmedlare var de mycket mer CPU-bundna ⁸.

Intressant nog tillskrevs en inte obetydlig del av CPU:n en ökning av Truffle-deoptimeringar relaterade till vår TS SCV-kod. Troligtvis orsakades detta av att varje synkbar laddare fick åtkomst till mer av vår SCV-kod. Oavsett krävdes en blygsam ökning av vår kodcache-storlek⁹. 

Processuppvärmning

Processuppvärmning för datainläsning har historiskt sett varit en utmaning. Tack och lov är det lite enklare i vår nya arkitektur. Vårt huvudgränssnitt för syncable-loaders är tillståndslösa datafrågor – så vi kan värma dem bara genom att spela upp eller spegla befintlig trafik mellan session-brokers och syncable-loaders.

Å andra sidan står vi fortfarande inför många av samma utmaningar som med uppvärmning av synkroniseringsservrar. I huvudsak krävs mycket CPU för att värma upp processer, och detta orsakar alla möjliga noisy neighbor-problem (för oss är den relevanta mätvärdet här partiellt stopp eftersom vi inte når k8s-begränsningsgränserna) vid start. En bra förbättring vi gjorde här var att använda in-place pod-resizing för att begränsa resurserna för en syncable-loader vid start, men låta den expandera efter start. 

Trots detta tar uppvärmningen av varje pod fortfarande några minuter. Efter att ha tittat på JFR-profiler tror vi att den främsta flaskhalsen är den otillräckliga kompileringen av TS SCV-kod under uppvärmning, och vi tror att det finns mer utrymme där. Vi undersöker aktivt hur vi mer exakt kan värma upp de relevanta vägarna med mer riktade metoder och omforma våra TS-gränssnitt¹⁰ för bättre kompilering.

Våra sessionsförmedlare är inte ansvariga för någon datainläsning och krävde i praktiken aldrig någon form av uppvärmning.

Hur hjälpte det?

Vår nya arkitektur minskade vår operativa komplexitet avsevärt, påskyndade implementeringar och kodhastighet och öppnade upp för framtida hastighets- och skalningsmöjligheter. 

Vår nya arkitektur skalar helt enkelt automatiskt till ändringar i den totala lästrafiken utan att kräva komplex trafikhantering. Varje typ av trafikskiftande åtgärd hanteras smidigt genom att helt enkelt starta om poddar. Behöver du starta alla sessioner? Gå igenom alla sessionsförmedlare. Behöver du rensa våra cachar? Gå igenom alla synkroniserbara laddare. Båda åtgärderna är säkra när det gäller drifttid.

Tidigare var produktutvecklingens hastighet främst begränsad av flaskhalsar i form av distribution av synkroniseringsservrar. I vår nya värld behöver vi bara distribuera synkroniserbara laddare för att distribuera produktkod. Detta har vi gjort genom att flytta syncable-loaders till sin egen distribuerbara cell som vi arbetar för att distribuera oftare (och så småningom tillsammans med produktkod). Distributionen av synkroniserbara laddare är redan cirka 40 % snabbare (cirka 20 minuter snabbare) än synkroniseringsservrar (vi kan tryggt öka mycket mer), och vi siktar på större ökningar framöver.

Det är värt att notera att prestandaförbättringar inte var ett mål för det här arbetet, men det är värt att diskutera med tanke på hur mycket av designen och implementeringen som involverade det. Latensen för att beräkna frågeresultat förbättrades faktiskt i vårt nya system (sannolikt på grund av bättre belastningsbalansering/justering/autoskalning). På motsvarande sätt är latensen för den initiala prenumerationen märkbart bättre. Å andra sidan är latensen för end-to-end-mutationsreflektion (dvs. hur lång tid det tar för en ändring att distribueras till andra sessioner) ungefär densamma. Spår visar att detta sannolikt beror på snedfördelningen i användningen av PubSub-flödet mellan sessionsförmedlare och syncable-loaders (syncable-loaders kan inte hantera en begäran förrän de är uppdaterade med begärans version).

Vad är nästa steg?

Vårt nuvarande system är mycket förbättrat, men det begränsar fortfarande produkthastigheten genom att kräva överväganden kring bakåt-/framåtkompatibilitet vid varje release. Men med alla våra förbättringar av implementeringshastigheten är det nu möjligt att implementera alla våra datamodelländringar tillsammans, vilket eliminerar behovet av att fundera på bakåt-/framåtkompatibilitet. Vi undersöker aktivt hur vi kan bygga detta inom en snar framtid.

En av våra främsta drivkrafter för det här arbetet var prestandaregressioner som är svåra att tillskriva. Det är värt att notera att detta är ett område där vi inte har gjort några betydande förbättringar till följd av det här arbetet. På plussidan är detta nu ett av de största problemen som vi tar itu med framöver. Till skillnad från huvuddelen av det arbete som diskuteras här är det ett betydligt mer tvärfunktionellt problem som involverar produktdomän, datamodell, ramverk och infrastrukturoverväganden. 

synkroniserbara laddararbetarpooler efter funktion

synkroniserbara laddararbetspooler efter funktion

Vi ser fram emot att utforska lösningar över plattformens infrastruktur (t.ex. arbetspooler efter funktion), plattformens ramverk och plattformens verktyg (t.ex. black box-/white box-testning).


Författarens biografi

Arvind Vijayakumar är ingenjör i LunaDb-teamet, där han arbetar med att hjälpa till att bygga och skala Asanas centrala plattform för datainläsning – den kritiska infrastrukturen som säkerställer att användarna alltid ser korrekta, reaktiva och blixtsnabba uppdateringar i våra webbappar och API:er.

Team Shout Outs

Det här arbetet med att skala LunaDb har varit en långvarig teaminsats under de senaste åren, med många medlemmar i LunaDb, både nuvarande och tidigare: Brandon Zhang, Alex Matevish, Sean Wentzel, Eric Walton, Spencer Yu, Sophia Yao, George Ong, Tyler Prete, Koushik Ghosh, Natan Dubitski, Vinodh Chandra Murthy med flera


Fotnoter

  1. Det är värt att notera att vi byggde den innan Facebook gjorde GraphQL tillgängligt som öppen källkod

  2. Det finns fler detaljer i det tidigare inlägget om processuppvärmning, men i korthet förlitar vi oss på att låta synkroniseringspods överskrida gränsen godtyckligt som ett sätt att snabbt skala upp vid lokala trafiktoppar

  3. Vi gör också uppdateringar med en granularitet per objekt snarare än per fält (av historiska skäl). Detta ökar datamängden som är involverad i omladdningar vid ogiltigförklaring.

  4. Vi använder en 128-bitars murmur-hash för att undvika kollisioner.

  5. Det är värt att notera att vi har använt websocket-komprimering under mycket lång tid, vilket förmodligen minskar de effekter som kunden upplever.

  6. Det är värt att notera att vi ursprungligen också hade vissa problem med överbelastningen av nätverkshoppet. Efter en del undersökningar fann vi att det mesta av överbelastningen faktiskt berodde på vårt servicenät (Istio/Envoy) och vi gjorde några fokuserade justeringar för att förbättra prestandan här.

  7. En viktig faktor för bevarat minne var att skriva om vår synkroniserbara lagring på serversidan så att den endast behöll hash-värden för data. Det var inte ett enkelt problem och vi kanske publicerar ett uppföljningsinlägg om det här! Utan det här fick de lagrade kunddata session-brokers att använda betydligt mer minne.

  8. Innan detta kördes alla synkroniseringsservrar på minnesoptimerade instanser. Synkroniserbara laddare drar å andra sidan nytta av nodtyper med mer CPU, såsom blandade instanser.

  9. En anmärkningsvärd egenskap med att köra TS på GraalVM på detta sätt är att det använder en hel del mer kodcache än vad som är vanligt i en mer standardmässig Scala/Java JVM-applikation.

  10. Såvitt vi kan se gör hög polymorfism TS-kompilering dyrare. Vi undersöker att byta till monomorfa gränssnitt och utnyttja rapportering av polymorfa specialiseringar