Comment faire une application multi-locataire à partir d'une application non-locataire

image


Je ne donnerai pas une définition de la multi-location, ils ont déjà écrit à ce sujet plusieurs fois ici et ici . Et il vaut mieux aller directement au sujet de l'article et commencer par les questions suivantes:


Pourquoi l'application n'est-elle pas immédiatement multi-locataire?


Il arrive que l'application soit initialement développée pour une installation uniquement côté client. Vous pouvez appeler une telle application en boîte ou un logiciel en tant que produit . Un client achète une box et déploie l'application sur ses serveurs (il existe de nombreux exemples de telles applications).


Mais au fil du temps, la société de développement peut penser qu'il serait bien de placer l'application dans le cloud pour qu'elle soit louée (logiciel en tant que service). Cette méthode de déploiement présente des avantages tant pour les clients que pour l'entreprise de développement. Les clients peuvent obtenir rapidement un système fonctionnel et ne pas se soucier du déploiement et de l'administration. Lors de la location d'une application, vous n'avez pas besoin de gros investissements ponctuels.


Et la société de développement recevra de nouveaux clients, ainsi que de nouvelles tâches: déploiement de l'application dans le cloud, administration, mise à jour vers de nouvelles versions, migration des données lors de la mise à jour, sauvegarde des données, surveillance de la vitesse et des erreurs, correction des problèmes s'ils se produisent.


Pourquoi l'application dans le cloud devrait-elle être multi-locataire?


Pour placer une application dans le cloud, il n'est pas nécessaire de la rendre multi-locataire. Mais il y aura ensuite le problème suivant: pour chaque client, vous devrez déployer un stand dédié dans le cloud avec l'application louée, et cela est déjà coûteux, à la fois en termes de consommation des ressources du stand cloud et en termes d'administration. Il est plus rentable d'implémenter l'hébergement multiclient dans l'application afin qu'une seule instance puisse servir plusieurs clients (organisations).


Si l'application attire simultanément 1 000 utilisateurs actifs, il est avantageux de regrouper les clients (organisations) afin qu'au total, ils donnent la charge souhaitée de 1 000 utilisateurs par instance d'application. Et puis, il y aura la consommation la plus optimale de ressources cloud.


Supposons que l'application est louée par une organisation pour 20 utilisateurs (employés de l'organisation). Ensuite, vous devez regrouper 50 de ces organisations afin d'atteindre la bonne charge. Il est important d'isoler les organisations les unes des autres. Une organisation loue une application, autorise uniquement ses employés à y accéder, stocke uniquement ses données et ne voit pas que d'autres organisations sont également desservies par la même application.


L'implémentation de l'hébergement multiclient ne signifie pas que l'application ne peut plus être déployée localement sur le serveur de l'organisation. Vous pouvez prendre en charge deux méthodes de déploiement en même temps:


  • application multi-locataire dans le cloud;
  • application à locataire unique sur le serveur client.

Notre application est venue d'une manière similaire: de non-locataire à multi-locataire. Et dans cet article, je vais partager quelques approches dans le développement de la multi-location.


Comment implémenter l'hébergement multiclient dans une application conçue comme non locataire?


Nous limiterons immédiatement le sujet, nous ne considérerons que le développement, nous n'aborderons pas les problèmes de test, de version, de déploiement et d'administration. Dans tous ces domaines, l'émergence de la multi-location doit également être prise en compte, mais pour l'instant nous ne parlerons que de développement.


Pour comprendre ce qu'est une application qui n'était pas locataire et est devenue multi-locataire, je décrirai son objet, une liste des services et technologies utilisés.


Il s'agit d'un système ECM (DirectumRX), composé de 10 services (5 services monolithiques et 5 microservices). Tous ces services peuvent être placés soit sur un serveur puissant, soit sur plusieurs serveurs.


Les services sont
  • Service Web - pour l'entretien des clients Web (navigateurs).
  • Service WCF - pour la maintenance des clients de bureau (applications WPF).
  • Service pour applications mobiles.
  • Service pour effectuer des processus d'arrière-plan.
  • Service de planification des processus d'arrière-plan.
  • Service d'exécution de schéma de workflow
  • Service d'exécution de bloc de workflow
  • Service de stockage de documents (données binaires).
  • Service de conversion de documents en html (prévisualisation dans un navigateur).
  • Service de stockage des résultats de conversion en html

Pile de technologies utilisées:
.NET + SQLServer / Postgres + NHibernate + IIS + RabbitMQ + Redis


Alors, que faire pour que les services deviennent multi-locataires? Pour ce faire, vous devez affiner les mécanismes suivants dans les services, à savoir ajouter des connaissances sur les locataires à:


  • stockage de données;
  • ORM;
  • mise en cache des données;
  • traitement des demandes;
  • traitement des messages de file d'attente;
  • configuration;
  • enregistrement;
  • effectuer des tâches d'arrière-plan;
  • interaction avec des microservices;
  • interaction avec le courtier de messages.

Dans le cas de notre candidature, c'étaient les principaux endroits qui nécessitaient des améliorations. Examinons-les séparément.


Choisir une méthode de stockage des données


Lorsque vous lisez des articles sur l'hébergement multiclient, la toute première chose qu'ils trient est de savoir comment organiser le stockage des données. En effet, ce point est important.


Pour notre système ECM, le stockage principal est une base de données relationnelle, qui contient environ 100 tables. Comment organiser le stockage des données de nombreuses organisations pour que l'organisation A ne voie en aucune façon les données de l'organisation B?


Plusieurs schémas sont connus (beaucoup a déjà été écrit sur ces schémas):


  • créer votre propre base de données pour chaque organisation (pour chaque locataire);
  • utiliser une base de données pour toutes les organisations, mais pour chaque organisation créer son propre schéma dans la base de données;
  • utilisez une base de données pour toutes les organisations, mais ajoutez une colonne "clé de locataire / d'organisation" dans chaque table.

Le choix du schéma n'est pas accidentel. Dans notre cas, il suffit de considérer les cas d'administration système pour comprendre l'option préférée. Les cas sont les suivants:


  • ajouter un locataire (une nouvelle organisation loue un système);
  • retirer le locataire (l'organisation a refusé de louer);
  • transférer le locataire vers un autre stand cloud (redistribuer la charge entre les stands cloud quand un stand cesse de faire face à la charge).

Prenons un cas de transfert de locataire. La tâche principale du transfert est de transférer les données de l'organisation vers un autre stand. Le transfert n'est pas difficile à faire si le locataire a sa propre base de données, mais ce sera un casse-tête si vous mélangez les données de différentes organisations dans 100 tables. Essayez d'extraire uniquement les données nécessaires des tables, transférez-les dans une autre base de données, où il y a déjà des données d'autres locataires, et afin que leurs identifiants ne se croisent pas.


Le cas suivant est l'ajout d'un nouveau locataire. Le cas n'est pas non plus simple. L'ajout de locataire est la nécessité de remplir les répertoires système, les utilisateurs, les droits, afin que vous puissiez vous connecter au système. Cette tâche est mieux résolue en clonant une base de données de référence, qui contient déjà tout ce dont vous avez besoin.


Le cas de suppression du locataire est très facilement résolu en désactivant la base de données des locataires.


Pour ces raisons, nous avons choisi un schéma: un locataire - une base de données .


ORM


Nous avons choisi la méthode de stockage des données, la question suivante: comment apprendre à l'ORM à travailler avec le schéma sélectionné?


Nous utilisons Nhibernate. Il était nécessaire que Nhibernate fonctionne avec plusieurs bases de données et passe périodiquement à la bonne, par exemple, en fonction de la requête http. Si nous traitons la demande de l'organisation A, alors la base de données A a été utilisée, et si la demande provient de l'organisation B, alors la base de données B.


NHibernate a une telle opportunité. Vous devez remplacer l'implémentation de NHibernate.Connection.DriverConnectionProvider . Chaque fois que NHibernate veut ouvrir une connexion à une base de données, il appelle DriverConnectionProvider pour obtenir une chaîne de connexion. Ici, nous allons le remplacer par le nécessaire:


public class MyDriverConnectionProvider : DriverConnectionProvider { protected override string ConnectionString => TenantRegistry.Instance.CurrentTenant.ConnectionString; } 

Qu'est-ce que TenantRegistry.Instance.CurrentTenant, je le dirai un peu plus tard.


Mise en cache des données


Les services mettent souvent en cache les données afin de minimiser les requêtes de base de données ou de ne pas calculer la même chose plusieurs fois. Le problème est que les caches doivent être décomposés par les locataires si les données des locataires sont mises en cache. Il n'est pas acceptable que le cache de données d'une organisation soit utilisé lors du traitement d'une demande d'une autre organisation. La solution la plus simple consiste à ajouter un identifiant de locataire à la clé de chaque cache:


 var tenantCacheKey = cacheKey + TenantRegistry.Instance.CurrentTenant.Id; 

Ce problème doit être rappelé lors de la création de chaque cache. Il y a beaucoup de caches dans nos services. Afin de ne pas oublier de prendre en compte l'identifiant du locataire dans chacun, il est préférable d'unifier le travail avec les caches. Par exemple, créez un mécanisme de mise en cache général qui mettra en cache hors de la boîte dans le contexte des locataires.


Journalisation


Tôt ou tard, quelque chose ira mal dans le système, vous devrez ouvrir le fichier journal et commencer à l'étudier. La première question est: au nom de quel utilisateur et de quelle organisation ces actions ont-elles été engagées?


C'est pratique lorsque dans chaque ligne du journal il y a un identifiant de locataire et un nom d'utilisateur de locataire. Ces informations deviennent aussi nécessaires que, par exemple, l'heure du message:


 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] 

Le développeur ne doit pas penser au locataire à écrire dans le journal, il doit être automatisé, caché "sous le capot" du système de journalisation.


Nous utilisons NLog, je vais donc vous donner un exemple. La façon la plus simple de sécuriser l'identifiant du locataire est de créer NLog.LayoutRenderers.LayoutRenderer , qui vous permet d'obtenir l'identifiant du locataire pour chaque entrée de journal:


  [LayoutRenderer("tenant")] public class TenantLayoutRenderer : LayoutRenderer { protected override void Append(StringBuilder builder, LogEventInfo logEvent) { builder.Append(TenantRegistry.Instance.CurrentTenant.Id); } } 

Et puis utilisez ce LayoutRenderer dans le modèle de journal:


 <target layout="${odate} ${message} [${user} :${tenant}]"/> 

Exécution de code


Dans les exemples ci-dessus, j'ai souvent utilisé le code suivant:


 TenantRegistry.Instance.CurrentTenant 

Il est temps de dire ce que cela signifie. Mais vous devez d'abord comprendre l'approche que nous suivons dans les services:


Toute exécution de code (traitement d'une demande http, traitement d'un message de file d'attente, exécution d'une tâche en arrière-plan dans un thread séparé) doit être associée à un locataire.

Cela signifie qu'à n'importe quel endroit de l'exécution du code, on peut demander: "Pour quel locataire ce thread fonctionne-t-il?" ou d'une autre manière, "Quel est le locataire actuel?"


TenantRegistry.Instance.CurrentTenant est le locataire actuel pour le flux actuel. Le flux et le locataire peuvent être liés dans nos applications. Ils sont connectés temporairement, par exemple, lors du traitement d'une demande http ou lors du traitement d'un message de la file d'attente. Une façon de lier le locataire à un flux se fait comme suit:


 //    . using (TenantRegistry.Instance.SwitchTo(tenantId)) { // ,     . var tenant = TenantRegistry.Instance.CurrentTenant; //     . var connectionString = tenant.ConnectionString; //  . var id = tenant.Id; } 

Un locataire lié à un thread peut être obtenu n'importe où dans le code, en contactant TenantRegistry - il s'agit d'un singleton, un point d'accès pour travailler avec les locataires. Par conséquent, Nhibernate et NLog peuvent accéder à ce singleton (aux points d'extension) pour trouver la chaîne de connexion ou l'identifiant du locataire.


Tâches d'arrière-plan


Les services ont souvent des tâches d'arrière-plan qui doivent être effectuées sur une minuterie. Les tâches en arrière-plan peuvent accéder à la base de données de l'organisation, puis la tâche en arrière-plan doit être effectuée pour chaque locataire. Pour ce faire, il n'est pas nécessaire de démarrer un minuteur ou un thread distinct pour chaque locataire. Il est possible d'effectuer une tâche dans différents locataires au sein d'un même thread / timer. Pour ce faire, dans le gestionnaire de minuteur, nous trions les locataires, associons chaque locataire à un flux et effectuons une tâche en arrière-plan:


 //    . foreach (var tenant in TenantRegistry.Instance.Tenants) { //    . using (TenantRegistry.Instance.SwitchTo(tenant.Id)) { //     . } } 

Deux locataires ne peuvent pas être attachés au flux en même temps; si on en attache un, l'autre se détache du flux. Nous utilisons activement cette approche afin de ne pas produire de threads / timers pour les tâches d'arrière-plan.


Comment corréler une demande http avec un locataire


Pour traiter la demande http du client, vous devez savoir de quelle organisation il est issu. Si l'utilisateur est déjà authentifié, l'identifiant du locataire peut être stocké dans le cookie d'authentification (si le travail avec l'application est effectué via le navigateur) ou dans le jeton JWT. Mais que se passe-t-il si l'utilisateur ne s'est pas encore authentifié? Par exemple, un utilisateur anonyme a ouvert un site Web d'application et souhaite s'authentifier. Pour ce faire, il envoie une demande avec un identifiant et un mot de passe. Dans la base de données de quelle organisation rechercher cet utilisateur?


En outre, des demandes anonymes seront reçues pour obtenir la page de connexion à l'application, et cela peut différer pour différentes organisations, par exemple, la langue de localisation.


Pour résoudre le problème de corrélation des requêtes http anonymes et de l'organisation (locataire), nous utilisons des sous-domaines pour les organisations. Le nom du sous-domaine est formé par le nom de l'organisation. Les utilisateurs doivent utiliser le sous-domaine pour travailler avec le système:


 https://company1.service.com https://company2.service.com 

Le même service Web multi-locataire est disponible à ces adresses. Mais maintenant, le service comprend de quelle organisation proviendra une demande http anonyme, en se concentrant sur le nom de domaine.
La liaison du nom de domaine et du locataire est effectuée dans le fichier de configuration du service Web:


 <tenant name="company1" db="database1" host="company1.service.com" /> <tenant name="company2" db="database2" host="company2.service.com" /> 

La configuration des services sera décrite ci-dessous.


Microservices. Stockage de données


Quand j'ai dit que le système ECM avait besoin de 100 tables, j'ai parlé de services monolithiques. Mais il arrive qu'un microservice nécessite un stockage relationnel, dans lequel 2-3 tables sont nécessaires pour stocker ses données. Idéalement, chaque microservice possède son propre stockage, auquel seul il a accès. Et le microservice décide comment stocker les données dans le contexte des locataires.


Mais nous sommes allés dans l'autre sens: nous avons décidé de stocker toutes les données de l'organisation dans une seule base de données. Si un microservice nécessite un stockage relationnel, il utilise la base de données d'organisation existante afin que les données ne soient pas dispersées sur différents stockages, mais collectées dans une seule base de données. Les services monolithiques utilisent la même base de données.


Les microservices fonctionnent uniquement avec leurs tables dans la base de données et n'essaient pas de travailler avec les tables d'un monolithe ou d'un autre microservice. Il y a des avantages et des inconvénients à cette approche.


Avantages:


  • les données de l'organisation en un seul endroit;
  • sauvegarde et restauration faciles des données de l'organisation;
  • Dans la sauvegarde, les données de tous les services sont cohérentes.

Inconvénients:


  • une base de données pour tous les services est un goulot étroit lors de la mise à l'échelle (les besoins en ressources SGBD augmentent);
  • les microservices ont un accès physique aux tables des autres, mais n'utilisent pas cette fonctionnalité.

Microservices. La connaissance des locataires n'est pas toujours requise.


Un microservice peut ne pas savoir qu'il fonctionne dans un environnement multi-locataire. Considérez l'un de nos services, qui est engagé dans la conversion de documents en html.


Ce que fait le service:


  1. Prend un message d'une file d'attente RabbitMQ pour convertir un document.
    • récupère l'identifiant du document et l'identifiant du locataire du message
  2. Téléchargez un document à partir d'un service de stockage de documents.
    • pour cela génère une requête dans laquelle il transmet l'identifiant du document et l'identifiant du locataire
  3. Convertit un document en html.
  4. Donne du HTML au service pour stocker les résultats de conversion.

Le service ne stocke pas de documents et ne stocke pas les résultats de conversion. Il a une connaissance indirecte des locataires: l'identifiant du locataire transite par le service en transit.


Microservices. Les sous-domaines ne sont pas nécessaires


J'ai écrit ci-dessus que les sous-domaines aident à résoudre le problème des demandes http anonymes:


 https://company1.service.com https://company2.service.com 

Mais tous les services ne fonctionnent pas avec des demandes anonymes, la plupart nécessitent une authentification déjà transmise. Par conséquent, les microservices qui fonctionnent via http ne se soucient souvent pas du nom d'hôte de la demande, ils reçoivent toutes les informations sur le locataire du jeton JWT ou du cookie d'authentification fourni avec chaque demande.


La configuration


Les services doivent être configurés pour qu'ils connaissent les locataires. À savoir:


  • spécifier les chaînes de connexion à la base de données des locataires;
  • lier les noms de domaine aux locataires;
  • spécifiez la langue et le fuseau horaire par défaut du locataire.

Les locataires peuvent avoir de nombreux paramètres. Pour nos services, nous définissons les paramètres des locataires dans les fichiers de configuration xml. Ce n'est ni web.config ni app.config. Il s'agit d'un fichier xml distinct, dont les modifications doivent pouvoir être interceptées sans redémarrer les services afin que l'ajout d'un nouveau locataire ne redémarre pas l'ensemble du système.


La liste des paramètres ressemble à ceci:


 <!--  . --> <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> 

Lorsqu'une nouvelle organisation loue un service, elle doit ajouter un nouveau locataire au fichier de configuration correspondant. Et il est souhaitable que d'autres organisations ne ressentent pas cela. Idéalement, il ne devrait pas y avoir de redémarrage des services.


Chez nous, tous les services ne peuvent pas récupérer une configuration sans redémarrer, mais les services les plus critiques (monolithes) sont capables de le faire.


Résumé


Lorsqu'une application devient multi-locataire, il semble que la complexité du développement ait considérablement augmenté. Mais ensuite, vous vous habituez au multitenant et vous traitez son soutien comme une exigence normale.


Il convient également de rappeler que l'hébergement multiclient n'est pas seulement le développement, mais aussi le test, l'administration, le déploiement, la mise à jour, les sauvegardes, les migrations de données. Mais mieux à leur sujet une autre fois.

Source: https://habr.com/ru/post/fr483784/


All Articles