So erstellen Sie eine mandantenfähige Anwendung aus einer nicht mandantenfähigen Anwendung

Bild


Ich werde keine Definition von Mandantenfähigkeit geben, sie haben bereits mehrmals hier und hier darüber geschrieben . Es ist besser, direkt zum Thema des Artikels zu gehen und mit den folgenden Fragen zu beginnen:


Warum ist die Anwendung nicht sofort mandantenfähig?


Es kommt vor, dass die Anwendung zunächst nur für die Installation auf der Clientseite entwickelt wird. Sie können eine solche Anwendung in der Verpackung oder als Software als Produkt bezeichnen . Ein Client kauft eine Box und stellt die Anwendung auf seinen Servern bereit (es gibt viele Beispiele für solche Anwendungen).


Mit der Zeit könnte die Entwicklerfirma jedoch der Meinung sein, dass es hilfreich wäre, die Anwendung in der Cloud zu platzieren, damit sie gemietet wird (Software as a Service). Diese Bereitstellungsmethode bietet sowohl für Kunden als auch für das Entwicklerunternehmen Vorteile. Kunden erhalten schnell ein funktionierendes System und müssen sich nicht um Bereitstellung und Verwaltung kümmern. Wenn Sie eine Anwendung mieten, benötigen Sie keine großen einmaligen Investitionen.


Das Entwicklerunternehmen erhält neue Kunden und neue Aufgaben: Bereitstellung der Anwendung in der Cloud, Verwaltung, Aktualisierung auf neue Versionen, Migration von Daten während der Aktualisierung, Datensicherung, Überwachung der Geschwindigkeit und Fehler sowie Behebung von Problemen, wenn diese auftreten.


Warum sollte die Anwendung in der Cloud mandantenfähig sein?


Um eine Anwendung in der Cloud zu platzieren, muss sie nicht mandantenfähig sein. Dann ergibt sich jedoch das folgende Problem: Sie müssen für jeden Client einen dedizierten Stand in der Cloud mit der geleasten Anwendung bereitstellen, und dies ist sowohl im Hinblick auf den Verbrauch von Cloud-Standressourcen als auch im Hinblick auf die Verwaltung bereits kostspielig. Es ist rentabler, Mandantenfähigkeit in der Anwendung zu implementieren, sodass eine Instanz mehrere Clients (Organisationen) bedienen kann.


Wenn die Anwendung 1000 gleichzeitig arbeitende Benutzer abruft, ist es vorteilhaft, Clients (Organisationen) so zu gruppieren, dass sie insgesamt die gewünschte Last von 1000 Benutzern pro Anwendungsinstanz ergeben. Und dann werden die Cloud-Ressourcen optimal genutzt.


Angenommen, die Anwendung wird von einer Organisation für 20 Benutzer (Mitarbeiter der Organisation) gemietet. Dann müssen Sie 50 dieser Organisationen gruppieren, um die richtige Last zu erreichen. Es ist wichtig, Organisationen voneinander zu isolieren. Eine Organisation mietet eine Anwendung, lässt nur ihre Mitarbeiter dorthin, speichert nur ihre Daten und sieht nicht, dass auch andere Organisationen von derselben Anwendung bedient werden.


Die Implementierung einer Mandantenfähigkeit bedeutet nicht, dass die Anwendung nicht mehr lokal auf dem Server des Unternehmens bereitgestellt werden kann. Sie können zwei Bereitstellungsmethoden gleichzeitig unterstützen:


  • mandantenfähige Anwendung in der Cloud;
  • Mandantenanwendung auf dem Client-Server.

Unsere Anwendung hat einen ähnlichen Weg beschritten: vom Nicht-Mandanten zum Multi-Mandanten. In diesem Artikel werde ich einige Ansätze zur Entwicklung von Mandantenfähigkeit vorstellen.


Wie implementiere ich Mandantenfähigkeit in einer Anwendung, die als Nicht-Mandanten ausgelegt ist?


Wir werden das Thema sofort einschränken, wir werden nur die Entwicklung in Betracht ziehen, wir werden nicht auf Fragen des Testens, der Freigabe einer Version, der Bereitstellung und der Verwaltung eingehen. In all diesen Bereichen sollte auch die Entstehung von Mandantenfähigkeit berücksichtigt werden, aber im Moment werden wir nur über Entwicklung sprechen.


Um zu verstehen, was eine Anwendung ist, die nicht Mandantenfähig und mandantenunabhängig war, beschreibe ich ihren Zweck, eine Liste der verwendeten Dienste und Technologien.


Dies ist ein ECM-System (DirectumRX), das aus 10 Diensten (5 monolithischen Diensten und 5 Mikrodiensten) besteht. Alle diese Dienste können entweder auf einem leistungsstarken Server oder auf mehreren Servern abgelegt werden.


Dienstleistungen sind
  • Webservice - für die Wartung von Web-Clients (Browsern).
  • WCF-Dienst - zum Warten von Desktop-Clients (WPF-Anwendungen).
  • Service für mobile Anwendungen.
  • Service zur Durchführung von Hintergrundprozessen.
  • Service zur Planung von Hintergrundprozessen.
  • Workflow Scheme Execution Service
  • Workflow Block Execution Service
  • Dokumentenspeicherdienst (Binärdaten).
  • Dienst zum Konvertieren von Dokumenten in HTML (Vorschau in einem Browser).
  • Dienst zum Speichern von Konvertierungsergebnissen in HTML

Stapel der verwendeten Technologien:
.NET + SQLServer / Postgres + NHibernate + IIS + RabbitMQ + Redis


Was kann also bewirken, dass Services mandantenfähig werden? Dazu müssen Sie die folgenden Mechanismen in Diensten verfeinern:


  • Datenspeicherung;
  • ORM;
  • Daten-Caching;
  • Anfragebearbeitung;
  • Verarbeiten von Warteschlangennachrichten;
  • Konfiguration;
  • Protokollierung;
  • Ausführen von Hintergrundaufgaben;
  • Wechselwirkung mit Mikrodiensten;
  • Interaktion mit dem Nachrichtenbroker.

Bei unserer Anwendung waren dies die wichtigsten Stellen, an denen Verbesserungen erforderlich waren. Betrachten wir sie getrennt.


Auswählen einer Datenspeichermethode


Wenn Sie Artikel zum Thema Mandantenfähigkeit lesen, ist das erste, was sie aussortieren, die Organisation der Datenspeicherung. In der Tat ist der Punkt wichtig.


Bei unserem ECM-System ist der Hauptspeicher eine relationale Datenbank mit etwa 100 Tabellen. Wie kann die Speicherung von Daten vieler Organisationen so organisiert werden, dass Organisation A die Daten von Organisation B in keiner Weise sieht?


Es sind mehrere Schemata bekannt (über diese Schemata wurde bereits viel geschrieben):


  • Erstellen Sie eine eigene Datenbank für jede Organisation (für jeden Mandanten).
  • Verwenden Sie eine Datenbank für alle Organisationen. Erstellen Sie jedoch für jede Organisation ein eigenes Schema in der Datenbank.
  • Verwenden Sie eine Datenbank für alle Organisationen, fügen Sie jedoch in jeder Tabelle eine Spalte "Mandant / Organisationsschlüssel" hinzu.

Die Wahl des Schemas ist nicht zufällig. In unserem Fall ist es ausreichend, die Fälle der Systemadministration zu berücksichtigen, um die bevorzugte Option zu verstehen. Fälle sind wie folgt:


  • Mieter hinzufügen (eine neue Organisation mietet ein System);
  • Mieter entfernen (die Organisation weigerte sich zu mieten);
  • Mieter auf einen anderen Cloud-Stand übertragen (die Last zwischen den Cloud-Ständen umverteilen, wenn ein Stand die Last nicht mehr bewältigt).

Betrachten Sie ein Mieterverteilergetriebe. Die Hauptaufgabe der Übertragung besteht darin, die Daten der Organisation auf einen anderen Stand zu übertragen. Das Übertragen ist nicht schwierig, wenn der Mandant über eine eigene Datenbank verfügt. Es bereitet jedoch Kopfzerbrechen, wenn Sie die Daten verschiedener Organisationen in 100 Tabellen zusammenfassen. Versuchen Sie, nur die erforderlichen Daten aus den Tabellen zu extrahieren, und übertragen Sie sie in eine andere Datenbank, in der bereits Daten von anderen Mandanten vorhanden sind, sodass sich deren Bezeichner nicht überschneiden.


Der nächste Fall ist das Hinzufügen eines neuen Mieters. Der Fall ist auch nicht einfach. Beim Hinzufügen eines Mandanten müssen Systemverzeichnisse, Benutzer und Rechte ausgefüllt werden, damit Sie sich überhaupt beim System anmelden können. Diese Aufgabe lässt sich am besten lösen, indem Sie eine Referenzdatenbank klonen, die bereits über alles verfügt, was Sie benötigen.


Der Tenant Removal-Fall lässt sich sehr einfach durch Deaktivieren der Tenant-Datenbank lösen.


Aus diesen Gründen haben wir uns für ein Schema entschieden: ein Mandant - eine Datenbank .


ORM


Wir wählten die Datenspeichermethode, die nächste Frage: Wie bringt man ORM bei, mit dem ausgewählten Schema zu arbeiten?


Wir verwenden Nhibernate. Es war erforderlich, dass Nhibernate mit mehreren Datenbanken arbeitet und regelmäßig auf die richtige umschaltet, zum Beispiel abhängig von der http-Anforderung. Wenn wir die Anforderung von Organisation A verarbeiten, wurde Datenbank A verwendet, und wenn die Anforderung von Organisation B stammt, wird Datenbank B verwendet.


NHibernate hat eine solche Gelegenheit. Sie müssen die Implementierung von NHibernate.Connection.DriverConnectionProvider überschreiben . Wenn NHibernate eine Datenbankverbindung öffnen möchte, ruft es DriverConnectionProvider auf, um eine Verbindungszeichenfolge abzurufen . Hier ersetzen wir es durch das Notwendige:


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

Was ist TenantRegistry.Instance.CurrentTenant Ich werde etwas später erzählen.


Daten-Caching


Services zwischenspeichern häufig Daten, um Datenbankabfragen zu minimieren oder um nicht dasselbe vielfach zu berechnen. Das Problem ist, dass die Caches nach Mandanten aufgeteilt werden müssen, wenn Mandantendaten zwischengespeichert werden. Es ist nicht akzeptabel, dass der Datencache einer Organisation verwendet wird, wenn eine Anforderung von einer anderen Organisation verarbeitet wird. Die einfachste Lösung besteht darin, dem Schlüssel jedes Caches eine Mandanten-ID hinzuzufügen:


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

Dieses Problem muss beim Erstellen der einzelnen Caches beachtet werden. Es gibt viele Caches in unseren Diensten. Um nicht zu vergessen, die Mandanten-ID in jeder zu berücksichtigen, ist es besser, die Arbeit mit Caches zu vereinheitlichen. Erstellen Sie beispielsweise einen allgemeinen Caching-Mechanismus, der im Kontext von Mandanten sofort zwischengespeichert wird.


Protokollierung


Früher oder später tritt im System ein Fehler auf. Sie müssen die Protokolldatei öffnen und mit dem Studium beginnen. Die erste Frage lautet: Für welchen Benutzer und für welche Organisation wurden diese Aktionen durchgeführt?


Es ist praktisch, wenn in jeder Zeile des Protokolls eine Mandanten-ID und ein Mandanten-Benutzername vorhanden sind. Diese Information wird so notwendig wie zum Beispiel die Nachrichtenzeit:


 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] 

Der Entwickler sollte nicht darüber nachdenken, welchen Mandanten er in das Protokoll schreiben soll, er sollte automatisiert und "unter der Haube" des Protokollierungssystems verborgen sein.


Wir verwenden NLog, deshalb werde ich ein Beispiel dafür geben. Am einfachsten können Sie die Mandanten- ID sichern, indem Sie NLog.LayoutRenderers.LayoutRenderer erstellen, mit dem Sie die Mandanten- ID für jeden Protokolleintrag abrufen können:


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

Verwenden Sie dann diesen LayoutRenderer in der Protokollvorlage:


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

Code-Ausführung


In den obigen Beispielen habe ich oft den folgenden Code verwendet:


 TenantRegistry.Instance.CurrentTenant 

Es ist Zeit zu sagen, was das bedeutet. Aber zuerst müssen Sie den Ansatz verstehen, den wir bei Dienstleistungen verfolgen:


Jede Codeausführung (Verarbeiten einer http-Anforderung, Verarbeiten einer Warteschlangennachricht, Ausführen einer Hintergrundaufgabe in einem separaten Thread) muss mit einem Mandanten verknüpft sein.

Dies bedeutet, dass an jeder Stelle in der Codeausführung gefragt werden kann: "Für welchen Mandanten funktioniert dieser Thread?" oder anders ausgedrückt: "Was ist der aktuelle Mieter?"


TenantRegistry.Instance.CurrentTenant ist der aktuelle Mandant für den aktuellen Stream. Stream und Mieter können in unseren Anwendungen verknüpft werden. Sie werden vorübergehend verbunden, z. B. während der Verarbeitung einer http-Anforderung oder einer Nachricht aus der Warteschlange. So binden Sie einen Mandanten an einen Stream:


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

Ein an einen Thread gebundener Mandant kann an einer beliebigen Stelle im Code durch Kontaktaufnahme mit TenantRegistry abgerufen werden. Hierbei handelt es sich um einen Singleton, einen Zugriffspunkt für die Arbeit mit Mandanten. Daher können Nhibernate und NLog (an Erweiterungspunkten) auf diesen Singleton zugreifen, um die Verbindungszeichenfolge oder die Mandanten-ID zu ermitteln.


Hintergrundaufgaben


Dienste haben häufig Hintergrundaufgaben, die auf einem Zeitgeber ausgeführt werden müssen. Hintergrundaufgaben können auf die Datenbank der Organisation zugreifen. Anschließend muss die Hintergrundaufgabe für jeden Mandanten ausgeführt werden. Dazu muss nicht für jeden Mandanten ein separater Timer oder Thread gestartet werden. Es ist möglich, eine Aufgabe in verschiedenen Mandanten innerhalb eines Threads / Timers auszuführen. Zu diesem Zweck sortieren wir im Timer-Handler die Mandanten, ordnen jedem Mandanten einen Stream zu und führen eine Hintergrundaufgabe aus:


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

Es können nicht zwei Mandanten gleichzeitig an den Datenfluss angehängt werden. Wenn wir einen anhängen, wird der andere vom Datenfluss getrennt. Wir verwenden diesen Ansatz aktiv, um keine Threads / Timer für Hintergrundaufgaben zu erstellen.


So korrelieren Sie eine http-Anfrage mit einem Mandanten


Um die http-Anfrage des Kunden zu verarbeiten, müssen Sie wissen, von welcher Organisation er kam. Wenn der Benutzer bereits authentifiziert ist, kann die Mandanten-ID im Authentifizierungscookie (wenn die Arbeit mit der Anwendung über den Browser ausgeführt wird) oder im JWT-Token gespeichert werden. Was aber, wenn der Benutzer sich noch nicht authentifiziert hat? Beispielsweise hat ein anonymer Benutzer eine Anwendungswebsite geöffnet und möchte sich authentifizieren. Dazu sendet er eine Anfrage mit Login und Passwort. In der Datenbank von welcher Organisation soll nach diesem Benutzer gesucht werden?


Anonyme Anfragen beziehen sich auch auf das Abrufen der Anmeldeseite für die Anwendung und können für verschiedene Organisationen unterschiedlich sein, z. B. die Sprache der Lokalisierung.


Um das Problem der Korrelation von anonymer http-Anfrage und Organisation (Mandant) zu lösen, verwenden wir Subdomains für Organisationen. Der Name der Unterdomäne setzt sich aus dem Namen der Organisation zusammen. Benutzer müssen die Unterdomäne verwenden, um mit dem System zu arbeiten:


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

Unter diesen Adressen ist derselbe mandantenfähige Webdienst verfügbar. Jetzt weiß der Dienst jedoch, von welcher Organisation eine anonyme http-Anfrage kommt, die sich auf den Domain-Namen konzentriert.
Die Bindung von Domainname und Mandant erfolgt in der Konfigurationsdatei des Webdienstes:


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

Informationen zum Konfigurieren von Diensten werden im Folgenden beschrieben.


Microservices. Datenspeicherung


Als ich sagte, dass das ECM-System 100 Tische benötigt, sprach ich über monolithische Dienste. Es kommt jedoch vor, dass ein Mikrodienst einen relationalen Speicher benötigt, in dem 2-3 Tabellen zum Speichern seiner Daten erforderlich sind. Im Idealfall verfügt jeder Mikrodienst über einen eigenen Speicher, auf den nur er Zugriff hat. Und der Microservice entscheidet, wie Daten im Kontext von Mietern gespeichert werden.


Wir sind jedoch den anderen Weg gegangen: Wir haben beschlossen, alle Daten der Organisation in einer Datenbank zu speichern. Wenn für einen Mikrodienst ein relationaler Speicher erforderlich ist, wird die vorhandene Organisationsdatenbank verwendet, sodass die Daten nicht auf verschiedene Speicher verteilt, sondern in einer Datenbank gesammelt werden. Monolithische Dienste verwenden dieselbe Datenbank.


Microservices arbeiten nur mit ihren Tabellen in der Datenbank und versuchen nicht, mit Tabellen eines Monolithen oder eines anderen Microservices zu arbeiten. Dieser Ansatz hat Vor- und Nachteile.


Vorteile:


  • Organisationsdaten an einem Ort;
  • einfache Sicherung und Wiederherstellung von Organisationsdaten;
  • In der Sicherung sind die Daten aller Dienste konsistent.

Nachteile:


  • Eine Datenbank für alle Services ist ein Engpass bei der Skalierung (die Anforderungen an die DBMS-Ressourcen steigen).
  • microservices haben physischen Zugriff auf die Tabellen des jeweils anderen, verwenden diese Funktion jedoch nicht.

Microservices. Kenntnisse der Mieter sind nicht immer erforderlich.


Ein Microservice weiß möglicherweise nicht, dass er in einer Umgebung mit mehreren Mandanten funktioniert. Betrachten Sie einen unserer Services, der sich mit der Konvertierung von Dokumenten in HTML befasst.


Was der Service macht:


  1. Nimmt eine Nachricht aus einer RabbitMQ-Warteschlange, um ein Dokument zu konvertieren.
    • Ruft die Dokument- und Mandanten-ID aus der Nachricht ab
  2. Laden Sie ein Dokument von einem Dokumentenspeicherdienst herunter.
    • hierfür wird eine Anfrage generiert, in der die Dokumentenkennung und die Mandantenkennung übertragen werden
  3. Konvertiert ein Dokument in HTML.
  4. Gibt HTML an den Dienst zum Speichern von Conversion-Ergebnissen.

Der Dienst speichert keine Dokumente und speichert keine Konvertierungsergebnisse. Er kennt die Mieter indirekt: Die Mieteridentifikation durchläuft die Dienstleistung auf dem Transportweg.


Microservices. Subdomains werden nicht benötigt


Ich habe oben geschrieben, dass Subdomains helfen, das Problem anonymer http-Anfragen zu lösen:


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

Nicht alle Dienste funktionieren jedoch mit anonymen Anforderungen. Die meisten erfordern eine bereits bestandene Authentifizierung. Daher ist es Mikrodiensten, die über http arbeiten, häufig egal, von welchem ​​Hostnamen die Anforderung stammt. Sie erhalten alle Informationen über den Mandanten aus dem JWT-Token oder dem Authentifizierungscookie, das mit jeder Anforderung geliefert wird.


Konfiguration


Die Dienste müssen so konfiguriert werden, dass sie die Mandanten kennen. Nämlich:


  • Geben Sie die Zeichenfolgen für die Verbindung zur Datenbank der Mandanten an.
  • Domainnamen an Mieter binden;
  • Geben Sie die Standardsprache und die Zeitzone des Mandanten an.

Mieter können viele Einstellungen haben. Für unsere Services legen wir die Mandanteneinstellungen in XML-Konfigurationsdateien fest. Dies ist nicht web.config und nicht app.config. Dies ist eine separate XML-Datei, deren Änderungen erfasst werden müssen, ohne dass die Dienste neu gestartet werden müssen, damit durch das Hinzufügen eines neuen Mandanten nicht das gesamte System neu gestartet wird.


Die Liste der Einstellungen sieht ungefähr so ​​aus:


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

Wenn eine neue Organisation einen Service mietet, muss sie einen neuen Mandanten zur Konfigurationsdatei hinzufügen. Und es ist wünschenswert, dass andere Organisationen dies nicht spüren. Im Idealfall sollte kein Neustart der Dienste erfolgen.


Bei uns können nicht alle Dienste eine Konfiguration ohne Neustart abrufen, aber die kritischsten Dienste (Monolithen) können dies.


Zusammenfassung


Wenn eine Anwendung mandantenfähig wird, scheint die Komplexität der Entwicklung dramatisch zuzunehmen. Aber dann gewöhnt man sich an Mandantenfähigkeit und behandelt die Unterstützung als normale Anforderung.


Beachten Sie auch, dass Mandantenfähigkeit nicht nur Entwicklung, sondern auch Test, Verwaltung, Bereitstellung, Aktualisierung, Sicherung und Datenmigration umfasst. Aber besser über sie ein anderes Mal.

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


All Articles