Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Мультиарендная архитектура в SaaS: изоляция клиентов, быстрый онбординг и экономия на инфраструктуре

Разработка и технологии14 апреля 2026 г.
Разбираем практичные модели мультиарендности: одна БД с RLS, схема на клиента и база на клиента. Покажу, как безопасно определять арендатора, настраивать Postgres RLS, изолировать нагрузку и катить миграции без простоя — чтобы быстрее подключать Enterprise‑клиентов и не раздувать счета за инфраструктуру.
Мультиарендная архитектура в SaaS: изоляция клиентов, быстрый онбординг и экономия на инфраструктуре

  • Содержание
    • Зачем бизнесу мультиарендность
    • Три модели изоляции: одна таблица, одна схема, отдельная база
    • Как определить арендатора и не промахнуться
    • Практика: Postgres RLS за 15 минут
    • Изоляция нагрузки и экономия
    • Миграции без простоев и с контролем
    • Наблюдаемость и биллинг по арендаторам
    • План внедрения за 4–6 недель
    • Антипаттерны и грабли
    • Чек‑лист выбора модели

Зачем бизнесу мультиарендность

Мультиарендность — это когда один продукт обслуживает много клиентов (арендаторов), но каждый видит только свои данные и получает предсказуемые показатели по скорости и надежности. Для бизнеса это:

  • Быстрый онбординг: не тратим дни на отдельные развёртывания.
  • Изоляция рисков: «шумный сосед» не валит остальных.
  • Гибкая коммерция: тарифы, лимиты и SLA по клиентам.
  • Экономия: общие ресурсы там, где можно; жесткая изоляция — когда нужно.

Когда особенно актуально:

  • Сегмент Enterprise с требованиями комплаенса (SOC 2, ISO 27001, ФЗ‑152).
  • Много мелких клиентов — критично дешевое масштабирование.
  • Разнообразные профили нагрузки (дневные пики, регионы).

Три модели изоляции: одна таблица, одна схема, отдельная база

Есть три базовые стратегии. Их можно смешивать по сегментам.

1) Одна база, одни и те же таблицы (tenant_id + RLS)

  • Суть: все данные в общих таблицах, у каждой строки есть tenant_id. Доступ ограничиваем политиками уровня строк (RLS) в Postgres.
  • Плюсы: минимальная стоимость, простые кросс‑арендные отчеты, лёгкий обмен ресурсами (кэш, соединения).
  • Минусы: повышенные требования к дисциплине запросов, нужно грамотно настроить RLS и индексы.
  • Когда выбирать: много клиентов, схожие требования, нет жёсткой изоляции на уровне инфраструктуры.

2) Одна база, отдельные схемы на арендатора

  • Суть: для каждого клиента создаем схему (schema), в ней свой набор таблиц.
  • Плюсы: лучше изоляция, проще разные версии схем (по необходимости), контроль ресурсов через аналитику по схемам.
  • Минусы: усложняется миграция и управление большим числом объектов; connection pool остается общий.
  • Когда выбирать: средний Enterprise, чуть разные модели данных или политика обновлений.

3) Отдельная база на арендатора

  • Суть: у каждого клиента своя БД (и, возможно, свой кластер).
  • Плюсы: максимальная изоляция по данным и производительности; легче соответствовать строгим требованиям.
  • Минусы: дороже, сложнее сопровождение и релизы, сложнее межклиентская аналитика.
  • Когда выбирать: крупные Enterprise, строгий комплаенс, большие чеки и индивидуальные SLO.

Правило большого пальца:

  • B2C/SMB — берите модель 1.
  • Смешанная линейка — 1 для мелких, 2 для средних, 3 для ключевых Enterprise.

Как определить арендатора и не промахнуться

Чаще всего арендатор определяется по одному из трёх признаков:

  • Поддомен: acme.example.com → tenant=acme
  • Поле в токене: JWT.claims.tenant
  • Заголовок/метка на уровне API‑шлюза

Требования к механизму:

  • Единый способ внутри бэкенда: в каждом запросе у вас есть надёжный tenant_id.
  • Привязка к сессии БД: чтобы политики RLS работали автоматически.
  • Защита от подмены: tenant_id берём только из доверенного источника (проверенный токен, маппинг домена), никогда — из произвольного поля запроса.

Антипаттерн: «прокидывать tenant_id ручками в каждый запрос к БД». Ошибиться тут очень легко. Лучше один раз «зафиксировать» контекст арендатора на уровне соединения с базой.

Практика: Postgres RLS за 15 минут

Покажу минимальную, но рабочую конфигурацию RLS с установкой tenant_id через параметр сессии.

Шаг 1. Схема данных и политики

-- Подключитесь к базе от имени владельца схемы
-- Создаём расширение uuid-ossp при необходимости
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Базовая таблица с tenant_id
CREATE TABLE IF NOT EXISTS public.invoices (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  tenant_id uuid NOT NULL,
  number text NOT NULL,
  amount numeric(12,2) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- Индекс по tenant_id для быстрого доступа
CREATE INDEX IF NOT EXISTS invoices_tenant_idx ON public.invoices(tenant_id);

-- Включаем RLS
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;

-- Политика чтения: показываем только строки с tenant_id = app.current_tenant
CREATE POLICY invoices_select_policy ON public.invoices
  FOR SELECT USING (
    tenant_id = current_setting('app.current_tenant', true)::uuid
  );

-- Политика вставки: запрещаем писать от чужого имени
CREATE POLICY invoices_insert_policy ON public.invoices
  FOR INSERT WITH CHECK (
    tenant_id = current_setting('app.current_tenant', true)::uuid
  );

-- Политика обновления: только свои строки
CREATE POLICY invoices_update_policy ON public.invoices
  FOR UPDATE USING (
    tenant_id = current_setting('app.current_tenant', true)::uuid
  )
  WITH CHECK (
    tenant_id = current_setting('app.current_tenant', true)::uuid
  );

-- Роль приложения с ограниченными правами
DO $$
BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_user') THEN
    CREATE ROLE app_user LOGIN PASSWORD 'app_password';
  END IF;
END $$;

GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE ON public.invoices TO app_user;

Шаг 2. Установка tenant_id на соединении и запросы из Node.js

Идея простая: после взятия соединения из пула, сразу делаем SET app.current_tenant = '' и дальше любые запросы «видят» только строки своего арендатора.

// app.js
// Минимальный Express + pg пример с контекстом арендатора через параметр сессии

const express = require('express');
const { Pool } = require('pg');

// В демо используем DSN локальной базы. Замените на свой при необходимости.
const pool = new Pool({
  connectionString: 'postgres://app_user:app_password@localhost:5432/app'
});

const app = express();
app.use(express.json());

// Пример обнаружения арендатора: из поддомена вида <tenant>.localhost
function resolveTenantFromHost(host) {
  // host может быть вида 'acme.localhost:3000' — берём первую часть
  const name = host.split(':')[0];
  const parts = name.split('.');
  if (parts.length > 1) {
    const sub = parts[0].toLowerCase();
    // В реальности маппим sub → UUID из своей таблицы tenants
    // Для примера фиксируем два клиента
    if (sub === 'acme') return '00000000-0000-0000-0000-0000000000ac';
    if (sub === 'beta') return '00000000-0000-0000-0000-0000000000be';
  }
  return null;
}

// Middleware: устанавливаем tenant в параметр сессии Postgres
async function withTenant(req, res, next) {
  const tenantId = resolveTenantFromHost(req.headers.host || '');
  if (!tenantId) return res.status(400).json({ error: 'Tenant not resolved' });

  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query("SET LOCAL app.current_tenant = $1", [tenantId]);
    // Привязываем клиент к запросу
    req.db = client;
    req.tenantId = tenantId;
    await next();
    await client.query('COMMIT');
  } catch (e) {
    await client.query('ROLLBACK');
    next(e);
  } finally {
    req.db.release();
  }
}

app.get('/invoices', withTenant, async (req, res) => {
  const { rows } = await req.db.query('SELECT id, number, amount FROM public.invoices ORDER BY created_at DESC LIMIT 50');
  res.json(rows);
});

app.post('/invoices', withTenant, async (req, res) => {
  const { number, amount } = req.body;
  const { tenantId } = req;
  const { rows } = await req.db.query(
    'INSERT INTO public.invoices (tenant_id, number, amount) VALUES ($1, $2, $3) RETURNING id, number, amount',
    [tenantId, number, amount]
  );
  res.status(201).json(rows[0]);
});

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal error' });
});

app.listen(3000, () => console.log('Server started on http://acme.localhost:3000'));

Почему SET LOCAL? Оно действует в рамках транзакции. Так мы гарантируем, что параметр не «протечет» в другой запрос при повторном использовании соединения из пула.

Проверка

  • Создайте пару записей с разными tenant_id.
  • Запросите GET /invoices с хоста acme.localhost
    — увидите только свои счета.

Изоляция нагрузки и экономия

Даже при общей БД важно не допускать «шумного соседа»:

  • Индексы по tenant_id во всех крупных таблицах.
  • Партиционирование по tenant_id или по дате (если данных очень много).
  • Отдельные очереди/воркеры для тяжёлых клиентов.
  • Лимиты на фоновые задачи по арендаторам (конкурентность, таймауты).
  • Стили запросов: всегда указывать селективные фильтры, избегать полнотабличных сканов в пиках.

Если у вас Kubernetes и модель 2/3 (схемы/базы на клиента), можно изолировать ресурсы на уровне пространства имён:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-acme-quota
  namespace: tenant-acme
spec:
  hard:
    requests.cpu: "2"
    requests.memory: 4Gi
    limits.cpu: "4"
    limits.memory: 8Gi
    pods: "20"

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

Миграции без простоев и с контролем

Миграции при мультиарендности — частый источник боли. Базовые правила:

  • Совместимость вперёд/назад: сначала добавить новые поля/индексы, потом использовать. Удаление — в отдельном релизе.
  • Пакетировать миграции: короткие шаги, безопасные по времени.
  • Для модели «схема/база на клиента» — поэтапный rollout: канареечные арендаторы → остальная масса.

Мини‑пример миграции для «схем на арендатора»: создаём таблицу в новой схеме по шаблону.

-- Создаём схему для нового арендатора и копируем структуру из public
CREATE SCHEMA IF NOT EXISTS tenant_acme AUTHORIZATION app_user;

-- Таблица по образцу (в т.ч. индексы)
CREATE TABLE IF NOT EXISTS tenant_acme.invoices (LIKE public.invoices INCLUDING ALL);

-- Назначаем права
GRANT SELECT, INSERT, UPDATE ON tenant_acme.invoices TO app_user;

Подход к деплою:

  • Прогоняем миграции в тени (dry‑run) и измеряем время.
  • На проде — сначала неразрушающие шаги, затем переключение фичи, затем уборка старого.

Наблюдаемость и биллинг по арендаторам

Сделайте tenant_id «первоклассным гражданином» в логах и метриках:

  • Логи: обязательно поле tenant в каждой записи (через middleware).
  • Метрики: счётчики RPS, ошибки, длительность запросов по арендаторам.
  • SLO per tenant: доля запросов быстрее X мс и ошибка‑бюджет.
  • Биллинг: суммарное потребление CPU/памяти/хранилища и объём операций по арендаторам.

Это помогает и продажам (обосновать тариф), и поддержке (увидеть «шумного» клиента), и продукту (понять реальное использование функций).

План внедрения за 4–6 недель

Неделя 1:

  • Решение по модели (1/2/3) на основе требований клиентов и бюджета.
  • Введение tenant_id в доменную модель (таблицы, события, логи).

Неделя 2:

  • Выбор механизма определения арендатора (домен, токен).
  • Рефакторинг бэкенда: единая прослойка, которая «фиксирует» tenant в сессии БД.

Неделя 3:

  • Включение RLS в Postgres для критичных таблиц.
  • Индексы по tenant_id.
  • Набор интеграционных тестов: «доступен только свой набор данных».

Неделя 4:

  • Метрики и логи с tenant полем.
  • Дашборды SLO per tenant.
  • Лёгкие лимиты на фоновую обработку по арендаторам.

Неделя 5–6:

  • Миграции «вперёд/назад», сценарии отката.
  • Нагрузочные тесты: имитация «шумного соседа».
  • Документация для продаж и безопасности: модель изоляции, план аудита.

Антипаттерны и грабли

  • Ручной прокид tenant_id в каждом запросе к БД: одна забытая проверка — и утечка. Используйте RLS или запросный прокси.
  • Смешанные транзакции для разных арендаторов: запретите это на уровне кода (валидация контекста).
  • «Один индекс на всех» при огромных данных: добавляйте составные индексы и партиции, иначе всё будет ехать.
  • Тяжёлые миграции в прайм‑тайм: только ночные окна или онлайн‑алгоритмы с поэтапным переключением.
  • Отсутствие тестов изоляции: заведите отдельный набор, который специально пытается «подглядеть» чужие строки.

Чек‑лист выбора модели

Ответьте на 7 вопросов:

  1. Нужна ли юридическая изоляция данных на уровне БД/кластера? Если да — модель 3.
  2. Сколько клиентов и как быстро растёт число? Если много и быстро — модель 1.
  3. Разные ли модели данных у клиентов? Если да — модель 2/3.
  4. Какой чек и бюджет на инфраструктуру? Чем ниже чек — тем логичнее модель 1.
  5. Нужны ли отдельные окна релизов? Если да — модель 2/3.
  6. Требуются ли отчёты по всем клиентам сразу? Проще в модели 1.
  7. Какой уровень зрелости команды DevOps/DBA? Чем выше — тем легче жить с моделью 3.

Итог: начните с простой и экономной модели «общая таблица + RLS», но проектируйте код так, чтобы при росте было не больно перейти к «схеме/базе на клиента». Чётко фиксируйте tenant в сессии БД, держите метрики и логи по арендаторам и не забывайте про дисциплину миграций. Это даст быстрый онбординг, предсказуемые SLO для ключевых клиентов и понятные затраты на инфраструктуру.


мультиарендностьPostgres RLSSaaS архитектура