EMZETT.
Войти

Мультитенантная архитектура

Коротко: Программная архитектура, в которой одно приложение обслуживает одновременно нескольких независимых клиентов («арендаторов») — у каждого свои данные, часто разделённые собственными поддоменами.

Подробнее: Типичный шаблон: customer1.my-platform.com, customer2.my-platform.com и т. д. работают на одной кодовой базе, но с раздельными данными по арендаторам. Обычно для этого нужна wildcard-запись DNS и платёжное решение, способное распределять деньги между несколькими получателями (например, Stripe Connect).

Наш контекст: Долгосрочная цель Emzett — платформу магазина позже предполагается «сдавать в аренду» другим, каждому клиенту на своём поддомене. По дорожной карте это последний и самый большой шаг расширения.

Подробности

Три степени изоляции

В мультитенантных системах центральное проектное решение — КАК разделять данные разных арендаторов; от «мягкого» к «строгому» существует примерно три распространённых подхода:

  1. Общие таблицы со столбцом tenant_id — все арендаторы живут в одних таблицах, каждая строка несёт идентификатор арендатора, и каждый запрос дополнительно фильтрует по нему. Проще всего в эксплуатации (одна база данных, одна схема, один пул соединений), но забытый фильтр WHERE tenant_id = ... может смешать данные клиентов — серьёзный риск безопасности, от которого на практике часто защищаются политиками безопасности на уровне строк в базе данных (Postgres поддерживает это нативно: политика гарантирует, что даже забытое WHERE в коде приложения не вернёт чужих строк, потому что база данных фильтрует сама).
  2. Одна схема на арендатора — та же база данных, но каждый арендатор получает собственную схему PostgreSQL (пространство имён, например tenant_customer1.orders вместо public.orders). Изоляция лучше, чем в подходе 1 (плохо написанный запрос не может случайно обратиться к другой схеме без явного указания), но миграции приходится выполнять для каждой схемы отдельно — заметная эксплуатационная нагрузка при сотнях арендаторов.
  3. Одна база данных на арендатора — максимальная изоляция (ошибка базы данных или всплеск производительности у арендатора A совсем не влияет на арендатора B), но больше всего усилий в эксплуатации, особенно при многих мелких арендаторах: у каждой базы данных свои резервные копии, свой мониторинг, свои миграции, а число открытых соединений растёт линейно с числом арендаторов.

Многие крупные SaaS-платформы (например, Slack в ранней архитектуре) начинают с подхода 1 и переходят к подходу 2 или 3 по мере роста клиентов и ужесточения требований соответствия (некоторые корпоративные клиенты по договору требуют физического разделения данных), часто лишь для крупнейших клиентов, тогда как мелкие продолжают делить подход 1 («гибридная» модель).

Маршрутизация по поддоменам

Определение поддомена обычно происходит на уровне middleware/proxy: заголовок Host запроса разбирается, из него выводится арендатор, и этот контекст передаётся всем последующим запросам к базе данных (например, через контекст запроса или переменную, заданную middleware, которая затем «пропускается» через всю обработку запроса, например с помощью AsyncLocalStorage в Node.js, без необходимости явно передавать её каждой функции).

// Упрощённый шаблон на уровне middleware/proxy
const host = request.headers.get("host"); // например "customer1.my-platform.com"
const tenant = host?.split(".")[0];

Для wildcard-поддоменов зоне DNS нужна соответствующая запись (*.my-platform.com → тот же IP сервера), а сертификат TLS также должен быть wildcard-сертификатом (или автоматической выдачей сертификатов на каждый поддомен, как предлагает Vercel для собственных доменов), иначе посетители получают предупреждение о сертификате.

Распределение платежей

Для платформы, которая сама принимает деньги от конечных клиентов и должна передавать их соответствующим арендаторам (модель маркетплейса), обычной интеграции Stripe недостаточно — для этого и существует Stripe Connect: каждый арендатор получает собственный (связанный) аккаунт Stripe, а платежи можно направлять напрямую или через платформу с разделением комиссии («application fee»), и платформе самой не нужно выступать юридическим получателем всей выручки арендаторов.

См. также: Записи DNS