Мультиарендность в SaaS (мультитенантность, multi-tenancy) — режим, когда один экземпляр приложения обслуживает много клиентов-арендаторов, и у каждого свои данные. Данные делят тремя способами: общая схема с идентификатором клиента в каждой таблице, отдельная схема на клиента или отдельная база на клиента. Чем сильнее изоляция, тем дороже эксплуатация: общая схема дешевле всего в обслуживании и сильнее всего зависит от дисциплины в коде, отдельная база даёт самую сильную изоляцию, но мониторинг, обновления и копии нужны на каждого клиента.
Выбор зависит от числа клиентов, от требований их служб безопасности и от разброса клиентов по объёму. Важнее самого выбора то, можно ли его потом изменить: что для этого закладывают, описано в разделе «Как сменить схему позже». Состав платформы и цены — на странице «Разработка SaaS-платформы».
Три схемы и как между ними выбирать
Первые три строки идут от дешёвой эксплуатации и изоляции на дисциплине кода к дорогой эксплуатации и самой сильной изоляции; четвёртая сочетает их.
| Схема | Цена в эксплуатации | Сила изоляции | Когда выбирать | Где ломается |
|---|---|---|---|---|
| Общая схема, идентификатор клиента в каждой таблице | Низкая: одна база, одно обновление структуры, одна копия | Зависит от дисциплины кода, усиливается политиками на уровне строк | Много небольших клиентов и типовые требования к безопасности | Пропущенный фильтр; фоновая задача, отчёт, кэш или поиск без идентификатора клиента; один «шумный» клиент замедляет остальных |
| Отдельная схема на клиента (в одной базе) | Средняя: одна база, но структуру обновляют по каждой схеме | Выше: у клиента свои таблицы; чужие недоступны без явного обращения, если на схему выдана отдельная роль, иначе изоляция держится на правильном пути поиска схем | Умеренное число клиентов, нужны простая выгрузка и удаление данных клиента | Миграции по множеству схем; клиент, у которого миграция не прошла, остаётся на старой версии; рост числа объектов замедляет служебные операции |
| Отдельная база на клиента | Высокая: мониторинг, обновления, копии и подключения на каждую базу | Самая сильная: возможны свои копии и сроки хранения, можно вынести на площадку клиента | Небольшое число крупных клиентов с жёсткими требованиями | Парк баз, который не обновить за одну ночь; число подключений; неравномерная нагрузка |
| Смешанная | Зависит от доли отдельных баз | Разная для разных клиентов | Основная масса на общей схеме, крупным клиентам отдельные базы | Нет реестра, где живёт каждый клиент, и единого слоя доступа к данным: схема превращается в набор исключений |
Выбор сводится к пяти вопросам:
- Что записано в договорах и анкетах безопасности ваших клиентов. Требование хранить данные отдельно или на своей площадке означает отдельную базу для этого клиента, остальным она не нужна.
- Нужны ли разные сроки хранения копий. В общей схеме срок один для всех.
- Как часто клиенты просят выгрузить или удалить данные целиком. Чем чаще, тем выгоднее схема или база на клиента.
- Насколько клиенты различаются по объёму и нагрузке. Один заметно более крупный клиент в общей схеме может замедлять остальных, тогда его выносят отдельно.
- Кто будет эксплуатировать результат. Парк отдельных баз без автоматики и мониторинга по каждой базе превращается в ручную работу.
Ответы записывают на обследовании с причиной выбора и способом перехода на другую схему.
Общая схема: как не пропустить фильтр
Самая опасная ошибка общей схемы — забытое условие по клиенту, и допустить её можно в любом запросе, отчёте, фоновой задаче и ключе кэша. Надёжнее, когда условие подставляет код, а не разработчик, и когда это проверяют при каждой сборке. Что для этого нужно:
- Идентификатор клиента в каждой таблице с данными клиента. Ограничение на повторы задаётся в пределах клиента: номер заказа или артикул не повторяется внутри организации, но может совпасть в разных организациях. Адрес почты пользователя — отдельное решение: либо один аккаунт на человека, который состоит в нескольких организациях, либо отдельный пользователь в каждой организации; во втором случае при входе нужно указывать организацию. Внешние ключи тоже включают идентификатор клиента.
- Индексы начинаются с идентификатора клиента, иначе каждый запрос будет читать лишнее.
- Один слой доступа к данным. Условие по клиенту подставляется там и только там. Запросы, написанные в обход слоя, запрещены в проверке кода.
- Клиент берётся из сессии, а не из параметров запроса. Пользователь не должен иметь возможность подставить чужой идентификатор.
- Фоновые задачи получают клиента явно. Задача без идентификатора клиента падает, а не обрабатывает всех.
- Кэш, поисковый индекс, очереди и файлы используют идентификатор клиента в ключе или пути. База защищена, а кэш с общим ключом отдаёт чужие ответы.
- Квоты на запросы и задачи. Один крупный клиент не должен замедлять остальных.
Политики на уровне строк в PostgreSQL: второй замок
Политики на уровне строк (row level security) добавляют фильтр по клиенту на уровне самой базы: даже если разработчик забыл условие, база не вернёт чужие строки. Это не замена проверки в коде, а второй замок. Он страхует от забытого условия, но не от SQL-инъекции: идентификатор клиента сообщает базе само приложение, и код, способный выполнить произвольный запрос, может подставить другой. Пример для таблицы заказов, в которой есть поле tenant_id с идентификатором клиента (имя параметра app.tenant_id проект выбирает сам):
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id =
nullif(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id =
nullif(current_setting('app.tenant_id', true), '')::uuid);Приложение в начале каждой транзакции сообщает базе, чей это запрос:
BEGIN;
SELECT set_config('app.tenant_id',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true);
SELECT * FROM orders; -- только строки этого клиента
COMMIT;Что важно знать:
- Роль приложения не должна обходить политики. Владелец таблицы по умолчанию обходит политики, суперпользователь и роли с атрибутом BYPASSRLS обходят их всегда. Приложение подключается ролью, которая не владеет таблицами, не является суперпользователем и не имеет BYPASSRLS; FORCE нужен на случай, если к таблицам обратится их владелец. Миграции и резервное копирование выполняет отдельная роль, которой у приложения нет. При FORCE политики действуют и на неё: миграция данных от её имени молча обновит ноль строк, а выгрузка pg_dump может завершиться ошибкой, пока у роли нет атрибута BYPASSRLS. Это проверяют на копии базы до первой боевой миграции.
- Значение задаётся внутри транзакции (третий параметр set_config равен true). Иначе при пуле соединений значение попадёт в следующий запрос другого клиента.
- Пустое значение не должно показывать всё. В примере пустая строка превращается в NULL, условие не совпадает ни с одной строкой, и запрос возвращает пустой результат.
- Представления и функции с правами владельца могут обойти политики, если их владелец вправе это делать. Такие объекты проверяют отдельно.
- Политики не защищают то, что вне базы: файлы, кэш, поисковый индекс и очереди.
- Влияние на скорость нужно мерить на своих данных; без индексов по идентификатору клиента политики дорого стоят.
Схема на клиента и база на клиента: что меняется
В схеме на клиента приложение в начале каждой транзакции задаёт путь поиска схем локально для транзакции (SET LOCAL search_path), и запросы работают с таблицами только этого клиента. Без «локально» при пуле соединений значение останется у следующего запроса другого клиента. Выгрузка и удаление данных клиента проще, зато структура существует в десятках и сотнях копий. Сотни схем в одной базе могут замедлять служебные операции, например резервное копирование и обновление структуры.
В базе на клиента для каждого клиента своя строка подключения. Нужны:
- Реестр клиентов в отдельной управляющей базе: адрес базы, версия структуры, статус, срок хранения копий.
- Управление подключениями. Каждой базе нужен свой пул, число подключений растёт вместе с числом клиентов.
- Мониторинг по каждой базе, иначе проблема одного клиента обнаружится от самого клиента.
- Автоматическое создание нового клиента: база, структура, пользователь, запись в реестре. Руками это не масштабируется.
- Копии по каждой базе отдельно, если нужны разные сроки хранения.
Для обеих схем обязателен единый слой доступа: код приложения не должен знать, где живёт клиент. Это же условие нужно для смешанной схемы и для перехода между схемами.
Как обновлять структуру данных по клиентам
В общей схеме миграция одна: обновили структуру, и все клиенты на новой версии. В схемах и базах на клиента миграцию прокатывают по списку, и какое-то время клиенты живут на разных версиях. Порядок такой:
- Реестр хранит версию структуры каждого клиента.
- Миграция запускается по каждому клиенту отдельно, результат записывается, неудавшаяся повторяется.
- Код приложения работает и со старой, и с новой структурой на время прокатки. Сначала добавляем новое (поле необязательное), затем переключаем код, затем отдельным релизом убираем старое.
- Крупным клиентам миграцию делают в согласованное окно, остальным волнами.
- Перед прокаткой миграцию пробуют на копии данных одного-двух клиентов.
В общей базе операции, которые надолго блокируют таблицу, затрагивают всех клиентов сразу, поэтому их проектируют отдельно: порциями и в часы низкой нагрузки.
Резервные копии и удаление данных одного клиента
Требование «выгрузить данные клиента» или «удалить его целиком» приходит рано или поздно. Сложность зависит от схемы.
| Операция | Общая схема | Схема на клиента | База на клиента |
|---|---|---|---|
| Выгрузить данные клиента | Запросы по всем таблицам с условием по клиенту; список таблиц поддерживается вместе со структурой | Выгрузка одной схемы | Выгрузка или копия одной базы |
| Удалить данные клиента | Удаление строк во всех таблицах в правильном порядке, плюс файлы, индекс поиска и кэш | Удаление схемы, плюс файлы и индекс | Удаление базы, плюс файлы и индекс |
| Восстановить одного клиента из копии | Копию восстанавливают целиком во временную базу и переносят строки клиента; отдельная процедура, которую нужно репетировать | Из логической копии можно восстановить одну схему; из физической копии всей базы — через временный сервер | Из копии этой базы, если копии снимаются по каждой базе отдельно; из копии всего сервера — через временный сервер |
| Срок хранения копий для разного клиента | Один для всех | Один для всех, если нет отдельных копий схем | Свой для каждого клиента, если копии снимаются по каждой базе отдельно или клиент вынесен на отдельный сервер; копия всего сервера хранится один срок для всех |
| Перенести клиента на другой сервер | Копирование строк по идентификатору клиента | Перенос схемы | Перенос базы |
Удалённые данные остаются в резервных копиях до истечения срока их хранения. Это записывают в условиях сервиса. Если клиент вправе потребовать удаления персональных данных, срок хранения копий и порядок удаления должны быть известны до такого запроса, а не после. Юридические формулировки готовит юрист заказчика, мы юридических консультаций не даём.
Как тестировать изоляцию: клиент A не должен видеть клиента B
Изоляцию проверяют отдельно от функциональных тестов. Функциональные тесты показывают, что клиент видит своё, но не доказывают, что он не видит чужое. Набор проверок:
- Два тестовых клиента с одинаковой структурой данных. В каждом создаются заказы, файлы, пользователи, комментарии.
- Прямое обращение по идентификатору. Из-под клиента A запросить объект клиента B по каждому адресу API, перебирая идентификаторы. Ожидаем отказ или «не найдено», но не данные.
- Списки, поиск, экспорт, отчёты и счётчики. В выдаче только свои строки, итоги не включают чужие.
- Фоновые задачи и уведомления. Письмо клиенту A не содержит данных клиента B.
- Файлы. Ссылка на файл клиента B из-под клиента A не открывается.
- Кэш и поисковый индекс. Данные клиента B не появляются в выдаче клиента A после запроса клиента B.
На уровне базы проверка такая: в транзакции указываем клиента A и пытаемся прочитать строки клиента B. Запускать её нужно под той же ролью, что использует приложение, и при непустых данных клиента B, иначе она проходит вхолостую. Запись проверяют так же: вставка строки с идентификатором клиента B из-под клиента A должна завершиться ошибкой.
BEGIN; -- под ролью приложения, у клиента B есть данные
SELECT set_config('app.tenant_id',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', true);
SELECT count(*) FROM orders
WHERE tenant_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
-- ожидаем 0
ROLLBACK;Ещё одна проверка не даёт забыть политику для новой таблицы. Она выводит обычные и секционированные таблицы схемы public, где защита на уровне строк не включена. Для схем на клиента запрос запускают по каждой схеме. Проверяется только включение защиты, а корректность самих политик покрывают тесты изоляции:
SELECT c.relname
FROM pg_class c
WHERE c.relkind IN ('r', 'p')
AND c.relnamespace = 'public'::regnamespace
AND NOT c.relrowsecurity;В списке должны оставаться только справочники, общие для всех клиентов. Тесты запускаются при каждой сборке, а не раз перед релизом, и новая таблица или новый адрес API без теста изоляции не принимается. Кроме автоматических проверок, раз в релиз проводится ручной прогон из-под каждой роли, как для личных кабинетов.
Как сменить схему позже
Смену схемы можно свести к переносу данных, а не к переписыванию приложения, если заранее заложено пять вещей:
| Что закладываем сразу | Что это даёт при переходе |
|---|---|
| Идентификатор клиента во всех таблицах, даже в схемах на клиента | Строки клиента можно перенести по идентификатору |
| Идентификаторы записей, не повторяющиеся между клиентами и базами (например, uuid) | При переносе в другую базу ключи не конфликтуют |
| Реестр клиентов: где живёт клиент, версия структуры, статус | Клиента переключают записью в реестре, если слой доступа берёт подключение из реестра; на время переключения останавливают фоновые задачи клиента и сбрасывают кэш реестра |
| Единый слой доступа к данным | Смена адреса подключения не требует правок по всему коду |
| Файлы и индекс поиска с префиксом клиента | Переносятся вместе с данными |
Порядок переноса крупного клиента из общей схемы в отдельную базу:
- Создать базу с той же версией структуры, что у остальных.
- Скопировать строки клиента, сверить количество строк и контрольные суммы по таблицам.
- В согласованное окно перевести клиента в режим «только чтение» и донести изменения, сделанные после первой копии, включая удалённые строки. Способ (выгрузка по времени изменения, логическая репликация) выбирают заранее, а длительность окна определяют репетицией на копии данных этого клиента.
- Переключить клиента в реестре на новую базу, проверить работу из-под тестового пользователя.
- Оставить старые данные нетронутыми на согласованный срок, затем удалить из общей схемы с учётом резервных копий.
Если ничего из пяти вещей не заложено, перенос превращается в проект по переписыванию слоя доступа к данным. Поэтому их закладывают в первой версии, даже если сегодня нужна одна общая схема.
Что делаем мы и с чего начать
К обследованию приходите с тремя вещами: списком клиентов и их требований к безопасности, прикидкой числа клиентов через год и через три года и перечнем того, что клиенты просят выгружать или удалять. По ним мы выбираем схему изоляции, записываем причину выбора, порядок перехода на другую схему и список проверок изоляции для приёмки. Обследование стоит 200 000 ₽ (2–3 недели) за один-два процесса и 350 000 ₽ (3–4 недели), если платформа связана с несколькими системами; стоимость вычитается из первого этапа, если продолжаете с нами.
Дальше MVP (400 000–1 200 000 ₽ за 2–3 месяца; в тарифах это ступень «Пилот, тяжёлый»): в нём закладываем пять вещей из раздела «Как сменить схему позже», политики на уровне строк и тесты изоляции, а отдельные базы для крупных клиентов оставляем на тот момент, когда появится требование. Исходники лежат в вашем репозитории с первого коммита, развёртывание описано инструкцией. Цены и ступени — на странице «Тарифы», описание услуги — на странице «Разработка SaaS-платформы». Мы не проводим аттестацию и не имеем лицензии ФСТЭК.
Частые вопросы
Что такое мультиарендность простыми словами?
Один экземпляр приложения обслуживает много клиентов, и у каждого свои данные, пользователи и настройки. Клиенты не видят друг друга, хотя работают в одной системе. Способ разделения данных определяет безопасность и стоимость эксплуатации.
Какую схему изоляции выбрать?
Для многих небольших клиентов с типовыми требованиями — общая схема с политиками на уровне строк; для небольшого числа крупных клиентов с жёсткими требованиями — отдельные базы. Смешанная схема подходит, если часть клиентов требует отдельную базу, а реестр клиентов уже заложен. Критерии выбора разобраны в первом разделе.
Достаточно ли политик на уровне строк в PostgreSQL?
Нет. Политики страхуют базу, но не защищают файлы, кэш, поисковый индекс и очереди. Основная проверка остаётся в коде, а политики работают как второй замок, пока роль приложения не обходит их.
Как удалить данные одного клиента?
В общей схеме удаляют строки во всех таблицах и файлы клиента, в схеме или базе на клиента удаляют схему или базу целиком. Данные остаются в резервных копиях до истечения срока их хранения, это нужно записать в условиях сервиса.
Нужна ли отдельная база каждому клиенту из-за персональных данных?
Само по себе нет. Персональные данные граждан России обычно должны храниться на серверах в России, но отдельная база на каждого клиента из него не следует. Отдельная база может следовать из требований к защите конкретной системы, отраслевых норм или договора с вашим клиентом; это уточняет юрист заказчика до выбора схемы, мы юридических консультаций не даём.
Что делать, если клиент потребовал отдельную базу?
Если заложены пять вещей из раздела «Как сменить схему позже», клиента переносят в отдельную базу по описанному порядку, а остальные остаются на общей схеме. Перенос оценивается отдельно по объёму данных и числу связанных систем. Эксплуатацию отдельной базы (мониторинг, копии, обновления) закладывают в условия для этого клиента. Если ничего из этого не заложено, сначала придётся переписывать слой доступа к данным.
Влияет ли схема на скорость работы?
Влияет через нагрузку. В общей схеме один крупный клиент может замедлять остальных, это решают квотами и индексами с идентификатором клиента. Цифры зависят от данных и нагрузки, поэтому их измеряют на ваших данных.
Сколько стоит выбрать и реализовать схему изоляции?
Выбор входит в обследование: 200 000 ₽ за один-два процесса и 350 000 ₽ при нескольких системах. Отдельной цены у реализации нет: она идёт в составе MVP, а MVP целиком стоит 400 000–1 200 000 ₽ за 2–3 месяца (в тарифах это ступень «Пилот, тяжёлый»). Общая схема с политиками на уровне строк быстрее всего в разработке. Схема и база на клиента добавляют работу уже в первой версии: реестр, создание клиента, прокатку миграций, оповещения по каждой базе. Что из этого входит в вилку, называем по итогам обследования.