
No daré una definición de arrendamiento múltiple, ya han escrito sobre esto varias veces aquí y aquí . Y es mejor ir directamente al tema del artículo y comenzar con las siguientes preguntas:
¿Por qué la aplicación no es inmediatamente multiempresa?
Sucede que la aplicación se desarrolla inicialmente para la instalación solo en el lado del cliente. Puede llamar a dicha aplicación en caja o software como producto . Un cliente compra una caja e implementa la aplicación en sus servidores (hay muchos ejemplos de tales aplicaciones).
Pero con el tiempo, la compañía desarrolladora puede pensar que sería bueno colocar la aplicación en la nube para que se alquile (software como servicio). Este método de implementación tiene ventajas tanto para los clientes como para la empresa desarrolladora. Los clientes pueden obtener rápidamente un sistema de trabajo y no preocuparse por la implementación y la administración. Al alquilar una aplicación, no necesita grandes inversiones únicas.
Y la empresa desarrolladora recibirá nuevos clientes, así como nuevas tareas: implementar la aplicación en la nube, administrar, actualizar a nuevas versiones, migrar datos durante la actualización, copia de seguridad de datos, monitoreo de velocidad y errores, solucionar problemas si ocurren.
¿Por qué la aplicación en la nube debe ser multiinquilino?
Para colocar una aplicación en la nube, no es necesario que sea multiinquilino. Pero luego habrá el siguiente problema: para cada cliente, deberá implementar un soporte dedicado en la nube con la aplicación arrendada, y esto ya es costoso, tanto en términos de consumo de recursos de soporte en la nube como en términos de administración. Es más rentable implementar la tenencia múltiple en la aplicación para que una instancia pueda servir a varios clientes (organizaciones).
Si la aplicación atrae a 1000 usuarios que trabajan simultáneamente, es ventajoso agrupar clientes (organizaciones) para que en total den la carga deseada de 1000 usuarios por instancia de aplicación. Y luego habrá el consumo más óptimo de recursos en la nube.
Supongamos que una organización alquila una aplicación para 20 usuarios (empleados de la organización). Luego, necesita agrupar 50 de estas organizaciones para alcanzar la carga correcta. Es importante aislar las organizaciones entre sí. Una organización alquila una aplicación, permite que solo sus empleados vayan allí, almacena solo sus datos y no ve que otras organizaciones también sean atendidas por la misma aplicación.
La implementación de arrendamiento múltiple no significa que la aplicación ya no se pueda implementar localmente en el servidor de la organización. Puede admitir dos métodos de implementación al mismo tiempo:
- aplicación multiinquilino en la nube;
- aplicación de inquilino único en el servidor del cliente.
Nuestra aplicación ha tenido un camino similar: de no inquilino a multiinquilino. Y en este artículo compartiré algunos enfoques para desarrollar la tenencia múltiple.
¿Cómo implementar el arrendamiento múltiple en una aplicación que está diseñada como no arrendatario?
Limitaremos inmediatamente el tema, solo consideraremos el desarrollo, no tocaremos temas de prueba, lanzamiento de una versión, implementación y administración. En todas estas áreas, también debe tenerse en cuenta la aparición de la tenencia múltiple, pero por ahora solo hablaremos sobre el desarrollo.
Para comprender qué es una aplicación que no era de inquilinos y se convirtió en multicliente, describiré su propósito, una lista de servicios y tecnologías utilizados.
Este es un sistema ECM (DirectumRX), que consta de 10 servicios (5 servicios monolíticos y 5 microservicios). Todos estos servicios se pueden colocar en un servidor potente o en varios servidores.
Los servicios son- Servicio web: para dar servicio a clientes web (navegadores).
- Servicio WCF: para dar servicio a clientes de escritorio (aplicaciones WPF).
- Servicio para aplicaciones móviles.
- Servicio para realizar procesos en segundo plano.
- Servicio de planificación de procesos en segundo plano.
- Servicio de ejecución de esquema de flujo de trabajo
- Servicio de ejecución de bloque de flujo de trabajo
- Servicio de almacenamiento de documentos (datos binarios).
- Servicio para convertir documentos a html (vista previa en un navegador).
- Servicio para almacenar resultados de conversión en html
Pila de tecnologías utilizadas:
.NET + SQLServer / Postgres + NHibernate + IIS + RabbitMQ + Redis
Entonces, ¿qué hacer para que los servicios se conviertan en múltiples inquilinos? Para hacer esto, debe refinar los siguientes mecanismos en los servicios, a saber, agregar conocimiento sobre los inquilinos a:
- almacenamiento de datos;
- ORM;
- almacenamiento en caché de datos;
- procesamiento de solicitudes;
- procesamiento de mensajes en cola;
- configuración
- tala
- realizar tareas de fondo;
- interacción con microservicios;
- interacción con el agente de mensajes.
En el caso de nuestra aplicación, estos fueron los principales lugares que requirieron mejoras. Consideremos por separado.
Elegir un método de almacenamiento de datos
Cuando lee artículos sobre multicliente, lo primero que están resolviendo es cómo organizar el almacenamiento de datos. De hecho, el punto es importante.
Para nuestro sistema ECM, el almacenamiento principal es una base de datos relacional, que tiene alrededor de 100 tablas. ¿Cómo organizar el almacenamiento de datos de muchas organizaciones para que la organización A no vea de ninguna manera los datos de la organización B?
Se conocen varios esquemas (ya se ha escrito mucho sobre estos esquemas):
- cree su propia base de datos para cada organización (para cada inquilino);
- use una base de datos para todas las organizaciones, pero para cada organización haga su propio esquema en la base de datos;
- use una base de datos para todas las organizaciones, pero agregue una columna "clave de inquilino / organización" en cada tabla.
La elección del esquema no es accidental. En nuestro caso, es suficiente considerar los casos de administración del sistema para comprender la opción preferida. Los casos son los siguientes:
- agregar inquilino (una nueva organización alquila un sistema);
- eliminar inquilino (la organización se negó a alquilar);
- transferir el inquilino a otro soporte de nube (redistribuir la carga entre los soportes de nube cuando un soporte deja de hacer frente a la carga).
Considere un caso de transferencia de inquilino. La tarea principal de la transferencia es transferir los datos de la organización a otro stand. La transferencia no es difícil de hacer si el inquilino tiene su propia base de datos, pero será un dolor de cabeza si combina los datos de diferentes organizaciones en 100 tablas. Intente extraer solo los datos necesarios de las tablas, transferirlos a otra base de datos, donde ya hay datos de otros inquilinos, y para que sus identificadores no se crucen.
El siguiente caso es la incorporación de un nuevo inquilino. El caso tampoco es simple. Agregar inquilinos es la necesidad de completar directorios del sistema, usuarios, derechos, para que pueda iniciar sesión en el sistema. Esta tarea se resuelve mejor clonando una base de datos de referencia, que ya tiene todo lo que necesita.
El caso de eliminación de inquilinos se resuelve muy fácilmente deshabilitando la base de datos de inquilinos.
Por estas razones, elegimos un esquema: un inquilino - una base de datos .
ORM
Elegimos el método de almacenamiento de datos, la siguiente pregunta: ¿cómo enseñarle a ORM a trabajar con el esquema seleccionado?
Usamos Nhibernate. Se requería que Nhibernate trabajara con varias bases de datos y cambiara periódicamente a la correcta, por ejemplo, dependiendo de la solicitud http. Si procesamos la solicitud de la organización A, se utilizó la base de datos A, y si la solicitud es de la organización B, entonces la base de datos B.
NHibernate tiene esa oportunidad. Debe anular la implementación de NHibernate.Connection.DriverConnectionProvider . Cada vez que NHibernate quiere abrir una conexión de base de datos, llama a DriverConnectionProvider para obtener una cadena de conexión. Aquí lo reemplazaremos con el necesario:
public class MyDriverConnectionProvider : DriverConnectionProvider { protected override string ConnectionString => TenantRegistry.Instance.CurrentTenant.ConnectionString; }
¿Qué es TenantRegistry.Instance.CurrentTenant que contaré un poco más tarde?
Almacenamiento en caché de datos
Los servicios a menudo almacenan datos en la memoria caché para minimizar las consultas a la base de datos o no para calcular la misma cosa muchas veces. El problema es que los inquilinos deben desglosar los cachés si los datos del inquilino se almacenan en caché. No es aceptable que el caché de datos de una organización se use al procesar una solicitud de otra organización. La solución más simple es agregar un identificador de inquilino a la clave de cada caché:
var tenantCacheKey = cacheKey + TenantRegistry.Instance.CurrentTenant.Id;
Este problema debe recordarse al crear cada caché. Hay muchos cachés en nuestros servicios. Para no olvidar tener en cuenta el identificador de inquilino en cada uno, es mejor unificar el trabajo con cachés. Por ejemplo, cree un mecanismo de almacenamiento en caché general que se almacene en la memoria caché de fábrica en el contexto de los inquilinos.
Registro
Tarde o temprano, algo saldrá mal en el sistema, deberá abrir el archivo de registro y comenzar a estudiarlo. La primera pregunta es: ¿en nombre de qué usuario y qué organización se comprometieron estas acciones?
Es conveniente cuando cada línea del registro tiene un identificador de inquilino y un nombre de usuario de inquilino. Esta información se vuelve tan necesaria como, por ejemplo, el tiempo del mensaje:
2019-05-24 17:05:27.985 <message> [User2 :Tenant1] 2019-05-24 17:05:28.126 <message> [User3 :Tenant2] 2019-05-24 17:05:28.173 <message> [User4 :Tenant3]
El desarrollador no debe pensar qué inquilino debe escribir en el registro, debe ser automático, oculto "bajo el capó" del sistema de registro.
Usamos NLog, así que daré un ejemplo al respecto. La forma más fácil de proteger el identificador de inquilino es crear NLog.LayoutRenderers.LayoutRenderer , que le permite obtener un identificador de inquilino para cada entrada de registro:
[LayoutRenderer("tenant")] public class TenantLayoutRenderer : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append(TenantRegistry.Instance.CurrentTenant.Id); } }
Y luego use este LayoutRenderer en la plantilla de registro:
<target layout="${odate} ${message} [${user} :${tenant}]"/>
Ejecución de código
En los ejemplos anteriores, a menudo utilicé el siguiente código:
TenantRegistry.Instance.CurrentTenant
Es hora de decir lo que eso significa. Pero primero debe comprender el enfoque que seguimos en los servicios:
Cualquier ejecución de código (procesar una solicitud http, procesar un mensaje de cola, realizar una tarea en segundo plano en un hilo separado) debe estar asociada con algún inquilino.
Esto significa que en cualquier lugar de la ejecución del código se puede preguntar: "¿Para qué inquilino funciona este hilo?" o de otra manera, "¿Cuál es el inquilino actual?"
TenantRegistry.Instance.CurrentTenant es el inquilino actual para la secuencia actual. Stream y el inquilino se pueden vincular en nuestras aplicaciones. Se conectan temporalmente, por ejemplo, al procesar una solicitud http o al procesar un mensaje de la cola. Una forma de vincular al inquilino a una secuencia se realiza así:
Un inquilino vinculado a un hilo se puede obtener en cualquier parte del código, contactando a TenantRegistry ; este es un punto único , un punto de acceso para trabajar con inquilinos. Por lo tanto, Nhibernate y NLog pueden acceder a este singleton (en los puntos de extensión) para averiguar la cadena de conexión o el identificador de inquilino.
Tareas de fondo
Los servicios a menudo tienen tareas en segundo plano que deben realizarse en un temporizador. Las tareas en segundo plano pueden acceder a la base de datos de la organización, y luego la tarea en segundo plano debe realizarse para cada inquilino. Para hacer esto, no es necesario iniciar un temporizador o subproceso por separado para cada inquilino. Es posible realizar una tarea en diferentes inquilinos dentro de un solo hilo / temporizador. Para hacer esto, en el controlador del temporizador, clasificamos los inquilinos, asociamos cada inquilino con una secuencia y realizamos una tarea en segundo plano:
No se pueden unir dos inquilinos al flujo al mismo tiempo; si adjuntamos uno, el otro se separa del flujo. Utilizamos activamente este enfoque para no producir hilos / temporizadores para tareas en segundo plano.
Cómo correlacionar una solicitud http con un inquilino
Para procesar la solicitud http del cliente, debe saber de qué organización vino. Si el usuario ya está autenticado, el identificador de inquilino se puede almacenar en la cookie de autenticación (si el trabajo con la aplicación se realiza a través del navegador) o en el token JWT. Pero, ¿qué pasa si el usuario aún no se ha autenticado? Por ejemplo, un usuario anónimo ha abierto un sitio web de aplicación y desea autenticarse. Para hacer esto, envía una solicitud con un nombre de usuario y contraseña. ¿En la base de datos de qué organización buscar este usuario?
Además, se recibirán solicitudes anónimas para obtener la página de inicio de sesión en la aplicación, y puede diferir para diferentes organizaciones, por ejemplo, el idioma de localización.
Para resolver el problema de la correlación de la solicitud anónima de http y la organización (inquilino), utilizamos subdominios para organizaciones. El nombre del subdominio está formado por el nombre de la organización. Los usuarios deben usar el subdominio para trabajar con el sistema:
https://company1.service.com https://company2.service.com
El mismo servicio web multiinquilino está disponible en estas direcciones. Pero ahora el servicio comprende de qué organización vendrá una solicitud http anónima, centrándose en el nombre de dominio.
El enlace del nombre de dominio y el inquilino se realiza en el archivo de configuración del servicio web:
<tenant name="company1" db="database1" host="company1.service.com" /> <tenant name="company2" db="database2" host="company2.service.com" />
Sobre la configuración de servicios se describirá a continuación.
Microservicios Almacenamiento de datos
Cuando dije que el sistema ECM necesita 100 tablas, hablé de los servicios monolíticos. Pero sucede que un microservicio requiere un almacenamiento relacional, en el que se necesitan 2-3 tablas para almacenar sus datos. Idealmente, cada microservicio tiene su propio almacenamiento, al que solo tiene acceso. Y el microservicio decide cómo almacenar datos en el contexto de los inquilinos.
Pero fuimos por el otro lado: decidimos almacenar todos los datos de la organización en una base de datos. Si un microservicio requiere almacenamiento relacional, entonces utiliza la base de datos de la organización existente para que los datos no estén dispersos en diferentes almacenes, sino que se recopilen en una base de datos. Los servicios monolíticos usan la misma base de datos.
Los microservicios funcionan solo con sus tablas en la base de datos, y no intente trabajar con tablas de un monolito u otro microservicio. Hay ventajas y desventajas de este enfoque.
Pros:
- datos de la organización en un solo lugar;
- fácil de respaldar y restaurar datos de la organización;
- En la copia de seguridad, los datos de todos los servicios son consistentes.
Contras:
- una base de datos para todos los servicios es estrecha cuando se escala (aumentan los requisitos para los recursos DBMS);
- Los microservicios tienen acceso físico a las tablas de los demás, pero no utilizan esta función.
Microservicios No siempre se requiere conocimiento de los inquilinos.
Un microservicio puede no saber que funciona en un entorno multiinquilino. Considere uno de nuestros servicios, que se dedica a convertir documentos a html.
Qué hace el servicio:
- Toma un mensaje de una cola RabbitMQ para convertir un documento.
- recupera el identificador del documento y el identificador del inquilino del mensaje
- Descargue un documento de un servicio de almacenamiento de documentos.
- para esto genera una solicitud en la que transmite el identificador del documento y el identificador del inquilino
- Convierte un documento a html.
- Da html al servicio para almacenar resultados de conversión.
El servicio no almacena documentos y no almacena resultados de conversión. Tiene conocimiento indirecto de los inquilinos: el identificador del inquilino pasa por el servicio en tránsito.
Microservicios No se necesitan subdominios
Escribí anteriormente que los subdominios ayudan a resolver el problema de las solicitudes anónimas de http:
https://company1.service.com https://company2.service.com
Pero no todos los servicios funcionan con solicitudes anónimas, la mayoría requiere autenticación ya aprobada. Por lo tanto, los microservicios que funcionan a través de http a menudo no les importa de qué nombre de host proviene la solicitud, reciben toda la información sobre el inquilino desde el token JWT o la cookie de autenticación que viene con cada solicitud.
Configuracion
Los servicios deben configurarse para que conozcan a los inquilinos. A saber:
- especifique las cadenas para conectarse a la base de datos de inquilinos;
- enlazar nombres de dominio a inquilinos;
- especifique el idioma predeterminado y la zona horaria del inquilino.
Los inquilinos pueden tener muchas configuraciones. Para nuestros servicios, establecemos configuraciones de inquilinos en archivos de configuración xml. Esto no es web.config ni app.config. Este es un archivo xml separado, cuyos cambios deben poder capturarse sin reiniciar los servicios para que agregar un nuevo inquilino no reinicie todo el sistema.
La lista de configuraciones es algo como esto:
<block name="TENANTS"> <tenant name="Jupiter" db="DirectumRX_Jupiter" login="admin" password="password" hyperlinkUriScheme="jupiter" hyperlinkFileExtension=".jupiter" hyperlinkServer="http://jupiter-rx.directum.ru/Sungero" helpAddress="http://jupiter-rx.directum.ru/Sungero/help" devHelpAddress="http://jupiter-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="admin@jupiter-company.ru" feedbackEmail="support@jupiter-company.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="5" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> <tenant name="Mars" db="DirectumRX_Mars" login="admin" password="password" hyperlinkUriScheme="mars" hyperlinkFileExtension=".mars" hyperlinkServer="http://mars-rx.directum.ru/Sungero" helpAddress="http://mars-rx.directum.ru/Sungero/help" devHelpAddress="http://mars-rx.directum.ru/Sungero/dev_help" language="Ru-ru" isAttributesSignatureAbsenceAllowed="false" endorsingSignatureLocksSignedProperties="false" administratorEmail ="root@mars-ooo.ru" feedbackEmail="support@mars-ooo.ru" isSendFeedbackAllowed="true" serviceUserPassword="password" utcOffset="-1" collaborativeEditingEnabled="false" collaborativeEditingForced="false" /> </block>
Cuando una nueva organización alquila un servicio, necesita agregar un nuevo inquilino al archivo de configuración. Y es deseable que otras organizaciones no sientan esto. Idealmente, no debería haber un reinicio de los servicios.
En nosotros, no todos los servicios pueden recoger una configuración sin reiniciar, pero los servicios más críticos (monolitos) pueden hacerlo.
Resumen
Cuando una aplicación se convierte en multiinquilino, parece que la complejidad del desarrollo ha aumentado dramáticamente. Pero luego te acostumbras a la multiempresa y tratas su apoyo como un requisito normal.
También vale la pena recordar que la tenencia múltiple no es solo desarrollo, sino también pruebas, administración, implementación, actualización, copias de seguridad, migraciones de datos. Pero mejor sobre ellos en otro momento.