Milvus多租户方案实战:用Partition Key实现数据隔离与高效检索

发布时间:2026/10/9 8:50:11
Milvus多租户方案实战:用Partition Key实现数据隔离与高效检索 刚接触向量数据库的时候我一度以为多租户只是个数据库层面顺手支持一下的小功能。真正把带十来个企业客户的RAG服务推进生产之后才发现多租户方案的选型能直接决定你半夜被叫起来几次。Milvus这类向量数据库也一样看起来无非是加个tenant_id字段过滤但要是架构没想清楚数据隔离、查询性能、冷热资源分配全都会变成雷。这篇文章把我在实际项目里折腾多租户的经验完整复盘一遍会先讲清楚多租户的概念和主流架构模式然后重点拆解Milvus上三种可实现的多租户方案最后给出一套基于Partition Key的完整实战代码。代码不是贴上去好看的参数怎么调、哪里是坑我都会写明白。适合正在做SaaS化AI应用、知识库产品或者想把Milvus从单客户改成多租户方案的同学参考。1. 多租户到底在解决什么问题1.1 一个故事讲清楚多租户你可以把多租户理解成合租。一套房子整租给一家人所有空间随便用这是单租户模式——客户和资源都是独占的爽是真的爽贵也是真的贵。合租则是一套房子里住好几户客厅、厨房、卫生间共享但每户有自己独立的房间和储物空间谁也不能随便翻别人的东西。多租户就是软件界的合租一套系统同时服务多个客户每个客户在共享的基础设施上拥有独立的房间——也就是自己的数据、配置和访问权限。我经历过的真实场景是这样的产品刚上线时第一个客户很顺利我们给他单独部署了一套Milvus服务数据完全不互相干扰。第二个、第三个客户陆续进来问题开始不受控——每来一个客户就起一套独立Milvus同一台机器上跑多个实例内存直接爆炸。备份脚本、监控面板、告警规则每个实例都得配一份哪个实例挂了一次要挨着排查。后来忍无可忍才决定认真设计多租户架构。这不是某个团队独有的问题。你一旦做SaaS就必然面对用尽量少的资源服务尽量多的客户和客户之间不能互相影响这对矛盾。多租户正是解决这对矛盾的核心手段。1.2 为什么不用每个客户一套系统有人会觉得既然独立部署最省心那就一直给每个客户部署一套呗反正现在容器化很方便。这个思路在客户数量少、体量大的B端场景不是不能用但你得想清楚它的代价。先看这张对比表对比维度每客户一套系统多租户共享系统资源成本线性增长每增一个客户都要买机器新增客户的边际成本很低几十上百也能扛运维复杂度高每套系统都要单独升级、备份、监控低一套系统统一维护版本一致数据隔离性天然物理隔离最强靠架构设计实现逻辑隔离需要约束自己定制化能力强可以给单个客户改任何东西弱改动会影响所有租户需要抽象出配置项扩展性横向加机器粗暴但直接需要根据租户量级做容量规划有一定复杂度当客户从2个变成20个独立部署的运维成本是指数上升的。而多租户系统的本质就是把每来一个客户就做一遍的事提前变成一次性能力。隔离、配额、权限、监控全部做成平台能力后续新增租户只需要开通账号、分配额度几分钟搞定。这也是主流SaaS产品几乎都走多租户路线的根本原因。1.3 多租户的三大核心诉求多租户不是把多个客户的数据放在一个库里这么简单。真正落地时要满足三个核心诉求第一数据隔离。租户A无论如何都不能读到租户B的数据。这既是业务底线也是合规底线。注意数据隔离不等于物理隔离逻辑隔离也能做到读不到但前提是过滤条件在所有数据访问路径上都被强制执行一个漏洞都不能有。第二资源隔离。一个租户跑大量查询或者灌入海量数据时不能让其他租户的延迟跟着劣化。完全做到资源强隔离很难通常会用配额、优先级、分区等手段做软隔离至少保证互相干扰有限。第三个性化配置。每个租户可能有不同的模型、不同的阈值、不同的权限结构。多租户架构要把这些差异抽象成可配置项而不是代码里写死。否则每接入一个新客户就改一次代码多租户就失去了意义。2. 多租户架构设计的三种经典模式2.1 独立数据库模式也叫Database-per-Tenant是最简单粗暴的模式每个租户一个完全独立的数据库实例或者至少一个独立的数据库。这种模式的优点非常明显——隔离性最强一个租户的异常查询、异常数据、甚至误删数据都不会影响到别人。针对单个租户做备份、恢复、数据导出也很方便甚至可以给大客户单独调优数据库参数。缺点也够呛数据库实例多了之后连接数、备份任务、版本升级全是运维负担资源利用率还很低小租户空占一个库却用不了多少资源。在实际选型里这种模式往往用在租户之间合规要求非常严格、不能共享任何存储的场景比如金融、医疗领域的核心数据。如果你的项目属于这类就别纠结省资源了安全和合规优先。2.2 共享数据库、独立Schema模式这种模式是同一个数据库实例每个租户一套独立的表结构。从数据库层面看大家物理上坐在同一个实例里但逻辑上是分开的Schema类似一栋楼里每户有一套独立的单元大门共用。它的好处是比独立数据库省资源数据库实例数量可控同时还能按Schema给租户做差异化的索引、权限配置。缺点是迁移麻烦如果一个租户的数据量太大需要单独挪走Schema级别迁移比整库迁移还要精细另外同一实例里某个租户写出了一条慢SQL仍然可能拖垮整个实例上的其他租户。这种模式比较适合中型SaaS租户数量大概几十到几百数据量中等而且每个租户的表结构确实存在差异。但如果所有租户的表结构完全一样继续用独立Schema其实是在给自己找麻烦。2.3 共享Schema、共享表的单库模式这是最纯粹的多租户模式所有租户的数据放在同一张表里用一个tenant_id字段区分归属。每次查询都必须带上tenant_id过滤条件或者让数据库的行级安全策略自动追加这个条件。这种模式的资源利用率最高维护也最简单——永远只有一套表结构、一套索引、一套升级脚本。它的核心代价是隔离性全看过滤条件是否无死角执行。一旦代码里某个查询漏掉了tenant_id就是跨租户数据泄露事故。同时单表数据量会膨胀得很快必须提前做好分区、索引设计否则查询性能会全面恶化。绝大多数SaaS产品最终都选择了这种模式因为租户数量一旦上千只有共享表才能撑得住。它对应的工程要求也最高强制校验租户过滤条件、统一的数据访问层、定期做容量治理缺一不可。2.4 三种模式怎么选选型维度独立数据库共享库独立Schema共享库共享表资源利用率低中高数据隔离强度物理级逻辑级逻辑级依赖强制过滤运维复杂度高中低单租户定制能力最强较强弱典型租户规模个位数到几十几十到几百几百到上万适合合规场景强合规中等合规常规SaaS这里没有绝对的最优只有匹配度。我个人的判断标准是先看合规约束再看租户规模最后看团队运维能力。合规要求严到必须物理隔离别节省租户规模不大但要求灵活选独立Schema要做大规模SaaS踏踏实实走共享表模式把隔离逻辑做扎实。3. 当多租户遇上向量数据库Milvus为什么不能照搬传统套路3.1 向量检索与关系型查询的本质差异如果你从传统MySQL的多租户经验直接平移过来特别容易踩坑。因为向量检索和关系型查询的底层逻辑完全不同。MySQL里WHERE tenant_id 10是精确匹配走普通BTree索引就能快速过滤出该租户的行查询范围天然可控。但Milvus里跑的是ANN搜索近似最近邻检索它的目标是在高维向量空间里找最近的K个向量。如果只用普通字段过滤检索流程是先做向量计算、再过滤结果等于把其他租户的向量也拉进来算了一遍既浪费算力又可能让结果集被干扰。更麻烦的是向量索引的结构——无论是IVF还是HNSW——都是针对整个集合的向量空间建立的。如果不同租户的数据混在一起索引会把所有租户的向量搅在一个图或一堆聚类里查询时很难做到精准的只在这个租户的向量子集里搜索。所以向量数据库做多租户需要的不是单纯的字段过滤而是过滤条件下推到索引和分段Segment层面的硬能力。3.2 Milvus多租户的三种实现方案Milvus里做多租户主流有三种姿势我逐个说清楚取舍。第一种是Collection-per-Tenant每个租户单独建一个Collection。隔离效果最好索引、加载、查询互不干扰还能按租户做精细的权限管理。缺点也很明显Collection数量一多元数据管理、资源占用都很可观超过几十上百个Collection维护起来就开始头疼。第二种是用Partition加Partition Key。单个Collection内部按tenant_id字段做分区写入时Milvus通过哈希把同一个租户的数据路由到固定分区查询时如果带上了租户过滤条件底层的查询执行器可以只扫描对应分区。这是在管理成本和隔离性能之间最均衡的方案也是我在生产环境里最终选定的方案。第三种是单Collection加普通字段过滤不建分区。实现最简单但每次查询都是全Collection扫描租户多了之后性能和隔离效果都堪忧。如果你只是做个Demo可以这么干生产环境强烈不建议。3.3 方案选型的决策因素我的建议是先在纸面上回答三个问题再选方案。问题一租户数量级是多少。预计只有二三十个大客户Collection-per-Tenant没毛病要做几百上千甚至上万的租户Collection-per-Tenant就是灾难必须回到集合内分区的路子。问题二某个租户的数据量是否可能远超其他租户。如果存在明显的超大租户Collection-per-Tenant更有弹性因为你可以单独为它扩容、调整索引参数用Partition Key的话所有租户共享同一个集合的索引和资源配额大租户容易变成那个把整张桌子占满的人。问题三冷热租户的干扰能否接受。共享集合意味着加载到内存的Segment是整个集合的一旦某个大租户的数据量撑爆内存其他租户的查询延迟会跟着恶化。可以说Collection-per-Tenant更侧重隔离性Partition Key更侧重规模与经济性。后面我给的实战代码就是Partition Key方案这也是目前最通用的一条路。4. Milvus实战用Partition Key落地多租户4.1 环境准备与连接需要准备的东西不多一个Milvus服务以及Python的pymilvus库。我这里用的是Milvus 2.4.x版本和pymilvus 2.4.xPartition Key功能在2.3版本开始正式可用版本太旧建议先升级。Milvus有两种部署形态Standalone单机模式和分布式集群模式。小规模验证用Standalone模式就够了它把元数据和数据都放在单机上足以撑起几千万级别的向量规模需要横向扩展到亿级以上再上分布式集群。连接的代码很简单from pymilvus import connections connections.connect( aliasdefault, hostlocalhost, port19530, )连接成功后顺手确认一下服务版本from pymilvus import utility print(utility.get_server_version())4.2 设计带租户字段的Schema接下来要定义一个关键字段租户ID。在Milvus里要把这个字段变成Parititon Key只需要在定义字段时加上is_partition_keyTrue。from pymilvus import FieldSchema, CollectionSchema, DataType tenant_id_field FieldSchema( nametenant_id, dtypeDataType.INT64, description租户ID, is_partition_keyTrue, ) doc_id_field FieldSchema( namedoc_id, dtypeDataType.INT64, description文档ID, ) # 假设向量维度为128实际按你的embedding模型输出维度来 embedding_field FieldSchema( nameembedding, dtypeDataType.FLOAT_VECTOR, dim128, description文档向量, ) schema CollectionSchema( fields[tenant_id_field, doc_id_field, embedding_field], description多租户文档集合, enable_dynamic_fieldFalse, )这里的is_partition_keyTrue是整车核心一行。它告诉Milvus写入数据的时候不要只把这个字段当成普通属性而是要在存储层按它的值做分区路由。你可以把tenant_id想象成小区地址同一个tenant_id的向量被送到同一栋楼查询时只搜索那栋楼效率自然显著优于全城搜索。另外我建议把enable_dynamic_field设为False。开启动态字段固然方便但生产环境里它会引入不可控的元数据膨胀也会让Schema演进变得混乱。提前定义好字段让写入和查询都是确定的这是多租户系统稳定性的基础。4.3 创建带Partition Key的集合有了Schema创建集合时指定分区数量这一步特别关键from pymilvus import Collection collection Collection( namedoc_tenant, schemaschema, num_partitions16, ) print(集合创建完成:, collection.name)注意num_partitions这个参数。它决定了集合内部会被切成多少个物理分区Partition Key就是通过哈希把不同tenant_id映射到这些分区上的。取值范围是1到64默认是16。我见过不少人把这个值随便设设得不好后面查询性能会很尴尬这一节最后我会单独展开讲怎么选。创建完成后建索引。索引类型我用HNSW召回和延迟平衡得比较好index_params { index_type: HNSW, metric_type: IP, params: {M: 16, efConstruction: 200}, } collection.create_index( field_nameembedding, index_paramsindex_params, ) collection.load()这里说明一点load()是把整个集合加载到内存。Partition Key模式下加载的粒度是Collection级别所以这个动作会把这个集合所有分区的索引都加载进来。如果租户总量很大而内存有限你就要考虑按冷热拆分集合而不是只靠Partition Key解决所有问题。4.4 写入与检索的完整代码写入数据时每条记录务必带上tenant_id字段。Milvus会根据它的哈希值自动决定数据进入哪个分区。import random # 模拟数据给两个租户各写入几十条 for tenant in (10, 20): data [] for i in range(50): data.append({ tenant_id: tenant, doc_id: i, embedding: [random.random() for _ in range(128)], }) collection.insert(data) collection.flush()flush()很关键。不执行flush数据可能还停留在内存缓冲区里后续查询或删除可能读不到最新数据。批量写入之后记得刷一下。查询的时候真正的重点来了——必须带上租户过滤表达式search_params { metric_type: IP, params: {ef: 64}, } query_vector [random.random() for _ in range(128)] results collection.search( data[query_vector], anns_fieldembedding, paramsearch_params, limit10, exprtenant_id 10, ) for hits in results: for hit in hits: print( doc_id:, hit.entity.get(doc_id), tenant_id:, hit.entity.get(tenant_id), 相似度:, hit.score, )你可以把exprtenant_id 10这个条件理解成给查询加了只准进10号楼的通行证。底层执行时Milvus会通过Partition Key的路由信息只搜索tenant_id 10对应分区内的向量索引而不是全集合暴力扫描。所以查询效率不会随着租户数量增长而线性劣化这也是Partition Key相对普通字段过滤最大的优势。这里必须反复强调一个点Partition Key是软隔离不是自动隔离。就算字段设计得再漂亮查询代码里漏写租户过滤表达式Milvus还是会扫全集合照样会串数据。实际工程中我强烈建议封一层自己的数据访问层所有search和query都强制注入当前登录租户的条件禁止业务代码直接裸调Milvus接口。4.5 参数选择num_partitions到底设多少很多人看到num_partitions的第一反应是越多越好分区多了查询可以并行嘛。这个理解是片面的。分区数确实是租户级隔离的格子数理论上多设一些可以降低每个格子里的数据量。但Milvus内部每个分区会对应若干SegmentSegment数量一多查询时的调度开销、内存里的索引加载开销都会变大。设成64的话通常会带来明显的资源浪费尤其是租户数量不多的时候大量分区空转白白占用管理成本。我的经验公式是这样的先估算最大租户数让分区数大于预计租户数即可但不要超过16到32这个量级。比如预期一百个租户设16个分区哈希能较好地分散数据预期三五百个租户可以考虑32再往上或者单租户数据量巨大就要考虑从Partition Key切换到Collection-per-Tenant这类更细粒度的方案了。另外num_partitions在Collection创建之后就固定了不能随意修改。所以一定要在创建前认真做容量规划而不是先随便设一个等数据量大了再后悔想改——到时候只能重建集合并迁移数据代价相当大。5. 常见坑与排查速查5.1 查询不强制租户过滤等于裸奔这是多租户落地里最危险的一个坑没有之一。我在代码上线的初期就犯过这个错底层封装了一个公共查询函数当时图省事允许调用方不传租户ID。结果有一个报表服务调用的时候漏传了直接全集合扫描把一个租户的查询结果返回尴尬地送到了另一个租户的页面上。幸好当时还在内测没有流到真实客户那里否则就是妥妥的数据泄露事故。后来的整改办法有三条缺一不可数据访问层强制要求租户ID参数缺失直接抛异常所有search、query、delete操作都经过同一层封装不直接暴露裸Milvus接口建立巡检任务定期抽样检查自定义表达式里是否包含租户条件。5.2 分区数量设得越大越好小心资源反噬前文已经说过num_partitions要控制在合理范围。这里再补充一个真实表现我一开始为了图隔离设了64个分区但实际只有两个真实租户结果每个分区都只有零星数据。Segment数量爆炸查询性能不但没变快反而因为要管理的空片段太多延迟涨了接近一倍。对于大多数中小规模SaaS来说16是比较稳妥的起步值。哪怕租户数量暂时很少固定用16也不会浪费太多资源后续租户涨起来还能覆盖。5.3 删除租户数据别以为drop掉就完事了当你要清退一个租户直觉操作是删除该租户的数据。但用Partition Key方案时你没法像Collection-per-Tenant那样直接drop掉整个Collection。你只能执行delete表达式collection.delete(exprtenant_id 20)这里有个底层知识要搞清楚Milvus的delete是逻辑删除会把对应记录打上删除标记真正的物理清理要等后台Compaction去合并Segment。所以在数据量很大的情况下删除后不会立刻看到磁盘空间释放查询如果遇到尚未compaction的陈旧segment也可能短暂出现还能查到已删数据的情况。我踩过这个坑之后养成了两个习惯一是删除前先确认expr命中了正确的租户二是删除后配合调用collection.compact()触发合并或者设置合理的Compaction调度窗口。5.4 冷热租户互相影响跨Collection拆分才是终极解Partition Key方案里所有租户共享同一个Collection的索引和内存。这意味着两个问题热门租户的大量查询会把内存和CPU打满影响冷门租户的延迟同时冷门租户的数据也会占着索引加载的内存让热门租户可用内存变小。如果你的租户之间冷热差异很明显我建议做混合方案默认租户留在共享的Partition Key集合里少数大客户或高热度客户拆出来单独创建Collection独占索引和资源。这样既保留了共享集合的规模经济性又给重点客户提供了稳定性的保障。5.5 排查速查表现象可能原因排查动作查询结果串了租户表达式里漏写租户条件检查自定义表达式确认包含tenant_id xx租户越多查询越慢num_partitions设置不合理或索引参数过小检查Collection的num_partitions和索引参数删除租户后空间久不释放Milvus逻辑删除未触发Compaction调低Compaction触发阈值或手动compact()某个租户查询把全集合内存打满大租户数据量过大与冷租户混在一起将大租户单独拆Collection写入Performance突然变差未定期flushSegment积累过多批量写入后及时flush合理控制批次最后再分享几个实操层面的收尾建议如果你现在正处于选型阶段我的建议是先别急着上Collection-per-Tenant虽然它最直观。租户数量只要不是少得可怜就用Partition Key方案起步把所有租户放一个集合里靠租户字段做分区。这样做的好处是代码路径单一、备份和监控都简单出现问题时排查范围也不大。将来如果某一个大客户确实需要独立的资源环境再把它迁移到独立的Collection里此时你当初预留的tenant_id字段会让你少改无数代码。还有一个很多人忽略的细节多租户不是上线后补一补就能做好的它应该从第一张Schema就开始设计。等到数据灌进去了再回头改造无论是数据迁移还是代码改造成本都会让团队非常难受。我在生产环境里用这套思路已经稳定支撑了几十个租户平时几乎不需要为租户隔离操心。希望这篇东西能让你在规划自己的Milvus多租户方案时少走一些我已经走过的弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询