Multi-Tenant RAG: SaaS-Architektur für 1000 Kunden mit einem System
Entwerfen Sie eine skalierbare Multi-Tenant-RAG-Architektur. Datenisolation, Performance, Sicherheit und Kostenoptimierung fuer ein RAG-SaaS mit Hunderten von Kunden.
Multi-Tenant RAG: SaaS-Architektur fuer 1000 Kunden mit einem System
Einen RAG-Chatbot fuer einen Kunden zu erstellen ist einfach. Ihn fuer 1000 Kunden gleichzeitig auf derselben Infrastruktur laufen zu lassen und dabei eine vollstaendige Datenisolation zu garantieren? Das ist eine ganz andere Herausforderung. Dieser Leitfaden beschreibt die Architekturen, Muster und Fallstricke des Multi-Tenant-RAG, basierend auf der Erfahrung von Ailog, das Hunderte von Kunden ueber eine gemeinsame Infrastruktur bedient.
TL;DR
- Multi-Tenancy = ein RAG-System fuer mehrere Kunden mit Datenisolation
- Drei Isolationsstrategien: Namespace/Partition, dedizierte Collection, dedizierte Datenbank
- Die kritische Entscheidung: Kompromiss zwischen Isolation, Kosten und operativer Komplexitaet
- Qdrant: Payload-Filtering (am haeufigsten), separate Collections oder separate Instanzen
- Kosten: von $0,50/Tenant/Monat (shared) bis $50/Tenant/Monat (dediziert)
- Sicherheit: Datenleckage zwischen Tenants ist das Risiko Nummer 1
Warum Multi-Tenancy?
Ohne Multi-Tenancy erfordert jeder neue Kunde eine dedizierte Infrastruktur. Das ist ein operativer und finanzieller Albtraum.
| Ansatz | 10 Kunden | 100 Kunden | 1000 Kunden |
|---|---|---|---|
| Dedizierte Infrastruktur | $500/Mon | $5.000/Mon | $50.000/Mon |
| Multi-Tenant shared | $200/Mon | $400/Mon | $2.000/Mon |
| Einsparung | 60% | 92% | 96% |
Dedizierte Infrastruktur (1 System pro Kunde):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Kunde A │ │ Kunde B │ │ Kunde C │ ... │Kunde 999 │
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │Vector│ │ │ │Vector│ │ │ │Vector│ │ │ │Vector│ │
│ │ DB │ │ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ ├──────┤ │ │ ├──────┤ │ │ ├──────┤ │ │ ├──────┤ │
│ │ LLM │ │ │ │ LLM │ │ │ │ LLM │ │ │ │ LLM │ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
❌ Skaliert nicht. Lineare Kosten.
Multi-Tenant (1 System, N Kunden):
┌─────────────────────────────────────────────────────┐
│ Geteiltes RAG-System │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Vektor-Datenbank │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │ A │ │ B │ │ C │ ... │ 999 │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ LLM Gateway (geteilt) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
✅ Skaliert. Sub-lineare Kosten.
Die drei Isolationsstrategien
Strategie 1: Namespace / Payload-Filtering
Alle Tenant-Daten befinden sich in derselben Collection, unterschieden durch ein tenant_id-Feld.
DEVELOPERpythonfrom qdrant_client import QdrantClient from qdrant_client.models import ( Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue ) client = QdrantClient(url="http://localhost:6333") # 1. Geteilte Collection erstellen client.create_collection( collection_name="shared_knowledge", vectors_config=VectorParams( size=1536, distance=Distance.COSINE ) ) # 2. Dokumente mit tenant_id indexieren def index_document(tenant_id: str, doc_id: str, embedding: list, content: str, metadata: dict): """Indexiert ein Dokument mit tenant_id als Payload.""" client.upsert( collection_name="shared_knowledge", points=[ PointStruct( id=doc_id, vector=embedding, payload={ "tenant_id": tenant_id, "content": content, **metadata } ) ] ) # 3. Mit Tenant-Filter suchen def search_tenant(tenant_id: str, query_embedding: list, top_k: int = 5) -> list: """Suche beschraenkt auf Tenant-Dokumente.""" results = client.search( collection_name="shared_knowledge", query_vector=query_embedding, query_filter=Filter( must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ] ), limit=top_k ) return results
Vorteile: Einfach, wirtschaftlich, keine Verwaltung mehrerer Collections. Nachteile: Nur logische Isolation, Leistungsabfall bei sehr grossen Indizes, Datenleck-Risiko bei Filtering-Bug.
Strategie 2: Dedizierte Collection pro Tenant
Jeder Tenant erhaelt seine eigene Collection in derselben Vektordatenbank-Instanz.
DEVELOPERpythonclass CollectionPerTenantStrategy: """Eine Qdrant-Collection pro Tenant.""" def __init__(self, qdrant_url: str): self.client = QdrantClient(url=qdrant_url) def create_tenant(self, tenant_id: str, vector_size: int = 1536): """Erstellt eine dedizierte Collection fuer einen neuen Tenant.""" collection_name = f"tenant_{tenant_id}" self.client.create_collection( collection_name=collection_name, vectors_config=VectorParams( size=vector_size, distance=Distance.COSINE ) ) return collection_name def delete_tenant(self, tenant_id: str): """Loescht alle Daten eines Tenants.""" collection_name = f"tenant_{tenant_id}" self.client.delete_collection(collection_name) def search(self, tenant_id: str, query_embedding: list, top_k: int = 5): """Suche in der Collection des Tenants.""" collection_name = f"tenant_{tenant_id}" return self.client.search( collection_name=collection_name, query_vector=query_embedding, limit=top_k )
Vorteile: Gute Isolation, vorhersagbare Leistung pro Tenant, einfache Datenloeschung. Nachteile: Mehr Collections zu verwalten, Speicher-Overhead, Collection-Anzahl-Limits.
Strategie 3: Dedizierte Datenbank pro Tenant
Jeder Tenant erhaelt seine eigene Vektordatenbank-Instanz. Reserviert fuer Grosskunden.
Vorteile: Vollstaendige Isolation, garantierte Leistung, regulatorische Compliance. Nachteile: Hohe Kosten, operative Komplexitaet, untergenutzte Ressourcen.
Strategievergleich
| Kriterium | Namespace/Filtering | Dedizierte Collection | Dedizierte Instanz |
|---|---|---|---|
| Datenisolation | Logisch | Stark | Vollstaendig |
| Leck-Risiko | Mittel | Niedrig | Nahezu null |
| Kosten/Tenant (100 Dok) | ~$0,50/Mon | ~$2/Mon | ~$20/Mon |
| Kosten/Tenant (10k Dok) | ~$5/Mon | ~$8/Mon | ~$50/Mon |
| Performance | Verschlechtert bei Skalierung | Gut | Ausgezeichnet |
| Max Tenants | 10.000+ | 1.000-5.000 | 100-500 |
| Datenloeschung | Komplex | Einfach | Sehr einfach |
| DSGVO-Konformitaet | Schwierig | Gut | Ausgezeichnet |
| Ops-Komplexitaet | Niedrig | Mittel | Hoch |
| Anwendungsfall | Self-Service SaaS | Pro SaaS | Enterprise |
Entscheidungsmatrix
Anzahl der Tenants?
├── > 5.000 → Namespace/Filtering (erforderlich)
├── 500 - 5.000 → Dedizierte Collection
├── < 500
│ ├── Strenge DSGVO / sensible Daten → Dedizierte Instanz
│ ├── Begrenztes Budget → Namespace/Filtering
│ └── Standard → Dedizierte Collection
└── < 10 (Grosskunden) → Dedizierte Instanz
Vollstaendige Multi-Tenant-Architektur
DEVELOPERpythonfrom enum import Enum class IsolationLevel(Enum): SHARED = "shared" # Namespace/Filtering COLLECTION = "collection" # Collection pro Tenant DEDICATED = "dedicated" # Dedizierte Instanz class MultiTenantRAGService: """Multi-Tenant-RAG-Service mit automatischem Routing.""" def __init__(self): self.shared_client = QdrantClient(url="http://qdrant-shared:6333") self.tenant_registry = {} def register_tenant(self, tenant_id: str, plan: str, isolation: IsolationLevel = None): """Registriert einen neuen Tenant.""" if isolation is None: isolation = self._determine_isolation(plan) config = {"plan": plan, "isolation": isolation} if isolation == IsolationLevel.COLLECTION: collection_name = f"tenant_{tenant_id}" self.shared_client.create_collection( collection_name=collection_name, vectors_config=VectorParams( size=1536, distance=Distance.COSINE ) ) config["collection"] = collection_name elif isolation == IsolationLevel.DEDICATED: instance_url = self._provision_dedicated(tenant_id, plan) config["instance_url"] = instance_url config["client"] = QdrantClient(url=instance_url) self.tenant_registry[tenant_id] = config def _determine_isolation(self, plan: str) -> IsolationLevel: """Bestimmt die Isolationsstufe basierend auf dem Plan.""" plan_mapping = { "free": IsolationLevel.SHARED, "starter": IsolationLevel.SHARED, "pro": IsolationLevel.COLLECTION, "business": IsolationLevel.COLLECTION, "enterprise": IsolationLevel.DEDICATED, } return plan_mapping.get(plan, IsolationLevel.SHARED) async def query(self, tenant_id: str, question: str, query_embedding: list) -> list: """Sucht Dokumente fuer einen bestimmten Tenant.""" config = self.tenant_registry[tenant_id] isolation = config["isolation"] if isolation == IsolationLevel.SHARED: return self.shared_client.search( collection_name="shared_knowledge", query_vector=query_embedding, query_filter=Filter(must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ]), limit=5 ) elif isolation == IsolationLevel.COLLECTION: return self.shared_client.search( collection_name=config["collection"], query_vector=query_embedding, limit=5 ) elif isolation == IsolationLevel.DEDICATED: return config["client"].search( collection_name="knowledge", query_vector=query_embedding, limit=5 ) async def delete_tenant_data(self, tenant_id: str): """Loescht alle Tenant-Daten (DSGVO).""" config = self.tenant_registry[tenant_id] isolation = config["isolation"] if isolation == IsolationLevel.SHARED: self.shared_client.delete( collection_name="shared_knowledge", points_selector=Filter(must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ]) ) elif isolation == IsolationLevel.COLLECTION: self.shared_client.delete_collection(config["collection"]) elif isolation == IsolationLevel.DEDICATED: self._deprovision_dedicated(tenant_id) del self.tenant_registry[tenant_id]
Performance-Isolation
Datenisolation allein reicht nicht. Ein ressourcenhungriger Tenant darf die Leistung fuer andere nicht verschlechtern.
Rate-Limiting pro Tenant
DEVELOPERpythonfrom datetime import datetime, timedelta from collections import defaultdict class TenantRateLimiter: """Rate-Limiter pro Tenant.""" def __init__(self): self.limits = { "free": {"rpm": 10, "rpd": 100, "tokens_per_day": 50000}, "starter": {"rpm": 30, "rpd": 1000, "tokens_per_day": 500000}, "pro": {"rpm": 100, "rpd": 10000, "tokens_per_day": 5000000}, "enterprise": {"rpm": 500, "rpd": 100000, "tokens_per_day": 50000000}, } self.usage = defaultdict(lambda: { "minute": [], "day": [], "tokens_today": 0 }) async def check_and_consume(self, tenant_id: str, plan: str, tokens: int = 0) -> bool: """Prueft und verbraucht Kontingent.""" limits = self.limits[plan] usage = self.usage[tenant_id] now = datetime.utcnow() usage["minute"] = [ t for t in usage["minute"] if now - t < timedelta(minutes=1) ] usage["day"] = [ t for t in usage["day"] if now - t < timedelta(days=1) ] if len(usage["minute"]) >= limits["rpm"]: return False if len(usage["day"]) >= limits["rpd"]: return False if usage["tokens_today"] + tokens > limits["tokens_per_day"]: return False usage["minute"].append(now) usage["day"].append(now) usage["tokens_today"] += tokens return True
Limits pro Plan
| Plan | Anfragen/Min | Anfragen/Tag | Tokens/Tag | Max Dokumente | Max Dok-Groesse |
|---|---|---|---|---|---|
| Free | 10 | 100 | 50K | 50 | 1 MB |
| Starter | 30 | 1.000 | 500K | 500 | 5 MB |
| Pro | 100 | 10.000 | 5M | 5.000 | 20 MB |
| Business | 300 | 50.000 | 20M | 50.000 | 50 MB |
| Enterprise | 500+ | 100.000+ | 50M+ | Unbegrenzt | 100 MB |
Kostenoptimierung
Geschaetzte Kosten pro Tenant-Profil
| Profil | Dokumente | Abfragen/Monat | Geschaetzte Kosten/Monat |
|---|---|---|---|
| Mikro (Schaufenster) | 20 | 500 | $0,80 |
| Klein (KMU) | 200 | 5.000 | $3,50 |
| Mittel (E-Commerce) | 2.000 | 50.000 | $25 |
| Gross (Enterprise) | 20.000 | 500.000 | $180 |
| Sehr gross | 100.000+ | 2M+ | $800+ |
Sicherheit: Verhinderung von Datenlecks
Risiko Nummer 1: Tenant-uebergreifende Datenlecks
DEVELOPERpythonclass TenantSecurityMiddleware: """Sicherheits-Middleware zur Verhinderung von Tenant-uebergreifenden Datenlecks.""" def __init__(self): self.audit_logger = AuditLogger() async def validate_request(self, request, tenant_id: str): """Validiert, dass die Anfrage fuer diesen Tenant legitim ist.""" auth_tenant = self._extract_tenant_from_auth(request) if auth_tenant != tenant_id: self.audit_logger.log_security_event( "TENANT_MISMATCH", f"Auth-Tenant {auth_tenant} != Anfrage-Tenant {tenant_id}" ) raise SecurityError("Tenant-Abweichung") tenant = await self._get_tenant(tenant_id) if not tenant or tenant.status != "active": raise SecurityError("Tenant nicht gefunden oder inaktiv") self.audit_logger.log_access(tenant_id, request.path) return True async def validate_response(self, response, tenant_id: str): """Stellt sicher, dass keine Daten eines anderen Tenants austreten.""" if hasattr(response, "documents"): for doc in response.documents: doc_tenant = doc.metadata.get("tenant_id") if doc_tenant and doc_tenant != tenant_id: self.audit_logger.log_security_event( "DATA_LEAK_PREVENTED", f"Dok von {doc_tenant} fast an {tenant_id} geliefert" ) response.documents.remove(doc) return response
Multi-Tenant-Sicherheits-Checkliste
| Kontrolle | Prioritaet | Implementiert? |
|---|---|---|
| Authentifizierung via API-Key mit Tenant-Bindung | Kritisch | |
| tenant_id-Filter bei ALLEN DB-Abfragen | Kritisch | |
| Response-Validierung (keine Lecks) | Kritisch | |
| Rate-Limiting pro Tenant | Hoch | |
| Audit-Log aller Zugriffe | Hoch | |
| Datenverschluesselung im Ruhezustand | Hoch | |
| Tenant-uebergreifende Penetrationstests | Hoch | |
| Isoliertes Backup pro Tenant | Mittel | |
| Zertifizierte Loeschung (DSGVO) | Mittel | |
| Ueberwachung von Zugriffsanomalien | Mittel |
FAQ
Wie handhabt man die Migration eines Tenants von einem Plan zu einem anderen?
Die Plan-Migration (z.B. Shared zu dedizierter Collection) erfordert das Kopieren von Daten von einem Speicherort zum anderen. Gehen Sie in drei Schritten vor: (1) Neue Collection erstellen, (2) Daten batchweise kopieren, (3) Routing umschalten. Waehrend der Migration koexistieren beide Raeume. Ueberpruefen Sie, dass alle Daten korrekt kopiert wurden, bevor Sie den alten Raum loeschen.
Wie skaliert man horizontal, wenn ein einzelner Qdrant-Server nicht mehr ausreicht?
Qdrant unterstuetzt natives Sharding und Replikation. Konfigurieren Sie einen Qdrant-Cluster mit N Knoten, und Collections werden automatisch verteilt. Fuer Multi-Tenant ist Sharding nach tenant_id ideal: Die Daten eines Tenants befinden sich auf demselben Shard, was die Filtering-Leistung optimiert. Pinecone und Weaviate bieten aehnliche verwaltete Loesungen.
Wie garantiert man die vollstaendige Loeschung der Daten eines Tenants (DSGVO)?
Mit der Strategie "dedizierte Collection" ist es einfach: Collection loeschen. Mit "Namespace/Filtering" ist es komplexer: Alle Punkte mit der tenant_id muessen geloescht werden, dann muss ueberprueft werden, dass keine Rueckstaende in Caches oder Backups verbleiben. Bewahren Sie ein Loeschungszertifikat mit Datum und Anzahl geloeschter Dokumente fuer die DSGVO-Konformitaet auf.
Wie wirkt sich die Anzahl der Tenants auf die Suchleistung aus?
Mit Namespace/Filtering verschlechtert sich die Suchleistung, wenn der Index 10M Vektoren ueberschreitet (alle Tenants zusammen). Die Suche muss alle Vektoren filtern, bevor Ergebnisse zurueckgegeben werden. Mit dedizierten Collections hat jeder Tenant einen kleinen unabhaengigen Index, sodass die Leistung unabhaengig von der Tenant-Anzahl konstant bleibt.
Wie berechnet man Tenants praezise ab?
Verfolgen Sie drei Metriken pro Tenant: (1) Anzahl indexierter Dokumente (Speicher), (2) Anzahl der Abfragen (Compute), (3) Anzahl verbrauchter LLM-Tokens (Generierung). Speichern Sie diese Metriken in einer Abrechnungstabelle, die in Echtzeit aktualisiert wird. Die Abrechnung kann pauschal (Plan-Limits) oder nutzungsbasiert (Pay-per-Query) erfolgen.
Fazit
Die Multi-Tenant-Architektur ist das Fundament jedes funktionierenden RAG-SaaS. Die Wahl der Isolationsstrategie wirkt sich direkt auf Kosten, Leistung und Sicherheit Ihrer Plattform aus.
Wichtige Erkenntnisse:
- Beginnen Sie mit Namespace/Filtering fuer kostenlose und Starter-Plaene - am wirtschaftlichsten
- Dedizierte Collections fuer Pro-Plaene: guter Kompromiss zwischen Isolation und Kosten
- Dedizierte Instanzen nur fuer Grosskunden mit regulatorischen Anforderungen
- Rate-Limiting ist unerlaeesslich zum Schutz der Plattform vor Missbrauch
- Tenant-uebergreifende Sicherheitsaudits muessen automatisiert und kontinuierlich sein
Ailog ist nativ als Multi-Tenant konzipiert, mit strikter Datenisolation und garantierter Leistung fuer jeden Kunden. Erstellen Sie Ihr Konto und stellen Sie Ihren RAG-Chatbot in Minuten auf einer geteilten und sicheren Infrastruktur bereit.
Ressourcen
- Qdrant Multi-Tenancy - Offizielle Dokumentation
- Pinecone Namespaces - Namespace-Isolation
- RAG Security & Compliance - Vollstaendige RAG-Sicherheit
- Production Deployment - Produktionsbereitstellung
- RAG-Kostenoptimierung - Kostenoptimierung
- DSGVO & Chatbot - DSGVO-Konformitaet
Tags
Verwandte Artikel
Sicherheit und Compliance für RAG: DSGVO, AI Act und Best Practices
Sichern Sie Ihr RAG-System: DSGVO-Konformität, europäischer AI Act, Datenschutz und Audit. Umfassender Leitfaden für Unternehmen.
RAG für KMU: Kompletter Leitfaden ohne Data-Team
Setzen Sie ein leistungsfähiges RAG-System in Ihrem KMU ein, ganz ohne fortgeschrittene technische Kenntnisse: No-code-Lösungen, kontrolliertes Budget und schnellen ROI.
Souveräner RAG: Hosting in Frankreich und europäische Daten
Setzen Sie einen souveränen RAG in Frankreich ein: lokales Hosting, DSGVO‑Konformität, Alternativen zu GAFAM und Best Practices für europäische Daten.