Перейти к основному содержанию
Перейти к основному содержанию

Многоклиентность

На SaaS‑платформе аналитики данных обычно несколько клиентов (тенантов), таких как организации, заказчики или бизнес-подразделения, используют одну и ту же инфраструктуру базы данных, при этом их данные остаются логически изолированными. Это позволяет различным пользователям безопасно получать доступ только к своим данным в рамках одной платформы.

В зависимости от требований существуют различные варианты реализации многоклиентности. Ниже приведено руководство по их реализации в ClickHouse Cloud.

Общая таблица

В этом подходе данные всех арендаторов хранятся в одной общей таблице, при этом поле (или набор полей) используется для идентификации данных каждого арендатора. Для максимальной производительности это поле следует включить в первичный ключ. Чтобы гарантировать, что пользователи могут получать доступ только к данным, принадлежащим их арендаторам, мы используем управление доступом на основе ролей, реализованное через политики строк.

Мы рекомендуем этот подход, так как его проще всего администрировать, особенно когда все арендаторы используют одну и ту же схему данных, а объёмы данных умеренные (< ТБ)

Объединение всех данных арендаторов в одной таблице повышает эффективность хранения за счёт оптимизированного сжатия данных и сокращения накладных расходов на метаданные. Кроме того, упрощаются обновления схемы, так как все данные централизованно управляются.

Этот метод особенно эффективен при работе с большим количеством арендаторов (потенциально до миллионов).

Однако альтернативные подходы могут быть предпочтительнее, если арендаторы используют разные схемы данных или ожидается, что со временем они будут расходиться.

В случаях, когда существует значительная разница в объёмах данных между арендаторами, более мелкие арендаторы могут испытывать ненужное влияние на производительность запросов. Обратите внимание, что эта проблема в значительной степени устраняется за счёт включения поля арендатора в первичный ключ.

Пример

Это пример реализации многотенантной модели с общей таблицей.

Сначала создадим общую таблицу с полем tenant_id, включённым в первичный ключ.

--- Создание таблицы events. Используется tenant_id как часть первичного ключа
CREATE TABLE events
(
    tenant_id UInt32,                 -- Идентификатор тенанта
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (tenant_id, timestamp)

Добавим тестовые данные.

-- Вставка примеров данных
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"}'),

Теперь создадим двух пользователей user_1 и user_2.

-- Создать пользователей
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'

Мы создаём политики строк, которые ограничивают доступ пользователей user_1 и user_2 только к данным их арендаторов.

-- Создать политики строк
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_2

Затем предоставьте для общей таблицы права GRANT SELECT, используя общую роль.

-- Создать роль
CREATE ROLE user_role

-- Предоставить права только на чтение для таблицы events.
GRANT SELECT ON default.events TO user_role
GRANT user_role TO user_1
GRANT user_role TO user_2

Теперь вы можете подключиться под пользователем user_1 и выполнить простой запрос SELECT. Будут возвращены только строки из первого тенанта.

-- Вход выполнен под пользователем 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"} │
   └───────────┴──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘

Отдельные таблицы

В этом подходе данные каждого арендатора хранятся в отдельной таблице в той же базе данных, что устраняет необходимость в специальном поле для идентификации арендаторов. Доступ пользователей ограничивается с помощью оператора GRANT, благодаря чему каждый пользователь может получить доступ только к таблицам, содержащим данные соответствующих арендаторов.

Использование отдельных таблиц — хороший выбор, когда у арендаторов разные схемы данных.

В сценариях с небольшим количеством арендаторов, но с очень большими наборами данных, где производительность запросов критична, такой подход может работать быстрее, чем модель общей (совместно используемой) таблицы. Поскольку нет необходимости отфильтровывать данные других арендаторов, запросы могут быть более эффективными. Кроме того, первичные ключи можно дополнительно оптимизировать, так как нет необходимости включать дополнительное поле (например, ID арендатора) в первичный ключ.

Учтите, что этот подход не масштабируется до тысяч арендаторов. См. ограничения использования.

Пример

Это пример реализации модели мультиарендности с отдельными таблицами.

Сначала создадим две таблицы: одну для событий из tenant_1 и одну для событий из tenant_2.

-- Создать таблицу для тенанта 1 
CREATE TABLE events_tenant_1
(
    id UUID,                    -- Уникальный ID события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может быть сфокусирован на других атрибутах

-- Создать таблицу для тенанта 2 
CREATE TABLE events_tenant_2
(
    id UUID,                    -- Уникальный ID события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id) -- Первичный ключ может быть сфокусирован на других атрибутах

Вставим тестовые данные.

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"}')

Затем создадим двух пользователей user_1 и user_2.

-- Создание пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'

Затем выполните GRANT SELECT на соответствующую таблицу.

-- Предоставление прав только на чтение таблицы events.
GRANT SELECT ON default.events_tenant_1 TO user_1
GRANT SELECT ON default.events_tenant_2 TO user_2

Теперь вы можете подключиться как user_1 и выполнить простой запрос SELECT из таблицы, соответствующей этому пользователю. Будут возвращены только строки первого тенанта.

-- Вход выполнен под пользователем user_1
SELECT *
FROM default.events_tenant_1

   ┌─id───────────────────────────────────┬─type────────┬───────────timestamp─┬─user_id─┬─data────────────────────────────────────┐
1. │ 7b7e0439-99d0-4590-a4f7-1cfea1e192d1 │ вход_пользователя  │ 2025-03-19 08:00:00 │    1001 │ {"device": "desktop", "location": "LA"} │
2. │ 846aa71f-f631-47b4-8429-ee8af87b4182 │ покупка    │ 2025-03-19 08:05:00 │    1002 │ {"item": "phone", "amount": 799}        │
3. │ 6b4d12e4-447d-4398-b3fa-1c1e94d71a2f │ выход_пользователя │ 2025-03-19 08:10:00 │    1001 │ {"device": "desktop", "location": "LA"} │
4. │ 83b5eb72-aba3-4038-bc52-6c08b6423615 │ покупка    │ 2025-03-19 08:45:00 │    1003 │ {"item": "monitor", "amount": 450}      │
5. │ 975fb0c8-55bd-4df4-843b-34f5cfeed0a9 │ вход_пользователя  │ 2025-03-19 08:50:00 │    1004 │ {"device": "desktop", "location": "LA"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘

Раздельные базы данных

Данные каждого арендатора хранятся в отдельной базе данных в одном и том же сервисе ClickHouse.

Этот подход полезен, если каждому арендатору требуется большое количество таблиц и, возможно, материализованных представлений, а также у них различаются схемы данных. Однако управление ими становится сложным при большом числе арендаторов.

Реализация аналогична подходу с раздельными таблицами, но вместо выдачи привилегий на уровне таблицы привилегии выдаются на уровне базы данных.

Обратите внимание, что этот подход не масштабируется до тысяч арендаторов. См. ограничения использования.

Пример

Пример реализации многопользовательской (multi-tenant) модели с раздельными базами данных.

Сначала создадим две базы данных: одну для tenant_1 и одну для tenant_2.

-- Создать базу данных для tenant_1
CREATE DATABASE tenant_1;

-- Создать базу данных для tenant_2
CREATE DATABASE tenant_2;
-- Создать таблицу для tenant_1
CREATE TABLE tenant_1.events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);

-- Создать таблицу для tenant_2
CREATE TABLE tenant_2.events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);

Вставим тестовые данные.

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"}')

Теперь давайте создадим двух пользователей user_1 и user_2.

-- Создание пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'
CREATE USER user_2 IDENTIFIED BY '<password>'

Затем предоставьте привилегии GRANT SELECT для соответствующей таблицы.

-- Предоставление прав только на чтение таблицы events.
GRANT SELECT ON tenant_1.events TO user_1
GRANT SELECT ON tenant_2.events TO user_2

Теперь вы можете подключиться под пользователем user_1 и выполнить простой SELECT-запрос к таблице events соответствующей базы данных. Будут возвращены только строки первого арендатора.

-- Вход выполнен под пользователем 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"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘

Разделение вычислительных ресурсов

Три описанных выше подхода также можно дополнительно изолировать с помощью Warehouses. Данные хранятся в общем объектном хранилище, но каждый арендатор может иметь собственный вычислительный сервис благодаря разделению вычислительных ресурсов с разным соотношением CPU/Memory.

Управление пользователями аналогично ранее описанным подходам, поскольку все сервисы в Warehouse используют общие настройки управления доступом.

Обратите внимание, что число дочерних сервисов в одном Warehouse ограничено небольшим количеством. См. ограничения Warehouse.

Отдельный облачный сервис

Наиболее радикальный подход — использовать отдельный сервис ClickHouse для каждого арендатора.

Этот менее распространённый метод может быть решением, если данные арендаторов требуется хранить в разных регионах — по юридическим причинам, требованиям безопасности или соображениям близости.

На каждом сервисе должна быть создана учётная запись пользователя, через которую он сможет получить доступ к данным соответствующего арендатора.

Такой подход сложнее в управлении и создаёт накладные расходы для каждого сервиса, так как каждому для работы требуется собственная инфраструктура. Сервисами можно управлять через ClickHouse Cloud API, а оркестрация также возможна через официальный провайдер Terraform.

Пример

Ниже приведён пример реализации модели мультиарендности на основе отдельных сервисов. Обратите внимание, что в примере показано создание таблиц и пользователей в одном сервисе ClickHouse; то же самое потребуется выполнить во всех сервисах.

Сначала создадим таблицу events

-- Создать таблицу для tenant_1
CREATE TABLE events
(
    id UUID,                    -- Уникальный идентификатор события
    type LowCardinality(String), -- Тип события
    timestamp DateTime,          -- Временная метка события
    user_id UInt32,               -- ID пользователя, инициировавшего событие
    data String,                 -- Данные события
)
ORDER BY (timestamp, user_id);

Добавим тестовые данные.

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"}')

Теперь давайте создадим двух пользователей user_1

-- Создать пользователей 
CREATE USER user_1 IDENTIFIED BY '<password>'

Затем выполните команду GRANT SELECT для соответствующей таблицы.

-- Предоставить права только на чтение для таблицы events.
GRANT SELECT ON events TO user_1

Теперь вы можете подключиться под пользователем user_1 к сервису арендатора 1 и выполнить простой запрос SELECT. Будут возвращены только строки, относящиеся к первому арендатору.

-- Вход выполнен от имени 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"} │
   └──────────────────────────────────────┴─────────────┴─────────────────────┴─────────┴─────────────────────────────────────────┘