在构建大规模多租户的RAG(检索增强生成)系统时,AI基础设施工程师经常面临一个核心挑战:如何在单个向量数据库集群内安全且高效地隔离数千个租户(Tenant)的数据和查询请求?主要有两种方案:为每个租户创建一个独立的 Collection(集合),或将所有租户的数据放入一个 Collection 中,并使用 Partition(分区)进行逻辑隔离。
本文将深入分析这两种方案的优缺点,并提供实操代码,展示如何在 Milvus 或 Zilliz 这样的向量数据库中,通过 Partition 机制实现数千租户的最优资源管理。
1. 方案对比:Collection vs. Partition
| 特性 | 独立 Collection | Partition 机制 (推荐) |
|---|---|---|
| 隔离级别 | 物理隔离(Schema、索引独立) | 逻辑隔离(数据文件分离,共享 Schema 和 Index) |
| 扩展性 | 极差。数千 Collection 导致元数据开销巨大,影响系统启动和维护效率。 | 极佳。单个 Collection 可支持数万个 Partition。 |
| 资源成本 | 高。每个 Collection 都要占用额外的内存和计算资源。 | 低。共享一套查询节点和索引节点资源。 |
| 查询效率 | 极高(无需过滤)。 | 较高(通过 Partition 标签快速定位数据,过滤效率极高)。 |
对于数千级别的租户场景,采用独立 Collection 的方法会迅速达到集群的元数据和连接限制,导致性能急剧下降。因此,Partition 机制是实现低成本、高效率多租户隔离的最佳实践。
2. 实现 Partition 租户隔离的步骤
我们将使用 PyMilvus 作为示例,演示如何将租户ID(tenant_id)映射到 Collection 的 Partition。
步骤一:定义 Schema 和 Partition Key
我们首先需要定义一个包含 tenant_id 字段的 Schema。虽然 tenant_id 可以作为普通字段,但为了实现基于 Partition 的高效隔离查询,我们需要在插入和查询时利用 Partition 名称。
from pymilvus import Collection, FieldSchema, CollectionSchema, DataType, connections
# 建立连接
connections.connect(host="localhost", port="19530")
# 定义Schema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="tenant_id", dtype=DataType.INT64, description="租户标识符"),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields, description="多租户RAG Collection")
# 创建集合
COLLECTION_NAME = "multi_tenant_vectors"
collection = Collection(name=COLLECTION_NAME, schema=schema)
print(f"Collection '{COLLECTION_NAME}' created successfully.")
步骤二:动态创建 Partition 并插入数据
当新租户接入时,我们不需要创建新的 Collection,只需要为其创建一个新的 Partition。Partition 的名称通常是基于租户ID构建的字符串,例如 p_{tenant_id}。
# 假设有两个租户:Tenant 1001 和 Tenant 1002
tenant_ids = [1001, 1002]
for tid in tenant_ids:
partition_name = f"p_{tid}"
# 1. 创建 Partition
if partition_name not in collection.get_partition_names():
collection.create_partition(partition_name)
print(f"Partition {partition_name} created for Tenant {tid}.")
# 2. 准备数据
data = [
[tid] * 5, # 5条数据属于该租户
[[float(i)] * 128 for i in range(5)]
]
# 3. 插入数据,指定 Partition Name
res = collection.insert(data, partition_name=partition_name)
print(f"Inserted {len(res.primary_keys)} entities into {partition_name}.")
# 确认数据是否加载
collection.flush()
步骤三:实现租户级别的隔离查询
这是实现高效隔离的关键。在查询(搜索或检索)时,通过指定 partition_names 参数,查询请求将只会在该租户对应的数据分区内执行。这极大地提高了查询效率,并确保了数据的严格隔离。
隔离查询示例 (只搜索 Tenant 1001 的数据):
# 准备查询向量
search_vector = [[0.5] * 128]
# 针对 Tenant 1001 进行隔离搜索
tenant_1_partition = [f"p_1001"]
search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
results = collection.search(
data=search_vector,
anns_field="vector",
param=search_params,
limit=3,
partition_names=tenant_1_partition, # 关键:只在指定分区搜索
output_fields=["tenant_id"]
)
print("\n--- Query Results for Tenant 1001 ---")
for hit in results[0]:
print(f"ID: {hit.id}, Distance: {hit.distance}, Tenant ID: {hit.entity.get('tenant_id')}")
# 针对 Tenant 1002 进行隔离搜索
tenant_2_partition = [f"p_1002"]
results_2 = collection.search(
data=search_vector,
anns_field="vector",
param=search_params,
limit=3,
partition_names=tenant_2_partition
)
print("\n--- Query Results for Tenant 1002 ---")
print(f"Found {len(results_2[0])} results in partition p_1002.")
3. 性能和资源考量
虽然 Partition 实现了逻辑隔离,但需要注意以下几点:
- 共享 Index: 所有的 Partition 共享同一个索引结构(如果已创建索引)。这意味着索引构建只进行一次,大幅节约资源。
- 查询节点压力: 所有租户的查询请求都通过同一组 Query Node 处理。如果某些租户的负载特别高,可能会影响到其他租户(即共享资源带来的抖动)。对于查询量极高的关键租户,可能需要额外的资源配额或者混合部署(少数高优租户使用独立 Collection,多数租户使用 Partition)。
- 数据安全: Partition 提供了强大的数据隔离能力,因为查询 API 严格限定了查询范围。除非通过错误的配置或绕过 Milvus API 直接访问底层存储,否则数据是安全的。
通过将 tenant_id 转化为 Partition Key,我们可以在单个高性能集群上高效地管理和隔离数千个租户,极大地优化了AI基础设施的部署效率和维护成本。
汤不热吧