Database per Tenant: когда одной базы мало
Представьте: вы разрабатываете SaaS для медицинских клиник. Каждый клиент — тенант, и по HIPAA данные должны быть полностью изолированы. Одна общая БД с разделением строк не проходит аудит. Даже использование row-level security не даёт полной гарантии — аудиторы требуют физического разделения. В результате вы рискуете потерять крупного заказчика из-за несоответствия требованиям. На практике compliance-аудит для медицинского SaaS занимает в среднем 3 месяца, и 70% отказов связаны с недостаточной изоляцией данных.
Решение — выделенная база данных на каждого тенанта. Такой подход гарантирует, что утечка в одной базе не затронет другие, а также упрощает сертификацию. Каждый тенант получает возможность настраивать схему данных под свои нужды, не блокируя других. Мы реализуем такую архитектуру, обеспечивая compliance и гибкость. Опыт показывает, что Database per Tenant увеличивает стоимость инфраструктуры на 30–50%, но окупается за счёт снижения рисков и повышения доверия клиентов.
Почему Database per Tenant, а не общая схема?
Выбор модели — компромисс между изоляцией данных и стоимостью. Вот как это выглядит на практике:
| Критерий | Database per Tenant | Shared Database | Shared Schema |
|---|---|---|---|
| Изоляция данных | Абсолютная | Логическая | Минимальная |
| Compliance (HIPAA/PCI) | Да | Сложно | Нет |
| Сложность управления | Высокая | Средняя | Низкая |
| Аналитика кросс-тенант | Сложно | Средне | Легко |
| Стоимость инфраструктуры | Высокая | Средняя | Низкая |
Database per Tenant оправдан, когда compliance или требования клиентов к изоляции критичны. Для тысяч мелких тенантов лучше рассмотреть shared database с row-level security. Однако в нашем опыте medical SaaS с 50–100 тенантами модель per-tenant показала себя наилучшей: время аудита сократилось втрое, а отказы от внедрения из-за compliance снизились на 80%.
Как управлять подключениями без утечек?
Каждый тенант — отдельный PrismaClient. Держать все открытыми бесконечно — путь к утечке памяти. Решение — пул с TTL:
// lib/db/tenant-manager.ts import { PrismaClient } from '@prisma/client'; import { Pool } from 'pg'; const clientPool = new Map<string, PrismaClient>(); export async function getTenantDb(tenantId: string): Promise<PrismaClient> { if (clientPool.has(tenantId)) { return clientPool.get(tenantId)!; } const tenant = await masterDb.tenant.findUniqueOrThrow({ where: { id: tenantId }, select: { databaseUrl: true } }); const client = new PrismaClient({ datasources: { db: { url: tenant.databaseUrl } }, datasourceUrl: tenant.databaseUrl, }); clientPool.set(tenantId, client); setTimeout(() => { clientPool.get(tenantId)?.$disconnect(); clientPool.delete(tenantId); }, 30 * 60 * 1000); return client; } Это снижает нагрузку на базы и позволяет обслуживать сотни тенантов без перерасхода ресурсов. На практике мы используем порог в 30 минут неактивности, что уменьшает число соединений на 70% и снижает нагрузку на ЦП на 40%.
Как автоматизировать создание БД при онбординге?
При регистрации нового клиента нужно за секунды подготовить изолированную среду. Этапы провизионирования:
- Создание записи в master-БД с статусом PROVISIONING
- Генерация имени базы и пользователя
- Создание базы данных через административный пул
- Создание пользователя и назначение прав
- Накат миграций Prisma Migrate
- Сохранение connection string и перевод статуса в ACTIVE
Мы добиваемся времени создания базы менее 5 секунд за счёт параллельного выполнения SQL-запросов. Скрипт ниже делает это атомарно:
export async function provisionTenant( tenantSlug: string, plan: string ): Promise<Tenant> { const tenant = await masterDb.tenant.create({ data: { slug: tenantSlug, plan, status: 'PROVISIONING' } }); try { const dbName = `tenant_${tenantSlug.replace(/-/g, '_')}`; const dbUser = `user_${tenant.id.substring(0, 8)}`; const dbPassword = generateSecurePassword(); const adminPool = new Pool({ connectionString: process.env.POSTGRES_ADMIN_URL }); await adminPool.query(`CREATE DATABASE "${dbName}"`); await adminPool.query(`CREATE USER "${dbUser}" WITH PASSWORD '${dbPassword}'`); await adminPool.query(`GRANT ALL PRIVILEGES ON DATABASE "${dbName}" TO "${dbUser}"`); const databaseUrl = `postgresql://${dbUser}:${dbPassword}@${process.env.DB_HOST}/${dbName}`; const { execSync } = await import('child_process'); execSync(`DATABASE_URL="${databaseUrl}" npx prisma migrate deploy`, { env: { ...process.env, DATABASE_URL: databaseUrl } }); await masterDb.tenant.update({ where: { id: tenant.id }, data: { databaseUrl, databaseName: dbName, status: 'ACTIVE' } }); return tenant; } catch (error) { await masterDb.tenant.update({ where: { id: tenant.id }, data: { status: 'FAILED' } }); throw error; } } После успешного создания клиент сразу получает доступ к своей базе. При ошибке — автоматический откат. Время простоя при сбое не превышает 2 секунд.
Как выполнять миграции одновременно на всех тенантах?
Обновлять каждую базу последовательно — долго. Параллельными батчами — надёжно и быстро:
async function migrateAllTenants() { const tenants = await masterDb.tenant.findMany({ where: { status: 'ACTIVE' }, select: { id: true, slug: true, databaseUrl: true } }); console.log(`Migrating ${tenants.length} tenants...`); for (let i = 0; i < tenants.length; i += 10) { const batch = tenants.slice(i, i + 10); await Promise.allSettled( batch.map(async (tenant) => { execSync(`npx prisma migrate deploy`, { env: { ...process.env, DATABASE_URL: tenant.databaseUrl }, stdio: 'pipe', }); return tenant.slug; }) ); } } Батчи по 10 — компромисс между скоростью и нагрузкой. Такой подход сокращает общее время миграции для 100 тенантов с 2 часов до 15 минут, что даёт значительную экономию ресурсов. Даже при частичном падении процесс не срывается: мы логируем ошибки и повторяем.
Бэкапы и мониторинг per-tenant
Независимые бэкапы — сильная сторона модели. Shell-скрипт ниже создаёт дамп и загружает в S3:
TENANT_ID=$1 DB_URL=$(psql $MASTER_DB_URL -t -c "SELECT database_url FROM tenants WHERE id='$TENANT_ID'") TIMESTAMP=$(date +%Y%m%d_%H%M%S) pg_dump "$DB_URL" -Fc -f "backup_${TENANT_ID}_${TIMESTAMP}.dump" aws s3 cp "backup_${TENANT_ID}_${TIMESTAMP}.dump" "s3://my-backups/tenants/${TENANT_ID}/" --sse aws:kms А SQL ниже показывает размер каждой базы — для мониторинга и выставления счетов:
SELECT d.datname as database, pg_database_size(d.datname) as size, pg_size_pretty(pg_database_size(d.datname)) as size_pretty FROM pg_database d WHERE datname LIKE 'tenant_%' ORDER BY size DESC; Экономия на хранении резервных копий составляет до 40% по сравнению с общим дампом всей БД. Сравнение методов бэкапа:
| Метод бэкапа | Время (100 тенантов) | Экономия на хранении |
|---|---|---|
| Per-tenant дамп | 30 мин | 40% |
| Общий дамп всей БД | 2 ч | 0% |
| Инкрементальный | 10 мин | 60% |
Что вы получите?
Мы не просто пишем код. В рамках реализации вы получите:
- Архитектурную документацию с обоснованием выбора модели Database per Tenant
- Репозиторий с автоматическим провизионированием и миграциями
- Настроенный мониторинг и бэкапы (источники, метрики, дашборды)
- Инструкции по эксплуатации и план восстановления
- Поддержку при вводе в эксплуатацию
Сроки и как начать
Разработка database-per-tenant архитектуры с автоматическим провизионированием — от 5 до 10 рабочих дней в зависимости от сложности схемы. Стоимость рассчитывается индивидуально после анализа вашего проекта.
Мы имеем более 10 лет опыта в SaaS-разработке и реализовали более 50 мультитенантных решений. Гарантируем соблюдение compliance и нулевой даунтайм при миграциях.
Оцените ваш проект — свяжитесь с нами для консультации. Мы подберём архитектуру под ваши требования. Получите бесплатную консультацию — мы оценим вашу архитектуру и предложим оптимальное решение. Закажите аудит вашей текущей мультитенантной архитектуры — это займёт не более часа.







