Архитектура multi-tenant SaaS
Модели мультитенантности, герметичная изоляция, тарифы и лимиты, шумные соседи и клиенты, работающие офлайн.
За 30 секунд
Multi-tenant SaaS обслуживает много компаний-клиентов (tenant'ов) с одной платформы. Ключевые решения — как разделяются данные tenant'ов (общая схема с tenant ID, отдельная схема на tenant'а или отдельная база на tenant'а) и как обеспечивается изоляция, чтобы ни один tenant никогда не увидел чужие данные. Вокруг этого — тарифы и лимиты, кэширование и очереди с учётом tenant'а, экспорт и удаление данных и справедливость между tenant'ами.
Почему я это использую
- Я этим занимаюсь, потому что утечка данных между tenant'ами — тот единственный баг, который SaaS не переживёт, поэтому изоляцию нужно закладывать в дизайн, а не прикручивать сверху.
- Модель мультитенантности на годы определяет стоимость, скорость подключения клиентов, миграции и соответствие требованиям; менять её потом крайне дорого.
- Тарифы, лимиты и переключатели фич превращают одну кодовую базу в продукты разной ценовой категории без форков.
- Реальный бизнес теряет интернет; клиенты, которые продолжают работать офлайн и безопасно синхронизируются, — вот что делает SaaS пригодным для работы за кассой.
Как это работает
01Три модели мультитенантности
Представьте офисное здание. Общая схема — это open space: строки всех tenant'ов лежат в одних и тех же таблицах и разделяются колонкой tenant_id. Это самый дешёвый и простой в эксплуатации вариант — одна миграция для всех и лёгкая аналитика по всем tenant'ам, — но изоляция целиком зависит от того, ограничен ли каждый запрос. Schema-per-tenant — этаж с отдельными кабинетами: каждый tenant получает свой набор таблиц в одной базе. Изоляция сильнее, восстановить отдельного tenant'а проще, но миграции нужно прогонять по сотням или тысячам схем, а пулинг соединений усложняется. Database-per-tenant — отдельные здания: самая сильная изоляция, настройка производительности, размещение и ключи шифрования для каждого tenant'а, простое удаление — и самая высокая стоимость эксплуатации. Многие продукты их смешивают: общая схема для большинства tenant'ов и выделенные базы для крупных или регулируемых клиентов.
- Общая схема + tenant_id: дешевле всего, масштабируется на множество мелких tenant'ов, изоляция через код и RLS.
- Схема на tenant'а: золотая середина; главная боль — веерный прогон миграций.
- База на tenant'а: сильнейшая изоляция и локализация данных; самая высокая стоимость и нагрузка на эксплуатацию.
02Изоляция в несколько слоёв
Изоляция никогда не должна зависеть от того, вспомнит ли разработчик про условие WHERE. Определяйте tenant'а один раз на запрос из доверенного источника (аутентифицированный токен, API-ключ или поддомен), никогда — из поля в теле запроса, которое контролирует клиент. Храните его в контексте запроса и заставьте слой данных его применять: базовый класс репозитория, расширение ORM или глобальный scope, который добавляет фильтр по tenant'у и бросает исключение, если его нет. Добавьте второй, независимый слой в самой базе — row-level security в PostgreSQL, — чтобы даже забытый фильтр ничего не возвращал. Пусть ответом по умолчанию на ID другого tenant'а будет 404, а не 403, чтобы атакующие не могли выяснить, какие ID существуют. А затем докажите это тестами: каждый модуль получает тест «чужой tenant», который пытается прочитать и изменить записи другого tenant'а и ожидает отказа.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- Per request, inside a transaction, using a non-owner app role:
BEGIN;
SET LOCAL app.tenant_id = '00000000-0000-0000-0000-000000000001';
SELECT id, total FROM orders; -- only this tenant's rows
COMMIT;
03Тарифы, лимиты и feature flag'и
Tenant'ы платят за разное, поэтому моделируйте права явно. Тариф определяет возможности (модули вроде лояльности или кухонных экранов) и лимиты (филиалы, пользователи, хранилище, вызовы API в минуту). Проверяйте возможности в одном месте — в политике или guard'е, — а не разбрасывайте «if plan === pro» по всему коду. Жёсткие лимиты применяйте при записи (создание одиннадцатого филиала падает с понятным сообщением), мягкие — через предупреждения и дашборды использования. Feature flag'и — отдельный инструмент: они постепенно включают функциональность для бета-tenant'ов, дают аварийные выключатели и позволяют делать индивидуальные исключения для tenant'ов, согласованные в сделках отдела продаж. Делайте и то и другое прозрачным для аудита, потому что «почему этот tenant может это делать?» — вопрос, который поддержка точно услышит. Точный учёт использования нужен и для биллинга, поэтому записывайте его событиями, а не пересчитывайте по живым таблицам.
04Кэширование, очереди и realtime с учётом tenant'а
Каждый общий компонент — место, где tenant'ы могут «протечь» друг к другу. Ключи кэша должны включать ID tenant'а (tenant:42:menu:v3), иначе одно кафе увидит меню другого. Фоновые задачи должны нести ID tenant'а в своих данных, а воркер должен восстановить контекст tenant'а, прежде чем трогать данные, — ровно так же, как это сделал бы запрос. Realtime-каналы требуют той же аккуратности: подписывайте клиентов на комнаты по tenant'у и по филиалу и авторизуйте каждую подписку на сервере. Файлы кладите по путям с префиксом tenant'а в объектном хранилище и отдавайте через короткоживущие подписанные URL. Логи и трейсы должны включать ID tenant'а, чтобы поддержка могла разбираться с одним клиентом, не просматривая всех. И наконец, rate limit'ы и параллелизм очередей должны быть на уровне tenant'а, чтобы импорт одного tenant'а не мог заморить голодом остальных.
- Имена ключей кэша, путей в хранилище и каналов всегда включают tenant'а.
- Задачи восстанавливают контекст tenant'а перед запуском.
- Rate limit'ы по tenant'ам и справедливые очереди против шумных соседей.
05Шумные соседи, экспорт и удаление данных
В общей системе один tenant, запустивший огромный отчёт или импорт, может замедлить всех. Защищайте остальных rate limit'ами и квотами по tenant'ам, statement timeout'ами для запросов, отдельными очередями или ограничением параллелизма для тяжёлых задач и переносом тяжёлой работы на непиковые часы. Следите за потреблением ресурсов каждым tenant'ом, чтобы замечать выбросы, и при необходимости переносите очень крупных tenant'ов на выделенные ресурсы — это одна из причин сохранять способность слоя данных направлять tenant'а в другую базу. Кроме того, tenant'ы владеют своими данными. Предложите полный экспорт в задокументированных форматах (CSV, JSON) и реализуйте удаление как настоящий процесс: мягкое удаление на льготный период, затем окончательное удаление из базы, файлов, кэшей и поисковых индексов, а бэкапы истекают по известному графику. Задокументируйте это для запросов в духе GDPR.
06Клиенты с офлайн-режимом и синхронизация
Кассовые терминалы, полевые приложения и киоски должны продолжать работать, когда пропадает интернет. Паттерн такой: клиент или локальный хаб держит локальную базу (часто SQLite) с нужными ему данными, записывает каждое изменение как команду с уникальным ID и отправляет эти команды на сервер, когда соединение возвращается. Сервер применяет каждую команду идемпотентно, дедуплицируя по ID команды и устройству, так что повторы после нестабильных переподключений безопасны. Правила разрешения конфликтов определяйте для каждой сущности: записи, которые только дописываются, вроде продаж, конфликтуют редко; общим записям вроде остатков или цен нужен понятный владелец, правило «сервер прав» или явное слияние. Аккуратно версионируйте локальную схему и миграции, потому что устройства обновляются в разное время. Давайте офлайн-устройству только те права и данные, которые нужны его филиалу, и используйте привязанные к устройству токены, которые можно отозвать.
Когда применять
- Один продукт, который продаётся множеству компаний, которые никогда не должны видеть данные друг друга.
- Клиенты очень разного размера, которым нужны фичи и лимиты в зависимости от тарифа.
- Бизнесы, которые продолжают работать во время сбоев сети (ритейл, общепит, выездная работа).
- Регулируемые клиенты, которым могут потребоваться выделенные базы или локализация данных.
Когда не стоит
- Внутренняя система для одного клиента: мультитенантность добавит сложности без пользы.
- Schema-per-tenant для десятков тысяч мелких tenant'ов: миграции и пулинг будут болезненными.
- Офлайн-синхронизация для приложений, которым действительно нужны только живые данные: она добавляет огромный объём тестирования.
Альтернативы
- Single-tenant инсталляции
Одна установка на клиента: сильная изоляция, но обновления и расходы умножаются.
- Гибридная мультитенантность
Общая схема для большинства tenant'ов, выделенные базы для корпоративных или регулируемых.
- Платформенные сервисы
Управляемая аутентификация с организациями и управляемые базы с ветвлением сокращают кастомную работу.
- Клиенты только онлайн
Проще офлайн-синхронизации, когда связь надёжна, а простой допустим.
Лучшие практики
- Определяйте tenant'а из аутентификации, никогда — из ввода, который контролирует пользователь.
- Обеспечивайте изоляцию дважды: в слое данных и в базе через row-level security.
- Возвращайте 404 для ресурсов другого tenant'а и пишите тест «чужой tenant» для каждого модуля.
- Включайте ID tenant'а в каждый ключ кэша, данные задачи, путь к файлу, канал и строку лога.
- Моделируйте тарифы, лимиты и feature flag'и явно и проверяйте их в одном месте.
- Применяйте rate limit'ы, квоты и таймауты запросов по tenant'ам против шумных соседей.
- Делайте команды офлайн-синхронизации идемпотентными с уникальными ID для каждого устройства.
Частые ошибки
- Доверять ID tenant'а, присланному в теле запроса или query string.
- Фоновые задачи или админские скрипты, которые обходят ограничение по tenant'у.
- Ключи кэша без tenant'а, из-за которых данные утекают между клиентами.
- Запускать приложение от владельца таблицы или суперпользователя, что обходит row-level security.
- Удалить tenant'а из основной базы, но оставить файлы, поисковые индексы и кэши.
Глубже
07Row-level security в продакшене: пулинг, производительность и роли
SET LOCAL не принимает bind-параметры, поэтому многие приложения собирают запрос строкой. Безопаснее использовать set_config('app.tenant_id', $1, true), где true означает «локально для этой транзакции»: так работают и параметры, и пулеры в transaction-режиме. В политиках используйте форму current_setting с missing_ok, чтобы незаданное значение давало NULL, которому не соответствует ни одна строка, а не ошибку. Производительность: политики добавляют условие в каждый запрос, поэтому ставьте tenant_id первым в составных индексах (tenant_id, created_at) и в уникальных ограничениях (tenant_id, slug). Функции, вызываемые внутри политик, должны быть простыми и STABLE; сложные подзапросы на каждую строку медленные. Используйте три роли: роль миграций, владеющую таблицами, роль приложения без BYPASSRLS и отдельную аудируемую роль обслуживания для настоящих межтенантных задач.
-- Inside each request transaction (works with bind parameters and poolers)
SELECT set_config('app.tenant_id', $1, true);
-- Indexes and constraints lead with the tenant
CREATE INDEX orders_tenant_created_idx ON orders (tenant_id, created_at DESC);
ALTER TABLE products ADD CONSTRAINT products_tenant_sku_key UNIQUE (tenant_id, sku);
-- App role cannot bypass policies
CREATE ROLE app_rw LOGIN NOBYPASSRLS;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_rw;
08Маршрутизация тенантов: tenant directory, ячейки и размещение
Держите tenant directory — небольшую высокодоступную таблицу или сервис, который сопоставляет каждого тенанта с местом, где лежат его данные: общий кластер A, выделенная база B, регион EU. Каждый запрос определяет тенанта, затем его размещение и подключается к нужному пулу. Этот один уровень косвенности делает возможными будущие переезды: перенесите крупного тенанта в собственную базу, скопировав данные, проиграв изменения, ненадолго заморозив записи и переключив запись в справочнике. Если пойти дальше, cell-based архитектура запускает несколько одинаковых стеков (приложение, база, кэш, очереди), каждый из которых обслуживает подмножество тенантов. Тогда неудачный деплой или перегрузка затрагивает одну ячейку, а не всех клиентов, и изменения раскатываются ячейка за ячейкой. Цена — больше инфраструктуры и инструментов для межъячеечных операций вроде глобального поиска в админке и биллинга, поэтому большинство продуктов начинают с одной ячейки и справочника, готового к расширению.
09Миграции и изменения схемы для множества тенантов
В общей схеме одна миграция сразу затрагивает данные всех тенантов, поэтому она должна быть online: добавляйте колонки nullable или с константным default, создавайте индексы CONCURRENTLY, делайте backfill небольшими батчами, затем добавляйте ограничения как NOT VALID и валидируйте отдельно. Никогда не блокируйте нагруженную таблицу на минуты. При schema-per-tenant или database-per-tenant проблема превращается в fan-out: тысячи миграций, которые могут частично упасть. Запускайте их через job runner со статусом по каждому тенанту, ретраями и лимитами параллелизма, держите приложение совместимым и со старой, и с новой схемой на время раскатки (expand, migrate, contract) и сделайте остановку на полпути безопасной. Отслеживайте версию схемы для каждого тенанта и отказывайтесь обслуживать тенанта, чью версию работающий код не поддерживает.
- Сначала expand, затем деплой кода, работающего с обеими схемами, contract — позже.
- CREATE INDEX CONCURRENTLY и backfill батчами для больших таблиц.
- Статус миграции по тенантам для конфигураций schema- или database-per-tenant.
10Внутренности офлайн-синхронизации: порядок, часы и конфликты
Часы устройств расходятся, поэтому одних wall-clock меток времени недостаточно, чтобы упорядочить изменения с нескольких устройств. Распространённые подходы: пусть сервер присваивает монотонно растущий порядковый номер при приёме команды, а клиенты забирают «всё после номера N»; или используйте hybrid logical clock, которые сочетают физическое время со счётчиком, оставаясь близкими к реальному времени и сохраняя причинность. Для конфликтов выбирайте правило для каждого поля: события только на дописывание никогда не конфликтуют; last-writer-wins прост, но молча теряет правки; слияние на уровне полей сохраняет непересекающиеся изменения; CRDT сливают счётчики, множества и текст автоматически ценой сложности. Делайте команды маленькими и основанными на намерении («добавить 2 единицы в заказ 15»), а не перезаписью всего состояния — они сливаются гораздо лучше. Ограничьте, сколько устройство может оставаться офлайн, прежде чем ему потребуется полная пересинхронизация.
Реальные ситуации
Инженер поддержки видит данные другого тенанта
Проблема
Эндпоинт отчёта, сделанный для внутреннего дашборда, при вызове с обычным токеном тенанта вернул строки нескольких компаний. На ревью этого никто не заметил.
Как решить
Сначала локализуйте: отключите эндпоинт, отзовите все выгруженные файлы и по логам выясните, кто его вызывал и что было возвращено, потому что это может быть инцидент с обязательным уведомлением. Затем выясните, почему не сработали слои защиты: скорее всего, сырой запрос в обход scoped repository и роль базы с BYPASSRLS. Переведите приложение на роль, не владеющую таблицами и без BYPASSRLS, принудительно включите RLS на всех таблицах тенантов и добавьте проверку в CI, которая выводит таблицы без политик. Напишите для эндпоинта тест «чужой тенант», а внутреннюю межтенантную отчётность проведите через отдельный аудируемый сервис и роль.
Импорт одного тенанта тормозит всех
Проблема
Новый крупный клиент импортирует два миллиона товаров. В течение часа latency API утраивается для всех тенантов, а фоновые задачи других тенантов ждут в очереди.
Как решить
Вынесите массовые импорты в отдельную очередь с лимитом параллелизма на тенанта и обрабатывайте их батчами с короткими транзакциями. Задайте statement timeout для роли импорта и планируйте крупные импорты вне пиковых часов. Применяйте rate limit по тенанту к импортам через API. Добавьте метрики времени запросов и задач по тенантам, чтобы выброс был виден за минуты. Если стабильная нагрузка этого тенанта остаётся намного выше остальных, через tenant directory переместите его на выделенные ресурсы базы и отразите это в его тарифе.
Корпоративный клиент требует собственную базу в EU
Проблема
Потенциальный клиент подпишет договор, только если его данные будут храниться в выделенной базе в конкретном регионе, а продукт работает на одном общем кластере.
Как решить
Если слой данных уже определяет подключения через tenant directory, это задача провижининга. Создайте выделенную базу в нужном регионе с той же схемой и пайплайном миграций, зарегистрируйте её в справочнике и подключите тенанта туда. Убедитесь, что все остальные хранилища следуют размещению: бакет object storage, бэкапы, поисковый индекс, логи и кэши — в этом регионе. Добавьте базу в fan-out миграций и мониторинг. Оцените это как премиум-тариф, потому что это добавляет фиксированные расходы на инфраструктуру и эксплуатацию.
Шпаргалка
- Включить и принудить RLS
ALTER TABLE t ENABLE ROW LEVEL SECURITY; ALTER TABLE t FORCE ROW LEVEL SECURITY;- Задать тенанта на транзакцию
SELECT set_config('app.tenant_id', $1, true);- Таблицы без RLS
SELECT relname FROM pg_class WHERE relkind = 'r' AND relnamespace = 'public'::regnamespace AND NOT relrowsecurity;- Роль приложения без bypass
CREATE ROLE app_rw LOGIN NOBYPASSRLS;- Индекс с тенантом в начале
CREATE INDEX CONCURRENTLY ON orders (tenant_id, created_at);- Ключ кэша в рамках тенанта
tenant:{tenantId}:menu:v{version}- Путь объекта с префиксом тенанта
tenants/{tenantId}/media/{uuid}.webp- Statement timeout для тяжёлых ролей
ALTER ROLE importer SET statement_timeout = '30s';
В моих реальных проектах
- В POS SaaS, которым я руковожу вместе с коллегой, иерархия такая: Organization, затем Company (tenant), затем Branch, с возможностями по тарифу и переключателями фич.
- Изоляция там обеспечивается дважды: явный companyId в каждом запросе плюс расширение Prisma, которое бросает исключение, если scope отсутствует; исключения должны быть внесены в allow-list с указанием причины, строки других tenant'ов возвращают 404, а тесты каждого модуля включают случай «чужой tenant».
- Его realtime-слой использует комнаты socket.io для каждой компании, филиала, терминала и пользователя, а у загрузки медиа есть квоты хранилища и подписанные приватные ссылки.
- Офлайн-хаб филиала с локальной базой SQLite позволяет продолжать продажи посетителям без интернета; команды синхронизации дедуплицируются по терминалу, а миграции хаба версионируются и закреплены по контрольной сумме.
Вопросы и ответы
Q1Какую модель мультитенантности вы выбрали бы для нового SaaS?
Для большинства новых продуктов — общую схему с tenant ID в каждой таблице, принадлежащей tenant'у. Её дешевле всего эксплуатировать, подключение tenant'а — это просто вставка строки, а одна миграция обновляет всех. Более слабую физическую изоляцию я компенсирую строгим контролем: ограниченный доступ к данным в коде, row-level security в PostgreSQL как второй слой и тесты «чужой tenant». Я сохраняю способность слоя данных позже направить tenant'а в собственную базу, потому что крупные или регулируемые клиенты могут этого потребовать. Schema-per-tenant — золотая середина, которую я бы выбрал только при умеренном числе tenant'ов.
Q2Как гарантировать, что один tenant никогда не увидит чужие данные?
Многослойной защитой, чтобы ни одна отдельная ошибка не привела к утечке. Tenant определяется один раз из аутентификации и сохраняется в контексте запроса. Слой данных автоматически добавляет фильтр по tenant'у и бросает исключение, если у запроса нет scope, а любое намеренное исключение внесено в allow-list и обосновано. База применяет row-level security как независимую проверку. Кэши, очереди, файлы и realtime-каналы несут ID tenant'а. Записи других tenant'ов возвращают 404. И наконец, это доказывают тесты: каждый модуль включает случай, который пытается прочитать и изменить данные другого tenant'а, плюс логирование, позволяющее провести аудит доступа.
Q3Что такое шумный сосед и как с ним справляться?
Это tenant, чья нагрузка ухудшает сервис для других на общей инфраструктуре, — например, большой импорт, дорогой отчёт или всплеск вызовов API. Я справляюсь с этим на нескольких уровнях: rate limit'ы и квоты по tenant'ам на уровне API, statement timeout'ы и лимиты запросов в базе, отдельные очереди или ограничение параллелизма для тяжёлых задач и перенос массовой работы на непиковое время. Метрики по tenant'ам показывают, кто что потребляет. Если одному tenant'у постоянно нужно гораздо больше, чем остальным, я переношу его на выделенные ресурсы, в идеале в отдельную базу, и соответствующим образом выставляю цену.
Q4Как работает row-level security в PostgreSQL и в чём её ловушки?
Вы включаете RLS на таблице и создаёте политики, которые обычно сравнивают tenant_id строки с настройкой сессии, выставляемой приложением на каждую транзакцию. После этого любой запрос, включая те, где разработчики забыли фильтр, видит только разрешённые строки; WITH CHECK не даёт записывать строки для другого tenant'а. Ловушки: суперпользователи и роли с BYPASSRLS её игнорируют, и владельцы таблиц тоже — если не включить FORCE ROW LEVEL SECURITY, — поэтому приложение должно работать под отдельной ролью, не являющейся владельцем. С пулингом соединений используйте SET LOCAL внутри транзакции, чтобы настройка не протекла в следующий запрос. И проиндексируйте tenant_id, потому что политики добавляют фильтры к каждому запросу.
Q5Как бы вы спроектировали офлайн-режим для кассового клиента?
Терминал или локальный хаб держит локальную базу с меню, ценами и открытыми заказами, которые ему нужны, и полноценно работает с ней. Каждое изменение записывается как команда с уникальным ID, ID устройства и таймстемпом и ставится в очередь. Когда соединение возвращается, команды отправляются по порядку, а сервер применяет каждую идемпотентно, так что повтор никогда не задублирует продажу. Продажи только дописываются и редко конфликтуют; для общих данных вроде цен владелец — сервер, и он рассылает обновления вниз. Фискальным и платёжным шагам нужны явные офлайн-варианты и последующая сверка, а устройства используют отзываемые токены.
Q6Что делать, если tenant просит экспортировать или удалить все свои данные?
Экспорт должен быть поддерживаемой функцией, а не разовым скриптом: фоновая задача, которая собирает каждую сущность tenant'а в задокументированные форматы, упаковывает файлы и выдаёт подписанную ссылку с ограниченным сроком действия. Удаление — процесс с этапами. Сначала мягкое удаление с льготным периодом на случай ошибки, затем окончательное удаление из базы, объектного хранилища, поисковых индексов, кэшей и у сторонних обработчиков. Бэкапы обычно редактировать нельзя, поэтому я документирую, что они истекают по фиксированному графику. Каждый шаг логируется, чтобы мы могли доказать, что запрос выполнен, — это важно для законов о персональных данных.
Вопросы уровня senior
S1Как бы вы мигрировали SaaS с общей схемой, чтобы поддержать выделенные базы для части тенантов?
Введите размещение до того, как кого-либо переносить. Сначала сделайте так, чтобы любой доступ к данным определял подключение тенанта через tenant directory, по умолчанию — общий кластер; задеплойте это без изменения поведения. Убедитесь, что ни один запрос не делает join между тенантами и что фоновые задачи, отчёты и инструменты админки тоже используют справочник. Затем постройте переезд: создайте целевую базу, скопируйте строки тенанта по tenant_id, стримьте последующие изменения через logical replication или окно dual-write, сверьте количества и контрольные суммы, заморозьте записи тенанта на несколько секунд, переключите запись в справочнике и разморозьте. Оставьте старые строки в read-only на период отката. Компромисс эксплуатационный: миграции, мониторинг и бэкапы теперь расходятся на несколько баз, поэтому автоматизируйте их, прежде чем широко предлагать выделенное размещение.
S2Как вы проектируете ключи шифрования для каждого тенанта и когда это оправдано?
Используйте envelope encryption. Каждый тенант получает data encryption key, которым шифруются чувствительные поля или файлы; этот ключ сам зашифрован key-encryption key, который хранится в key management service и никогда не лежит в открытом виде. Расшифрованные ключи ненадолго кэшируются в памяти. Плюсы: crypto-shredding, когда уничтожение ключа тенанта делает нечитаемыми все его данные, включая копии в бэкапах; customer-managed keys для корпоративных контрактов; и меньший радиус поражения при утечке одного ключа. Минусы: зашифрованные поля сложно индексировать и искать, каждое чтение платит небольшую цену расшифровки, ротация ключей требует re-wrapping, а доступность KMS становится зависимостью. Это оправдано для регулируемых данных и корпоративных сделок; для большинства полей достаточно шифрования на уровне диска плюс строгого контроля доступа.
S3Как сохранить точность лимитов тарифа при конкурентном доступе?
Подсчитать строки и затем вставить — это гонка: оба запроса видят девять филиалов из десяти и оба создают ещё по одному. Применяйте лимиты там, где записи сериализуются. Варианты: строка-счётчик использования на тенанта и ресурс, обновляемая условным запросом (увеличить, только если ниже лимита) в той же транзакции, что и вставка; или блокировка строки тенанта на время проверки и вставки. Для высоконагруженных измеряемых лимитов, например вызовов API, точное соблюдение дорого, поэтому используйте счётчик в Redis со скользящим окном и принимайте небольшое превышение, а биллинг считайте по авторитетным событиям использования. Для каждого лимита решите, жёсткий он (договорной, соблюдать строго) или мягкий (предупредить, допустить кратковременное превышение), и показывайте тенантам их текущее использование, чтобы лимиты никогда не становились сюрпризом.
Глоссарий
- Тенант
- Организация-клиент, чьи пользователи и данные изолированы от всех остальных клиентов.
- Row-level security
- Политики базы данных, фильтрующие, какие строки сессия может читать или писать.
- Шумный сосед
- Тенант, чьё интенсивное использование ухудшает производительность для остальных на общих ресурсах.
- Tenant directory
- Соответствие каждого тенанта базе данных, региону или ячейке, где лежат его данные.
- Ячейка (cell)
- Независимая копия всего стека, обслуживающая подмножество тенантов и ограничивающая радиус поражения.
- Entitlement
- Возможность или лимит, которые даёт тариф тенанта и проверяет приложение.
- Data residency
- Требование хранить и обрабатывать данные в определённой стране или регионе.
- Hybrid logical clock
- Метка времени, сочетающая физическое время и счётчик для упорядочивания событий между устройствами.
- CRDT
- Структура данных, параллельные правки которой всегда сливаются в один результат без координации.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.