Viele Mandanten – ein System: Multi-Tenant-Architektur in Software-Projekten
Anwendungen für viele Kunden benötigen eine Multi-Tenant-Architektur, bei der je nach Anforderung verschiedene Modelle infrage kommen.
(Bild: erstellt mit Midjourney durch iX)
- Kai Weingärtner
Der Trend zu Software-as-a-Service (SaaS) stellt Anbieter von Softwareanwendungen vor neue Herausforderungen. Wie kann man eine Anwendung kosteneffizient für eine wachsende Anzahl von Kunden betreiben und gleichzeitig deren Ansprüche an Performance, Sicherheit und Anpassbarkeit erfüllen?
Eine klug gewählte Multi-Tenant-Architektur schafft die nötigen Strukturen. Die Tenants (zu Deutsch: Mandanten) teilen sich eine Anwendung mit anderen Tenants, wobei alle voneinander isoliert sind, sodass sich die Anwendung für den einzelnen Mandanten wie eine dedizierte verhält. Ein Tenant umfasst meist eine Gruppe von Nutzern, kann aber auch ein einzelner Nutzer sein. Im B2B-Kontext ist er häufig ein Unternehmen, während es im B2C-Umfeld auch eine einzelne Anwenderin oder ein einzelner Anwender sein kann.
Ein typischer Treiber für die Einführung von Multi-Tenancy ist der Weg von installierter Software hin zu einem Software-as-a-Service-Geschäftsmodell. Im Gegensatz zu pro Kunde installierter Software oder Managed-Service-Angeboten wird eine Multi-Tenant-Anwendung als ein einzelnes Produkt entwickelt und betrieben.
Der Mehrwert liegt im reduzierten Wartungs- und Betriebsaufwand, wenn eine Version für viele Kunden arbeitet. Zudem verteilen sich die Kosten für die Infrastruktur auf die Kunden. Die Herausforderung liegt jedoch darin, die Komplexität des Multi-Tenant-Betriebes nicht auf die Service-Entwicklung durchschlagen zu lassen und gleichzeitig den Anforderungen in Bezug auf Daten- und Performance-Isolation sowie Infrastruktur- und Betriebseffizienz gerecht zu werden.
Vorüberlegungen – Anforderungen und Kundenwünsche
Es gibt einige Fragen, die Architektinnen und Architekten früh klären sollten, da sie das Design deutlich beeinflussen. Wichtig ist, welche Kunden die Anwendung ansprechen soll und welche Anforderungen diese stellen. In bestimmten Branchen gibt es darüber hinaus regulatorische Vorgaben. Wenn Kunden eine dedizierte Infrastruktur wünschen oder konkrete Performance-Erwartungen haben, müssen die Architekten genau prüfen, welche Ressourcen sie trennen oder teilen können.
Außerdem spielen die Anzahl der Mandanten und die Datenmengen eine große Rolle. Können sich die Tenants ein gemeinsames Datenschema performant teilen? Stoßen die Provider-Ressourcen an ein Limit, wenn jeder Tenant eine separate Datenbankinstanz erhält? Reicht der manuell gestützte Onboarding-Prozess aus oder muss er vollautomatisch geschehen?
Auch das geplante Abrechnungsmodell kann die Architektur beeinflussen, insbesondere was die Überwachung angeht. Eine Monatspauschale mit Request-Kontingent erfordert andere Metriken als eine Berechnung nach Nutzerzahl.
Zuletzt sollten Planerinnen und Planer auch den Bedarf an Customizing grob abstecken: Wenn der Kunde ein individuelles Styling erwartet, ist das ein Teil der Tenant-Konfiguration. Wie flexibel muss die Anwendung bei der Integration von Drittsystemen sein, zum Beispiel einem kundeneigenen Identity Provider? Ein klares Zielbild hilft hier, die richtigen Weichen zu stellen.
Silos, Pools und alles dazwischen
Multi-Tenancy muss nicht bedeuten, dass sich die Mandanten alle Anwendungsressourcen und die komplette Infrastruktur teilen, sondern heißt nur, dass die Gesamtanwendung als ein einzelnes Produkt dasteht. Daher sind folgende Modelle im Einsatz: das Silo-Modell, das Pool-Modell und Mischformen aus beiden, wie das Bridge-Modell (siehe Abbildung 1).
Das Silo-Modell weist jedem Tenant eine dedizierte Infrastruktur zu, zum Beispiel eine Datenbankinstanz oder eine eigene VM für die Anwendungslogik. Auch wenn die Infrastruktur hier nicht besonders kosteneffizient ist, hat dieser Ansatz einige Vorzüge: Infrastrukturkosten lassen sich jedem Mandanten einfach zuordnen und die Datenisolation ist automatisch gegeben. Lokale Entwicklerinnen und Entwickler behandeln die Anwendung wie eine Single-Tenant-Anwendung. Dadurch ergibt sich ein einfacherer Migrationspfad für bestehende, als klassische Single-Tenant-Produkte gestartete Anwendungen.
Ein Nachteil neben den Kosten ist, dass Admins beim Onboarding von Tenants Infrastruktur provisionieren müssen. Das heißt, die Bereitstellung muss automatisiert sein, um Wildwuchs zu vermeiden. Das Deployment von Updates nimmt hier ebenfalls mehr Zeit in Anspruch, erlaubt aber sukzessive Rolling-Updates und Canary-Releases.
Beim Pool-Modell teilen sich alle oder mehrere Mandanten eine Infrastruktur. Die Tenant-Isolation erfolgt hier im Code, während die gemeinsame Nutzung der Infrastruktur die Kosten minimiert und Deployments beschleunigt. Jedoch muss man sich mit dem Noisy-Neighbor-Problem auseinandersetzen, bei dem ein Mandant so viele Ressourcen nutzt, dass er andere beeinträchtigt. Bei Kunden, die eine zugesicherte Performance erwarten, sollte daher eine Performance-Isolation zum Einsatz kommen. Außerdem ist zu prüfen, ob die Ressourcen beispielsweise einer Datenbankinstanz an ihre Skalierungslimits stoßen.
Oft ist auch eine Kombination von Silo- und Pool-Modellen sinnvoll, zum Beispiel im Service-Layer ein Pool und im Database-Layer ein Silo, was eine hohe Datenisolation mit Kostenersparnis verbindet. Dieses Modell wird auch Bridge-Modell genannt. In einer Microservices-Architektur können sich auch einzelne Services im Modell unterscheiden, wenn etwa ein Silo-Modell nur bei Services mit hohen Isolationsanforderungen (etwa Zahlungsdaten) notwendig ist.
Die Wahl des Modells gründet sich auf vielen Faktoren und hat weitreichende Implikationen bei der Umsetzung. Sofern keiner der genannten Faktoren dagegen spricht, ist das Pool-Modell aufgrund der Effizienz die erste Wahl.
Die Control Plane und ihre Aufgaben
Der erste Blick beim Design einer Multi-Tenant-Architektur fällt oft auf Aspekte wie die Datenisolation oder die Skalierung der Services. Hinzu kommen aber eine Reihe von Aufgaben rund um Lebenszyklus und Betrieb der Mandanten, was die Admins unabhängig von der Fachlichkeit einheitlich und zentral steuern müssen.
Diese Aufgaben sollte eine Control Plane abbilden, die parallel zur Anwendung (in diesem Kontext auch Data oder Application Plane genannt) arbeitet (siehe Tod Golding: Building Multi-Tenant Saas Architectures; 2024). Je stärker die Zahl der Tenants wächst, umso wichtiger wird die Control Plane, um die zentrale Steuerung der Mandanten zu gewährleisten (siehe Abbildung 2).
Eine weitere Funktion der Control Plane ist das Onboarding von Tenants. Dieses generiert Metadaten wie einen Tenant Identifier, die es erlauben, Anfragen richtig zu routen. Bei einer Silo-Datenbank kämen weitere hinzu, wie die Verbindungsparameter zur Datenbank. Anschließend erfolgt eine Provisionierung der spezifischen Infrastruktur und der Konfiguration. Bei einer Automatisierung baut diese im Silo-Modell eine neue Datenbank auf und legt einen neuen User-Pool für User-Identities, also einzelne Nutzer innerhalb eines Tenants, an. Ein früher, einheitlich automatisierter Onboarding-Prozess ist sinnvoll, da sonst mittelfristig der Wartungsaufwand steigt und dem Mehrwert von Multi-Tenancy zuwiderläuft.
User-Identities sind insofern relevant für die Control Plane, als dass diese darüber die Tenant-Beziehung herstellt und somit Tenant-Metadaten der Application Plane zur Verfügung stellen kann.
Eine Billing-Komponente der Control Plane verwaltet die Abrechnungspläne der Kunden und bezieht bei Bedarf Nutzungsstatistiken der Mandanten von der Metrik-Komponente. Metriken und Log-Aggregation sind zudem für mandantenspezifische Nutzungs- und Fehleranalysen wichtig. Dazu liefern die Services der Application Plane immer eine Tenant-Id zur Identifikation in Logs und Metriken mit.
(Bild: iX)
Dieser Artikel ist auch im iX/Developer-Sonderheft zu finden, das sich an Softwarearchitektinnen und Softwarearchitekten richtet. Neben den klassischen Architekturinhalten zu Methoden und Pattern gibt es Artikel zu Soziotechnischen Systemen, Qualitätssicherung oder zu Architektur und Gesellschaft. Domain Driven Design ist ebenso ein Thema wie Team Topologies und Sicherheit.
Als Autoren konnten wir bekannte Experten gewinnen, die ihr Wissen in vielen spannenden Artikeln – wie dem hier vorliegenden – sowohl für Architektureinsteiger als auch Spezialisten weitergeben.
Datenisolation – aber wie?
Die Pool- und Silo-Modelle lassen sich auch auf den Data-Layer anwenden. Bei relationalen Datenbanken lassen sich die Modelle so noch weiter herunterbrechen:
- Eine Datenbankinstanz pro Mandant (physisches Silo)
- Ein Datenbankschema pro Mandant (logisches Silo)
- Eine Diskriminator-Spalte, die die Daten aufteilt (Pool)
Der erste Ansatz erfordert Automatisierung in der Bereitstellung der Infrastruktur, bietet dann aber als Mehrwert eine spezifisch skalierbare Performance. Er ermöglicht auch eine einfache Verschlüsselung mit einem Key des Kunden. Wenn der Service-Layer ebenfalls dem Silo-Modell folgt, können Verbindungsparameter als Teil der Umgebungskonfiguration des Service in der Infrastruktur liegen. Ist der Service-Layer gepoolt, liefert die Datenzugriffsschicht eines Tenant Configuration Service die Verbindungsparameter für die entsprechend konfigurierte Datenbank dynamisch. Datenbank-Frameworks bieten hierzu oft Integrationspunkte (ein Beispiel mit Hibernate).
Der Schema-pro-Mandant-Ansatz ermöglicht eine effizientere Nutzung der Infrastruktur. Das Onboarding muss nur das Tenant-Schema erzeugen. Die Anbindung im Service-Layer funktioniert ähnlich wie beim ersten Ansatz durch Konfiguration der Datenbankverbindung. Beide Ansätze erfordern, dass Admins Datenmodelländerungen pro Mandant ausrollen. Auch wenn dies mit einem Werkzeug zur Schemamigration automatisiert erfolgt, kann es bei vielen Tenants den Release-Prozess verlangsamen.
Der dritte Ansatz mit Diskriminator-Spalte führt in jeder Tabelle eine Tenant-Id ein, die bei jeder Abfrage mitgegeben wird. Hier teilen sich die Tenants eine Datenbank und ein Schema, was das Deployment beschleunigt. Dafür müssen Entwicklerinnen und Entwickler die Tenant-Filterung in der Anwendung konsequent umsetzen. Das sollte nicht Aufgabe des Serviceteams sein, sondern in der Datenzugriffsschicht als Teil des Service-Basisframeworks erfolgen. Manche Datenbank-Frameworks bieten hier Möglichkeiten, Queries dynamisch zu erweitern (ein Beispiel mit Hibernate-Tenant-Ids).
Noch zuverlässiger ist hier eine Datenbank mit Row-Level-Security (RLS), die die Filterung selbst übernimmt. Das folgende Listing zeigt ein Beispiel mit PostgreSQL, das eine Tabelle mit RLS aufsetzt:
CREATE TABLE kunden (tenant_id varchar, knr varchar, name varchar);
ALTER TABLE kunden ENABLE ROW LEVEL SECURITY;
CREATE POLICY kunden_tenant_isolation_policy ON kunden
USING (tenant_id = current_setting('app.tenant_id')::VARCHAR);
INSERT INTO kunden (tenant_id, knr, name) VALUES
('1', '111-222', 'Acme'),
('2', '222-333', 'Contoso');
Das nächste Listing stellt einen Zugriff darauf dar:
database=> SELECT * FROM kunden;
ERROR: unrecognized configuration parameter "app.tenant_id"
database=> SET app.tenant_id TO '1';
SET
database=> SELECT * FROM kunden;
tenant_id | knr | name
-----------+---------+------
1 | 111-222 | Acme
(1 row)
Die Session-Variable (hier app.tenant_id
) übergibt den Mandanten, was beim Aufbauen der Connection im Data-Access-Layer geschehen kann. Zum Nachvollziehen des Beispiels ist wichtig, dass ein anderer User als der Table Owner den zweiten Part ausführt, damit die Richtlinie greift.
Die drei genannten Ansätze lassen sich in vielen Fällen auch auf nichtrelationale Datenbanken übertragen. So bietet beispielsweise AWS DynamoDB RLS via Identity-and-Access-Management-(IAM)-Berechtigungen. Es lohnt sich, die Möglichkeiten der Datenisolation vor der Wahl der Datenbank zu prüfen.
Performance-Isolation
Bei geteilter Infrastruktur sollten Architektinnen und Architekten immer im Blick behalten, wie sie den Mandanten die erwartete Performance zusichern. Dazu müssen sie die Erwartungen zuerst definieren und dabei gegebenenfalls die vertraglichen Zusagen (SLAs) berücksichtigen. Hier kann ihnen die Erstellung von Tenant-Personas helfen, um die unterschiedlichen Kundenbedürfnisse zu verstehen. Wenn die Erwartungen sehr voneinander abweichen, hilft eine Preisstaffelung, bei der Premium-Kunden zum Beispiel eine dedizierte Silo-Infrastruktur erhalten, während andere kostengünstig im Pool-Modell laufen.
Wesentlich für eine angemessene Performance sind ein detailliertes Monitoring, das spezifische Metriken darstellt, und ein Skalierungsmodell, das es erlaubt, bedarfsgerecht auf Basis der Metriken nachzuskalieren. Darüber hinaus kann eine Limitierung sinnvoll sein, beispielsweise in Form eines Request-Limits durch ein API-Gateway oder ein Ressourcen-Limit durch Resource Quotas in Kubernetes. Da dies das Nutzungserlebnis der limitierten Anwender beeinträchtigt, sollte es nur dazu dienen, Extremfälle abzufangen.
Tenants authentifizieren und Requests routen
Neben der Authentifizierung des Aufrufers ist das Erste, was bei einem Aufruf einer Multi-Tenant-Anwendung passieren muss, die Identifikation des Tenants. Denn wie die Nutzerkennung wird die Tenant-Id Bestandteil des Aufrufkontextes, der für spätere Routing- und Filter-Entscheidungen erforderlich ist. Gängige Praxis ist, den Aufrufkontext als JSON Web Token (JWT) durchzureichen und die Tenant-Id in einem zusätzlichen Claim zu hinterlegen. Damit kann im Silo-Modell jeder Service das Token zur mandantenspezifischen Autorisierung nutzen oder im Pool-Modell die Tenant-Id in einem Request-Filter auslesen und später dem Data-Access-Layer zur Verfügung stellen.
Mitunter müssen Entwicklerinnen und Entwickler mandantenbezogene Identity-Quellen aufbauen, um Identitäten mit einem Kunden-Identity-Provider zusammenzuführen. Oft wollen Kunden ihre eigenen Nutzer verwalten, was getrennte User-Pools erfordert. Um die richtige Identity-Quelle zu identifizieren, kann das Routing beispielsweise über eine mandantenspezifische Subdomain erfolgen. Darüber stellt das Tenant-Management ein Mapping zur Identity-Quelle her und delegiert die Authentifizierung dorthin. Nach erfolgreicher Authentifizierung (Schritt 1 in Abbildung 4) wird der Aufrufkontext hergestellt und bei darauf folgenden Service-Aufrufen durchgereicht (Schritt 2).
Im Silo-Modell müssten Architektinnen und Architekten außerdem eine Lösung für das Routen der Requests an die richtigen Services finden. Hier eignet sich ein Reverse-Proxy, der basierend auf einem mandantenspezifischen Request-Attribut die Anfragen weiterleitet. Die Weiterleitung erfolgt beispielsweise über eine spezifische Quell-Subdomain, einen HTTP-Header oder auch Token-Claims. Als Reverse-Proxy kann beispielsweise ein Layer-7-Loadbalancer oder ein Ingress-Controller für Kubernetes zum Einsatz kommen.
Mehraufwände in der Service-Entwicklung vermeiden
Architektinnen und Architekten einer Multi-Tenant-Anwendung sollten darauf hinarbeiten, dass die Entwicklerteams von Fachservices nicht über Multi-Tenancy nachdenken müssen. Stattdessen sollte das Service-Framework oder die Service-Konfiguration dies kapseln. Im Silo-Modell ist es möglich, die Isolation durch die Umgebung der Services zu steuern und die Services selbst weitgehend von Routing-Entscheidungen freizuhalten. Zum Beispiel kann eine Umgebungsvariable die Datenbankverbindung enthalten oder ein Service-Mesh das Aufrufziel dynamisch auswählen.
Im Pool-Modell müssen die Planerinnen und Planer an den neuralgischen Stellen Standard-Libraries bereitstellen, die die Isolation übernehmen. Dazu gehört die Ermittlung des Tenants aus dem Aufrufkontext (beispielsweise durch einen Request-Filter), die Ermittlung von mandantenspezifischen Verbindungsdaten aus einem Tenant-Metadaten-Service und die Konfiguration einer Datenbankverbindung im Data-Access-Layer. Für das mandantenspezifische Monitoring ist es außerdem wichtig, dass Logs und Metriken die Tenant-Id enthalten, daher sollte auch hier ein Service-Standard bestehen.
Die Teams sollten die vorgeschriebene Nutzung dieser Standard-Libraries kennen und der Build-Prozess sollte die Verwendung auch bei Bedarf verifizieren.
Fazit: Multi-Tenancy frühzeitig angehen
Multi-Tenancy ermöglicht es, Anwendungen für eine Vielzahl von Kunden kosteneffizient zu betreiben. Die vorgestellten Einflussfaktoren und Überlegungen zeigen, dass es dabei nicht die eine Antwort gibt, die für alle passt. Die Bedürfnisse der Kunden und die fachlichen sowie technischen Rahmenbedingungen haben einen entscheidenden Einfluss auf die Architektur. Die hier vorgestellten Ansätze bilden typische Fragen und Vorgehensweisen ab, denen Architektinnen und Architekten in ihrer Arbeit begegnen werden.
Wenn sich abzeichnet, dass eine Anwendung mehrere Kunden bedienen soll, müssen alle Beteiligten frühzeitig über die Herausforderungen von Multi-Tenancy nachdenken. Wildwuchs entsteht nur dann, wenn die Service-Entwicklungsteams mit dem Thema alleine gelassen werden. Das lässt sich von vorneherein vermeiden, wenn Architekten ein standardisiertes Onboarding in einer Control Plane und einheitliche Wege für die Daten- und Performance-Isolation entwickeln.
(who)