
Мультиарендность — это когда один продукт обслуживает много клиентов (арендаторов), но каждый видит только свои данные и получает предсказуемые показатели по скорости и надежности. Для бизнеса это:
Когда особенно актуально:
Есть три базовые стратегии. Их можно смешивать по сегментам.
Правило большого пальца:
Чаще всего арендатор определяется по одному из трёх признаков:
Требования к механизму:
Антипаттерн: «прокидывать tenant_id ручками в каждый запрос к БД». Ошибиться тут очень легко. Лучше один раз «зафиксировать» контекст арендатора на уровне соединения с базой.
Покажу минимальную, но рабочую конфигурацию RLS с установкой tenant_id через параметр сессии.
-- Подключитесь к базе от имени владельца схемы
-- Создаём расширение uuid-ossp при необходимости
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Базовая таблица с tenant_id
CREATE TABLE IF NOT EXISTS public.invoices (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
number text NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Индекс по tenant_id для быстрого доступа
CREATE INDEX IF NOT EXISTS invoices_tenant_idx ON public.invoices(tenant_id);
-- Включаем RLS
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
-- Политика чтения: показываем только строки с tenant_id = app.current_tenant
CREATE POLICY invoices_select_policy ON public.invoices
FOR SELECT USING (
tenant_id = current_setting('app.current_tenant', true)::uuid
);
-- Политика вставки: запрещаем писать от чужого имени
CREATE POLICY invoices_insert_policy ON public.invoices
FOR INSERT WITH CHECK (
tenant_id = current_setting('app.current_tenant', true)::uuid
);
-- Политика обновления: только свои строки
CREATE POLICY invoices_update_policy ON public.invoices
FOR UPDATE USING (
tenant_id = current_setting('app.current_tenant', true)::uuid
)
WITH CHECK (
tenant_id = current_setting('app.current_tenant', true)::uuid
);
-- Роль приложения с ограниченными правами
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_user') THEN
CREATE ROLE app_user LOGIN PASSWORD 'app_password';
END IF;
END $$;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE ON public.invoices TO app_user;
Идея простая: после взятия соединения из пула, сразу делаем SET app.current_tenant = '
// app.js
// Минимальный Express + pg пример с контекстом арендатора через параметр сессии
const express = require('express');
const { Pool } = require('pg');
// В демо используем DSN локальной базы. Замените на свой при необходимости.
const pool = new Pool({
connectionString: 'postgres://app_user:app_password@localhost:5432/app'
});
const app = express();
app.use(express.json());
// Пример обнаружения арендатора: из поддомена вида <tenant>.localhost
function resolveTenantFromHost(host) {
// host может быть вида 'acme.localhost:3000' — берём первую часть
const name = host.split(':')[0];
const parts = name.split('.');
if (parts.length > 1) {
const sub = parts[0].toLowerCase();
// В реальности маппим sub → UUID из своей таблицы tenants
// Для примера фиксируем два клиента
if (sub === 'acme') return '00000000-0000-0000-0000-0000000000ac';
if (sub === 'beta') return '00000000-0000-0000-0000-0000000000be';
}
return null;
}
// Middleware: устанавливаем tenant в параметр сессии Postgres
async function withTenant(req, res, next) {
const tenantId = resolveTenantFromHost(req.headers.host || '');
if (!tenantId) return res.status(400).json({ error: 'Tenant not resolved' });
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query("SET LOCAL app.current_tenant = $1", [tenantId]);
// Привязываем клиент к запросу
req.db = client;
req.tenantId = tenantId;
await next();
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
next(e);
} finally {
req.db.release();
}
}
app.get('/invoices', withTenant, async (req, res) => {
const { rows } = await req.db.query('SELECT id, number, amount FROM public.invoices ORDER BY created_at DESC LIMIT 50');
res.json(rows);
});
app.post('/invoices', withTenant, async (req, res) => {
const { number, amount } = req.body;
const { tenantId } = req;
const { rows } = await req.db.query(
'INSERT INTO public.invoices (tenant_id, number, amount) VALUES ($1, $2, $3) RETURNING id, number, amount',
[tenantId, number, amount]
);
res.status(201).json(rows[0]);
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal error' });
});
app.listen(3000, () => console.log('Server started on http://acme.localhost:3000'));
Почему SET LOCAL? Оно действует в рамках транзакции. Так мы гарантируем, что параметр не «протечет» в другой запрос при повторном использовании соединения из пула.
Даже при общей БД важно не допускать «шумного соседа»:
Если у вас Kubernetes и модель 2/3 (схемы/базы на клиента), можно изолировать ресурсы на уровне пространства имён:
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-acme-quota
namespace: tenant-acme
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "20"
Это сдерживает рост потребления и делает поведение предсказуемым.
Миграции при мультиарендности — частый источник боли. Базовые правила:
Мини‑пример миграции для «схем на арендатора»: создаём таблицу в новой схеме по шаблону.
-- Создаём схему для нового арендатора и копируем структуру из public
CREATE SCHEMA IF NOT EXISTS tenant_acme AUTHORIZATION app_user;
-- Таблица по образцу (в т.ч. индексы)
CREATE TABLE IF NOT EXISTS tenant_acme.invoices (LIKE public.invoices INCLUDING ALL);
-- Назначаем права
GRANT SELECT, INSERT, UPDATE ON tenant_acme.invoices TO app_user;
Подход к деплою:
Сделайте tenant_id «первоклассным гражданином» в логах и метриках:
Это помогает и продажам (обосновать тариф), и поддержке (увидеть «шумного» клиента), и продукту (понять реальное использование функций).
Неделя 1:
Неделя 2:
Неделя 3:
Неделя 4:
Неделя 5–6:
Ответьте на 7 вопросов:
Итог: начните с простой и экономной модели «общая таблица + RLS», но проектируйте код так, чтобы при росте было не больно перейти к «схеме/базе на клиента». Чётко фиксируйте tenant в сессии БД, держите метрики и логи по арендаторам и не забывайте про дисциплину миграций. Это даст быстрый онбординг, предсказуемые SLO для ключевых клиентов и понятные затраты на инфраструктуру.