Sur une plateforme SaaS d'analyse de données, il est courant que plusieurs tenants, tels que des organisations, des clients ou des unités opérationnelles, partagent la même infrastructure de base de données tout en conservant une séparation logique de leurs données. Cela permet à différents utilisateurs d'accéder en toute sécurité à leurs propres données sur la même plateforme.
Selon les besoins, il existe différentes façons de mettre en œuvre la multi-tenance. Vous trouverez ci-dessous un guide expliquant comment les mettre en œuvre avec ClickHouse Cloud.
Dans cette approche, les données de tous les tenants sont stockées dans une seule table partagée, avec un champ (ou un ensemble de champs) servant à identifier les données de chaque tenant. Pour maximiser les performances, ce champ doit être inclus dans la clé primaire. Pour garantir que vous ne puissiez accéder qu'aux données de vos tenants respectifs, nous utilisons le contrôle d'accès basé sur les rôles, mis en œuvre au moyen de politiques de lignes.
Nous recommandons cette approche car c'est la plus simple à gérer, en particulier lorsque tous les tenants partagent le même schéma de données et que les volumes de données restent modérés (< TBs)
En consolidant toutes les données des tenants dans une seule table, l'efficacité du stockage est améliorée grâce à une meilleure compression des données et à une réduction de la surcharge liée aux métadonnées. De plus, les mises à jour du schéma sont simplifiées puisque toutes les données sont gérées de façon centralisée.
Cette méthode est particulièrement efficace pour gérer un grand nombre de tenants (potentiellement des millions).
Cependant, d'autres approches peuvent être plus adaptées si les tenants ont des schémas de données différents ou sont susceptibles de diverger au fil du temps.
Lorsqu'il existe un écart important de volume de données entre les tenants, les plus petits peuvent subir une dégradation inutile des performances des requêtes. Notez que ce problème est largement atténué en incluant le champ tenant dans la clé primaire.
Voici un exemple d’implémentation d’un modèle de multi-tenance avec table partagée.
Commençons par créer une table partagée avec un champ tenant_id inclus dans la clé primaire.
--- Create table events. Using tenant_id as part of the primary key
CREATE TABLE events
(
tenant_id UInt32, -- Tenant identifier
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (tenant_id, timestamp)Insérons des données fictives.
-- Insert some dummy rows
INSERT INTO events (tenant_id, id, type, timestamp, user_id, data)
VALUES
(1, '7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
(1, '846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
(1, '6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
(2, '7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
(2, '6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
(2, '43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
(1, '83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
(1, '975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}'),
(2, 'f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
(2, '5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}'),Créons ensuite deux utilisateurs, user_1 et user_2.
-- Create users
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'Nous créons des politiques de lignes qui limitent user_1 et user_2 pour qu’ils n’accèdent qu’aux données de leurs tenants respectifs.
-- Create row policies
CREATE ROW POLICY user_filter_1 ON default.events USING tenant_id=1 TO user_1
CREATE ROW POLICY user_filter_2 ON default.events USING tenant_id=2 TO user_2Accordez ensuite les privilèges GRANT SELECT sur la table partagée via un rôle commun.
-- Create role
CREATE ROLE user_role
-- Grant read only to events table.
GRANT SELECT ON default.events TO user_role
GRANT user_role TO user_1
GRANT user_role TO user_2Vous pouvez maintenant vous connecter en tant que user_1 et exécuter une simple requête SELECT. Seules les lignes du premier tenant sont renvoyées.
-- Logged as user_1
SELECT *
FROM events ┌─tenant_id─┬─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 1 │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 1 │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 1 │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 1 │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 1 │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└───────────┴──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘Tables séparées
Dans cette approche, les données de chaque tenant sont stockées dans une table distincte au sein de la même base de données, ce qui évite d'avoir à utiliser un champ spécifique pour identifier les tenants. L'accès des utilisateurs est contrôlé à l'aide d'une instruction GRANT, de sorte que chaque utilisateur ne puisse accéder qu'aux tables contenant les données de ses tenants.
L'utilisation de tables séparées est un bon choix lorsque les tenants ont des schémas de données différents.
Pour les scénarios impliquant un petit nombre de tenants avec de très grands jeux de données, où les performances des requêtes sont essentielles, cette approche peut être plus performante qu'un modèle à table partagée. Comme il n'est pas nécessaire de filtrer les données des autres tenants, les requêtes peuvent être plus efficaces. De plus, les clés primaires peuvent être davantage optimisées, puisqu'il n'est pas nécessaire d'inclure un champ supplémentaire (tel qu'un ID de tenant) dans la clé primaire.
Notez que cette approche ne passe pas à l'échelle pour des milliers de tenants. Voir les limites d'utilisation.
Voici un exemple d'implémentation d'un modèle de multi-tenancy à tables séparées.
Commençons par créer deux tables : une pour les événements du tenant_1 et une pour les événements du tenant_2.
-- Create table for tenant 1
CREATE TABLE events_tenant_1
(
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (timestamp, user_id) -- Primary key can focus on other attributes
-- Create table for tenant 2
CREATE TABLE events_tenant_2
(
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (timestamp, user_id) -- Primary key can focus on other attributesInsérons des données fictives.
INSERT INTO events_tenant_1 (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')
INSERT INTO events_tenant_2 (id, type, timestamp, user_id, data)
VALUES
('7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
('6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
('43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
('f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
('5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}')Créons ensuite deux utilisateurs user_1 et user_2.
-- Create users
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'Accordez ensuite les privilèges GRANT SELECT sur la table correspondante.
-- Grant read only to events table.
GRANT SELECT ON default.events_tenant_1 TO user_1
GRANT SELECT ON default.events_tenant_2 TO user_2Vous pouvez maintenant vous connecter en tant que user_1 et exécuter une simple sélection depuis la table correspondant à cet utilisateur. Seules les lignes du premier tenant sont renvoyées.
-- Logged as user_1
SELECT *
FROM default.events_tenant_1 ┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘Bases de données séparées
Les données de chaque tenant sont stockées dans une base de données distincte au sein du même service ClickHouse.
Cette approche est utile si chaque tenant a besoin d'un grand nombre de tables et éventuellement de vues matérialisées, et dispose d'un schéma de données différent. Cependant, elle peut devenir difficile à gérer si le nombre de tenants est élevé.
L'implémentation est similaire à l'approche des tables séparées, mais au lieu d'accorder des privilèges au niveau de la table, les privilèges sont accordés au niveau de la base de données.
Notez que cette approche ne passe pas à l'échelle pour des milliers de tenants. Consultez les limites d'utilisation.
Voici un exemple d’implémentation d’un modèle de multi-tenance à bases de données séparées.
Commençons par créer deux bases de données, une pour tenant_1 et une pour tenant_2.
-- Create database for tenant_1
CREATE DATABASE tenant_1;
-- Create database for tenant_2
CREATE DATABASE tenant_2;-- Create table for tenant_1
CREATE TABLE tenant_1.events
(
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (timestamp, user_id);
-- Create table for tenant_2
CREATE TABLE tenant_2.events
(
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (timestamp, user_id);Insérons des données fictives.
INSERT INTO tenant_1.events (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')
INSERT INTO tenant_2.events (id, type, timestamp, user_id, data)
VALUES
('7162f8ea-8bfd-486a-a45e-edfc3398ca93', 'user_login', '2025-03-19 08:12:00', 2001, '{"device": "mobile", "location": "SF"}'),
('6b5f3e55-5add-479e-b89d-762aa017f067', 'purchase', '2025-03-19 08:15:00', 2002, '{"item": "headphones", "amount": 199}'),
('43ad35a1-926c-4543-a133-8672ddd504bf', 'user_logout', '2025-03-19 08:20:00', 2001, '{"device": "mobile", "location": "SF"}'),
('f50aa430-4898-43d0-9d82-41e7397ba9b8', 'purchase', '2025-03-19 08:55:00', 2003, '{"item": "laptop", "amount": 1200}'),
('5c150ceb-b869-4ebb-843d-ab42d3cb5410', 'user_login', '2025-03-19 09:00:00', 2004, '{"device": "mobile", "location": "SF"}')Créons maintenant deux utilisateurs : user_1 et user_2.
-- Create users
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'Ensuite, accordez les privilèges GRANT SELECT sur la table correspondante.
-- Grant read only to events table.
GRANT SELECT ON tenant_1.events TO user_1
GRANT SELECT ON tenant_2.events TO user_2Vous pouvez maintenant vous connecter en tant que user_1 et exécuter une requête SELECT simple sur la table events de la base de données appropriée. Seules les lignes du premier tenant sont renvoyées.
-- Logged as user_1
SELECT *
FROM tenant_1.events ┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘Compute-compute separation
Les trois approches décrites ci-dessus peuvent également être davantage isolées en utilisant les Warehouses. Les données sont partagées via un stockage objet commun, mais chaque tenant peut disposer de son propre service de compute grâce à la compute-compute separation, avec un ratio CPU/mémoire différent.
La gestion des utilisateurs est similaire aux approches décrites précédemment, puisque tous les services d'un warehouse partagent les mêmes contrôles d'accès.
Notez que le nombre de services enfants dans un warehouse est limité. Voir les limitations des warehouses.
Service cloud séparé
L’approche la plus radicale consiste à utiliser un service ClickHouse distinct pour chaque tenant.
Cette méthode, moins courante, peut être une solution si les données des tenants doivent être stockées dans différentes régions, pour des raisons juridiques, de sécurité ou de proximité.
Un compte utilisateur doit être créé sur chaque service afin que l’utilisateur puisse accéder aux données de son tenant.
Cette approche est plus difficile à gérer et entraîne un surcoût pour chaque service, car chacun nécessite sa propre infrastructure pour fonctionner. Les services peuvent être gérés via l’API ClickHouse Cloud, et l’orchestration est également possible via le provider Terraform officiel.
Voici un exemple de mise en œuvre d’un modèle de multi-tenance avec des services distincts. Notez que cet exemple montre la création de tables et d’utilisateurs sur un service ClickHouse ; il faudra reproduire la même configuration sur tous les services.
Commençons par créer la table events
-- Create table for tenant_1
CREATE TABLE events
(
id UUID, -- Unique event ID
type LowCardinality(String), -- Type of event
timestamp DateTime, -- Timestamp of the event
user_id UInt32, -- ID of the user who triggered the event
data String, -- Event data
)
ORDER BY (timestamp, user_id);Insérons des données factices.
INSERT INTO events (id, type, timestamp, user_id, data)
VALUES
('7b7e0439-99d0-4590-a4f7-1cfea1e192d1', 'user_login', '2025-03-19 08:00:00', 1001, '{"device": "desktop", "location": "LA"}'),
('846aa71f-f631-47b4-8429-ee8af87b4182', 'purchase', '2025-03-19 08:05:00', 1002, '{"item": "phone", "amount": 799}'),
('6b4d12e4-447d-4398-b3fa-1c1e94d71a2f', 'user_logout', '2025-03-19 08:10:00', 1001, '{"device": "desktop", "location": "LA"}'),
('83b5eb72-aba3-4038-bc52-6c08b6423615', 'purchase', '2025-03-19 08:45:00', 1003, '{"item": "monitor", "amount": 450}'),
('975fb0c8-55bd-4df4-843b-34f5cfeed0a9', 'user_login', '2025-03-19 08:50:00', 1004, '{"device": "desktop", "location": "LA"}')Créons ensuite deux utilisateurs user_1
-- Create users
CREATE USER user_1 IDENTIFIED BY '<password>'Accordez ensuite le privilège GRANT SELECT sur la table correspondante.
-- Grant read only to events table.
GRANT SELECT ON events TO user_1Vous pouvez maintenant vous connecter en tant que user_1 au service du tenant 1 et exécuter une simple requête SELECT. Seules les lignes du premier tenant sont renvoyées.
-- Logged as user_1
SELECT *
FROM events ┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ user_login │ 2025-03-19 08:00:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ purchase │ 2025-03-19 08:05:00 │ 1002 │ {"item": "phone", "amount": 799} │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ user_logout │ 2025-03-19 08:10:00 │ 1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ purchase │ 2025-03-19 08:45:00 │ 1003 │ {"item": "monitor", "amount": 450} │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ user_login │ 2025-03-19 08:50:00 │ 1004 │ {"device": "desktop", "location": "LA"} │
└──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘