# 在 MyScale 中实现多租户

随着 LLM 越来越被广泛认可和采用,越来越多的开发者开始探索使用 CVP(ChatGPT、向量数据库、提示)堆栈来构建面向不同用户或用户群体的应用程序。在向量数据库中实现多租户支持是满足这一需求的关键。本指南将帮助您了解并在 MyScale 中实现多租户。

# 理解多租户

向量数据库(应用程序)中的多租户是指数据库能够为多个租户或用户(或用户组)提供服务,同时保持它们的数据在逻辑上相互隔离。每个租户的数据在多租户架构中被单独存储和管理,尽管它们共享相同的物理基础设施和资源。

在向量数据库应用程序中实现多租户应满足四个主要要求:

  • 性能中立性:强调在所有多租户和单用户工作负载中保持一致和可比较的性能,包括所有的 CRUD(创建、读取、更新、删除)操作。
  • 数据隔离:每个租户的数据应该被单独存储。
  • 可扩展性:应用程序应支持大量的租户。
  • 高效的入驻和退出:添加或删除一个租户不应影响其他租户。

# 实现多租户的策略

在 MyScale 中实现多租户需要仔细的规划和考虑,以确保数据隔离、安全性和性能。

以下是一些有效实现多租户的策略和最佳实践:

# 基于表的多租户

在 MyScale 中,每个表都是单独存储的,因此可以在单个集群中创建多个表。对于每个租户,可以创建一个专用的表。例如,下面的 SQL 语句创建了一个用于聊天机器人租户的表:

CREATE TABLE db.message_chatbot1
(
    user_id      FixedString(16),
    message_id   FixedString(16),
    timestamp    DateTime,
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY message_id

这种策略下的向量搜索与单租户的类似。您可以通过删除表来删除一个租户,而不会影响其他租户。然而,这种策略可能会导致资源浪费,对于大规模的多租户来说并不适用。因此,在 MyScale 集群中使用 pod 大小为 x1 时,不建议有超过 100 个表。

这种策略还允许使用基于角色的访问控制(RBAC),通过为每个表分配不同的用户和角色来实现细粒度的访问控制。

# 基于元数据过滤的多租户

基于元数据过滤的多租户是一种在数据库系统中实现多租户的方法,其中多个租户的数据存储在同一张表中。根据与每个数据记录相关联的元数据或属性来控制和隔离对数据的访问,从而实现高效的存储,同时提供强大的数据隔离和安全性。

在实践中,基于元数据过滤的多租户可以通过基于分区键、基于主键或两者结合的方式来实现。

# 基于分区键的多租户

通过为每个租户分配一个唯一的分区来实现数据隔离。下面的 SQL 语句描述了通过用户 ID 进行分区的表的创建,其中每个用户对应一个租户(分区):

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    message_id   FixedString(16),
    timestamp    DateTime,
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY message_id
PARTITION BY user_id

在执行向量搜索时,使用租户字段作为过滤器来定位相应的分区,如下面的 SQL 语句所示:

SELECT message_id, distance(data, [...]) AS dist
FROM db.message_app 
WHERE user_id = 'xxxxxxxxxxxxxxxx'
ORDER BY dist LIMIT 10

此外,您可以使用以下命令轻松删除一个分区(租户):

ALTER TABLE db.message_app DROP PARTITION 'xxxxxxxxxxxxxxxx'

提示

为了获得最佳的 INSERT 和查询性能,建议在 MyScale 集群中使用 pod 大小为 x1 时,将表中的分区总数保持在 100 以下。

# 基于主键的多租户

在这种策略中,租户字段是主键,加快了租户数据的检索速度。

这种策略不限制租户的数量。来自不同租户的数据存储在一个分区中。例如,下面的 SQL 语句描述了如何创建一个将每个用户视为租户的表:

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    timestamp    DateTime,
    message_id   FixedString(16),
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY (user_id, message_id)

这种策略下的向量搜索与基于分区键的策略类似。

然而,如下面的 SQL 语句所示,删除一个租户的速度较慢,需要使用 DELETE FROM 语句:

DELETE FROM TABLE db.message_app WHERE user_id = 'xxxxxxxxxxxxxxxx' 

# 分区 + 主键的多租户

通过将分区和主键的多租户策略结合起来,可以平衡资源隔离和租户可扩展性。这种方法将来自多个租户的数据存储在同一个分区中,支持大规模的多租户,同时保持适度的隔离。

例如,下面的 SQL 语句演示了如何创建一个将租户数据分布在十个分区中的表:

CREATE TABLE db.message_app
(
    user_id      FixedString(16),
    timestamp    DateTime,
    message_id   FixedString(16),
    message_embedding  Array(Float32),
    CONSTRAINT check_length CHECK length(message_embedding) = 768
) ENGINE = MergeTree
ORDER BY (user_id, message_id)
PARTITION BY sipHash64(user_id) % 10

执行向量搜索和删除租户与基于主键的策略相同。然而,插入性能可能较慢,特别是在处理单个插入块中的多个分区时。因此,将具有相同分区的数据一次性插入一个块是这种策略的最佳实践。

# 总结

以下是我们推荐和使用的多租户策略的总结:

多租户模型 性能开销 隔离级别 租户规模 入驻/退出易用性
基于表的
基于分区键的
基于主键的
分区 + 基于主键的