paint-brush
Django SaaS-Architektur: Single-Tenant vs. Multi-Tenant – Welche ist die richtige für Sie?von@pbityukov
2,734 Lesungen
2,734 Lesungen

Django SaaS-Architektur: Single-Tenant vs. Multi-Tenant – Welche ist die richtige für Sie?

von Pavel Bityukov9m2023/11/01
Read on Terminal Reader

Zu lang; Lesen

Django ist ein beliebtes Framework, das Sie zur Entwicklung einer Anwendung für Ihr Unternehmen auswählen können. Was aber, wenn Sie eine SaaS-Anwendung erstellen möchten, die von mehreren Kunden verwendet wird? Welche Architektur sollten Sie wählen? Mal sehen, wie diese Aufgabe angegangen werden kann.
featured image - Django SaaS-Architektur: Single-Tenant vs. Multi-Tenant – Welche ist die richtige für Sie?
Pavel Bityukov HackerNoon profile picture

Django ist ein beliebtes Framework, das Sie zur Entwicklung einer Anwendung für Ihr Unternehmen auswählen können. Was aber, wenn Sie eine SaaS-Anwendung erstellen möchten, die von mehreren Kunden verwendet wird? Welche Architektur sollten Sie wählen? Mal sehen, wie diese Aufgabe angegangen werden kann.

Single-Tenant-Architektur

Der einfachste Ansatz besteht darin, für jeden Ihrer Kunden eine separate Instanz zu erstellen. Nehmen wir an, wir haben eine Django-Anwendung und eine Datenbank. Dann müssen wir für jeden Client eine eigene Datenbank und Anwendungsinstanz ausführen. Das bedeutet, dass jede Anwendungsinstanz nur einen Mandanten hat.

Die Single-Tenant-Architektur mit dedizierten Anwendungs- und Datenbankinstanzen


Dieser Ansatz ist einfach zu implementieren: Sie müssen einfach eine neue Instanz jedes vorhandenen Dienstes starten. Gleichzeitig kann es jedoch zu einem Problem kommen: Jeder Client erhöht die Kosten der Infrastruktur erheblich. Es ist möglicherweise keine große Sache, wenn Sie nur wenige Kunden haben möchten oder jede Instanz winzig ist.


Nehmen wir jedoch an, dass wir ein großes Unternehmen aufbauen, das 100.000 Organisationen einen Unternehmens-Messenger zur Verfügung stellt. Stellen Sie sich vor, wie teuer es sein kann, die gesamte Infrastruktur für jeden neuen Kunden zu duplizieren! Und wenn wir die Anwendungsversion aktualisieren müssen, müssen wir sie für jeden Client bereitstellen, sodass die Bereitstellung ebenfalls verlangsamt wird .

Multi-Tenant-Architektur

Es gibt einen anderen Ansatz, der in einem Szenario hilfreich sein kann, in dem wir viele Clients für die Anwendung haben: eine mandantenfähige Architektur. Das bedeutet, dass wir mehrere Clients haben, die wir Mandanten nennen, aber alle nur eine Instanz der Anwendung verwenden.

Die mandantenfähige Architektur mit gemeinsam genutzter Anwendungs- und Datenbankinstanz

Während diese Architektur das Problem der hohen Kosten dedizierter Instanzen für jeden Client löst, führt sie zu einem neuen Problem: Wie können wir sicherstellen, dass die Daten des Clients sicher von anderen Clients isoliert sind?


Wir werden folgende Ansätze diskutieren:

  1. Verwendung einer gemeinsam genutzten Datenbank und eines gemeinsam genutzten Datenbankschemas : Wir können anhand des Fremdschlüssels, den wir jeder Datenbanktabelle hinzufügen müssen, identifizieren, welcher Mandant die Daten besitzt.


  2. Verwendung einer gemeinsam genutzten Datenbank, aber separater Datenbankschemata : Auf diese Weise müssen wir nicht mehrere Datenbankinstanzen verwalten, sondern erhalten ein gutes Maß an Mandantendatenisolation.


  3. Verwendung separater Datenbanken : Es sieht ähnlich aus wie das Einzelmandanten-Beispiel, wird aber nicht dasselbe sein, da wir weiterhin eine gemeinsam genutzte Anwendungsinstanz verwenden und die zu verwendende Datenbank auswählen, indem wir den Mandanten überprüfen.


Lassen Sie uns tiefer in diese Ideen eintauchen und sehen, wie wir sie in die Django-Anwendung integrieren können.

Eine gemeinsam genutzte Datenbank mit gemeinsamem Schema

Diese Option ist vielleicht die erste, die mir in den Sinn kommt: den Tabellen einen ForeignKey hinzuzufügen und ihn zur Auswahl geeigneter Daten für jeden Mandanten zu verwenden. Es hat jedoch einen großen Nachteil: Die Daten der Mieter sind überhaupt nicht isoliert, sodass ein kleiner Programmierfehler ausreichen kann, um die Daten des Mieters an den falschen Client weiterzugeben.


Nehmen wir ein Beispiel der Datenbankstruktur aus der Django-Dokumentation :

 from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)


Wir müssen ermitteln, welche Datensätze welchem Mieter gehören. Daher müssen wir in jeder vorhandenen Tabelle eine Tenant und einen Fremdschlüssel hinzufügen:

 class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)


Um den Code ein wenig zu vereinfachen, können wir ein abstraktes Basismodell erstellen, das in jedem anderen von uns erstellten Modell wiederverwendet wird.

 class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)


Wie Sie sehen, bestehen hier mindestens zwei große Risiken: Ein Entwickler kann vergessen, ein Mandantenfeld zum neuen Modell hinzuzufügen , oder ein Entwickler kann vergessen, dieses Feld beim Filtern der Daten zu verwenden.


Der Quellcode für dieses Beispiel ist auf GitHub zu finden: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .

Eine gemeinsam genutzte Datenbank mit separaten Schemata

Unter Berücksichtigung der Risiken des gemeinsam genutzten Schemas betrachten wir eine andere Option: Die Datenbank wird weiterhin gemeinsam genutzt, wir erstellen jedoch für jeden Mandanten ein eigenes Schema. Zur Implementierung können wir uns eine beliebte Bibliothek von Django-Tenants ansehen ( Dokumentation ).


Fügen wir django-tenants zu unserem kleinen Projekt hinzu (die offiziellen Installationsschritte finden Sie hier ).


Der erste Schritt ist die Bibliotheksinstallation per pip :

pip install django-tenants


Ändern Sie die Modelle: Das Tenant befindet sich jetzt in einer separaten App. Question und Choice haben keine Verbindung mehr zum Mieter. Da sich die Daten verschiedener Mandanten in separaten Schemata befinden, müssen wir die einzelnen Datensätze nicht mehr mit den Mandantenzeilen verknüpfen.


Die Dateimieter/models.py

 from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ...


Die Datei polls/models.py

 from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)

Beachten Sie, dass Question und Choice keinen Fremdschlüssel mehr für Tenant haben!


Die andere Sache, die geändert wurde, ist, dass sich der Mandant jetzt in einer separaten App befindet: Dies dient nicht nur der Trennung der Domänen, sondern ist auch wichtig, da wir die tenants im gemeinsamen Schema speichern müssen und polls für jeden Mandanten erstellt werden Schema.


Nehmen Sie Änderungen an der Datei settings.py vor, um mehrere Schemas und Mandanten zu unterstützen:

 DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain"


Als Nächstes erstellen und wenden wir die Migrationen an:

python manage.py makemigrations

python manage.py migrate_schemas --shared


Als Ergebnis sehen wir, dass das öffentliche Schema erstellt wird und nur gemeinsam genutzte Tabellen enthält.

Die Datenbankstruktur nach der ersten Ausführung des migrate_schemas-Befehls

Wir müssen einen Standardmandanten für das public Schema erstellen:

python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant


Setzen Sie is_primary auf True , wenn Sie dazu aufgefordert werden.


Und dann können wir mit der Erstellung der echten Mandanten des Dienstes beginnen:

 python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2


Beachten Sie, dass es jetzt zwei weitere Schemata in der Datenbank gibt, die polls enthalten:

Das Datenbankschema nach dem Erstellen der Mandanten

Jetzt erhalten Sie die Fragen und Auswahlmöglichkeiten aus verschiedenen Schemata, wenn Sie APIs für die Domänen aufrufen, die Sie für die Mandanten eingerichtet haben – fertig!


Auch wenn die Einrichtung bei einer Migration der bestehenden App komplizierter und möglicherweise sogar schwieriger aussieht, bietet der Ansatz selbst dennoch viele Vorteile, wie zum Beispiel die Sicherheit der Daten.


Den Code des Beispiels finden Sie hier .

Separate Datenbanken

Der letzte Ansatz, den wir heute besprechen werden, geht sogar noch weiter und verfügt über separate Datenbanken für die Mieter.


Dieses Mal werden wir ein paar Datenbanken haben:

Separate Datenbanken für die Mieter


Wir speichern die gemeinsam genutzten Daten wie die Zuordnung des Mandanten zu den Datenbanknamen in der default_db und erstellen für jeden Mandanten eine separate Datenbank.


Dann müssen wir die Datenbankkonfiguration in der Datei „settings.py“ festlegen:

 DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }


Und jetzt können wir die Daten für jeden Mandanten abrufen, indem wir die QuerySet-Methode using :

 Questions.objects.using('tenant_1')…


Der Nachteil der Methode besteht darin, dass Sie alle Migrationen auf jede Datenbank anwenden müssen, indem Sie Folgendes verwenden:

python manage.py migrate --database=tenant_1


Es ist möglicherweise auch weniger praktisch, für jeden Mandanten eine neue Datenbank zu erstellen, als die django-tenants zu verwenden oder nur einen Fremdschlüssel wie beim Shared-Schema-Ansatz zu verwenden.


Wirklich gut ist hingegen die Isolation der Mandantendaten: Die Datenbanken können physisch getrennt werden. Ein weiterer Vorteil besteht darin, dass wir nicht darauf beschränkt sind, nur Postgresql zu verwenden, wie es von den django-tenants benötigt wird, sondern wir können jede Engine auswählen, die unseren Anforderungen entspricht.


Weitere Informationen zum Thema „Mehrere Datenbanken“ finden Sie in der Django- Dokumentation .

Vergleich


Einzelmieter

MT mit gemeinsamem Schema

MT mit separatem Schema

MT mit separaten Datenbanken

Datenisolation

✅Hoch

❌Am niedrigsten

✅Hoch

✅Hoch

Es besteht die Gefahr, dass versehentlich Daten verloren gehen

✅Niedrig

❌Hoch

✅Niedrig

✅Niedrig

Infrastrukturkosten

❌Mit jedem Mieter höher

✅Niedriger

✅Niedriger

✅❌ Niedriger als bei Einzelmietern

Bereitstellungsgeschwindigkeit

❌Mit jedem Mieter niedriger

✅❌ Migrationen werden langsamer sein, da sie für jedes Schema ausgeführt werden müssen

✅❌ Migrationen werden langsamer sein, da sie für jede Datenbank ausgeführt werden müssen

Einfach umzusetzen

❌ Erfordert viele Änderungen, wenn der Dienst bereits als Single-Tenant-App implementiert wurde

Abschluss

Um das oben Genannte zusammenzufassen: Es sieht so aus, als gäbe es kein Allheilmittel für das Problem. Jeder Ansatz hat seine Vor- und Nachteile. Es liegt also an den Entwicklern, zu entscheiden, welchen Kompromiss sie eingehen können.


Getrennte Datenbanken bieten die beste Isolation für die Daten des Mandanten und sind einfach zu implementieren. Sie verursachen jedoch höhere Wartungskosten: Da die Datenbank aktualisiert werden muss, ist die Anzahl der Datenbankverbindungen höher.


Die Implementierung einer gemeinsam genutzten Datenbank mit einem separaten Schema ist etwas kompliziert und kann bei der Migration zu Problemen führen.


Die Implementierung mit einem einzigen Mandanten ist am einfachsten zu implementieren, verursacht jedoch Kosten durch übermäßigen Ressourcenverbrauch, da Sie über eine vollständige Kopie Ihres Dienstes pro Mandant verfügen.