paint-brush
SaaS-архитектура Django: одноарендная или мультитенантная — что подойдет именно вам?к@pbityukov
2,880 чтения
2,880 чтения

SaaS-архитектура Django: одноарендная или мультитенантная — что подойдет именно вам?

к Pavel Bityukov9m2023/11/01
Read on Terminal Reader

Слишком долго; Читать

Django — это популярный фреймворк, который вы можете выбрать для разработки приложения для своей компании. Но что, если вы хотите создать приложение SaaS, которое будут использовать несколько клиентов? Какую архитектуру выбрать? Давайте посмотрим, как можно подойти к этой задаче.
featured image - SaaS-архитектура Django: одноарендная или мультитенантная — что подойдет именно вам?
Pavel Bityukov HackerNoon profile picture

Django — это популярная платформа, которую вы можете выбрать для разработки приложения для своей компании. Но что, если вы хотите создать приложение SaaS, которое будут использовать несколько клиентов? Какую архитектуру выбрать? Давайте посмотрим, как можно подойти к этой задаче.

Одноарендная архитектура

Самый простой подход — создать отдельный экземпляр для каждого вашего клиента. Допустим, у нас есть приложение Django и база данных. Затем для каждого клиента нам нужно запустить собственную базу данных и экземпляр приложения. Это означает, что каждый экземпляр приложения имеет только одного клиента.

Одноарендная архитектура с выделенными экземплярами приложений и баз данных.


Этот подход прост в реализации: вам нужно просто запустить новый экземпляр каждого имеющегося у вас сервиса. Но в то же время это может вызвать проблему: каждый клиент существенно увеличит стоимость инфраструктуры. Это может не иметь большого значения, если вы планируете иметь всего несколько клиентов или если каждый экземпляр крошечный.


Однако давайте предположим, что мы строим крупную компанию, которая предоставляет корпоративный мессенджер 100 000 организациям. Представьте себе, насколько дорого может обойтись дублирование всей инфраструктуры для каждого нового клиента! И, когда нам нужно обновить версию приложения, нам нужно развернуть ее для каждого клиента, поэтому развертывание тоже будет замедлено .

Многопользовательская архитектура

Существует еще один подход, который может помочь в сценарии, когда у приложения много клиентов: мультитенантная архитектура. Это означает, что у нас есть несколько клиентов, которых мы называем арендаторами , но все они используют только один экземпляр приложения.

Мультитенантная архитектура с общим приложением и экземпляром базы данных.

Хотя эта архитектура решает проблему высокой стоимости выделенных экземпляров для каждого клиента, она порождает новую проблему: как мы можем быть уверены, что данные клиента надежно изолированы от других клиентов?


Мы обсудим следующие подходы:

  1. Использование общей базы данных и схемы общей базы данных . Мы можем определить, какой арендатор владеет данными, по внешнему ключу, который нам нужно добавить в каждую таблицу базы данных.


  2. Использование общей базы данных, но отдельных схем базы данных . Таким образом, нам не нужно будет поддерживать несколько экземпляров базы данных, но мы получим хороший уровень изоляции данных арендатора.


  3. Использование отдельных баз данных : это похоже на пример с одним арендатором, но не будет таким же, поскольку мы по-прежнему будем использовать общий экземпляр приложения и выбирать, какую базу данных использовать, проверяя арендатора.


Давайте углубимся в эти идеи и посмотрим, как интегрировать их с приложением Django.

Общая база данных с общей схемой

Этот вариант может быть первым, который приходит на ум: добавить в таблицы ForeignKey и использовать его для выбора подходящих данных для каждого арендатора. Однако у него есть огромный недостаток: данные арендаторов вообще не изолированы, поэтому небольшой ошибки программирования может быть достаточно для утечки данных арендатора не тому клиенту.


Давайте возьмем пример структуры базы данных из документации Django :

 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)


Нам нужно будет определить, какие записи принадлежат какому арендатору. Итак, нам нужно добавить таблицу Tenant и внешний ключ в каждую существующую таблицу:

 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)


Чтобы немного упростить код, мы можем создать абстрактную базовую модель, которая будет повторно использоваться в каждой другой создаваемой нами модели.

 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)


Как видите, здесь есть как минимум два основных риска: разработчик может забыть добавить поле арендатора в новую модель или разработчик может забыть использовать это поле при фильтрации данных.


Исходный код этого примера можно найти на GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .

Общая база данных с отдельными схемами

Помня о рисках общей схемы, давайте рассмотрим другой вариант: база данных по-прежнему будет общей, но мы создадим выделенную схему для каждого арендатора. Для реализации мы можем посмотреть популярную библиотеку django-tenants ( документация ).


Давайте добавим django-tenants в наш небольшой проект (официальные этапы установки можно найти здесь ).


Первым шагом является установка библиотеки через pip :

pip install django-tenants


Измените модели: модель Tenant теперь будет в отдельном приложении. Модели Question и Choice больше не будут иметь связи с арендатором. Поскольку данные разных арендаторов будут находиться в отдельных схемах, нам больше не потребуется связывать отдельные записи со строками арендаторов.


Файл tenants/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 ...


Файл 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)

Обратите внимание, что у вопросов и вариантов выбора больше нет внешнего ключа к Тенанту!


Еще одно изменение заключается в том, что арендатор теперь находится в отдельном приложении: это не только для разделения доменов, но и важно, поскольку нам нужно будет хранить таблицу tenants в общей схеме, а таблицы polls будут созданы для каждого арендатора. схема.


Внесите изменения в файл settings.py для поддержки нескольких схем и клиентов:

 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"


Далее давайте создадим и применим миграции:

python manage.py makemigrations

python manage.py migrate_schemas --shared


В результате мы увидим, что публичная схема будет создана и будет содержать только общие таблицы.

Структура базы данных после первого запуска командыmigr_schemas

Нам нужно будет создать арендатора по умолчанию для public схемы:

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


Если вас спросят, установите для is_primary значение True .


И затем мы можем приступить к созданию реальных арендаторов сервиса:

 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


Обратите внимание, что теперь в базе данных есть еще две схемы, содержащие таблицы polls :

Схема базы данных после создания клиентов

Теперь вы будете получать вопросы и варианты ответов из разных схем при вызове API в доменах, которые вы настроили для клиентов — и все готово!


Хотя установка выглядит более сложной и, возможно, даже более сложной, если вы перенесете существующее приложение, сам подход по-прежнему имеет множество преимуществ, таких как безопасность данных.


Код примера можно найти здесь .

Отдельные базы данных

Последний подход, который мы обсудим сегодня, идет еще дальше и включает отдельные базы данных для арендаторов.


На этот раз у нас будет несколько баз данных:

Отдельные базы данных для арендаторов


Мы будем хранить общие данные, такие как сопоставление арендаторов с именами баз данных, в default_db и создадим отдельную базу данных для каждого арендатора.


Затем нам нужно будет установить конфигурацию баз данных в файле settings.py:

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


И теперь мы сможем получить данные для каждого арендатора, using метод QuerySet:

 Questions.objects.using('tenant_1')…


Недостатком этого метода является то, что вам придется применить все миграции к каждой базе данных, используя:

python manage.py migrate --database=tenant_1


Также может быть менее удобно создавать новую базу данных для каждого арендатора по сравнению с использованием django-tenants или просто с использованием внешнего ключа, как в подходе с общей схемой.


С другой стороны, изоляция данных арендатора действительно хороша: базы данных можно физически разделить. Еще одним преимуществом является то, что мы не будем ограничены использованием только Postgresql, как того требуют django-tenants , мы можем выбрать любой движок, который будет соответствовать нашим потребностям.


Дополнительную информацию по теме нескольких баз данных можно найти в документации Django.

Сравнение


Одноарендатор

MT с общей схемой

MT с отдельной схемой

МТ с отдельными базами данных

Изоляция данных

✅Высокий

❌Самый низкий

✅Высокий

✅Высокий

Риск случайной утечки данных

✅Низкий

❌Высокая

✅Низкий

✅Низкий

Стоимость инфраструктуры

❌Выше с каждым арендатором

✅Нижний

✅Нижний

✅❌ Ниже, чем у одноквартирных домов

Скорость развертывания

❌Снижение с каждым арендатором

✅❌ Миграции будут медленнее, поскольку их необходимо выполнять для каждой схемы.

✅❌ Миграции будут медленнее, поскольку их необходимо выполнять для каждой базы данных.

Легко реализовать

❌ Требует большого количества изменений, если сервис уже реализован как однотенантное приложение.

Заключение

Подводя итог всему вышесказанному, похоже, что для этой проблемы не существует серебряной пули, каждый подход имеет свои плюсы и минусы, поэтому разработчики сами должны решить, какой компромисс они могут найти.


Отдельные базы данных обеспечивают лучшую изоляцию данных арендатора и просты в реализации, однако их обслуживание обходится дороже: при обновлении базы данных количество подключений к базе данных увеличивается.


Общая база данных с отдельной схемой сложна в реализации и может иметь некоторые проблемы с миграцией.


Один арендатор является наиболее простым в реализации, но он обходится вам чрезмерным потреблением ресурсов, поскольку у вас есть полная копия вашего сервиса на каждого арендатора.