Blog

Resolução de cold start da Cal.com

Keith WilliamsMay 15, 2023

Contexto

Durante algum tempo, a app da Cal.com podia sofrer de tempos de carregamento longos em cold start, que iam de 7 segundos a 30 segundos, em casos extremos. Este comportamento nem sempre acontecia devido à natureza dos cold starts, mas acontecia com frequência suficiente para se tornar um problema que precisávamos de resolver o mais rapidamente possível.

Para quem não está familiarizado com cold starts, há muitos materiais por aí a explicar o problema de forma geral, por isso não vamos entrar nisso. O que esperamos fazer com este artigo é explicar a causa raiz exata que estava a afetar o nosso sistema em particular, induzida por cold starts num ambiente serverless.

Descobertas e experiências

Para começar, vamos olhar para o seguinte registo da Vercel:

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

Para a função /api/trpc/[trpc], vemos uma Duração do cold boot de 298ms mas uma Duração da execução de 7.57s. Isto é um bocado estranho, não é? O cold boot em si não é o problema; o que está a ser carregado durante o cold boot é que está a fazer com que a Duração da execução aumente bastante. 

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

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

Depois de vermos o Tamanho da função de 33.1 MB, a nossa equipa, a comunidade e as pessoas da Vercel e da Prisma meteram mãos à obra e começaram análises ao bundle e registos de compilação para tentar descobrir que packages estavam a sobrecarregar as nossas funções. Embora este Tamanho da função esteja abaixo do limite de 50MB, acreditávamos que podia ainda assim ser, pelo menos em parte, o culpado.

Identificámos algumas dependências grandes a serem carregadas na recuperação da nossa sessão, o que pôde ser reduzido (PR), e carregámos preguiçosamente as dependências da nossa app store (PR).

O desempenho melhorou em cada pedido após estas atualizações, mas não foi a redução de vários segundos que esperávamos. Então, o que mais poderia ser? Será que o Prisma é lento a ligar-se durante o cold start na nossa configuração serverless?

Hipótese 2: Prisma

Já tínhamos o Prisma Data Proxy a correr em produção para gerir o nosso connection pooling, por isso éramos cépticos de que fosse esse o problema, mas ainda assim queríamos fazer o devido trabalho de verificação para termos a certeza disso.

Num branch experimental, montámos um endpoint de teste separado das nossas rotas de API e páginas numa rota app distinta (usando o novo App Router do Next.js) que apenas devolvia a contagem de utilizadores no sistema. O desempenho desta rota em cold starts podia chegar a 1 segundo, o que nos mostrou que o Prisma pode ter um ligeiro atraso em cold starts, mas, novamente, não eram os 7-15 segundos que estávamos a ver em média.

Hipótese 3: _app e _document

A partir da análise do bundle que tinha sido feita, era claro que a nossa app carrega muitas dependências. Isso deu origem à ideia de reduzir o que era carregado em cada pedido de API. Sabendo que as rotas de API no Next.js carregam o ficheiro _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).

Mais uma vez, isto é uma melhoria interessante em termos de otimização de dependências, mas ainda tínhamos os nossos tempos de espera longos.

Causa raiz

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

Seguindo esta linha de pensamento, foi executado um teste para separar o router tRPC mais simples na sua própria rota de API. No nosso caso, esse era o nosso router público. Após a implementação, 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 desta rota. Isto pareceu confirmar que o nosso único router tRPC era, de facto, o estrangulamento.

Em termos simples, tínhamos demasiados routers tRPC a entrar no router principal de viewer exposto na rota /api/trpc/[trpc]. Isto significava que a primeira chamada a esta função, fosse para carregar termos de i18n ou para obter o horário de um utilizador, precisava de importar todos os 20 routers e todas as respetivas dependências. 

Como se vê aqui no que acabou por ser o PR que resolveu a maior parte dos nossos problemas de cold start longos, havia a suspeita de que o router de slots era demasiado grande e era a origem dos problemas. Infelizmente, depois de separar o router de slots na sua própria rota de API do Next.js, continuávamos a ver tamanhos de bundle grandes e tempos de cold start longos. Por estes motivos, este PR foi deixado intocado enquanto outras hipóteses eram exploradas.

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

Então, porque é que separar os routers tRPC nas 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 correm em paralelo e, por isso, o efeito de carregar dependências de todos os routers não se acumula. Agora ainda podemos ver tempos de cold start de 2-3 segundos em qualquer rota tRPC, mas como são chamadas em paralelo pelo browser, o tempo total de espera pode ser de 3 segundos, e não de 7-15 segundos (ou 30 segundos nos casos extremos).

Reconhecemos que ainda há mais trabalho a fazer para reduzir ainda mais estes tempos, mas as reduções até agora têm sido significativas.

Aqui tens um issue para promover possíveis alterações ao tRPC em ambientes serverless.

Resultados principais

Diferenças no desempenho dos endpoints

Agradecimentos

Um enorme agradecimento e um muito obrigado a todas as pessoas que contribuíram para resolver este problema. Desde escrever scripts personalizados que ajudaram a determinar que dependências estavam a atrasar o nosso sistema até abrir PRs, agradecemos mesmo o apoio e o esforço investidos na resolução destes problemas. Esperamos que este artigo e os PRs possam dar-te alguma visão sobre as experiências que fizemos e a causa raiz que acabámos por encontrar. 

Um agradecimento especial a Julius Marminge da tRPC.io, Guillermo Rauch, Malte Ubl e Jimmy Lai da Vercel e a Lucas Smith. 🙏

O que vem a seguir

Embora esta seja uma conquista enorme, continuamos a trabalhar arduamente para reduzir ainda mais os cold starts, mas também para melhorar a experiência de self-hosting sem uma arquitetura serverless. Fica atento!

Keith Williams

Post by

Keith Williams

Head of Engineering