Serverless: Managing database connections

En el pasado articulo se realizaba una pequeña introducción al modelo de arquitectura serverless, 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 serverless 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 los procesos serverless. 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, ya que 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 a la anterior es el uso intensivo de la 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, o el incremento de los tiempos de respuesta y tráfico de red.

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: MongoDB, Couchbase, Aws DinamoDB, Google Cloud Data Storage…

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.

Conclusión

En conclusión, aquellas soluciones de persistencia que ofrecen una API REST se adecuan mejor al modelo serverless, 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:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s