Detrás de las 1.000 ejecuciones simultáneas de Lambda - Cómo funcionan el warm pool de Firecracker y el worker manager
Se explica el mecanismo con el que Lambda gestiona las micro VM dentro del límite predeterminado de 1.000 ejecuciones simultáneas. También se presentan la estrategia de reutilización mediante el warm pool, las decisiones de ubicación del worker manager y las optimizaciones internas que reducen la tasa de cold start.
Ciclo de vida del entorno de ejecución de Lambda
Cuando llega una invocación de una función de Lambda, el servicio Lambda asigna un entorno de ejecución (Execution Environment). El entorno de ejecución es un sandbox que se ejecuta sobre una micro VM de Firecracker y contiene el código de la función, el runtime y las variables de entorno configuradas. El ciclo de vida del entorno de ejecución consta de 3 fases. En la fase INIT, se inicia el runtime y se ejecuta el código del ámbito global situado fuera del handler. En la fase INVOKE, se invoca la función handler. En la fase SHUTDOWN, se ejecuta el proceso de finalización del runtime. Lo importante es que, una vez completada la fase INVOKE, el entorno de ejecución no se destruye de inmediato, sino que se mantiene en estado «congelado» durante cierto tiempo. Cuando llega la siguiente solicitud, el entorno de ejecución congelado se «descongela» y se reutiliza. Esto es un warm start. Un entorno de ejecución en estado congelado no consume tiempo de CPU y solo ocupa memoria. Lambda gestiona un pool de estos entornos de ejecución congelados, y eso es el «warm pool».
Estrategia de gestión del warm pool
La gestión del warm pool de Lambda es un problema de optimización que busca el equilibrio entre minimizar la tasa de cold start y maximizar la eficiencia de los recursos. Si se mantienen los entornos de ejecución en el warm pool durante mucho tiempo, los cold starts disminuyen, pero como siguen ocupando memoria, la capacidad de los hosts físicos se ve presionada. Por el contrario, si se destruyen enseguida, los recursos se liberan, pero los cold starts aumentan. El algoritmo con el que se logra este equilibrio forma parte de la implementación interna de Lambda y no es público. Lo único que está documentado oficialmente son dos puntos: que un entorno de ejecución que ha terminado una invocación se mantiene en estado congelado y se reutiliza para invocaciones posteriores, y que Lambda finaliza ese entorno de ejecución en cualquier momento. Tampoco se ha publicado el tiempo de retención del warm pool, que cambia de forma dinámica según el patrón de invocación de la función y la situación de capacidad de los hosts físicos. Por lo tanto, en el diseño es necesario escribir el código partiendo de la premisa de que es rápido si se reutiliza, pero que funciona correctamente aunque se destruya en cualquier momento. Incluso en una implementación que reutiliza conexiones y clientes inicializados fuera del handler, es seguro tratar una inicialización equivalente a la primera como algo que puede ocurrir en cualquier momento. Por otra parte, Firecracker se publica como código abierto en calidad de base de virtualización ligera que funciona en los servicios Lambda y Fargate, y su diseño se puede leer en un artículo presentado en NSDI 2020.
El worker manager - En qué host físico ubicar
El worker manager de Lambda es el componente que decide en qué host físico (worker) se ubica un nuevo entorno de ejecución. Esta decisión de ubicación necesita satisfacer varias restricciones al mismo tiempo. La primera es que el host físico tenga capacidad libre (CPU, memoria). La segunda es que los entornos de ejecución de un mismo cliente se distribuyan entre varios hosts físicos (aislamiento de fallos). La tercera es que, en las funciones que requieren conexión a una VPC, se pueda obtener una ENI en una subred de la VPC de destino. Antes de la mejora de septiembre de 2019, las funciones de Lambda conectadas a una VPC tenían que esperar a que se completaran la creación y la conexión de una ENI en cada cold start. AWS explicó el problema de entonces como «el tiempo necesario para crear y conectar una interfaz de red alarga los cold starts», y no se ha publicado el número de segundos de retraso añadido. Además, el número de ENI aumentaba en paralelo a la concurrencia. Por ello, también se enfrentaban a la vez a 3 techos: el consumo de direcciones IP de la subred, el límite de ENI por cuenta y el límite de tasa de la API de creación de ENI. En el método actual, las ENI se crean de antemano al crear la función o al actualizar la configuración de VPC. Los entornos de ejecución se conectan mediante un túnel a las ENI gestionadas por Hyperplane (la base de virtualización de funciones de red de AWS que también utilizan Network Load Balancer y NAT Gateway). Como las ENI se comparten entre entornos de ejecución, bastan unas pocas por cada combinación de grupo de seguridad y subred, y el aumento de la concurrencia dejó de traducirse directamente en un aumento de ENI. Con este cambio, los cold starts de las funciones de Lambda conectadas a una VPC pasaron a ser casi equivalentes a los de las funciones sin VPC.
Gestión de la concurrencia y límites de ráfaga
La concurrencia predeterminada de 1.000 por cuenta de Lambda se comparte entre todas las funciones de la región. Si la función A utiliza 800 ejecuciones simultáneas, la función B solo puede utilizar 200. En cambio, la velocidad a la que aumenta la concurrencia (la tasa de escalado) no es compartida, sino por función. La documentación oficial la define como «1.000 instancias de entorno de ejecución cada 10 segundos (o 10.000 solicitudes por segundo cada 10 segundos) por cada región / cada función». El modelo anterior, en el que cada región tenía un margen de ráfaga inicial y después la concurrencia solo crecía en una cantidad fija por minuto, se sustituyó con la revisión de noviembre de 2023. Como el límite pasó a ser por función, aunque una función agote su tasa de escalado, la velocidad de arranque de otra función de la misma región no se ve afectada. Las invocaciones que llegan por encima de esta velocidad se limitan (throttling), lo que produce un error en las invocaciones síncronas y reintentos desde la cola interna en las asíncronas. Como referencia de velocidad, se puede alcanzar el valor predeterminado de 1.000 en 10 segundos, e incluso al pasar de 3.000 a 10.000 tras un aumento del límite, el nivel es de unos 70 segundos. Este límite es un diseño que da a la infraestructura interna, como el arranque de las micro VM de Firecracker, la obtención de ENI y la asignación de capacidad de los hosts físicos, el tiempo necesario para seguir el ritmo de los aumentos bruscos de carga.
Dos generaciones de medidas contra el cold start - Provisioned Concurrency y SnapStart
Existen dos generaciones de medios para evitar el cold start durante el arranque. Provisioned Concurrency es una función que inicializa de antemano un número especificado de entornos de ejecución y los mantiene en estado caliente, y puede responder en milisegundos de dos dígitos sin esperar a las restricciones de la tasa de escalado. La otra es Lambda SnapStart. Con SnapStart, en el momento de publicar una versión de la función, Lambda la ejecuta hasta la inicialización y captura el estado de memoria y disco del entorno de ejecución inicializado como una instantánea de la micro VM de Firecracker. Después, cifra la instantánea y la almacena en caché. En la invocación y en el escalado horizontal, Lambda reanuda el entorno de ejecución a partir de la instantánea en caché en lugar de ejecutar la fase INIT desde cero. Por ello, cambia el papel de la propia inicialización del entorno de ejecución, que es el tema principal de este artículo. Los runtimes compatibles son Java 11 o posterior, Python 3.12 o posterior y .NET 8 o posterior, y SnapStart solo es válido en las versiones publicadas y en los alias que apuntan a ellas. No admite el uso combinado con Provisioned Concurrency, Amazon EFS, Amazon S3 Files ni el almacenamiento efímero de más de 512 MB. Además, una misma instantánea se reutiliza como estado inicial de varios entornos de ejecución. Por ello, es necesario diseñar partiendo de la premisa de que el lado del handler vuelve a generar la unicidad de los ID y números aleatorios generados en la inicialización y el estado de las conexiones de red establecidas en la inicialización.
Qué tener en cuenta al reutilizar entornos de ejecución
La reutilización de entornos de ejecución (warm start) es ventajosa desde el punto de vista del rendimiento, pero tiene efectos secundarios que los desarrolladores deben tener en cuenta. El primero es que se conserva el estado de las variables globales. Los valores de las variables globales establecidos en la invocación anterior permanecen en la siguiente invocación. Aprovechar esto para almacenar en caché las conexiones a la base de datos (agrupación de conexiones) es un patrón recomendado. Sin embargo, si se guardan datos específicos de una solicitud en variables globales, se produce un error por el que los datos de la solicitud anterior se filtran en la siguiente. El segundo es que se conservan los archivos del directorio /tmp. Como los archivos escritos en /tmp en la invocación anterior permanecen, es posible que se agote la capacidad de disco (hasta 10 GB). El tercero es que quedan procesos en segundo plano. Si se inicia un procesamiento asíncrono dentro de la función handler y se devuelve la respuesta sin esperar a que finalice, es posible que ese procesamiento asíncrono siga en ejecución en la siguiente invocación.
Referencias (recursos oficiales de AWS)
Las fuentes primarias de esta página son el sitio web y la documentación oficiales de AWS. Consulta las páginas oficiales siguientes para conocer las especificaciones y los precios más recientes.
Si esta página y la documentación oficial difieren, prevalece la documentación oficial.