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