
En el pasado articulo se realizaba una pequeña introducción al modelo de arquitectura FaaS, detallando su funcionamiento así como alguna de las ventajas y desventajas que aportaba.
Entre los puntos negativos se citaba la complejidad de gestionar las conexiones a las bases de datos, a consecuencia de la capacidad de escalado de este modelo de solución. Por este motivo, en el presente artículo se pretende detallar algunas de las buenas prácticas para tratar de gestionar esta problemática.
Caso de uso
Se dispone de una función stateless desplegada sobre las 3 grandes plataformas cloud que interacciona con una base de datos relacional. Su ejecución se dispara vía petición HTTP.
Problemática
En un modelo de arquitectura FaaS conviven, al menos, tantas instancias de una función como peticiones simultaneas se registren en dicho momento, y cada una de ellas se ejecuta en un contenedor o máquina virtual totalmente aislada.
Por lo tanto, y a diferencia de una arquitectura de microservicios, la cantidad de conexiones abiertas a la base de datos será proporcional al número de ejecuciones, ya que no comparten un pool con el que reutilizarlas.
Esto, unido a la capacidad de escalado, puede llevar a alcanzar el número máximo de conexiones abiertas de la base de datos y saturarla.
Corrurrencia y aislamiento de recursos
Antes de comenzar a explorar las diferentes alternativas para dar solución a la casuística presentada, es importante conocer como gestiona internamente cada proveedor cloud la concurrencia y aislamiento de recursos en un modelo FaaS.
AWS / GPC
Tanto en la nube de Amazon como en la de Google se crea una instancia para cada ejecución de una función, es decir, en caso de recibir 3 peticiones simultáneas se crearán 3 instancias diferentes. Cada una de ellas estará totalmente aislada del resto y contendrá sus propios recursos de CPU, RAM o almacenamiento.
AWS emplea FireCracker para dar vida a dichas instancias, una tecnología de virtualización open source que se encarga de crear y correr máquinas virtuales ligeras denominadas microVMs, la cuales combinan la propiedades de seguridad y aislamiento de la virtualización con la potabilidad de los contenedores. Gracias a ello, logra resolver los problemas de seguridad que presentan los contenedores y reducir el tiempo de arranque.
Google en cambio opta por Knative, una plataforma open source basada en Kubernetes para crear, desplegar y gestionar funciones. A diferencia de FireCracker, las funciones se ejecutan sobre contenedores.
Azure
En Azure por el contrario, una instancia es capaz de correr múltiples ejecuciones de una función de forma concurrente, de tal manera que comparten recursos entre ellas. Es decir, en caso de recibir 3 peticiones de forma concurrente, una única instancia o host es capaz de dar servicio a todas ellas.
Esta solución presenta tanto aspectos positivos como negativos. Por un lado, el hecho de compartir instancia permite reducir costes, reutilizar conexiones a la base de datos y minimizar la problemática presentada anteriormente.
Por desgracia, el rendimiento se ve fuertemente penalizado si las funciones a ejecutar requieren de un uso intensivo de CPU.
Soluciones
Reutilización de las conexiones
Una forma de minimizar el problema es reutilizar las conexiones entre las distintas ejecuciones en un mismo host. Como se comentaba en el anterior artículo, una vez ejecutado el código el contenedor se queda en estado “warm” durante un periodo de tiempo para no tener que padecer de cold start.
Es posible sacar provecho de ello estableciendo la conexión a la base de datos fuera de la función a ejecutar. En el caso de Amazon y según especifica la documentación de AWS Lambda, cualquier variable fuera de la función se congelará entre invocaciones y podrá ser reutilizada.
const mysql = require('mysql');
var client = mysql.createConnection({
// Connection info
}).connect();
module.exports.handler = (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
client.query('SELECT * FROM USERS', function (error, results) {
callback(null, results)
});
}
De este modo se reutiliza continuamente la misma conexión y se ahorra el tiempo que se requiere para establecerla, lo que se traduce en más de 200ms por ejecución.
Por desgracia, la solución planteada tiene el handicap de que en caso de caerse la conexión con la base de datos, las sucesivas ejecuciones en el mismo contenedor fallarán. Por lo tanto, una mejor aproximación es hacer uso de un pool (limitado a una única conexión) y obtener la conexión del pool en cada ejecución.
const mysql = require('mysql');
const pool = mysql.createPool({
host: {Your Host},
user: {Your Username},
password: {Your Password},
database: {Your Database},
port: 3306
});
module.exports.handler = (event, context, callback) => {
pool.getConnection((err, con)=>{
con.query('SELECT * FROM USERS', function (error, results) {
callback(null, results)
});
}
A pesar de no tratarse de una solución propiamente dicha, sí que ayuda a paliar el problema. Ahora bien, existe un último factor a tener en cuenta, ¿Que ocurre cuando el proveedor cloud mata el contenedor tras varias horas de uso? ¿Cuándo se cierra definitivamente la conexión con la base de datos?
La respuesta es nunca. Por lo tanto y dado que no existe un hook que indique la destrucción del contenedor, será necesario establecer un tiempo de vida máximo reducido para cada conexión.
Cache
Otra buena medida complementaria para paliar la problemática es el uso intensivo de una cache para las consultas de datos más comunes. De nuevo, es importante conocer los protocolos de conexión que soporta el producto seleccionado así como el número máximo de conexiones simultaneas.
Microservicios de acceso al dato
Una solución igualmente valida pero menos elegante es delegar las operaciones de base de datos sobre un microservicio que exponga una API REST.
De esta forma es el microservicio quien se encarga de gestionar las conexiones y operativa de la base de datos y la función en cambio, la que contiene la lógica de negocio más pesada. En resumidas cuentas, se pretende construir una fachada que permita HTTP(s) REST como medio de comunicación con la base de datos.
Esta solución sin embargo acarrea algunos inconvenientes como el hecho de tener siempre levantados dos o más microservicios para garantizar la alta disponibilidad, con los costes que esto acarrea, la gestión de su escalabilidad o el incremento de los tiempos de respuesta y tráfico de red.
Salvando las distancias, el concepto es similar al que propone AWS mediante su Amazon RDS Proxy, un servicio gestionado que se sitúa entre la aplicación y la base de datos relacional para administrar de manera eficiente las conexiones mediante un pool compartido y mejorar la escalabilidad.

HTTP(s)
La solución más empleada y la que se puede encontrar en todas las arquitecturas de referencia es hacer uso de una base de datos que exponga una API REST con la que realizar las operaciones: AWS DinamoDB, AWS Aurora Serverless, Couchbase, Google Cloud Spanner, MongoDB…
Esto implica que no es necesario establecer y mantener una conexión permanente con la base de datos, lo que unido a las capacidades de escalado que ofrecen algunos de los productos en entornos cloud, hace que la capa de persistencia deje de ser el cuello de botella.
Conclusiones
En conclusión, aquellas soluciones de persistencia que ofrecen una API REST se adecuan mejor al modelo FaaS, si bien sigue siendo necesario conocer tanto su modelo de escalado como su coste. Sobra decir que la modalidad “pago por uso” se adapta como anillo al dedo.
En caso de no disponerlo, es conveniente aplicar las medidas comentadas con el fin de paliar el efecto de cuello de botella.
Referencias
Se recomienda encarecidamente leer los siguientes artículos que han servido de base para el escrito:
- Concurrency and Isolation in Serverless Functions
- Best Practices for using AWS Lambda with RDS-RDBMS Solutions
- From 0 to 1000 Instances: How Serverless Providers Scale Queue Processing
- https://www.jeremydaly.com/reuse-database-connections-aws-lambda/
- https://www.jeremydaly.com/manage-rds-connections-aws-lambda/