Multi-tenant databases: estratégias de isolamento
1. Fundamentos de Arquitetura Multi-tenant
Em aplicações SaaS (Software as a Service), um tenant representa um cliente ou inquilino que utiliza uma instância compartilhada do sistema. A arquitetura multi-tenant permite que múltiplos clientes compartilhem a mesma aplicação e infraestrutura de banco de dados, mantendo seus dados logicamente isolados.
Os requisitos fundamentais de isolamento incluem:
- Segurança: garantir que um tenant jamais acesse dados de outro
- Performance: evitar que um tenant consuma recursos excessivos e prejudique os demais
- Personalização: permitir customizações de schema ou configurações por tenant
O principal trade-off reside entre o nível de isolamento desejado e o custo operacional. Quanto maior o isolamento, maior a complexidade de gerenciamento e o custo de infraestrutura.
2. Estratégia 1: Banco de Dados por Tenant (Database per Tenant)
Nesta abordagem, cada tenant possui seu próprio banco de dados físico. O isolamento é completo.
CREATE DATABASE tenant_empresa_a;
CREATE DATABASE tenant_empresa_b;
Vantagens:
- Backup e restore independentes por tenant
- Customização total de schema por cliente
- Isolamento de performance absoluto
Desafios:
- Gerenciamento de centenas ou milhares de conexões simultâneas
- Custo elevado de infraestrutura
- Complexidade em migrações globais de schema
Exemplo de conexão dinâmica por tenant:
-- Pseudocódigo para conexão baseada em tenant
function conectarBanco(tenant_id):
db_name = "tenant_" + tenant_id
return new Connection(host, port, db_name, user, password)
3. Estratégia 2: Schema por Tenant (Schema per Tenant)
Compartilha-se o mesmo servidor de banco de dados, mas cada tenant possui um schema isolado.
CREATE SCHEMA IF NOT EXISTS tenant_123;
CREATE SCHEMA IF NOT EXISTS tenant_456;
-- Criando tabelas dentro do schema do tenant
CREATE TABLE tenant_123.clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100)
);
CREATE TABLE tenant_456.clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100),
telefone VARCHAR(20) -- Personalização permitida
);
Limitações:
- Limite de schemas por instância (depende do SGBD)
- Monitoramento mais complexo para identificar tenants problemáticos
- Migrações precisam ser executadas em cada schema individualmente
Para automatizar migrações:
-- Script para aplicar migração em todos os schemas
DO $$
DECLARE
schema_nome TEXT;
BEGIN
FOR schema_nome IN
SELECT nspname FROM pg_namespace
WHERE nspname LIKE 'tenant_%'
LOOP
EXECUTE format(
'ALTER TABLE %I.pedidos ADD COLUMN desconto NUMERIC(5,2) DEFAULT 0',
schema_nome
);
END LOOP;
END $$;
4. Estratégia 3: Tabela Compartilhada com Coluna de Tenant (Shared Table)
Todos os tenants compartilham as mesmas tabelas, diferenciados por uma coluna tenant_id.
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL,
cliente_nome VARCHAR(100),
valor_total NUMERIC(10,2),
data_criacao TIMESTAMP DEFAULT NOW()
);
-- Índice parcial para performance por tenant
CREATE INDEX idx_pedidos_tenant_789
ON pedidos(tenant_id, data_criacao)
WHERE tenant_id = 789;
-- Particionamento por tenant (PostgreSQL 10+)
CREATE TABLE pedidos_part (
id SERIAL,
tenant_id INTEGER NOT NULL,
cliente_nome VARCHAR(100),
valor_total NUMERIC(10,2)
) PARTITION BY LIST (tenant_id);
CREATE TABLE pedidos_tenant_789 PARTITION OF pedidos_part
FOR VALUES IN (789);
CREATE TABLE pedidos_tenant_790 PARTITION OF pedidos_part
FOR VALUES IN (790);
Riscos importantes:
- Vazamento acidental de dados se faltar o filtro WHERE tenant_id = ?
- Contenção de locks em operações bulk que afetam múltiplos tenants
- Dificuldade de backup granular por tenant
5. Comparação entre Estratégias: Isolamento vs. Custo
| Estratégia | Isolamento | Custo | Performance | Complexidade |
|---|---|---|---|---|
| Database per Tenant | Máximo | Alto | Excelente | Alta |
| Schema per Tenant | Alto | Médio | Boa | Média |
| Shared Table | Baixo | Baixo | Variável | Baixa |
Casos de uso típicos:
- Database per Tenant: clientes enterprise com requisitos regulatórios (LGPD, HIPAA)
- Schema per Tenant: SaaS B2B com necessidades moderadas de customização
- Shared Table: aplicações free-tier ou trials com custo mínimo
Estratégias híbridas: muitos sistemas combinam abordagens. Por exemplo, tenants premium recebem banco próprio, enquanto tenants básicos compartilham tabelas.
6. Implementação Prática com SQL
Row-Level Security (RLS) no PostgreSQL
A RLS aplica automaticamente filtros por tenant, eliminando o risco de vazamento acidental:
-- Habilitar RLS na tabela
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
-- Criar política que filtra por tenant_id
CREATE POLICY pedidos_tenant_policy ON pedidos
USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);
-- Configurar o tenant na sessão
SET app.current_tenant_id = '789';
-- Query segura: retorna apenas dados do tenant 789
SELECT * FROM pedidos;
Provisionamento dinâmico de banco por tenant
-- Função para criar banco e schema automaticamente
CREATE OR REPLACE FUNCTION provisionar_tenant(p_tenant_id INTEGER)
RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE DATABASE tenant_%s', p_tenant_id);
EXECUTE format('
CREATE SCHEMA IF NOT EXISTS tenant_%s;
SET search_path TO tenant_%s;
CREATE TABLE clientes (...);
CREATE TABLE pedidos (...);
', p_tenant_id, p_tenant_id);
END;
$$ LANGUAGE plpgsql;
-- Uso
SELECT provisionar_tenant(999);
7. Monitoramento e Manutenção em Ambiente Multi-tenant
Identificação de "noisy neighbors"
-- Consulta para identificar queries lentas por tenant
SELECT
current_setting('app.current_tenant_id') AS tenant_id,
query,
total_exec_time,
calls,
mean_exec_time
FROM pg_stat_statements
WHERE query LIKE '%tenant_id%'
ORDER BY total_exec_time DESC
LIMIT 10;
Throttling por tenant
-- Exemplo com extensão pg_stat_statements
CREATE OR REPLACE FUNCTION check_tenant_quota(p_tenant_id INTEGER)
RETURNS BOOLEAN AS $$
DECLARE
query_count INTEGER;
BEGIN
SELECT COUNT(*) INTO query_count
FROM pg_stat_activity
WHERE state = 'active'
AND query LIKE '%tenant_id = ' || p_tenant_id || '%';
RETURN query_count < 5; -- Limite de 5 queries simultâneas
END;
$$ LANGUAGE plpgsql;
Backup seletivo e restore granular
Para estratégia de banco por tenant, o backup é naturalmente granular:
# Backup de um tenant específico
pg_dump -h localhost -U admin -d tenant_empresa_a > backup_empresa_a.sql
# Restore
psql -h localhost -U admin -d tenant_empresa_a < backup_empresa_a.sql
Para shared table, é necessário exportar com filtro:
pg_dump --table=pedidos --data-only --where="tenant_id=789" minha_base > pedidos_tenant_789.sql
Conclusão
A escolha da estratégia de isolamento multi-tenant depende diretamente do modelo de negócio, requisitos de segurança e orçamento disponível. Não existe solução universal: sistemas SaaS maduros frequentemente adotam abordagens híbridas, começando com tabela compartilhada e evoluindo para banco por tenant conforme os clientes crescem.
O SQL moderno oferece ferramentas poderosas como Row-Level Security e particionamento que mitigam os principais riscos da abordagem compartilhada. Independentemente da estratégia escolhida, invista em monitoramento contínuo e automação de provisionamento para manter a sanidade operacional à medida que o número de tenants cresce.
Referências
-
PostgreSQL Documentation: Row Security Policies — Documentação oficial sobre implementação de políticas de segurança em nível de linha no PostgreSQL, essencial para estratégias de tabela compartilhada.
-
Microsoft Azure: Multi-tenant SaaS Database Patterns — Guia completo da Microsoft sobre padrões de arquitetura multi-tenant para bancos de dados SaaS, incluindo comparação detalhada entre as estratégias.
-
AWS: SaaS Multi-Tenant Database Design Patterns — Whitepaper técnico da AWS abordando design patterns para bancos multi-tenant, com exemplos práticos de implementação.
-
Citus (Microsoft): Multi-tenant Data Modeling with PostgreSQL — Guia especializado sobre modelagem de dados multi-tenant usando PostgreSQL e sharding, com foco em performance e escalabilidade.
-
Percona Blog: Multi-Tenant Database Strategies for MySQL and PostgreSQL — Artigo técnico comparando estratégias de isolamento multi-tenant entre MySQL e PostgreSQL, com benchmarks e considerações operacionais.