Multitenancy experience

Cómo trabajar con 8 bases de datos sin morir en el intento.

Facundo Rodríguez
Flux IT Thoughts

--

Sin preámbulos: así se me presentó el trabajo para definir la arquitectura del proyecto GDU (Gestión de Usuarios) de Gire Soluciones. ¿El desafío? Trabajar con 8 bases de datos, o N bases de datos.

A nivel funcional, el proyecto requería administrar varias entidades de negocio de otra aplicación (llamada Remote Deposit), soportar un proceso de modificación que permita la autorización o el rechazo de las solicitudes de cambio, y realizar modificaciones con impacto diferido (programadas) cada cierta frecuencia.

Remote Deposit es una aplicación pensada para entidades financieras, que permite realizar el depósito de cheques de forma electrónica, en múltiples bancos. Es normativa del Banco Central, que cada entidad bancaria tenga su propio servidor de base de datos aislado del resto. Con lo cual, Remote Deposit trabaja con varias bases de datos en servidores diferentes.

Partiendo de estos requerimientos, el reto abarcaba una serie de necesidades a cubrir:

  • Interacciones con 8 bases de datos diferentes: una BD por banco (las BD están instaladas en diferentes servidores).
  • No se podía migrar ni modificar la información de las BD.
  • Cada banco tenía que estar aislado de los otros, y por cada uno contábamos con un conjunto de usuarios propio de GDU, más la info legacy propia de Remote Deposit.
  • Diferencias estructurales en algunas tablas de BD, para ciertos bancos.
  • Diferencias funcionales en algunos flujos, para ciertos bancos.
  • Convivir con un framework de autenticación legacy (.Net Membership).

Arquitecto mode-on

Frente a estos desafíos, la idea del cliente que más sonaba antes de mi llegada era: “hay que desarrollar ocho aplicaciones”. ¿Ocho aplicaciones? Pensando en el esfuerzo que se necesita para desarrollar una aplicación, lo que significa darle soporte y mantenimiento, construir ocho en un proyecto corto (de 4–6 meses) era prácticamente una misión imposible.

Como arquitecto, esta parte del reto estaba en mis manos. Fue entonces cuando opté por construir una aplicación multitenant.

Una arquitectura Multitenant se basa en tener una única aplicación que pueda manejar varias fuentes de datos. Cada “tenant” o fuente de datos, tiene su grupo de usuarios, su propia información y privilegios específicos. Además, cada uno de ellos está aislado de los otros.

Si bien el manejo de estas aplicaciones es más complejo, en ciertos casos es la opción más indicada. GDU era uno de esos casos.

La hora de las decisiones

A partir de lo investigado, comencé a tomar algunas decisiones con respecto a la arquitectura de la aplicación.

Decidí construir una única aplicación multitenant que interactúe con los 8 servidores de BD, donde cada tenant:

  • Representa un banco y la conexión a una BD particular.
  • Tiene su propio grupo de usuarios, su propia información.
  • Es independiente de otros tenants.

Además tomé las siguientes decisiones adicionales a nivel aplicación:

  • La aplicación soporta las diferencias entre tablas legacy. Tanto en el backend como en el frontend.
  • La API REST del backend es Stateless.
  • El tenant se codifica con el resto de la información del token JWT.
  • Cualquier cambio estructural de las tablas en la BD de Remote Deposit, a partir de la implementación de GDU, se aplica en todos los esquemas, con el objetivo de eliminar diferencias entre bases de datos y simplificar la lógica de GDU.

Como stack tecnológico decidimos utilizar: para la API REST de backend, Java + Spring Boot (1.5.8.release última versión al inicio del proyecto) incluyendo Spring MVC, Spring Data y Hibernate. Para el frontend web, optamos por AngularJS + UI Router.

Multitenancy con Java y Spring

Para implementar el multitenant en el backend, utilicé AbstractRoutingDataSource de Spring, de forma de redireccionar el datasource a utilizar en cada interacción.

Por cada interacción del usuario con la API REST, implementé un filtro que carga, en el contexto del thread HTTP del servidor que está ejecutando la petición, el tenant a utilizar (ThreadLocal).

En la implementación del AbstractRoutingDataSource simplemente se consulta al TenantContext cuál es el datasource a utilizar.

Cuando inicia la aplicación, se carga la lista de conexiones de BD desde el application.yml o application.properties.

Fragmento del application.yml

Luego, en la configuración de bases de datos de Spring, se crean N pools de conexiones a las diferentes bases de datos configuradas.

Para el pool de conexiones utilice Hikari, que simplifica las reconexiones a BD caídas y tiene buena performance. Además de definir el bean datasource de la aplicación de tipo TenantDataSource (que tiene uno por defecto), y el mapa de posibles datasources a resolver.

Cada vez que un usuario interactúa con la API, se obtiene desde el token JWT cuál es el tenant del usuario, luego de la revisión de seguridad correspondiente. El tenant es cargado en el TenantContext, y de ahí en más, para la ejecución de esa petición, el manejo de transacciones y las interacciones de bases de datos se hacen sobre ese tenant en particular.

Además de esto adapté Liquibase para que también ejecute de forma multitenant, manteniendo sincronizada la ejecución de los changeset/scripts en las N bases de datos.

Al iniciar la aplicación, Liquibase se conecta a los diferentes datasources configurados y aplica los cambios que correspondan en cada BD. Esto simplificó en sobremanera el mantenimiento de los diferentes esquemas de BD y de la aplicación en general.

PoC en sprint 0

El sprint 0 me permitió hacer una prueba de concepto (PoC) y probar el funcionamiento multitenant. En ese momento pude implementar un servicio REST de prueba, para guardar y recuperar información de una entidad que, cambiando el tenant, impacta en diferentes BD.

El tiempo alcanzó para extender Liquibase de forma multitenant, armar el documento de arquitectura de software DAS, y presentarlo al cliente con la seguridad de que el core de la arquitectura funcionaba. Estaba todo listo para arrancar el desarrollo de la aplicación.

Lo bueno, lo malo y lo feo de una aplicación multitenant

Con todo el camino recorrido, después de seis meses de desarrollo en el proyecto, más seis meses de mantenimiento evolutivo y soporte, puedo hacer un balance de lo positivo y lo negativo de implementar este tipo de arquitectura:

Lo bueno

  • Simplificación en el proceso de desarrollo, versionado y despliegue.
  • Una única aplicación a mantener y dar soporte.
  • Extensible: sumar un nuevo tenant es implementar las diferencias que pueda tener con la lógica base. Si no hay diferencias en la lógica base, es sólo modificar configuración.
  • Reducción drástica de costos en el desarrollo y el mantenimiento.
  • El concepto escala no solo para base de datos, sino para cualquier recurso que utilice la aplicación (por ejemplo, Liquibase, LDAP, SMTP, etc.).
  • Sumar/quitar un tenant implica sólo cambios de configuración.
  • Si no existen diferencias funcionales, la lógica de la aplicación es única, y el manejo multitenant es transparente.

Lo malo

  • Cualquier cambio que se introduzca en la aplicación puede afectar a otros tenants, con lo cual es imperioso el testeo de la aplicación.
  • Las modificaciones sobre un tenant requieren testeo en otros, para garantizar la ausencia de bugs.

Lo feo

  • Si existen diferencias funcionales en los tenants, se sobrecarga la lógica de la aplicación para implementar las diferencias.
  • Los casos de test deben ser adaptados para cubrir las diferencias funcionales.
  • Retrabajo de testeo, para garantizar que todos los tenants están funcionando correctamente.

Hitos y aprendizajes

El proyecto GDU ya está en marcha, actualmente con 3 bancos productivos. No tuvimos ningún bug con respecto al manejo multitenant, con la implementación tal como la describí antes. Del camino recorrido en este año de trabajo, puedo nombrar los hitos más importantes:

  1. El hecho de haber contado con un sprint 0 que permita garantizar la viabilidad de la arquitectura fue fundamental.
  2. Otro factor clave fue el hecho normalizar las estructuras de las tablas en todos los esquemas. Un cambio de estructura en alguna tabla de base de datos (para un banco particular), se propaga en todos (con un valor nulo por defecto). Eso nos permitió manejar una lógica común para todos los tenants y simplificar el código de la aplicación.
  3. Por último, las pruebas y el testing de la aplicación fueron parte fundamental, ya que la multiplicidad de tenants suma un factor riesgo importante. Para mitigar ese riesgo, hicimos hincapié en los tests de unidad e integración de la API Rest, terminado el blindaje con los tests automatizados de la aplicación en ambientes controlados.

Tenemos la funcionalidad core de GDU testeada completamente con tests automatizados que se ejecutan ante cualquier modificación implementada, lo que minimiza el riesgo de sumar algún bug en la aplicación.

Conclusiones

No me imagino cómo habría sido el viaje sin haber planteado la arquitectura de GDU de esta forma (seguramente con el mar mucho más picado y algún tsunami en el medio). De algo estoy seguro: gracias a la arquitectura multitenant implementada, en el equipo de GDU llegamos a buen puerto.

Seguiré compartiendo con ustedes más experiencias del mundo de la arquitectura de software. ¡Hasta la próxima nota!

Conocé más sobre Flux IT: Website · Instagram · LinkedIn · Twitter · Dribbble

--

--