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 даёт и побочные удобства: тенанта можно обновлять в его собственное окно обслуживания, его нагрузка не касается соседей. Цена: сто баз это сто бэкапов, сто мониторингов, сто прогонов миграций и нетривиальный счёт за железо.
Одна таблица на всех. Изоляция держится на WHERE tenant_id в каждом запросе.
- новый тенант
- один INSERT
- миграция схемы
- один прогон
- blast radius
- вся база
- noisy neighbour
- в полный рост
- новый тенант
- CREATE SCHEMA + миграции
- миграция схемы
- прогон на каждую схему
- blast radius
- одна схема
- noisy neighbour
- частично
- новый тенант
- развернуть целую базу
- миграция схемы
- прогон на каждую базу
- blast radius
- одна база
- noisy neighbour
- нет (по БД)
Переключай режимы и следи за двумя строками фактов: «новый тенант» дешевле всего слева, «blast radius» уже всего справа. Между этими двумя осями и живёт выбор.
Сводная таблица, по которой решается домашка:
| Ось | Pool | Bridge | Silo |
|---|---|---|---|
| Стоимость тенанта | копейки | умеренная | высокая |
| Blast radius ошибки | вся база | одна схема | одна база |
| Noisy neighbour | в полный рост | частично | нет (по БД) |
| Миграции | один прогон | прогон на схему | прогон на базу |
| Compliance, residency | тяжело | тяжело | естественно |
| Новый тенант | INSERT | CREATE 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.