Mejores prácticas para la reutilización de contenedores AWS Lambda

Optimización de los inicios cálidos al conectar AWS Lambda a otros servicios

AWS Lambda proporciona una alta escalabilidad debido a que no tiene servidor ni estado, lo que permite que muchas copias de la función lambda se generen instantáneamente (como se describe aquí). Sin embargo, al escribir el código de la aplicación, es probable que desee acceder a algunos datos con estado. Esto significa conectarse a un almacén de datos, como una instancia de RDS o S3. Sin embargo, conectarse a otros servicios desde AWS Lambda agrega tiempo a su código de función. También puede haber efectos secundarios debido a la alta escalabilidad, como alcanzar el número máximo de conexiones permitidas a una instancia de RDS. Una opción para contrarrestar esto es utilizar la reutilización de contenedores en AWS Lambda para mantener la conexión y reducir el tiempo de ejecución de lambda.

Aquí hay algunos diagramas útiles para explicar el ciclo de vida de una solicitud lambda.

Lo siguiente ocurre durante un arranque en frío, cuando se invoca su función por primera vez o después de un período de inactividad:

  • El código y las dependencias se descargan.
  • Se inicia un nuevo contenedor.
  • El tiempo de ejecución es de arranque.

La acción final es iniciar su código, lo que ocurre cada vez que se invoca la función lambda. Si el contenedor se reutiliza para una invocación posterior de la función lambda, podemos saltar al inicio del código. Esto se denomina inicio en caliente, y este es el paso que podemos optimizar al conectarnos a otros servicios definiendo la conexión fuera del alcance del método del controlador.

Conexión a otros servicios de AWS desde Lambda

Ejemplo: conectarse a la instancia de RDS, los iconos de AWS provienen de aquí

Tenemos un ejemplo básico y común para analizar: queremos conectarnos a un recurso contenedor para obtener datos de enriquecimiento. En este ejemplo, una carga útil JSON viene con una ID y la función Lambda se conecta a una instancia RDS que ejecuta PostgreSQL para encontrar el nombre correspondiente de la ID para que podamos devolver la carga útil enriquecida. Debido a que la función lambda se conecta a RDS, que vive en una VPC, la función lambda ahora también necesita vivir en una subred privada. Esto agrega un par de pasos al arranque en frío: se debe adjuntar una interfaz de red elástica VPC (ENI) (como se menciona en el blog de Jeremy Daly, esto agrega tiempo a sus arranques en frío).

Nota: podríamos evitar usar una VPC si tuviéramos que usar un almacenamiento de clave / valor con DynamoDB en lugar de RDS.

Revisaré dos soluciones para esta tarea, la primera es mi solución "ingenua", mientras que la segunda solución se optimiza para tiempos de inicio cálidos al reutilizar la conexión para invocaciones posteriores. Luego compararemos el rendimiento de cada solución.

Opción 1: conectarse a RDS dentro del controlador

Este ejemplo de código muestra cómo podría abordar ingenuamente esta tarea: la conexión de la base de datos está dentro del método del controlador. Hay una consulta de selección simple para obtener el nombre de la ID antes de devolver la carga útil, que ahora incluye el nombre.

Veamos cómo funciona esta opción durante una pequeña prueba con una ráfaga de 2000 invocaciones con una concurrencia de 20. La duración mínima es de 18 ms con un promedio de 51 ms y un poco más de 1 segundo como máximo (la duración del arranque en frío).

Lambda Duración

El siguiente gráfico muestra que hay un número máximo de ocho conexiones a la base de datos.

Número de conexiones a la base de datos RDS en una ventana de 5 minutos.

Opción 2: usar una conexión global

La segunda opción es definir la conexión como un global fuera del método del controlador. Luego, dentro del controlador, agregamos un control para ver si la conexión existe y solo nos conectamos si no existe. Esto significa que la conexión solo se realiza una vez por contenedor. Establecer la conexión de esta manera con el condicional establecido significa que no necesitamos establecer una conexión si la lógica del código no lo requiere.

Ya no estamos cerrando la conexión a la base de datos, por lo que la conexión permanece para una invocación posterior de la función. La reutilización de la conexión reduce significativamente la duración del arranque en caliente: la duración promedio es aproximadamente 3 veces más rápida y la mínima es de 1 ms en lugar de 18 ms.

Lambda Durations

Conectarse a una instancia de RDS es una tarea que requiere mucho tiempo, y no tener que conectarse para cada invocación es beneficioso para el rendimiento. Cuando nos conectamos a la base de datos para una consulta simple de la base de datos, logramos un recuento máximo de conexiones de la base de datos de 20, que coincide con el nivel de concurrencia (realizamos 20 invocaciones concurrentes x 100 veces). Cuando se detiene el estallido de invocaciones, las conexiones se cierran gradualmente.

Ahora que AWS ha aumentado la asignación de duración de lambda a 15 minutos, esto significa que las conexiones a la base de datos podrían durar más y podría estar en peligro de alcanzar el número máximo de conexiones RDS. Las conexiones máximas predeterminadas se pueden sobrescribir en la configuración del grupo de parámetros RDS, aunque aumentar el número máximo de conexiones podría ocasionar problemas con la asignación de memoria. Las instancias más pequeñas pueden tener un valor predeterminado de max_ connections de menos de 100. Tenga en cuenta estos límites y solo agregue la lógica de la aplicación para conectarse a la base de datos cuando sea necesario.

Uso de una conexión global para otras tareas

Lambda conectando a S3

Una tarea común que podríamos necesitar realizar con Lambda es acceder a los datos con estado desde S3. El fragmento de código a continuación es un plano de la función Python Lambda proporcionado por AWS, al que puede navegar iniciando sesión en la consola de AWS y haciendo clic aquí. Puede ver en el código que el cliente S3 está completamente definido fuera del controlador cuando se inicializa el contenedor, mientras que para el ejemplo de RDS la conexión global se estableció dentro del controlador. Ambos enfoques establecerán las variables globales que les permitirán estar disponibles para invocaciones posteriores.

Fragmento de código de anteproyecto lambda s3-get-object https://console.aws.amazon.com/lambda/home?region=us-east-1#/create/new?bp=s3-get-object-python

Descifrar variables de entorno

La consola lambda le ofrece la opción de encriptar las variables de entorno para mayor seguridad. El siguiente fragmento de código es un ejemplo Java proporcionado por AWS de un script auxiliar para descifrar variables de entorno de una función Lambda. Puede navegar al fragmento de código siguiendo este tutorial (específicamente el paso 6). Debido a que DECRYPTED_KEY se define como una clase global, la función y lógica decryptKey () solo se llama una vez por contenedor lambda. Por lo tanto, veremos una mejora significativa en las duraciones de arranque en caliente.

https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions y https://docs.aws.amazon.com/lambda/latest/dg/tutorial-env_console.html

Uso de variables globales en otras soluciones de FaaS

Este enfoque no está aislado de AWS Lambda. El método de usar una conexión global también se puede aplicar a las funciones sin servidor de otros proveedores de la nube. La página de consejos y trucos de Google Cloud Functions ofrece una buena explicación para las variables no perezosas (cuando la variable siempre se inicializa fuera del método del controlador) versus las variables perezosas (la variable global solo se establece cuando es necesario) variables globales.

Otras mejores prácticas

Aquí hay algunas otras mejores prácticas para tener en cuenta.

Pruebas

Usar FaaS facilita tener una arquitectura de microservicios. Y tener piezas pequeñas y discretas de funcionalidad va de la mano con pruebas de unidad efectivas. Para ayudar a sus pruebas unitarias:

  • Recuerde excluir las dependencias de prueba del paquete lambda.
  • Separe la lógica del método del controlador, como lo haría con un método principal de un programa.

Dependencias y tamaño del paquete

Reducir el tamaño del paquete de implementación significa que la descarga del código será más rápida en la inicialización y, por lo tanto, mejorará los tiempos de arranque en frío. Elimine las bibliotecas no utilizadas y el código muerto para reducir el tamaño del archivo ZIP de implementación. AWS SDK se proporciona para los tiempos de ejecución de Python y JavaScript, por lo que no es necesario incluirlos en su paquete de implementación.

Si Node.js es su tiempo de ejecución Lambda preferido, puede aplicar minificación y uglificación para reducir el tamaño de su código de función y minimizar el tamaño de su paquete de implementación. Algunos, pero no todos, los aspectos de minificación y uglificación pueden aplicarse a otros tiempos de ejecución, por ejemplo. no puede eliminar espacios en blanco del código de Python, pero puede eliminar comentarios y acortar nombres de variables.

Configurar la memoria

Experimente para encontrar la cantidad óptima de memoria para la función Lambda. Paga por la asignación de memoria, por lo que duplicar la memoria significa que debe pagar el doble por milisegundo; pero la capacidad de cómputo aumenta con la memoria asignada, por lo que podría disminuir el tiempo de ejecución a menos de la mitad de lo que era. Ya hay algunas herramientas útiles para seleccionar la configuración de memoria óptima para usted, como esta.

Para concluir…

Una cosa a considerar es si es necesario aplicar el método de reutilización de conexión. Si su función lambda solo se invoca con poca frecuencia, como una vez al día, entonces no se beneficiará de la optimización para arranques en caliente. A menudo hay una compensación entre la optimización del rendimiento y la legibilidad de su código: ¡el término "uglification" habla por sí mismo! Además, agregar variables globales a su código para reutilizar las conexiones a otros servicios puede hacer que su código sea más difícil de rastrear. Se me ocurren dos preguntas:

  • ¿Un nuevo miembro del equipo entenderá su código?
  • ¿Podrán usted y su equipo depurar el código en el futuro?

Pero es probable que haya elegido Lambda por su escala y desee un alto rendimiento y bajos costos, así que encuentre el equilibrio que se ajuste a las necesidades de su equipo.

Estas opiniones son las del autor. A menos que se indique lo contrario en esta publicación, Capital One no está afiliado ni está respaldado por ninguna de las compañías mencionadas. Todas las marcas comerciales y otra propiedad intelectual utilizada o mostrada son propiedad de sus respectivos dueños. Este artículo es © 2019 Capital One.