Раздел 9 · Backend

Multi-tenancy: изоляция данных и provisioning тенанта

senior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Multi-tenancy: изоляция данных и provisioning тенанта

Под-серия про микросервисы закончилась топологией: как собрать систему из кусков и не потерять её из виду. Новая под-серия про другое измерение роста. Афиша научилась продавать билеты, теперь она хочет продавать себя: сетям кинотеатров, театрам, организаторам фестивалей. Один деплой, сто клиентов, и каждый клиент уверен, что система принадлежит ему одному. Этот урок про три способа хранить данные ста клиентов, про то, как не показать одному клиенту чужие данные ни при каких обстоятельствах, и про то, что происходит между кнопкой «зарегистрироваться» и работающим аккаунтом.

Один деплой, сто клиентов

Сначала договоримся о словах. Тенант это не пользователь. Сеть кинотеатров «Синема-Парк» как клиент афиши это один тенант, внутри которого живут пятьдесят сотрудников со своими логинами, ролями и правами. Пользователи принадлежат тенанту, данные принадлежат тенанту, лимиты и план принадлежат тенанту. Multi-tenancy это дисциплина, которая позволяет всем этим тенантам жить в одном приложении и не подозревать о существовании друг друга.

Второе слово из словаря AWS SaaS Lens (ресурсы): control plane. В любом SaaS есть два контура. Application plane обслуживает рабочие запросы: показать события, продать билет. Control plane управляет самими тенантами: завести, настроить, выставить счёт, заблокировать за неуплату. Даже в монолите это разделение стоит провести на уровне модулей: код «работаем для тенанта» и код «управляем тенантом» меняются с разной скоростью и по разным причинам, прямо по заветам урока про границы.

И сразу зафиксируем ставки. В B2C утечка данных между пользователями это серьёзный инцидент. В B2B SaaS показать тенанту A строку тенанта B означает показать одной компании коммерческие данные другой, возможно, её прямого конкурента. После такого не извиняются в твиттере, после такого теряют клиентов и проходят внешний аудит. Поэтому всё, что дальше, пропитано одной идеей: изоляция тенантов это не фича, а инвариант системы.

Режимы изоляции: pool, bridge, silo

Первое архитектурное решение SaaS: как физически лежат данные разных тенантов. Индустрия сошлась на трёх режимах с устоявшимися именами.

Pool: одна база, одна схема, общие таблицы. Каждая таблица несёт колонку tenant_id, каждая строка знает своего владельца:

CREATE TABLE events (
  id         bigserial PRIMARY KEY,
  tenant_id  uuid NOT NULL REFERENCES tenants(id),
  title      text NOT NULL,
  starts_at  timestamptz NOT NULL
);

-- каждый индекс начинается с tenant_id: запросы всегда фильтруют по нему
CREATE INDEX events_tenant_starts_idx ON events (tenant_id, starts_at);

Pool это дефолт стартапа, и заслуженно: одна база, один бэкап, один прогон миграций, новый тенант стоит ровно один INSERT. Сводная аналитика по всем клиентам пишется обычным запросом. Цена тоже понятна: каждый запрос обязан фильтровать по tenant_id, и одна забытая WHERE отдаёт чужие данные. Вся тяжесть изоляции ложится на код, и следующая глава целиком про то, как эту тяжесть выдержать.

Bridge: одна база, схема на тенанта. Postgres позволяет держать в одной базе много схем с одинаковым набором таблиц: tenant_a.events, tenant_b.events. Соединение выбирает схему через search_path, а приложение решает, какую схему подставить, по контексту запроса. Колонка tenant_id больше не нужна: запрос физически не может дотянуться до чужой схемы, забытая WHERE перестаёт быть угрозой. Выгрузить или восстановить одного тенанта просто: pg_dump --schema. Близкая вариация того же режима: партиционирование общих таблиц по tenant_id, когда схема одна, но строки тенантов физически разведены по партициям.

Платим операционкой: миграция схемы теперь бежит не один раз, а по разу на тенанта, и тысячи схем заметно раздувают системные каталоги Postgres. К этой цене мы вернёмся в главе про provisioning, на живом примере.

Silo: база (или кластер) на тенанта. Максимальная изоляция: у каждого тенанта своя БД, иногда свой выделенный кластер, иногда вообще отдельная инсталляция в его облаке. Это режим enterprise-сделок и регулируемых отраслей. Банк не спрашивает, удобно ли тебе, он приносит требование data residency и список аудитов. Silo даёт и побочные удобства: тенанта можно обновлять в его собственное окно обслуживания, его нагрузка не касается соседей. Цена: сто баз это сто бэкапов, сто мониторингов, сто прогонов миграций и нетривиальный счёт за железо.

Pool, bridge, silo · где лежат данные трёх тенантов
Postgres · afisha
events
1aДюна
2bГамлет
3aКонцерт
4cСтендап

Одна таблица на всех. Изоляция держится на WHERE tenant_id в каждом запросе.

новый тенант
один INSERT
миграция схемы
один прогон
blast radius
вся база
noisy neighbour
в полный рост
новый тенант
CREATE SCHEMA + миграции
миграция схемы
прогон на каждую схему
blast radius
одна схема
noisy neighbour
частично
новый тенант
развернуть целую базу
миграция схемы
прогон на каждую базу
blast radius
одна база
noisy neighbour
нет (по БД)

Переключай режимы и следи за двумя строками фактов: «новый тенант» дешевле всего слева, «blast radius» уже всего справа. Между этими двумя осями и живёт выбор.

Сводная таблица, по которой решается домашка:

ОсьPoolBridgeSilo
Стоимость тенантакопейкиумереннаявысокая
Blast radius ошибкився базаодна схемаодна база
Noisy neighbourв полный ростчастичнонет (по БД)
Миграцииодин прогонпрогон на схемупрогон на базу
Compliance, residencyтяжелотяжелоестественно
Новый тенантINSERTCREATE SCHEMA + миграцииразвернуть базу

Отдельной строкой: noisy neighbour. Заметь, что изоляция данных и изоляция производительности это разные оси. Pool с безупречной фильтрацией всё равно страдает, когда один тенант запускает выгрузку за три года и съедает CPU базы. И наоборот: silo по данным не спасает, если приложение у всех общее и тяжёлый тенант забил очередь воркеров. Производительность лечится не схемами, а квотами: rate limit на тенанта, лимиты на тяжёлые операции, вынос крупных клиентов в отдельный пул ресурсов. Подробно про квоты и entitlements будет в уроке про enterprise-функции.

Последнее и самое важное про выбор: его делают в начале проекта, и он почти односторонний. Из pool можно вырасти: выгрузить крупного клиента по tenant_id и переселить в собственную базу, превратив систему в гибрид. Обратный путь, слияние сотни silo в общий pool, означает конфликты идентификаторов, дедупликацию справочников и миграцию под нагрузкой: технически возможно, экономически почти никогда не оправдано. Поэтому зрелые SaaS живут гибридом: long tail мелких клиентов в pool, enterprise в silo, и сам silo продаётся в прайсе как строчка «single-tenant deployment».

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.