Как реализовать масштабируемый multi-tenant SaaS с изоляцией данных

Вы строите SaaS-продукт, и рано или поздно встаёт вопрос: как сделать так, чтобы данные одного клиента никогда не попали к другому, при этом система держала нагрузку от сотен и тысяч арендаторов и не разваливалась при добавлении нового. Это и есть суть multi-tenant архитектуры — один экземпляр приложения обслуживает многих клиентов, но каждый видит только свои данные.

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

Что значит «изоляция данных» на самом деле

Изоляция данных — это не просто фильтрация по идентификатору. Это гарантия, что при любом сбое, баге, ошибке в коде или неправильном запросе пользователь тенанта А не увидит данные тенанта Б. Уровни изоляции бывают разные, и выбор между ними — это первое, что нужно определить до написания кода.

Три подхода к изоляции данных

Есть три классических способа организовать хранение данных в multi-tenant системе. У каждого своя цена, свой уровень изоляции и свои ограничения.

1. Одна база, tenant_id в каждой таблице

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

Плюсы: легко стартовать, просто мигрировать, минимальные накладные расходы на инфраструктуру. Подходит, когда тенантов десятки или сотни.

Минусы: если вы забыли добавить WHERE tenant_id = ? в одном запросе — вы утечете данные между клиентами. Это не теоретический риск, это реальная ошибка, которая случается в продакшене. Чем больше разработчиков в команде, тем выше вероятность.

2. Отдельная схема на тенанта

В одной базе данных создаётся отдельная схема (schema) для каждого арендатора. При подключении приложение переключает search_path или использует префикс схемы.

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

Минусы: миграции схемы нужно применять ко всем тенантам. При тысячах тенантов это превращается в ад — время на обновление схемы растёт линейно. Пул соединений тоже усложняется.

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

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

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

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

Сравнение подходов

Параметр Одна БД + tenant_id Схема на тенанта БД на тенанта
Изоляция данных Логическая Логическая + частично физическая Физическая
Стоимость инфраструктуры Низкая Низкая Высокая
Сложность миграций Низкая Средняя Высокая
Количество тенантов До ~500 До ~1000–2000 Без ограничений
Риск утечки между тенантами Высокий Средний Минимальный
Восстановление одного клиента Сложное Среднее Простое
Кастомизация схемы под клиента Сложная Возможна Легко реализуема

Как выбрать подход под вашу ситуацию

Если вы стартап и у вас первые 50–200 клиентов — берите одну БД с tenant_id. Это быстро, дёшево и позволяет сосредоточиться на продукте, а не на инфраструктуре. Главное — настроить автоматические тесты, которые проверяют, что каждый запрос к БД содержит фильтр по тенанту.

Если вы B2B-платформа с клиентами среднего размера и требованиями к аудиту — схема на тенанта даст баланс между изоляцией и стоимостью. Вы сможете восстановить данные отдельного клиента и покажете аудитору, что данные разделены.

Если вы работаете с крупными корпорациями, госсектором или регулируемыми отраслями — отдельная БД на тенанта оправдана. Клиенты сами будут требовать такой уровень изоляции, и вы сможете обосновать более высокую цену продукта.

Реализация: что нужно сделать помимо выбора БД

Выбор способа хранения — только начало. Масштабируемый multi-tenant SaaS требует проработки нескольких слоёв одновременно.

Идентификация тенанта в запросе

Запрос приходит — и вы должны понять, какому тенанту он принадлежит. Обычно это делается на уровне middleware: извлекается из JWT-токена, поддомена или заголовка, и сохраняется в контексте запроса. Контекст прокидывается вниз до слоя доступа к данным.

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

Автоматическая фильтрация запросов

Если вы используете ORM, настройте глобальные фильтры (global query filters). В Entity Framework это HasQueryFilter, в Django — custom Manager, в Prisma — middleware на уровне клиента. Идея в том, что каждый запрос автоматически получает условие по tenant_id, и разработчик не может забыть его добавить.

Но глобальные фильтры — не панацея. Они не покрывают сложные агрегации, raw-запросы и массовые операции. Для них нужен отдельный контроль — как минимум код-ревью и автотесты.

Управление миграциями

Миграции в multi-tenant системе — отдельная боль. Если у вас 500 тенантов и 500 схем (или баз), обычная миграция превращается в долгоиграющий процесс.

Подход: пишете скрит миграции, который проходит по всем тенантам и применяет изменения. Запускаете его асинхронно, с логированием прогресса и возможностью откатить отдельного тенанта. Обязательно — мониторинг: если миграция упала на 300-м тенанте из 500, вы должны это увидеть и продолжить с того же места.

Для отдельных БД на тенанта используйте оркестратор — например, отдельный сервис, который хранит список всех баз и управляет подключениями к ним.

Пул соединений и маршрутизация

Когда баз много, пул соединений становится проблемой. Нельзя держать открытым соединение к каждой БД для каждого инстанса приложения. Решения: прокси-слой вроде PgBouncer для PostgreSQL, или динамическая маршрутизация — приложение определяет тенанта, открывает соединение к нужной БД, работает, закрывает.

Для подхода с одной БД это не актуально — один пул, одна точка подключения.

Кэширование с учётом тенантов

Redis или Memcached — отличное ускорение, но ключи должны включать tenant_id. Иначе один клиент закэширует данные, и другой их увидит. Лучше всего — префикс ключа вроде tenant_123:user_456. Или отдельный Redis database / кластер для крупных клиентов.

Очереди и фоновые задачи

Любая асинхронная обработка должна нести в себе контекст тенанта. Когда вы кладёте задачу в очередь, в её теле должен быть tenant_id. Воркер, который забирает задачу, устанавливает правильный контекст перед выполнением. Иначе фоновый процесс может работать с чужими данными.

Частые ошибки, которые видел в проектах

  1. Забыли про tenant_id в raw-запросе. Все ORM-запросы отфильтрованы, а когда кто-то написал прямой SQL для отчёта — фильтра нет. Решение: линтеры и код-ревью для любого сырого SQL.
  2. Кэш без изоляции. Добавили Redis для ускорения, забыли про префиксы. Клиент А видит данные клиента Б. Решение: абстракция над кэшем, которая автоматически добавляет tenant-префикс.
  3. Миграция упала на середине. Обновили схему у половины тенантов, вторая половина осталась на старой. Решение: миграции с возможностью отката и идемпотентностью.
  4. Один клиент положил базу. Крупный тенант сгенерировал огромную нагрузку, и остальные 99 начали тормозить. Решение: rate limiting на уровне тенанта, мониторинг ресурсов по клиентам, возможность выделить крупных в отдельный пул.
  5. Хранение tenant_id на клиенте. Приложение передаёт tenant_id в API, и сервер ему верит. Любой может подставить чужой ID. Решение: tenant_id извлекается только из серверного токена.
  6. Нет мониторинга по тенантам. Вы видите, что всё тормозит, но не знаете, какой клиент виноват. Решение: метрики с тегом tenant_id, логи с фильтрацией.

Практические рекомендации

Начните с простого. Одна БД, tenant_id, глобальные фильтры в ORM. Этого достаточно для первых сотен клиентов. Не усложняйте архитектуру заранее.

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

Логируйте доступ к данным. Каждый запрос к БД должен содержать tenant_id в логах. Когда (не если, а когда) возникнет вопрос «почему этот пользователь видит чужие данные», вы сможете разобраться за минуты, а не за дни.

Планируйте рост тенантов заранее. Если вы начинаете с одной БД, заложите в структуру возможность шардирования — например, хэширование tenant_id для распределения по нескольким физическим базам в будущем.

Используйте готовые инструментам. Для PostgreSQL — Row Level Security как дополнительный слой защиты. Для маршрутизации — PgBouncer, Citus. Для SaaS-специфичных задач — библиотеки вроде django-tenants или laravel-tenancy, которые решают типовые проблемы из коробки.

Когда какой подход оправдан: короткий гайд

Менее 100 тенантов, стандартные требования к безопасности → одна БД + tenant_id. Быстро, дёшево, достаточно надёжно при правильных фильтрах.

100–1000 тенантов, есть требования к аудиту и восстановлению → схема на тенанта. Лучшая изоляция при умеренной сложности.

Более 1000 тенантов или работа с крупными клиентами → отдельная БД на тенанта. Максимальная изоляция, но требует зрелой инфраструктуры.

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

Итог

Масштабируемый multi-tenant SaaS — это не одна архитектурная 决策ение, а цепочка выборов: как хранить данные, как фильтровать запросы, как управлять миграциями, как кэшировать, как мониторить. Начните с простого подхода, автоматизируйте проверку изоляции тестами, и усложняйте архитектуру только когда текущее решение начнёт буксовать. Раньше времени отдельные базы на тенанта — это не надёжность, а лишняя сложность и расходы.

Главное правило: изоляция данных — это не разовая настройка, а процесс, который должен быть встроен в каждый слой приложения, от middleware до фоновых задач, и проверен автоматическими тестами.

Dfncfg.ru