Blog

Resolución del arranque en frío de Cal.com

Keith WilliamsMay 15, 2023

Contexto

Durante un tiempo, la app de Cal.com podía sufrir tiempos de carga largos en arranque en frío, que iban de 7 segundos a 30 segundos en casos extremos. Este comportamiento no siempre ocurría debido a la naturaleza de los arranques en frío, pero sí ocurría con suficiente frecuencia como para convertirse en un problema que necesitábamos abordar cuanto antes.

Para quienes no estén familiarizados con los arranques en frío, hay muchos materiales por ahí que describen el problema en general, así que no vamos a cubrir eso. Lo que esperamos hacer con este blog es explicar la causa raíz exacta que afectaba a nuestro sistema en particular, provocada por los arranques en frío en un entorno sin servidor.

Hallazgos y experimentos

Para empezar, veamos el siguiente registro de Vercel:

Estadísticas de rendimiento de la función sin servidor de Vercel

Para la función /api/trpc/[trpc], vemos una Duración del arranque en frío de 298ms pero una Duración de ejecución de 7.57s. Es un poco extraño, ¿verdad? El arranque en frío en sí no es el problema, sino lo que se carga durante ese arranque en frío lo que está haciendo que la Duración de ejecución se dispare muchísimo. 

Así que, ¿por dónde empezamos para encontrar la causa raíz de esto?

Hipótesis 1: tamaño del paquete de la función

Tras ver el Tamaño de la función de 33.1 MB, nuestro equipo, la comunidad y la gente de Vercel y Prisma se lanzaron directamente a analizar paquetes y a registrar la compilación para intentar averiguar qué paquetes estaban lastrando nuestras funciones. Aunque este Tamaño de la función está por debajo del límite de 50MB, creíamos que aun así podía ser, al menos en parte, el culpable.

Identificamos algunas dependencias grandes que se cargaban como parte de la recuperación de sesión, algo que pudimos reducir (PR), y cargamos de forma diferida las dependencias de nuestra tienda de apps (PR).

El rendimiento sí mejoró en cada solicitud tras estas actualizaciones, pero no fue la reducción de varios segundos que esperábamos. Entonces, ¿qué más podía ser? ¿Prisma tarda en conectarse durante un arranque en frío en nuestra configuración sin servidor?

Hipótesis 2: Prisma

Ya teníamos Prisma Data Proxy funcionando en producción para gestionar nuestro pool de conexiones, así que sospechábamos que no era eso, pero aun así queríamos hacer nuestra debida diligencia para asegurarnos.

En una rama experimental, levantamos un endpoint de prueba separado de nuestras rutas de API y páginas en una ruta app distinta (usando el nuevo App Router de Next.js) que solo devolvía el número de usuarios del sistema. El rendimiento de esta ruta en arranques en frío podía llegar a 1 segundo, lo que nos mostró que Prisma puede tener un pequeño retraso en arranques en frío, pero de nuevo, no eran los 7-15 segundos que veíamos de media.

Hipótesis 3: _app y _document

A partir del análisis del paquete que se había hecho, estaba claro que nuestra app carga muchas dependencias. Eso nos dio la idea de reducir lo que se cargaba en cada solicitud de API. Sabiendo que las rutas de API en Next.js cargan el archivo _app.tsx, decidimos implementar un PageWrapper que contiene todas las dependencias necesarias para las rutas que no son de API (PR).

De nuevo, esta es una mejora interesante en términos de optimización de dependencias, pero todavía seguíamos teniendo tiempos de espera largos.

Causa raíz

Después de publicar muchos PR específicos de rendimiento que ayudaron con la organización del código, la velocidad y el tamaño del paquete, seguíamos sin encontrar la bala de plata. Era hora de volver al punto de partida y pensar en términos de cuellos de botella, ya que muchos problemas de rendimiento están relacionados con cuellos de botella, ¿no?

Siguiendo esta línea de pensamiento, se ejecutó una prueba para dividir el router tRPC más sencillo en su propia ruta de API. En nuestro caso, era nuestro router público. Tras el despliegue, el endpoint /api/trpc/viewer.public.i18n pasó a ser /api/trpc/public/i18n. Vimos una bajada de 15 segundos a 2 segundos en los tiempos de arranque en frío para esta ruta. Esto parecía indicar que nuestro único router tRPC era efectivamente el cuello de botella.

Dicho simplemente, teníamos demasiados routers tRPC entrando en el router principal de viewer expuesto en la ruta /api/trpc/[trpc]. Eso significaba que la primera llamada a esta función, ya fuera para cargar términos de i18n o para obtener el horario de un usuario, necesitaba importar los 20 routers y todas sus dependencias. 

Como se ve aquí, en lo que finalmente fue el PR que resolvió la mayoría de nuestros problemas de arranque en frío, se sospechó que el router de slots era demasiado grande y era el origen de los problemas. Por desgracia, después de dividir el router de slots en su propia ruta de API de Next.js, seguíamos viendo tamaños de paquete grandes y tiempos largos de arranque en frío. Por estas razones, este PR se dejó intacto mientras se perseguían otras hipótesis.

Tras descubrir que la ruta de i18n cargaba en 2 segundos en arranques en frío, el PR mencionado antes se amplió para que todos los routers tRPC tuvieran su propia ruta de API. Al dividirlos, Vercel crea funciones separadas por ruta.

Entonces, ¿por qué dividir los routers tRPC en sus propias rutas de API de Next.js arregla el problema? La respuesta sencilla, en nuestro caso, es que aunque una página llame a 5 rutas tRPC distintas para cargar datos, se ejecutarán en paralelo y, por tanto, el efecto de cargar dependencias para todos los routers no se acumula. Ahora seguimos pudiendo ver tiempos de arranque en frío de 2-3 segundos en cualquier ruta tRPC dada, pero como el navegador las llama en paralelo, el tiempo de espera total puede ser de 3 segundos, no de 7-15 segundos (o 30 segundos en los casos extremos).

Reconocemos que aún queda más trabajo por hacer para reducir todavía más estas velocidades, pero las reducciones hasta ahora han sido significativas.

Aquí tienes un issue para impulsar posibles cambios en tRPC en entornos sin servidor.

Resultados clave

Diferencias en el rendimiento de los endpoints

Reconocimientos

Un gran reconocimiento y gracias a todas las personas que contribuyeron a resolver este problema. Desde escribir scripts personalizados que ayudaron a determinar qué dependencias estaban lastrando nuestro sistema hasta abrir PRs, agradecemos muchísimo el apoyo y el esfuerzo dedicados a resolver estos problemas. Esperamos que este blog y los PRs puedan aportar algo de contexto sobre los experimentos que hicimos y la causa raíz que finalmente encontramos. 

Gracias especiales a Julius Marminge de tRPC.io, Guillermo Rauch, Malte Ubl y Jimmy Lai de Vercel y a Lucas Smith. 🙏

Qué sigue

Aunque este ha sido un gran logro, seguimos trabajando duro tanto para reducir aún más los arranques en frío como para mejorar la experiencia de autoalojamiento sin una arquitectura sin servidor. ¡Permanece atento!

Keith Williams

Post by

Keith Williams

Head of Engineering