Creando aplicaciones multi tenant en Azure

Con la explosión de las soluciones del Cloud las aplicaciones se tienen que adecuar a este nuevo modelo de desarrollo. En este artículo se va a presentar una solución teórica, con un ejemplo en ASP.NET de cómo crear una arquitectura multitenancy utilizando los servicios de Azure. Aunque esta solución tiene un ejemplo con una tecnología de Microsoft, es perfectamente válido para otros lenguajes como Java, PHP, Node.js, ect pero se haría de otra manera.

Un poco de historia

Para entender por qué son necesarias este tipo de arquitecturas ahora mismo hay que entender que soluciones se creaban antes del Cloud. Estas soluciones no son muy diferentes de las que se generan en el Cloud pero hay que tener en cuenta lo fácil y sencillo que es crear servicios y consumirlos en el Cloud para entender este modelo.

Si se piensa en un despliegue típico de una solución web que utilice un servidor de base de datos sería algo parecido a esto:

clip_image001

Web Frontent

Una aplicación ASP.NET donde todos los usuarios acceden a la misma url www.contoso.com de servicio e inician sesión con sus credenciales. Esta aplicación consume servicios que el servidor de aplicaciones expone donde está la lógica de negocio del frontal web y finalmente esos datos están almacenados en la base de datos.

Base de datos

El esquema de la base de datos no es importante en sí, pero hay que tener en cuenta como se han dado de alta a los diferentes clientes. Normalmente se tendría una tabla que representa a los clientes dados de alta en el servicio y se hacen relaciones con otras tablas para saber qué datos son específicos de cada cliente. El esquema podría ser algo así.

clip_image003

Como se puede observar en el diagrama, se utiliza un discriminador para filtrar los datos que son específicos de un cliente. Siempre se realiza la asociación utilizando la tabla clientes para tener una relación entre tablas que permita mantener una integridad referencial. Los datos que hay en las tablas son datos de todos los clientes en todo momento.

Rendimiento

Con este modelo de desarrollo todos los usuarios finales operan dentro de la misma base de datos, pero en filas diferentes, lo que permite acceder de forma concurrente. Pero al estar todos los usuarios utilizando la misma base de datos el grado de concurrencia de las tablas es bastante grande.

Conforme el número de usuarios va creciendo la concurrencia en la base de datos aumenta. Pero este crecimiento no es lineal, es decir, que algunos clientes grandes tendrán un impacto en el rendimiento de la base de datos más grande que otros clientes más pequeños, haciendo que todos tengan la misma experiencia.

Multi-tenant

Aquí es donde se puede introducir el concepto de multi tenant. La idea es bastante sencilla, en vez de tener una base de datos para todos los clientes, se tiene una base de datos con el mismo esquema para cada cliente.

Ventajas

Con este mecanismo en el que se tiene una base de datos por cada cliente, permite que si se está usando Azure SQL como base de datos, se pueda escalar cada base de datos de forma independiente entre los clientes consiguiendo así distintos niveles de servicio. De esta manera si se tiene un cliente muy grande con muchos usuarios se podrá elegir una base de datos P4 de 500 DTU y si tiene un cliente pequeño una edición S1 de 20 DTU, pero ambas bases de datos tienen el mismo esquema. De esta manera el coste de operaciones relacionadas con el servicio se puede predecir y se puede cambiar en base a las necesidades del cliente, ya que, Azure SQL permite cambiar de nivel hacia arriba y hacia abajo en todo momento.

Además, se pueden tener escenarios de testeo A/B con los clientes para futuras versiones de la aplicación, haciendo así que los clientes puedan tener diferentes versiones de la aplicación.

Inconvenientes

El modelo multi-tenant no son sólo ventajas, sino que también plantea ciertos desafíos. Uno de los más importantes es la estrategia de actualización de los esquemas / datos de las bases de datos. Como es sabido, con bases de datos en producción con clientes accediendo al sistema, hacer modificaciones en el esquema de la base de datos puede ser un proceso doloroso, si eso lo multiplicamos por todas las bases de datos, el problema puede ser enorme.

En ese sentido, herramientas como Entity Framework Code First y su tecnología de migraciones, hacen posible que cada cambio en el esquema se pueda hacer hacia adelante y hacia atrás de forma pseudo automatizada.

En los escenarios de Cloud la clave siempre es la automatización, piense por ejemplo cual sería el proceso de provisiona un nuevo cliente, se debería de crear una base datos vacía, y en el caso de que se utilice Entity Framework actualizar la base de datos de destino con el esquema y datos necesarios.

Implementación

Para hacer posible que este escenario hay que hacer ciertas modificaciones en el código fuente para dar cabida a este escenario.

El primer cambio de todos consiste en tener un mecanismo que permita identificar al usuario. Este mecanismo se puede implementar de varias maneras, como por ejemplo a través de la url. En este ejemplo:https://cliente1.miservicio.com/ o https://www.miservicio.com/cliente1

En ambas urls, el nombre del cliente forma parte de la url que permite que desde la tecnología de servidor que elijamos seamos capaces de identificar el cliente. Esto también se puede hacer a través de una cookie que se establezca en el proceso de login del usuario final.

¿Para qué se necesita identificar al cliente?

En el desarrollo tradicional que se introducía al principio del artículo, normalmente la cadena de conexión a la base de datos sería una constante en el código fuente o el fichero de configuración. En el caso de ASP.NET el fichero web.config.

Pero en este nuevo escenario la cadena de conexión cambia dependiendo del cliente que este consumiendo el servicio, con lo que tener una única cadena de conexión no es algo que funcione a priori.

Lo que hay que hacer es, en el paso más temprano del ciclo de vida de una petición http identificar al cliente, ir a una base de datos de infraestructura a buscar los datos de conexión del cliente y establecer esa información de forma ambiental durante toda esa petición. De esta manera cuando se quiera establecer una conexión con la base de datos, no se lee el valor de configuración, sino que se obtiene de esta variable ambiental.

Base de datos de infraestructura

En el párrafo anterior se especificaba que hay que usar una base de datos de infraestructura. Esta base de datos, que será común a todos los clientes contiene información del propio servicio, es decir, que clientes están provisionados y cuantos usuarios tienen asociados al servicio. De esa manera en esa base de datos se encontrará una tabla llamada Clients, donde cada una de las filas representa uno de los clientes provisionados. Un posible ejemplo sería este:

Id

Nombre

Base de datos

Version

DateCreated

LastTimeSchemaUpdated

3

Cliente1

Connstring

21

09/10/2015

21/09/2015

De esta manera con el mecanismo anterior de identificación lo que hay que hacer es buscar una fila en la que el nombre cliente coincida con el segmento de la url. Cuando se encuentre el cliente, el siguiente paso es establecer el valor de la cadena de conexión en una variable que se pueda usar durante toda la petición.

clip_image005

Demo en ASP.NET

Para implementar el ejemplo de multi-tenant en ASP.NET utilizando el patrón de urls basads en https://www.miservicio.com/cliente1 (esto se puede cambiar en cualquier momento), se podría hacer de esta manera.

En el ciclo de vida de una petición de ASP.NET hay un evento que se produce cuando empieza la petición que es el momento adecuado para detectar, que cliente es e ir a la base de datos de infraestructura para detectar la cadena de conexión a la base de datos.

 private void OnBeguinRequest(object sender, EventArgs e)
{
    HttpContext context = HttpContext.Current;
    var clientId = context.Request.Url.Segments.LastOrDefault();
    using (MultitenantDbContext dbContext = new MultitenantDbContext())
    {
        var client = dbContext.TentanUsers
            .Where(p => p.Name == clientId)
            .Select(p => p).FirstOrDefault();
        if (client != null)
        {
            context.Items[ClientConnnextionStringKey] = client.ConnnectionString;
        }
        else
        {
            throw new InvalidOperationException("Client not found");
        }
    }
}

Así de esta manera, en un único punto se detecta el cliente, se almacena esa información en la colección de elementos de contexto de Http actual de ASP.NET. En otro tipo de lenguajes o runtimes, la forma de hacer esto, puede variar, pero la idea es la misma.

El siguiente paso es utilizar esta cadena de conexión en el punto que sea necesario, por ejemplo en un controlador donde que se quiera acceder a datos para un determinado cliente.

 public ActionResult Index()
{
    string connextionString = 
    (string)this.HttpContext.Items[MvcApplication.ClientConnnextionStringKey];
    using (ClientDbContext context = new ClientDbContext(connextionString))
    {

    }
    return View();
}

Conclusiones

Esta es una primera aproximación para un escenario multi tenant. A partir de aquí se puede evolucionar el concepto, para un despliegue más complejo. Como se comentó al principio del artículo, gracias a la API de administración del portal de Azure, se puede automatizar el proceso de creación de las base de datos de SQL Azure en el caso de que se tenga que provisionar un nuevo cliente.

Luis Guerrero Guirado.

Technical Evangelist Microsoft Azure.

@guerrerotook