Datenbankarchitekturen für Multi-Tenant-Webanwendungen
James Reed
Infrastructure Engineer · Leapcell

Aufbau skalierbarer Webanwendungen mit Multi-Tenant-Datenbanklösungen
In der heutigen Cloud-nativen Welt sind Software-as-a-Service (SaaS)-Anwendungen allgegenwärtig geworden. Ein gemeinsames Merkmal vieler SaaS-Angebote ist das Konzept der Multi-Tenancy, bei dem eine einzige Instanz der Anwendung mehrere Kunden oder „Tenants“ bedient. Dieser Ansatz bietet erhebliche Vorteile in Bezug auf Kosteneffizienz, optimierte Wartung und vereinfachte Bereitstellung. Das Design einer robusten und skalierbaren Datenbankarchitektur für solche Anwendungen stellt jedoch einzigartige Herausforderungen dar. Dieser Artikel untersucht verschiedene Datenbankarchitekturmuster für Multi-Tenant-Webanwendungen, ihre zugrunde liegenden Prinzipien und praktische Überlegungen zu ihrer Implementierung.
Die Grundlage der Multi-Tenancy verstehen
Bevor wir uns mit den Architekturmustern befassen, wollen wir einige Kernkonzepte im Zusammenhang mit Multi-Tenancy in einer Datenbank klären.
- Tenant: Eine eigenständige Gruppe von Benutzern oder Organisationen, die dieselbe Anwendungsinstanz gemeinsam nutzen, aber über isolierte Daten von anderen Gruppen verfügen. Jeder Tenant agiert so, als hätte er seine eigene dedizierte Softwareumgebung.
- Datenisolierung: Die grundlegende Anforderung bei Multi-Tenancy. Tenants dürfen nicht auf die Daten anderer Tenants zugreifen oder von ihnen betroffen sein. Dies ist entscheidend für Sicherheit, Datenschutz und Compliance.
- Schema: Die Struktur einer Datenbank, die Tabellen, Spalten, Beziehungen und Datentypen definiert.
- Performance-Isolierung: Sicherstellen, dass die Aktionen eines Tenants die von anderen Tenants erlebte Leistung nicht negativ beeinflussen. Dies ist oft schwieriger als die Datenisolierung.
- Anpassung: Die Möglichkeit, Aspekte der Anwendung oder des Datenschemas für einzelne Tenants zuzuschneiden, ohne andere zu beeinträchtigen.
Das Ziel jeder Multi-Tenant-Datenbankarchitektur ist es, diese Anliegen effektiv auszubalancieren, wobei Faktoren wie Skalierbarkeit, Kosten, Sicherheit und betriebliche Komplexität berücksichtigt werden.
Architektur für mehrere Tenants
Es gibt drei primäre Architekturmuster für die Handhabung von Multi-Tenancy auf Datenbankebene, jedes mit seinen eigenen Kompromissen:
1. Separate Datenbank pro Tenant
Dies ist der einfachste und sicherste Ansatz. Jeder Tenant hat seine eigene dedizierte Datenbankinstanz.
Prinzip: Vollständige physische Trennung der Daten. Die Daten jedes Tenants befinden sich in seiner eigenen isolierten Datenbank.
Implementierung: Wenn sich ein neuer Tenant registriert, wird eine neue Datenbank für ihn bereitgestellt. Die Anwendung stellt basierend auf dem authentifizierten Tenant eine Verbindung zur entsprechenden Datenbank her.
Beispiel (Vereinfachter Pseudocode mit einem Connection Pool):
# In einem Web-Framework wie Flask oder Django from flask import g, request import psycopg2 DATABASE_CONFIG = { "tenant_a": {"host": "db_a_host", "database": "tenant_a_db", "user": "user_a", "password": "password_a"}, "tenant_b": {"host": "db_b_host", "database": "tenant_b_db", "user": "user_b", "password": "password_b"}, # ... weitere Tenants } def get_tenant_db_connection(): tenant_id = request.headers.get('X-Tenant-ID') # Oder aus Session, Subdomain usw. if tenant_id not in DATABASE_CONFIG: raise Exception("Ungültige Tenant-ID") config = DATABASE_CONFIG[tenant_id] if not hasattr(g, 'db_connection'): g.db_connection = psycopg2.connect( host=config['host'], database=config['database'], user=config['user'], password=config['password'] ) return g.db_connection @app.route('/data') def get_data(): conn = get_tenant_db_connection() cursor = conn.cursor() cursor.execute("SELECT * FROM some_table") data = cursor.fetchall() return {"data": data}
Vorteile:
- Stärkste Datenisolierung: Kein Risiko von Datenlecks zwischen Tenants auf Datenbankebene.
- Vereinfachte Backups und Wiederherstellungen: Einzelne Tenant-Daten können unabhängig gesichert und wiederhergestellt werden.
- Performance-Isolierung: Leistungsprobleme in der Datenbank eines Tenants beeinträchtigen andere Tenants weniger wahrscheinlich, vorausgesetzt, es werden separate Datenbankserver oder ausreichende Ressourcen zugewiesen.
- Einfachere Anpassung: Schemaänderungen für einen Tenant wirken sich nicht auf andere aus.
- Compliance: Oft die bevorzugte Wahl für strenge regulatorische Compliance-Anforderungen.
Nachteile:
- Höchster Betriebsaufwand: Die Verwaltung von Hunderten oder Tausenden separater Datenbanken kann komplex und ressourcenintensiv sein (Überwachung, Patches, Upgrades).
- Höhere Infrastrukturkosten: Jede Datenbankinstanz verbraucht Ressourcen, was im Vergleich zu Shared-Ansätzen zu potenziell höheren Kosten führt, insbesondere für kleine Tenants.
- Unterauslastung der Ressourcen: Kleine Tenants nutzen ihre dedizierten Datenbankressourcen möglicherweise nicht vollständig.
- Komplexe Migrationen: Schema-Migrationen über viele Datenbanken hinweg können schwierig zu orchestrieren sein.
2. Separates Schema pro Tenant
Bei diesem Ansatz teilen sich alle Tenants dieselbe Datenbankserverinstanz, aber jeder Tenant hat sein eigenes dediziertes Schema innerhalb dieser Datenbank.
Prinzip: Logische Trennung der Daten innerhalb einer gemeinsam genutzten physischen Datenbank.
Implementierung: Wenn ein neuer Tenant bereitgestellt wird, wird ein neues Schema (z. B. tenant_a_schema
, tenant_b_schema
) in der gemeinsamen Datenbank erstellt. Alle Tabellen, Views usw. für diesen Tenant werden innerhalb ihres jeweiligen Schemas erstellt. Die Anwendung passt ihre Abfragen an, um Tabellennamen mit dem Schema des aktuellen Tenants zu präsenzieren.
Beispiel (Vereinfachter Pseudocode mit PostgreSQL):
# In einem Web-Framework from flask import g, request import psycopg2 SHARED_DB_CONFIG = { "host": "shared_db_host", "database": "multi_tenant_db", "user": "shared_user", "password": "shared_password" } def get_shared_db_connection(): if not hasattr(g, 'db_connection'): g.db_connection = psycopg2.connect( host=SHARED_DB_CONFIG['host'], database=SHARED_DB_CONFIG['database'], user=SHARED_DB_CONFIG['user'], password=SHARED_DB_CONFIG['password'] ) return g.db_connection @app.before_request def set_tenant_schema(): tenant_id = request.headers.get('X-Tenant-ID') if not tenant_id: raise Exception("Tenant-ID nicht angegeben") conn = get_shared_db_connection() cursor = conn.cursor() # Suchpfad für die aktuelle Sitzung festlegen cursor.execute(f"SET search_path TO {tenant_id}_schema, public;") conn.commit() # Wichtig für DDL/Schema-Änderungen oder nur für Sitzungseinstellungen @app.route('/data') def get_data(): conn = get_shared_db_connection() cursor = conn.cursor() # Abfrage ohne explizites Schema-Präfix, da der Suchpfad dies handhabt cursor.execute("SELECT * FROM some_table") data = cursor.fetchall() return {"data": data}
Vorteile:
- Gute Datenisolierung: Starke logische Trennung, die versehentlichen Zugriff auf Daten zwischen Tenants verhindert, wenn Anwendungen korrekt Schema-Präfixe oder Suchpfade verwenden.
- Geringerer Betriebsaufwand als separate Datenbanken: Einfachere Verwaltung einer einzelnen Datenbankinstanz (Überwachung, Backups, Upgrades).
- Effizientere Ressourcennutzung: Ressourcen werden über Tenants hinweg gebündelt.
- Einfachere Schemaverwaltung: Gemeinsame Schemaänderungen können einmal angewendet werden und betreffen alle Tenants (wenn ihre Schemata identisch sind).
Nachteile:
- Weniger Performance-Isolierung: Ein einzelner Datenbankserver impliziert gemeinsame Ressourcen. Ein „Noisy Neighbor“-Tenant kann andere beeinträchtigen.
- Komplexere Backups/Wiederherstellungen: Die Wiederherstellung eines einzelnen Tenants bedeutet oft, die gesamte Datenbank wiederherzustellen und dann das Schema des Tenants zu extrahieren/wieder zu importieren.
- Potenzial für Schema-Drift: Wenn Tenants benutzerdefinierte Schemaänderungen erfordern, kann die Verwaltung eindeutiger Schemata innerhalb einer einzelnen Datenbank komplex werden.
- Sicherheitsabhängigkeit von der Anwendungsschicht: Die korrekte Schema-Verarbeitung ist im Anwendungscode entscheidend; ein Fehler könnte Daten preisgeben.
3. Gemeinsame Datenbank, gemeinsames Schema mit Tenant-Diskriminator
Dies ist die ressourceneffizienteste Methode, bei der sich alle Tenants dieselbe Datenbank und dasselbe Schema teilen. Die Datenisolierung erfolgt über eine „Tenant-ID“-Spalte in jeder Tabelle, die Tenant-spezifische Daten speichert.
Prinzip: Logische Trennung der Daten innerhalb einer gemeinsam genutzten Datenbank und eines gemeinsam genutzten Schemas, erzwungen durch anwendungsseitige Filterung.
Implementierung: Jede relevante Tabelle enthält eine tenant_id
-Spalte (oder ähnlich). Alle Abfragen müssen eine WHERE tenant_id = <aktuelle_tenant_id>
-Klausel enthalten.
Beispiel (Vereinfachter Pseudocode):
# In einem Web-Framework from flask import g, request import psycopg2 SHARED_DB_CONFIG = { "host": "shared_db_host", "database": "multi_tenant_db", "user": "shared_user", "password": "shared_password" } def get_db_connection(): if not hasattr(g, 'db_connection'): g.db_connection = psycopg2.connect( host=SHARED_DB_CONFIG['host'], database=SHARED_DB_CONFIG['database'], user=SHARED_DB_CONFIG['user'], password=SHARED_DB_CONFIG['password'] ) return g.db_connection @app.route('/data') def get_data(): conn = get_db_connection() cursor = conn.cursor() tenant_id = request.headers.get('X-Tenant-ID') if not tenant_id: raise Exception("Tenant-ID nicht angegeben") # Entscheidend ist, dass jede Abfrage nach tenant_id gefiltert werden muss cursor.execute("SELECT * FROM some_table WHERE tenant_id = %s", (tenant_id,)) data = cursor.fetchall() return {"data": data} # Beispiel für einen ORM-basierten Ansatz mit Tenant-Filterung (z. B. SQLAlchemy) # from sqlalchemy import create_engine, Column, Integer, String # from sqlalchemy.orm import sessionmaker, declarative_base # # Base = declarative_base() # # class Item(Base): # __tablename__ = 'items' # id = Column(Integer, primary_key=True) # tenant_id = Column(String, nullable=False) # Wesentlicher Tenant-Diskriminator # name = Column(String) # # # In Ihrer Sitzungsverwaltung oder ORM-Abfrageerstellung: # # session.query(Item).filter(Item.tenant_id == current_tenant_id).all()
Vorteile:
- Geringster Betriebsaufwand: Die Verwaltung eines einzelnen Datenbankservers und eines einzelnen Schemas ist am einfachsten.
- Geringste Infrastrukturkosten (anfänglich): Hervorragende Ressourcennutzung, da alle Daten konsolidiert sind.
- Einfachste Schema-Migrationen: Änderungen werden einmal am einzelnen Schema vorgenommen.
- Hohe Skalierbarkeit für viele Tenants: Ideal für Anwendungen, die eine sehr große Anzahl kleiner Tenants erwarten.
Nachteile:
- Schwächste Datenisolierung (stark von der Anwendungslogik abhängig): Ein einzelner Programmierfehler oder eine vergessene
WHERE
-Klausel kann Daten zwischen Tenants preisgeben. Erfordert strenge Tests und robuste ORM-Funktionen oder Abfrage-Abfangmechanismen. - Keine Performance-Isolierung: Ein einzelner großer Tenant, der intensive Abfragen ausführt, kann alle anderen Tenants beeinträchtigen.
- Komplexe Backups/Wiederherstellungen für einzelne Tenants: Das Extrahieren von Daten für einen einzelnen Tenant erfordert Filterung, und die Wiederherstellung kann eine selektive Neuinterpretation bedeuten.
- Potenzielle Probleme mit Datenwachstum: Eine einzelne Tabelle mit Millionen von Zeilen von vielen Tenants kann zu Leistungsproblemen führen (z. B. Index-Bloat, große Tabellenscans), wenn sie nicht richtig indiziert und optimiert ist.
- Begrenzte Anpassung: Alle Tenants teilen sich dasselbe Schema; Anpassungen sind schwierig oder unmöglich, ohne das Schema erheblich mit generischen Feldern (z. B. JSONB-Spalten) zu erweitern.
Fazit
Die Wahl der richtigen Multi-Tenant-Datenbankarchitektur ist eine entscheidende Entscheidung, die die Skalierbarkeit, Sicherheit, Kosten und Wartbarkeit einer SaaS-Anwendung beeinflusst. Während die „Separate Datenbank pro Tenant“-Methode die höchste Isolation und Sicherheit bietet, geht dies auf Kosten von betrieblicher Komplexität und Infrastrukturkosten. Die „Gemeinsame Datenbank, gemeinsames Schema mit Tenant-Diskriminator“-Methode bietet die größte Ressourceneffizienz und Einfachheit bei der Schemaverwaltung, erfordert jedoch eine sorgfältige Datenisolation auf Anwendungsebene. Das „Separate Schema pro Tenant“-Muster bildet ein Gleichgewicht und bietet gute Isolation bei reduziertem Betriebsaufwand im Vergleich zu vollständig separaten Datenbanken. Letztendlich hängt der beste Ansatz von den spezifischen Anforderungen Ihrer Anwendung ab, einschließlich Ihrer Toleranz für betriebliche Komplexität, Sicherheitsanforderungen und der erwarteten Anzahl und Größe Ihrer Tenants.