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