Por

Keith Williams

15 may 2023

Resolución de Inicio en Frío de Cal.com

Contexto

Durante algún tiempo, la aplicación Cal.com podía sufrir tiempos de carga de arranque en frío largos que oscilaban entre 7 segundos y 30 segundos, en casos extremos. Este comportamiento no siempre ocurría debido a la naturaleza de los arranques en frío, pero ocurría con suficiente frecuencia como para que se convirtiera en un problema que necesitábamos resolver lo antes posible.

Para aquellos que no están familiarizados con los arranques en frío, hay muchos materiales que describen el problema en general, por lo que no lo cubriremos. Lo que esperamos hacer con este blog es explicar la causa raíz exacta que estaba afectando a nuestro sistema en particular, que fue inducida por los arranques en frío en un entorno sin servidor.

Hallazgos y Experimentos

Para empezar, veamos el siguiente registro de Vercel:

Para la /api/trpc/[trpc] función, vemos una Duración de Arranque en Frío de 298ms pero una Duración de Ejecución de 7.57s. Esto es un poco raro, ¿verdad? El arranque en frío en sí no es el problema, sino que lo que se carga al arranque en frío está causando que la Duración de Ejecución se inflé considerablemente. 

Entonces, ¿dónde comenzamos a buscar la causa raíz de esto?

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

Después de ver el Tamaño de la Función de 33.1 MB, nuestro equipo, la comunidad y la gente de Vercel y Prisma se sumergieron directamente y comenzaron análisis de paquetes y registros de compilación para tratar de encontrar qué paquetes estaban sobrecargando nuestras funciones. A pesar de que este Tamaño de la Función está por debajo del límite de 50MB, creíamos que todavía podría ser al menos en parte el culpable.

Identificamos algunas dependencias grandes que se cargaban como parte de nuestra recuperación de sesión y logramos simplificarlas (PR) y cargamos de manera diferida nuestras dependencias de la tienda de aplicaciones (PR).

El rendimiento mejoró en cada solicitud después de estas actualizaciones, pero no fue la reducción de varios segundos que esperábamos. Entonces, ¿qué más podría ser? ¿Es Prisma lento para conectarse tras el 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 manejar nuestra agrupación de conexiones, por lo que éramos escépticos de que fuera el problema, pero aun así, queríamos hacer nuestra debida diligencia para asegurarnos de eso.

En una rama experimental, configuramos un punto final de prueba segregado de nuestras rutas de API y páginas en una ruta separada app (usando el nuevo Enrutador de Aplicaciones de Next.js) que solo contaba la cantidad de usuarios en el 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 ligero retraso en los arranques en frío, pero nuevamente, no fue los 7-15 segundos que estábamos viendo en promedio.

Hipótesis 3: _app y _document

Por el análisis de paquetes que se había realizado, estaba claro que nuestra aplicación carga muchas dependencias. Eso provocó 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 contenga todas las dependencias necesarias para rutas que no son de API (PR).

Nuevamente, esta es una buena mejora en términos de optimización de dependencias, pero todavía teníamos nuestros largos tiempos de espera.

Causa Raíz

Después de enviar cargas de PRs específicos de rendimiento que ayudaron con la organización del código, la velocidad y el tamaño del paquete, aún no habíamos encontrado la solución definitiva. Era hora de regresar al principio y pensar en términos de cuellos de botella, ya que muchos problemas de rendimiento están relacionados con cuellos de botella, ¿verdad?

Usando este razonamiento, se realizó una prueba para dividir el router tRPC más simple en su propia ruta de API. Para nosotros, ese fue nuestro router público. Después del despliegue, el punto final /api/trpc/viewer.public.i18n ahora era /api/trpc/public/i18n. Vimos una disminución de 15 segundos a 2 segundos en los tiempos de arranque en frío para esta ruta. Esto parecía sacar a la luz el hecho de que nuestro único router tRPC era, de hecho, el cuello de botella.

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

Visto aquí en lo que fue finalmente el PR que resolvió la mayoría de nuestros largos problemas de arranque en frío, había una sospecha de que el router de slots era demasiado grande y era la raíz de los problemas. Lamentablemente, después de dividir el router de slots en su propia ruta de API de Next.js, seguíamos viendo grandes tamaños de paquete y largos tiempos de arranque en frío. Por estas razones, este PR quedó sin tocar mientras se perseguían otras hipótesis.

Después del descubrimiento de que la ruta de i18n cargaba en 2 segundos en arranques en frío, se construyó sobre el PR mencionado anteriormente para que todos los routers tRPC tuvieran su propia ruta de API. Al ser divididos, Vercel crea funciones separadas por ruta.

Entonces, ¿por qué dividir los routers tRPC en sus propias rutas de API de Next.js soluciona el problema? La respuesta simple, para nuestro caso, es que incluso si una página llama a 5 diferentes rutas tRPC para cargar datos, se ejecutarán concurrentemente y, por lo tanto, el efecto de cargar dependencias de todos los routers no se acumula. Ahora podemos seguir viendo tiempos de arranque en frío de 2-3 segundos en cualquier ruta tRPC dada, pero dado que se llaman concurrentemente por el navegador, el tiempo total de espera puede ser de 3 segundos, no de 7-15 segundos (o 30 segundos en casos extremos).

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

Aquí hay un problema para promover cambios potenciales para tRPC en entornos sin servidor.

Resultados Clave

Differences in Endpoint Performance

Agradecimientos

Un gran agradecimiento y gracias a todos los que contribuyeron a resolver este problema. Desde escribir scripts personalizados que ayudaron a determinar qué dependencias estaban ralentizando nuestro sistema hasta abrir PRs, realmente apreciamos el apoyo y el esfuerzo dedicados a resolver estos problemas. Esperamos que este blog y los PRs puedan proporcionar un poco de información sobre los experimentos que realizamos y la causa raíz que finalmente encontramos. 

Un agradecimiento especial a Julius Marminge de tRPC.io, Guillermo Rauch, Malte Ubl y Jimmy Lai de Vercel y Lucas Smith. 🙏

¿Qué sigue?

Mientras que este ha sido un gran logro, seguimos trabajando arduamente tanto para reducir los arranques en frío aún más como para mejorar la experiencia de autohospedaje sin una arquitectura sin servidor. ¡Estén atentos!