Compartilhar via


Aplicativos multilocatário

Este guia unificado combina os documentos conceituais ("multilocatário") e tutoriais ("tutorial-multilocatário") em um único recurso completo. Você aprenderá a (1) modelar e distribuir um esquema SaaS, (2) carregar e consultar dados com eficiência e (3) operar em escala. A operação em escala inclui alterações de esquema, dados compartilhados, rebalanceamento, isolamento de inquilinos e personalização de JSONB.

Observação

Este artigo pressupõe que você tenha um cluster Citus (clusters elásticos no Banco de Dados do Azure para PostgreSQL ou autogerenciado) e acesso ao coordenador via psql. Consulte Introdução se você ainda precisar de uma configuração local.

1. Entender o padrão

Aplicativos SaaS multilocatários armazenam dados de muitos clientes (locatários) em um banco de dados compartilhado, fornecendo a cada locatário uma exibição isolada. O Citus permite que você mantenha a semântica normal do PostgreSQL — esquemas, junções, restrições, transações — enquanto dimensiona horizontalmente fragmentando tabelas entre nós de trabalho.

Principais metas:

Goal Como o Citus ajuda
OLTP e análise rápidos por locatário Dados de locatário colocados em um nó evitam conversas entre nós.
Simplicidade operacional Ponto de extremidade lógico único do PostgreSQL; SQL padrão e drivers.
Crescimento elástico Adicionar nós; rebalancear fragmentos online.
Manipular distorção de locatário Isole grandes locatários em fragmentos e nós dedicados.
Campos personalizados por locatário JSONB (e índices parciais/GIN) por inquilino.

2. Esquema de análise de anúncios de exemplo

Use uma carga de trabalho de análise de dados de anúncios (empresas, campanhas, anúncios, junto com tabelas de fatos de seleção e impressão). Esquema de estilo de nó único inicial (simplificado) antes dos ajustes de distribuição:

CREATE TABLE companies (
  id bigserial PRIMARY KEY,
  name text NOT NULL,
  image_url text,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL
);
CREATE TABLE campaigns (
  id bigserial PRIMARY KEY,
  company_id bigint REFERENCES companies(id),
  name text NOT NULL,
  cost_model text NOT NULL,
  state text NOT NULL,
  monthly_budget bigint,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL
);
CREATE TABLE ads (
  id bigserial PRIMARY KEY,
  campaign_id bigint REFERENCES campaigns(id),
  name text NOT NULL,
  image_url text,
  target_url text,
  impressions_count bigint DEFAULT 0,
  clicks_count bigint DEFAULT 0,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL
);

Princípio de distribuição

Escolha um identificador de locatário (aqui company_id) e verifique se cada tabela: (a) tem essa coluna diretamente ou (b) pode se associar por meio de uma cadeia de chaves estrangeiras que a inclui. Para restrições mais fortes e desempenho de roteamento, insira a coluna de distribuição em pares de chave primária composta e chave estrangeira.

3. Tornar o esquema adequado para distribuição

Adapte tabelas para que as chaves primárias e estrangeiras incluam company_id (ou equivalente). Aqui está um exemplo do formulário final para uma tabela de fatos:

CREATE TABLE ads (
  id bigserial,
  company_id bigint,
  campaign_id bigint,
  name text NOT NULL,
  image_url text,
  target_url text,
  impressions_count bigint DEFAULT 0,
  clicks_count bigint DEFAULT 0,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL,
  PRIMARY KEY (company_id, id),
  FOREIGN KEY (company_id, campaign_id) REFERENCES campaigns(company_id, id)
);

Repita esse processo para outras tabelas (consulte o exemplo conceitual original) para que cada junção e restrição distribuída seja local a um trabalhador por um determinado valor de locatário.

4. Criar e distribuir tabelas

Conecte-se ao coordenador (instalação nativa: porta 9700; Exemplo do Docker mostrado):

docker exec -it citus psql -U postgres

Crie as tabelas adaptadas e, em seguida, fragmente-as:

SELECT create_distributed_table('companies',   'id');        -- small dimension; distributing by id ok
SELECT create_distributed_table('campaigns',   'company_id');
SELECT create_distributed_table('ads',         'company_id');
SELECT create_distributed_table('clicks',      'company_id');
SELECT create_distributed_table('impressions', 'company_id');

Por que misturar id com companies e company_id em outros lugares? A contagem de linhas da empresa é modesta e usar a chave primária evita desnormalização extra. As tabelas downstream ainda são colocadas por meio de company_id.

5. Carregar dados de exemplo

Baixe CSVs:

for f in companies campaigns ads clicks impressions geo_ips; do \
  curl -O https://examples.citusdata.com/mt_ref_arch/${f}.csv; \
done

Se você estiver usando o Docker, copie-os em:

for f in companies campaigns ads clicks impressions geo_ips; do \
  docker cp ${f}.csv citus:. ; \
done

No psql:

\copy companies   from 'companies.csv'   with csv
\copy campaigns   from 'campaigns.csv'   with csv
\copy ads         from 'ads.csv'         with csv
\copy clicks      from 'clicks.csv'      with csv
\copy impressions from 'impressions.csv' with csv

6. Consultas e transações por locatário

Consultas com WHERE company_id = ? (e junções com restrições) são direcionadas para um trabalhador. Todos os recursos do SQL, como junções, funções de janela e transações, permanecem disponíveis:

SELECT name, cost_model, state, monthly_budget
FROM campaigns
WHERE company_id = 5
ORDER BY monthly_budget DESC
LIMIT 10;

UPDATE campaigns
SET monthly_budget = monthly_budget - 2
WHERE company_id = 5;

BEGIN;
UPDATE campaigns SET monthly_budget = monthly_budget + 1000 WHERE company_id = 5 AND id = 40;
UPDATE campaigns SET monthly_budget = monthly_budget - 1000 WHERE company_id = 5 AND id = 41;
COMMIT;

Funções distribuídas (reduzir viagens de ida e volta)

Encapsular o trabalho para cada locatário de múltiplas instruções em uma função e marcá-la como distribuída.

CREATE OR REPLACE FUNCTION delete_campaign(company_id int, campaign_id int)
RETURNS void LANGUAGE plpgsql AS $fn$
BEGIN
  DELETE FROM ads       WHERE campaign_id = delete_campaign.campaign_id AND company_id = delete_campaign.company_id;
  DELETE FROM campaigns WHERE id = campaign_id AND company_id = delete_campaign.company_id;
END; $fn$;

SELECT create_distributed_function('delete_campaign(int,int)', 'company_id', colocate_with := 'campaigns');
SELECT delete_campaign(5, 46);

Exemplo de análise avançada

SELECT a.campaign_id,
       RANK() OVER (PARTITION BY a.campaign_id ORDER BY count(*) DESC) AS rnk,
       count(*) AS n_impressions,
       a.id
FROM ads a
JOIN impressions i
  ON i.company_id = a.company_id
 AND i.ad_id      = a.id
WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions DESC;

7. Tabelas compartilhadas (referência)

Alguns dados de consulta devem estar presentes para cada cliente sem duplicação. Use tabelas de referência para que cada trabalhador tenha uma cópia completa sincronizada:

CREATE TABLE geo_ips (
  addrs cidr PRIMARY KEY,
  latlon point NOT NULL CHECK (-90 <= latlon[0] AND latlon[0] <= 90 AND -180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist(addrs inet_ops);
SELECT create_reference_table('geo_ips');
\copy geo_ips from 'geo_ips.csv' with csv

A consulta (a junção por locatário permanece local porque a pesquisa é replicada):

SELECT c.id, clicked_at, latlon
FROM geo_ips, clicks c
WHERE addrs >> c.user_ip
  AND c.company_id = 5
  AND c.ad_id = 290;

8. Alterações de esquema online

A DDL emitida no coordenador propaga atomicamente (2PC) para os trabalhadores:

ALTER TABLE ads ADD COLUMN caption text;

Consulte A criação e modificação de DDL (objetos distribuídos) para obter detalhes e limitações.

9. Personalização por locatário com JSONB

Armazene atributos de variável no JSONB e indexe seletivamente:

SELECT user_data->>'is_mobile' AS is_mobile, count(*)
FROM clicks
WHERE company_id = 5
GROUP BY user_data->>'is_mobile'
ORDER BY count DESC;

CREATE INDEX click_user_data_is_mobile ON clicks ((user_data->>'is_mobile')) WHERE company_id = 5;
CREATE INDEX click_user_data ON clicks USING gin(user_data);

10. Expansão e rebalanceamento

Adicione um nó de trabalho (portal ou citus_add_node) e, em seguida, rebalance os fragmentos:

SELECT citus_rebalance_start();

As leituras continuam durante o reequilíbrio. Dependendo da edição e da versão, gravações em fragmentos móveis podem bloquear brevemente (comunidade) ou continuar (artigo de nuvem ou versões recentes).

11. Manipulando usuários importantes (de alto uso)

O viés geralmente é Zipfian: alguns locatários dominam o tamanho e o tráfego. Isolar um locatário grande em seu próprio fragmento (e, opcionalmente, nó):

SELECT isolate_tenant_to_new_shard('companies', 5, 'CASCADE') AS shard_id;  -- returns shard id

Em seguida, mova esse fragmento (e os fragmentos relacionados) para um nó escolhido. Verifique wal_level >= logical:

SELECT citus_move_shard_placement(
  <shard_id>,
  'source_host', source_port,
  'dest_host',   dest_port
);

Verifique por meio das consultas pg_dist_placement.

12. Próximas etapas

Você criou um aplicativo multilocatário no Citus que é dimensionado horizontalmente, dá suporte à semântica avançada do SQL e manipula o isolamento de locatário. Continue seu percurso explorando guias de migração, integrações de estrutura e padrões operacionais avançados:

Este artigo substitui páginas conceituais e tutoriais anteriores separadas.