logo
Menu
Arranque en frío: 30% más rápido con una línea de código

Arranque en frío: 30% más rápido con una línea de código

Los arranques en frío de AWS Lambda pueden mitigarse con paquetes de despliegue más pequeños usando empaquetadores como ncc sin afectar costos.

Published Mar 25, 2024
Last Modified Apr 2, 2024

Introducción

Hola a todos comunidad, en este artículo abordaremos los arranques en frío de las funciones Lambda, un aspecto recurrente y "crítico" dentro del ámbito serverless. Analizaremos cómo el tiempo de respuesta al invocar nuestras funciones se ve afectado, dado que estos servicios generan entornos de ejecución efímeros, los cuales necesitan un proceso de creación y configuración antes de su utilización.

Entornos de ejecución de funciones lambda

Para este artículo, asumiré que están familiarizados con AWS Lambda, pero es pertinente repasar su funcionamiento. AWS Lambda nos facilita ejecutar código en respuesta a eventos, como llamadas HTTP, al sincronizarse con otros servicios, manteniendo una gestión integral por parte de AWS en términos de escalabilidad, actualizaciones de seguridad y administración de la infraestructura subyacente. Esto permite que los equipos de desarrollo se concentren exclusivamente en la codificación.
En este contexto, AWS Lambda crea entornos de ejecución aislados que agrupan todos los elementos necesarios para ejecutar nuestro código. Esto abarca un paquete de despliegue (código más dependencias), el entorno de ejecución, el motor de ejecución, extensiones para personalizar el entorno, y un sistema de APIs que facilita la interacción entre los componentes del entorno de ejecución y el servicio de AWS Lambda.
La creación de un entorno de ejecución forma parte de una serie de fases. AWS Lambda, en su documentación, define 3 fases secuenciales del ciclo de vida de los entornos de ejecución. La fase de inicio (INIT), la fase de invocación (INVOKE) y la fase de apagado (SHUTDOWN), cada una con sus "sub-fases" que igualmente deben ser concluidas antes de avanzar de una fase a otra.

Ciclo de vida de los entornos de ejecución

Podemos entender el ciclo de vida como el viaje en un coche. Este coche hipotético tiene toda la maquinaria necesaria para avanzar, sin embargo, está apagado y el dueño del coche drena la gasolina cada vez que se apaga el coche.

Fase de inicio (INIT):

  • Llenado del tanque (paquete .zip de despliegue y layers): Así como se llena el tanque de gasolina de un coche, en Lambda, se descarga el código desde la fuente, que puede ser AWS Lambda, un bucket de S3 o una imagen de ECR si se usa un contenedor de Docker para el despliegue.
  • Aparatos periféricos (extensiones): Equiparables a los sistemas eléctricos y sensores en un coche, las extensiones en Lambda añaden funcionalidades o monitoreo adicional.
  • Preparación del motor (runtime): Similar a preparar el motor de un coche, AWS Lambda prepara el runtime que ejecutará nuestro código.
  • Preparación del motor (ejecución del código de iniciación): Es importante dejar que el motor caliente antes de conducir, así como en Lambda, el código que envuelve a la función handler se carga y se ejecuta.

Fase de invocación (INVOKE):

  • Conducción del coche (ejecución del código en el handler): Una vez que el coche está en marcha y todo está listo, se puede conducir hacia el destino. En Lambda, esto equivale a la ejecución del handler para procesar los eventos entrantes.

Fase de inactividad (no es una fase tal cuál):

  • Detención sin apagar el coche (periodo donde no se ejecuta el código): Si el coche se detiene sin apagarse, se puede reanudar la marcha sin esperar que se caliente de nuevo. En Lambda, tras una invocación, el entorno de ejecución puede mantenerse "caliente", facilitando invocaciones subsiguientes más rápidas.

Fase de apagado (SHUTDOWN):

  • Apagar el coche (eliminar el entorno de ejecución): Al igual que se apaga el coche después de esperar mucho tiempo para no gastar gasolina, AWS Lambda terminará el entorno de ejecución después de un período de inactividad, liberando los recursos.
Esta última fase del ciclo de vida involucra tener que encender el coche y esperar a que todo el sistema esté listo antes de un nuevo viaje, así como la función lambda tendrá que descargar nuevamente nuestro código, iniciar las extensiones, iniciar el runtime y ejecutar el código de iniciación del handler antes de poder ejecutar el código de nuestro handler.
Fases por las que pasa el entorno de ejecución a través del tiempo.
Execution environment life cycle
La fase INIT se desencadena en tres circunstancias específicas: la primera invocación de la función, cuando un entorno de ejecución previamente existente ha sido eliminado, o ante un escalado horizontal que requiere la creación de nuevos entornos para gestionar un incremento en el tráfico. Estas situaciones implican la generación de nuevos entornos de ejecución, asociados comúnmente con los "arranques en frío", debido a la necesidad de realizar toda la configuración inicial. Por lo tanto, la fase INIT es de gran importancia, ya que en ella podemos realizar optimizaciones que reduzca el tiempo de arranque en frío.

Abordando el tema del tamaño de la función lambda

Numerosos textos en Internet abordan este tema y proponen diferentes concejos para reducir el tiempo de arranque en frío, a pesar de que este tipo de invocaciones suelen representar menos del 1% de las invocaciones totales en un entornos productivos con patrones regulares de invocación. Entre los consejos más comunes están:
Aunque varias de los tips mencionados en esos blogs pueden ser algo "forzados", existe uno en especial que me parece realmente útil y lógico: disminuir el tamaño del archivo .zip de despliegue.

Reducir el tamaño del paquete .zip de despliegue

Reducir el tamaño del archivo de despliegue es crucial para optimizar los tiempos de arranque en frío en AWS Lambda, donde el límite para el paquete de despliegue descomprimido es de 250 MB, y de 50 MB para el comprimido. En entornos Node.js, la carpeta node_modules puede exceder fácilmente los 100 MB, incluso después de eliminar dependencias de desarrollo con npm prune --production.
Imagen humoristica del tamaño de los node_modules
Imagen humoristica del tamaño de los node_modules

Haciendo el empaquetado con vercel/ncc

Herramientas como webpack, esbuild y ncc extraen y empaquetan solo el código esencial en un solo archivo, reduciendo considerablemente el tamaño del paquete de despliegue para proyectos desarrollados en JavaScript. En mi experiencia, esta estrategia ha permitido reducir el tamaño del paquete de despliegue hasta en un 100x menor en entornos productivos, lo que se traduce en una disminución de hasta el 40% en los tiempos de arranque en frío, eliminando así la descarga de módulos innecesarios.
Sin embargo, el lenguaje que más suelo utilizar en mi día a día es TypeScript pero AWS Lambda aún no soporta TypeScript nativamente, esto implica transpilar mi código a JavaScript, . Es crucial entender que la transpilación por sí sola no compacta el código se necesita un empaquetador adicional para este fin.
Nuestro código puede ser transpilado y empaquetado simultaneamente usando ncc de vercel, una librería que permite realizar empaquetados y que trabaja nativamente con TypeScripts. Como mencionan en la documentación solo requerimos dos cosas: un archivo tsconfig.json que utilizará el transpilador de typescript y ejecutar el comando de compilación. El archivo tsconfig.json de por si es necesario en proyectos con typescript ya que sirve para indicar como será transpilado el código, es decir, con qué sintaxis, que sistema de módulos se utilizará, cómo se realizará la resolución del sistema de módulos, se establecen los alias para las dependencias, entre otras cosas.

Código de ejemplo

Para realizar el empaquetado de nuestro paquete de despliegue tenemos muchas opciones, pero ncc de vercel nos permite fácilmente trabajar con TypeScript por lo que podemos crear el empaquetado con una sola línea de código.
Empecemos con un proyecto sencillo, una función lambda desencadenada por un API Gateway con un único EP utilizado para crear un registro de un cliente nuevo. Un ejemplo sencillo, no vale la pena complicarnos en el código, ya que lo interesante de este post será crear justamente el empaquetado.
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { UserService } from "src/service/UserService";
import { UserModel } from "src/model/UserModel";

export const handler = async (event: any) => {
const parsedBody = JSON.parse(event.body);
const { name, email } = parsedBody;
if (!name || !email) {
return {
statusCode: 400,
body: JSON.stringify({ message: "Missing required fields" }),
};
}

const userService = new UserService(UserModel);
const user = await userService.createUser(name, email);
return {
statusCode: 200,
body: JSON.stringify(user),
headers: {
"Content-Type": "application/json",
},
};
};
Como mencioné, es una aplicación muy sencilla que nos permite realizar una inserción a una tabla de DynamoDB de forma muy sencilla al utilizar dynamoose, un ORM que podemos utilizar para interactuar con DynamoDB y que nos permite no tener que involucrarnos con el uso del cliente del SDK.
tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist/"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

Infraestructura como código

A continuación, usaremos serverless framework, una herramienta que nos permite generar nuestra infraestructura como código de forma sencilla a través de la generación de un archivo de configuración en formato yml. El archivo serverless.yml para la creación de los recursos.
serverless.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
service: users-service

provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
region: us-east-1
logs:
httpApi:
format: '{"requestTime":"$context.requestTime","requestId":"$context.requestId","httpMethod":"$context.httpMethod","path":"$context.path","routeKey":"$context.routeKey","status":$context.status,"responseLatency":$context.responseLatency}'
iam:
role:
statements:
- Effect: "Allow"
Action:
- "dynamodb:*"
Resource:
- Fn::GetAtt:
- UsersDynamoDbTable
- Arn
environment:
USERS_TABLE:
Ref: UsersDynamoDbTable

# ----- PARTE IMPORTANTE EMPIEZA -----
functions:
usersCreate:
events:
- httpApi: 'POST /users/create'
handler: dist/index.handler
package:
exclude:
- node_modules/**
- .gitignore
- .git/**
# ----- PARTE IMPORTANTE TERMINA -----

resources:
Resources:
UsersDynamoDbTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: name
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: name
KeyType: HASH
TableName: UsersColdStart
Finalmente, con todo lo anterior configurado adecuadamente, podemos ejecutar este comando para realizar el despliegue:
ncc build src/index.ts -o dist && serverless deploy

Resultados de usar ncc

Y listo, tenemos una lambda empaquetada con un tamaño de paquete de despliegue minimizado, en este caso, 100x menor que sin usar esta técnica. Ahora, esto no necesariamente significa que tiene un impacto del 100x en el tiempo de arranque en frío ya que existen más sub-fases durante la fase INIT, pero podemos medirlo.
Comparacion de arranque en frio
Comparación del tamaño de los paquetes de despliegue
Para esto generé un cron job que se ejecuta cada 5 min y realiza una petición a nuestra lambda de crear usuario desplegado utilizando el empaquetador y otra función que no utiliza el empaquetador, además, fuerzo la fase INIT de la lambda con otro cron job que actualiza las variables de entorno de ambas funciones de crear cliente cada 2 min, lo que provoca que el entorno de ejecución creado sea eliminado y por lo tanto, la lambda siempre tenga que realizar un arranque en frío. El siguiente gráfico compara el arranque en frío de la lambda sin el empaquetador (color azul) y con ncc (color naranja).
Diferencia en el tiempo de arranque en frio
Tiempo de arranque en frio usando bundlers

 
Parece poco tiempo por ejecución, ya que la diferencia es en promedio de 100 milisegundos (aproximadamente un 30% más rápido), pero al final el tiempo ahorrando va sumando hasta convertirse en minutos y después a horas de latencia ahorradas con unas simples líneas de código simplemente con reducir el tamaño del paquete de despliegue.

Costos asociados

Realmente no hay ningún ahorro en costos ya que los arranques en frio no forman parte del tiempo cobrado por la ejecución de nuestras funciones, ya que este solo es sobre el tiempo de invocación.
Obviamente en el ejemplo que realicé para este post existen otros servicios que generan cargos, ya sean las escrituras de DynamoDB o las consultas por API Gateway, sin embargo, el usar un empaquetador no tiene el más mínimo impacto en estos costos.

Conclusión

En resumen, los arranques en frío en AWS Lambda representan un reto en la optimización de la ejecución serverless, causados por la naturaleza efímera de los entornos de ejecución que necesitan ser creados para cada invocación después de periodos de inactividad o en la primera invocación usualmente debido a la necesitad de implementar entornos de ejecución concurrentes causados por un aumento en el tráfico.
Para mitigar este efecto, se recomienda el uso de paquetes de despliegue más pequeños, la optimización de recursos y estrategias como evitar ejecuciones en VPC cuando sea posible. Las herramientas de empaquetado como ncc de Vercel para TypeScript pueden reducir significativamente el tamaño de los paquetes de despliegue, lo cual es crucial en la fase de inicio, y por ende, reduce los tiempos de arranque en frío. Aunque estos arranques en frío representan una pequeña fracción de las invocaciones totales en un entorno productivo, optimizar este proceso puede acumular ahorros significativos en tiempo de ejecución, sin afectar los costos directos, ya que los arranques en frío no se cobran pero sí afectan la latencia general del sistema.
En conclusión, la gestión eficiente de los entornos de ejecución y la optimización de los paquetes de despliegue son esenciales para mejorar la experiencia serverless y minimizar los impactos de los arranques en frío en AWS Lambda.
 

Comments