Вы строите SaaS-продукт, и рано или поздно встаёт вопрос: как сделать так, чтобы данные одного клиента никогда не попали к другому, при этом система держала нагрузку от сотен и тысяч арендаторов и не разваливалась при добавлении нового. Это и есть суть multi-tenant архитектуры — один экземпляр приложения обслуживает многих клиентов, но каждый видит только свои данные.
Проблема в том, что «просто добавить tenant_id в каждую таблицу» — это первый шаг, а не решение. На практике к этому добавляются вопросы безопасности, производительности, миграций, кастомизации и стоимости инфраструктуры. Разберёмся, как всё это собрать в рабочую систему.
- Что значит «изоляция данных» на самом деле
- Три подхода к изоляции данных
- 1. Одна база, tenant_id в каждой таблице
- 2. Отдельная схема на тенанта
- 3. Отдельная база данных на тенанта
- Сравнение подходов
- Как выбрать подход под вашу ситуацию
- Реализация: что нужно сделать помимо выбора БД
- Идентификация тенанта в запросе
- Автоматическая фильтрация запросов
- Управление миграциями
- Пул соединений и маршрутизация
- Кэширование с учётом тенантов
- Очереди и фоновые задачи
- Частые ошибки, которые видел в проектах
- Практические рекомендации
- Когда какой подход оправдан: короткий гайд
- Итог
Что значит «изоляция данных» на самом деле
Изоляция данных — это не просто фильтрация по идентификатору. Это гарантия, что при любом сбое, баге, ошибке в коде или неправильном запросе пользователь тенанта А не увидит данные тенанта Б. Уровни изоляции бывают разные, и выбор между ними — это первое, что нужно определить до написания кода.
Три подхода к изоляции данных
Есть три классических способа организовать хранение данных в 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. Воркер, который забирает задачу, устанавливает правильный контекст перед выполнением. Иначе фоновый процесс может работать с чужими данными.
Частые ошибки, которые видел в проектах
- Забыли про tenant_id в raw-запросе. Все ORM-запросы отфильтрованы, а когда кто-то написал прямой SQL для отчёта — фильтра нет. Решение: линтеры и код-ревью для любого сырого SQL.
- Кэш без изоляции. Добавили Redis для ускорения, забыли про префиксы. Клиент А видит данные клиента Б. Решение: абстракция над кэшем, которая автоматически добавляет tenant-префикс.
- Миграция упала на середине. Обновили схему у половины тенантов, вторая половина осталась на старой. Решение: миграции с возможностью отката и идемпотентностью.
- Один клиент положил базу. Крупный тенант сгенерировал огромную нагрузку, и остальные 99 начали тормозить. Решение: rate limiting на уровне тенанта, мониторинг ресурсов по клиентам, возможность выделить крупных в отдельный пул.
- Хранение tenant_id на клиенте. Приложение передаёт tenant_id в API, и сервер ему верит. Любой может подставить чужой ID. Решение: tenant_id извлекается только из серверного токена.
- Нет мониторинга по тенантам. Вы видите, что всё тормозит, но не знаете, какой клиент виноват. Решение: метрики с тегом 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 до фоновых задач, и проверен автоматическими тестами.
