Blog

Cal.com Cold Start Resolution

Keith WilliamsMay 15, 2023

Achtergrond

De Cal.com-app kon een tijdlang last hebben van lange cold start-laadtijden, variërend van 7 seconden tot 30 seconden in extreme gevallen. Dit gedrag trad door de aard van cold starts niet altijd op, maar het gebeurde vaak genoeg dat het een probleem werd dat we zo snel mogelijk moesten aanpakken.

Voor wie niet bekend is met cold starts: er is genoeg materiaal te vinden dat het probleem in het algemeen uitlegt, dus dat gaan we hier niet behandelen. Met deze blog willen we precies uitleggen wat de grondoorzaak was die specifiek ons systeem trof, veroorzaakt door cold starts in een serverless omgeving.

Bevindingen en experimenten

Laten we om te beginnen kijken naar de volgende log uit Vercel:

Vercel serverless-functie prestatiedata

Voor de functie /api/trpc/[trpc] zien we een Cold Boot Duration van 298ms maar een Execution Duration van 7,57s. Dat is een beetje vreemd, toch? De cold boot zelf is niet het probleem, maar wat er tijdens die cold boot wordt geladen zorgt ervoor dat de Execution Duration flink oploopt.

Waar beginnen we dan om de grondoorzaak te vinden?

Hypothese 1: Functiebundelgrootte

Nadat we de Function Size van 33.1 MB zagen, doken ons team, de community en de mensen van Vercel en Prisma er meteen in en begonnen ze met bundle-analyses en compilation logging om te proberen te achterhalen welke packages onze functies zwaar maakten. Hoewel deze Function Size onder de limiet van 50MB ligt, dachten we dat het nog steeds minstens gedeeltelijk de boosdoener kon zijn.

We identificeerden een paar grote dependencies die werden geladen tijdens het ophalen van onze sessie en die konden worden ingekrompen (PR) en we lazy-loaden onze app-store dependencies (PR).

De prestaties verbeterden wel bij elke request na deze updates, maar het was niet de reductie van meerdere seconden waarop we hadden gehoopt. Wat kon het dan nog meer zijn? Is Prisma traag met verbinden bij een cold start in onze serverless setup?

Hypothese 2: Prisma

We hadden al Prisma Data Proxy in productie draaien om onze connection pooling af te handelen, dus we waren sceptisch dat dat het probleem was, maar we wilden het toch grondig controleren om daar zeker van te zijn.

In een experimentele branch hebben we een test-endpoint opgezet, losgekoppeld van onze API-routes en pagina's, in een aparte app-route (met de nieuwe App Router van Next.js) dat alleen het aantal gebruikers in het systeem ophaalde. De prestaties van deze route bij cold starts konden oplopen tot 1 seconde, wat ons liet zien dat Prisma bij cold starts een kleine vertraging kan hebben, maar opnieuw: het was niet de 7-15 seconden die we gemiddeld zagen.

Hypothese 3: _app en _document

Uit de bundle-analyse die was uitgevoerd, was duidelijk dat onze app heel veel dependencies laadt. Dat bracht ons op het idee om te verminderen wat er bij elke API-request wordt geladen. Omdat API-routes in Next.js het bestand _app.tsx laden, besloten we een PageWrapper te implementeren die alle dependencies bevat die nodig zijn voor niet-API-routes (PR).

Ook dit is een mooie verbetering op het gebied van dependency-optimalisatie, maar we hadden nog steeds lange wachttijden.

Grondoorzaak

Na het uitbrengen van talloze perf-specifieke PR's die hielpen met codeorganisatie, snelheid en bundlegrootte, hadden we de gouden kogel nog steeds niet gevonden. Het was tijd om een stap terug te doen en in termen van bottlenecks te denken, omdat veel prestatieproblemen toch met bottlenecks te maken hebben, toch?

Met die gedachtegang werd een test uitgevoerd om de eenvoudigste tRPC-router in zijn eigen API-route op te splitsen. Voor ons was dat onze publieke router. Na de deployment was het endpoint /api/trpc/viewer.public.i18n nu /api/trpc/public/i18n. We zagen een daling van 15 seconden naar 2 seconden in cold start-tijden voor deze route. Dit leek te bevestigen dat onze enkele tRPC-router inderdaad de bottleneck was.

Simpel gezegd hadden we te veel tRPC-routers die in de hoofdviewer-router zaten, die wordt blootgesteld op de route /api/trpc/[trpc]. Dat betekende dat de allereerste call naar deze functie, of het nu was om i18n-termen te laden of om het schema van een gebruiker op te halen, alle 20 routers en al hun dependencies moest importeren.

Zoals hier te zien is in wat uiteindelijk de PR was die het grootste deel van onze langdurige cold-startproblemen oploste, bestond het vermoeden dat de slots-router te groot was en de oorzaak van de problemen vormde. Helaas zagen we, nadat we de slots-router in zijn eigen Next.js API-route hadden opgesplitst, nog steeds grote bundlegroottes en lange cold start-tijden. Daarom werd deze PR ongewijzigd gelaten terwijl we andere hypotheses onderzochten.

Na de ontdekking dat de i18n-route bij cold starts in 2 seconden laadde, werd de eerder genoemde PR verder uitgebouwd zodat alle tRPC-routers hun eigen API-route kregen. Als je ze opsplitst, maakt Vercel per route aparte functies aan.

Dus waarom lost het opsplitsen van de tRPC-routers in hun eigen Next.js API-routes het probleem op? Het eenvoudige antwoord voor ons geval is dat zelfs als een pagina 5 verschillende tRPC-routes aanroept om data te laden, die gelijktijdig worden uitgevoerd en het effect van het laden van dependencies voor alle routers dus niet wordt opgestapeld. We zien nu nog steeds cold start-tijden van 2-3 seconden op een willekeurige tRPC-route, maar omdat ze door de browser gelijktijdig worden aangeroepen, kan de totale wachttijd 3 seconden zijn, niet 7-15 seconden (of 30 seconden in extreme gevallen).

We beseffen dat er nog meer werk nodig is om deze snelheden nog verder te verlagen, maar de reducties tot nu toe waren aanzienlijk.

Hier is een issue om mogelijke wijzigingen voor tRPC in serverless omgevingen te promoten.

Belangrijkste resultaten

Verschillen in endpointprestaties

Dankbetuigingen

Een grote shout-out en dank aan iedereen die heeft bijgedragen aan het oplossen van dit probleem. Van het schrijven van aangepaste scripts die hielpen bepalen welke dependencies ons systeem vertraagden tot het openen van PR's: we waarderen de steun en de inzet enorm die is geleverd om deze problemen op te lossen. We hopen dat deze blog en de PR's wat inzicht kunnen geven in de experimenten die we hebben uitgevoerd en de grondoorzaak die we uiteindelijk hebben gevonden.

Speciale dank aan Julius Marminge van tRPC.io, Guillermo Rauch, Malte Ubl en Jimmy Lai van Vercel en Lucas Smith. 🙏

Wat is er nu?

Hoewel dit een enorme mijlpaal is, werken we nog steeds hard om cold starts verder te verminderen, maar ook om de self-hosting-ervaring zonder serverless architectuur te verbeteren. Blijf op de hoogte!

Keith Williams

Post by

Keith Williams

Head of Engineering