Skip to content
Назад в блог

Архитектура SaaS: Single-Tenant против Multi-Tenant баз данных

12 мин. чтения
Архитектура SaaS: Single-Tenant против Multi-Tenant баз данных

Выбор архитектуры базы данных для B2B SaaS — это одно из самых ответственных решений. Оно напрямую влияет на стоимость серверов, скорость разработки новых функций, сложность проведения миграций и соответствие стандартам защиты данных (таким как GDPR, HIPAA или 152-ФЗ).

Архитектурная ошибка на старте обойдется крайне дорого при попытке исправить ее в будущем. В этой статье мы подробно проанализируем три основных паттерна организации баз данных, сравним их ключевые характеристики и напишем безопасную реализацию разделения данных с помощью PostgreSQL Row-Level Security (RLS).

Три парадигмы Multi-Tenant архитектуры

+-----------------------+   +-----------------------+   +-----------------------+
|  Паттерн А: Раздельные|   |   Паттерн Б: Раздельные|   |   Паттерн В: Общая    |
|      базы данных      |   |        схемы          |   |     база и таблицы    |
+-----------------------+   +-----------------------+   +-----------------------+
|  [DB 1]      [DB 2]   |   |  [База Данных]        |   |  [База Данных]        |
|  Клиент А    Клиент Б |   |   ├── Схема Клиент А  |   |   └── [Таблица]       |
|                       |   |   └── Схема Клиент Б  |   |        ├── Строка кл.А|
| (Макс. изоляция/цена) |   | (Средняя изоляция)    |   |        └── Строка кл.Б|
+-----------------------+   +-----------------------+   +-----------------------+

1. Сравнение архитектурных подходов

Давайте сопоставим все три паттерна по критически важным для бизнеса и разработки критериям:

КритерийПаттерн А: Выделенные БДПаттерн Б: Выделенные схемыПаттерн В: Общая таблица + RLS
Изоляция данныхМаксимальная: Нулевой риск случайного перекрестного доступа.Высокая: Разделение на уровне пространств имен СУБД.Средняя: Риск утечки при ошибках в SQL-запросах или политиках.
Стоимость серверовМаксимальная: Огромные накладные расходы на ресурсы и пулы соединений.Средняя: Общее дисковое пространство, но нагрузка на кэш каталогов.Минимальная: Максимально эффективное использование железа.
Сложность миграцийОчень высокая: Требуется последовательно обновлять сотни баз.Высокая: Циклическое применение миграций по всем схемам.Простая: Стандартный накат одной миграции на всю базу.
Пулы соединенийСложно: Отдельный пул под каждого клиента.Средне: Общий пул, но с постоянным переключением схем.Просто: Единый стандартный пул соединений для всех.
Бэкап и ВосстановлениеИдеально: Можно восстановить только одного клиента из резервной копии.Средне: Требуется выборочный дамп отдельных схем (pg_dump).Сложно: Восстановление одного клиента требует выборки данных.

2. Реализация Row-Level Security (RLS) в PostgreSQL

Если вы выбираете наиболее экономичный и простой в поддержке Паттерн В (Общие таблицы), безопасность данных необходимо гарантировать на уровне ядра базы данных.

Механизм Row-Level Security (RLS) в PostgreSQL позволяет автоматически фильтровать строки при любых запросах в зависимости от переменных текущей сессии.

Шаг 1: Создание таблиц и включение RLS

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

-- Таблица клиентов (тенантов)
CREATE TABLE tenants (
  id SERIAL PRIMARY KEY,
  company_name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Таблица проектов с привязкой к клиенту
CREATE TABLE projects (
  id SERIAL PRIMARY KEY,
  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name VARCHAR(255) NOT NULL,
  status VARCHAR(50) DEFAULT 'draft',
  created_at TIMESTAMP DEFAULT NOW()
);

-- Индексируем tenant_id (Критически важно для скорости запросов!)
CREATE INDEX idx_projects_tenant ON projects(tenant_id);

-- Включаем защиту строк RLS для таблицы проектов
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

Шаг 2: Создание политики безопасности

Настроим политику, которая сопоставляет поле tenant_id с переменной сессии app.current_tenant_id.

CREATE POLICY tenant_isolation_policy ON projects
  AS ASYMMETRIC
  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);

Шаг 3: Выполнение запросов из кода приложения (Node.js)

Бэкенд-контроллер (например, в Nuxt server или Express) выполняет операции внутри SQL-транзакции, передавая ID текущего пользователя перед выполнением запроса.

import { Client } from 'pg';

async function fetchTenantProjects(tenantId: number): Promise<any[]> {
  const client = new Client({ connectionString: process.env.DATABASE_URL });
  await client.connect();

  try {
    // Начинаем транзакцию
    await client.query('BEGIN');

    // Устанавливаем ID текущего клиента для этой транзакции
    await client.query(`SET LOCAL app.current_tenant_id = $1`, [tenantId]);

    // Даже без указания WHERE tenant_id = X база данных вернет только нужные строки
    const result = await client.query('SELECT * FROM projects');

    await client.query('COMMIT');
    return result.rows;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    await client.end();
  }
}

3. Рекомендации по масштабированию и поддержке

  1. Всегда покрывайте автотестами: Напишите интеграционные тесты, которые пытаются прочитать проекты Клиента Б через сессию Клиента А. Тест должен завершаться ошибкой доступа или возвращать пустой массив.
  2. Заранее готовьтесь к экспорту: Некоторые крупные корпоративные клиенты со временем могут потребовать выделенный сервер. Ваша структура должна позволять быстро отфильтровать все связанные данные по tenant_id и перенести их на отдельный инстанс.
  3. Не забывайте про композитные индексы: Если вы ищете проекты по дате создания внутри конкретного клиента, создайте композитный индекс (tenant_id, created_at).

Например, на языковом портале LingvoHabit мы успешно развернули изоляцию на уровне строк (Паттерн В), используя индексированные схемы тенантов, что позволило удерживать расходы на СУБД на минимальном уровне при обслуживании тысяч активных студентов.

Источники и документация

  • PostgreSQL: Row Security Policies — официальная документация по RLS
  • PostgreSQL: Schemas — логические схемы для Паттерна Б
  • GDPR.eu — требования к изоляции и переносимости персональных данных

Правильный выбор архитектуры базы данных обеспечивает баланс между бюджетом стартапа и строгими требованиями безопасности. Использование PostgreSQL RLS позволяет создать сверхдешевую, быструю в разработке и при этом надежно защищенную SaaS-архитектуру. Чтобы узнать, как эти решения интегрируются в общий процесс запуска продукта, изучите мое руководство Как собрать B2B SaaS за 30 дней.

Если вы проектируете архитектуру базы данных для своего SaaS-сервиса и хотите получить независимый экспертный аудит решений, изучите мою страницу Разработки SaaS-платформ или запишитесь на детальную Консультацию по архитектуре баз данных.

Частые вопросы

Какой паттерн multi-tenancy выбрать для старта SaaS?

Для большинства B2B-стартапов — общую базу с Row-Level Security (Паттерн В): минимальные расходы на серверы, простые миграции и достаточная изоляция при правильно написанных политиках. Выделенные базы оправданы только при жёстких регуляторных требованиях клиентов.

Насколько безопасна изоляция через PostgreSQL RLS?

При включённых политиках RLS база сама фильтрует строки по tenant_id на уровне СУБД — даже ошибочный SQL-запрос приложения не вернёт чужие данные. Обязательное условие: интеграционные тесты, которые пытаются прочитать данные одного клиента через сессию другого.

Что делать, если крупный клиент потребует выделенную базу?

Проектировать экспорт заранее: если все таблицы связаны через tenant_id, данные одного клиента выгружаются стандартной фильтрацией и переносятся на отдельный инстанс без переписывания приложения.