Blog

Cal.com: solução para cold start

Keith WilliamsMay 15, 2023

Contexto

Por um tempo, o app da Cal.com podia sofrer com longos tempos de carregamento no cold start, variando de 7 segundos a 30 segundos em casos extremos. Esse comportamento nem sempre acontecia por causa da natureza dos cold starts, mas era frequente o bastante para se tornar um problema que precisávamos resolver o quanto antes.

Para quem não está familiarizado com cold starts, existem muitos materiais por aí descrevendo o problema em geral, então não vamos cobrir isso. O que queremos fazer com este blog é explicar a causa raiz exata que afetava especificamente o nosso sistema, causada por cold starts em um ambiente serverless.

Descobertas e Experimentos

Para começar, vamos olhar o log a seguir da Vercel:

Estatísticas de desempenho de função serverless da Vercel

Para a função /api/trpc/[trpc], vemos uma Cold Boot Duration de 298ms, mas uma Execution Duration de 7.57s. Isso é meio estranho, né? O cold boot em si não é o problema; o que está sendo carregado durante o cold boot é que está fazendo a Execution Duration crescer muito.

Então, por onde começamos para encontrar a causa raiz disso?

Hipótese 1: tamanho do bundle da função

Depois de ver o Function Size de 33.1 MB, nossa equipe, a comunidade e o pessoal da Vercel e da Prisma mergulharam direto nisso e começaram análises de bundle e logs de compilação para tentar descobrir quais pacotes estavam pesando nas nossas funções. Embora esse Function Size esteja abaixo do limite de 50MB, acreditávamos que ele ainda poderia ser pelo menos em parte o culpado.

Identificamos algumas dependências grandes sendo carregadas como parte da recuperação da nossa sessão, que puderam ser reduzidas (PR), e fizemos lazy loading das dependências da nossa app store (PR).

O desempenho melhorou em cada requisição depois dessas atualizações, mas não foi a redução de vários segundos que esperávamos. Então, o que mais poderia ser? A Prisma é lenta para conectar no cold start na nossa configuração serverless?

Hipótese 2: Prisma

Já tínhamos a Prisma Data Proxy rodando em produção para lidar com nosso connection pooling, então suspeitávamos que esse não fosse o problema, mas ainda assim queríamos fazer nossa devida diligência para ter certeza disso.

Em uma branch experimental, subimos um endpoint de teste segregado das nossas rotas de API e páginas, em uma rota separada do app (usando o novo App Router do Next.js) que só retornava a contagem de usuários no sistema. O desempenho dessa rota em cold starts podia chegar a 1 segundo, o que nos mostrou que a Prisma pode ter um pequeno atraso em cold starts, mas, de novo, não eram os 7 a 15 segundos que vínhamos vendo em média.

Hipótese 3: _app e _document

A partir da análise de bundle que havia sido feita, ficou claro que nosso app carrega muitas dependências. Isso gerou a ideia de reduzir o que era carregado em cada requisição de API. Sabendo que as rotas de API no Next.js carregam o arquivo _app.tsx, decidimos implementar um PageWrapper que contém todas as dependências necessárias para rotas que não são de API (PR).

De novo, isso é uma boa melhoria em termos de otimização de dependências, mas ainda tínhamos tempos de espera longos.

Causa raiz

Depois de lançar vários PRs focados em performance que ajudaram com organização do código, velocidade e tamanho do bundle, ainda não tínhamos encontrado a bala de prata. Era hora de voltar ao começo e pensar em termos de gargalos, já que muitos problemas de performance estão relacionados a gargalos, certo?

Seguindo essa linha de raciocínio, foi feito um teste para dividir o router tRPC mais simples em sua própria rota de API. No nosso caso, era o nosso router público. Depois do deploy, o endpoint /api/trpc/viewer.public.i18n passou a ser /api/trpc/public/i18n. Vimos uma redução de 15 segundos para 2 segundos nos tempos de cold start dessa rota. Isso pareceu confirmar que o nosso único router tRPC realmente era o gargalo.

Simplificando, tínhamos routers tRPC demais entrando no router principal do viewer exposto na rota /api/trpc/[trpc]. Isso significava que a primeira chamada para essa função, fosse para carregar termos de i18n ou para obter a agenda de um usuário, precisava importar todos os 20 routers e todas as dependências deles.

Visto aqui no que acabou sendo o PR que resolveu a maioria dos nossos problemas de cold start longos, havia a suspeita de que o router de slots era grande demais e era a origem dos problemas. Infelizmente, depois de dividir o router de slots em sua própria rota de API do Next.js, ainda estávamos vendo tamanhos grandes de bundle e tempos longos de cold start. Por esses motivos, esse PR foi deixado intocado enquanto outras hipóteses eram investigadas.

Depois da descoberta de que a rota de i18n carregava em 2 segundos nos cold starts, o PR mencionado acima foi então expandido para que todos os routers tRPC tivessem sua própria rota de API. Quando dividida, a Vercel cria funções separadas por rota.

Então por que dividir os routers tRPC em suas próprias rotas de API do Next.js resolve o problema? A resposta simples, no nosso caso, é que mesmo que uma página chame 5 rotas tRPC diferentes para carregar dados, elas vão rodar em paralelo e, assim, o efeito de carregar dependências de todos os routers não se acumula. Agora ainda podemos ver tempos de cold start de 2 a 3 segundos em qualquer rota tRPC, mas como elas são chamadas em paralelo pelo navegador, o tempo total de espera pode ser de 3 segundos, e não de 7 a 15 segundos (ou 30 segundos nos casos extremos).

Reconhecemos que ainda há mais trabalho a ser feito para reduzir ainda mais esses tempos, mas as reduções até agora foram significativas.

Aqui está uma issue para promover possíveis mudanças no tRPC em ambientes serverless.

Resultados principais

Diferenças no desempenho do endpoint

Agradecimentos

Um grande agradecimento e obrigado a todas as pessoas que contribuíram para resolver esse problema. Desde escrever scripts personalizados que ajudaram a determinar quais dependências estavam sobrecarregando nosso sistema até abrir PRs, realmente agradecemos o apoio e o esforço dedicados a resolver esses problemas. Esperamos que este blog e os PRs possam trazer um pouco de visão sobre os experimentos que rodamos e a causa raiz que acabamos encontrando.

Agradecimentos especiais a Julius Marminge da tRPC.io, Guillermo Rauch, Malte Ubl e Jimmy Lai da Vercel e Lucas Smith. 🙏

O que vem a seguir

Embora isso tenha sido uma grande conquista, ainda estamos trabalhando duro para reduzir ainda mais os cold starts, mas também para melhorar a experiência de self-hosting sem uma arquitetura serverless. Fique ligado!

Keith Williams

Post by

Keith Williams

Head of Engineering