做向量检索做到第四篇,前面我们把数据接入、索引构建、查询调优都聊得差不多了,这一篇专门讲一个最容易被低估、又最容易在关键时刻卡住整个项目的问题:容量。标题里的“4.”不是随便写的,这是整个系列里我自己踩坑最多的一块,也是群里被问得最频繁的话题——线上索引数据量翻了几倍之后,内存开始吃紧,查询延迟从几十毫秒飙到几百毫秒,到底是该加机器,还是该换索引,还是该直接迁移到另一个数据库?大多数团队在这个路口都会犹豫一阵子,犹豫完之后往往选了一个不是最优的答案。
这篇文章就围绕向量容量、扩容和选型三条主线展开。容量讲的是怎么在项目初期就算清楚“一条向量到底占了多大地方”,扩容讲的是数据量上来之后有哪些路线可以走、每种路线的代价是什么,选型讲的是在索引类型、数据库产品和部署形态之间怎么做权衡。适合正在做RAG应用、推荐系统、图像搜索、语义检索的开发者,也适合后端架构师和DBA——只要你手上有一批向量数据,并且未来一年大概率会翻几倍,这篇文章就值得读完。
1. 向量容量到底在算些什么:从一条记录到整个集群
1.1 一条向量吃掉多少空间:维度、精度、元数据
很多人一上来就喜欢问“我1000万条768维的向量,要多少内存”,这类问题其实没办法直接回答,因为“一条向量”在系统里的真实占用,远不止向量本身那一堆浮点数。先拆解最小单位,一条完整的向量记录通常包含三部分:向量本身、唯一ID、标量元数据。
向量本身的存储取决于维度和精度。以最常见的float32为例,每个维度占4字节。那么一条768维的向量就是768乘以4,等于3072字节,约3KB。如果是1536维(OpenAI的text-embedding-3-small就是这个量级),一条向量就是6KB。如果换成float16,每个维度只占2字节,同样的768维一条就只有1.5KB。如果做int8量化,每个维度1字节,768维就是768字节。这个差异在千万级数据量下会被放大得非常夸张。
ID字段也不能忽略,通常用int64或者UUID。int64每条约8字节,1000万条就是80MB,看着不多,但有些系统会为ID额外建一套映射关系,实际开销会翻倍。元数据更是个无底洞,同样是嵌入式文档,有的项目只存一个user_id和timestamp,有的项目挂了十几个标签字段、几十KB的原始文本切片。后者的元数据体积可能是向量本身的五到十倍。
真正让容量估算翻车的是索引结构。以HNSW为例,这个算法本质上是在内存里构建一张多层图,每条向量除了自己的坐标之外,还要维护若干层级的邻居指针。M参数(每个节点的最大连接数)一旦设置为16,每个节点理论上要存双向共32个指针,每个指针如果用int64存储,那就是每条向量额外256字节。加上多层图结构中上层的额外节点,实际每条向量的索引开销基本和向量本身体积相当,甚至会更高。所以我在实际项目里估算HNSW索引内存时,从来不用“原始向量大小”来算,而是直接用“原始向量大小的1.5到3倍”来兜底。
1.2 容量估算的一个可复现模板
拿一个具体例子算一遍,假设你有1000万条768维向量,float32精度,有一个int64的ID,还有一个平均500字节的元数据JSON,索引选的是HNSW且M=16。
| 存储项 | 估算方式 | 1000万条估算值 |
|---|---|---|
| 原始向量 | 1000万 × 768 × 4字节 | 约28.6 GB |
| ID字段 | 1000万 × 8字节 | 约80 MB |
| 标量元数据 | 1000万 × 500字节 | 约4.7 GB |
| HNSW图结构 | 1000万 × 256字节起 | 约2.5 GB起 |
| 系统与程序开销 | 堆外内存、缓存、小对象 | 约2-5 GB |
按这个模板,总内存需求大约在38GB到45GB之间。但请注意,HNSW索引在Faiss、Milvus、Qdrant这些实现里,实际内存占用往往比上面这张表还要高,因为索引构建过程中的临时内存、查询时的work memory、以及之前说的多层图结构冗余,都会额外吃内存。经验值就是:直接把“原始向量大小”乘以2到3,再和元数据开销相加,基本等于你需要的物理内存下限。
为什么容量估算这么重要?因为向量数据库和传统数据库对资源瓶颈的反馈方式完全不一样。传统关系库内存吃紧时顶多慢一点,很多操作还能落盘继续跑;但HNSW这类内存索引一旦超过可用内存,要么进程直接OOM,要么系统拼命swap,查询延迟直接恶化到不可用。我见过最典型的惨案是:一个人声情并茂地演示语义搜索Demo,本地测一切正常,结果数据量从50万涨到800万之后,服务直接卡死。根因就是当初部署时只按“原始向量体积”买了一台16GB内存的机器,完全没算索引和图结构的开销。
所以,容量规划不是上线那天做的事,而是数据接入之前就要量化成数字的环节。先把估算模板记下来,后面扩容和选型全都依赖这个底数。
2. 扩容的四条路线:加机器、减占用、换结构、分数据
2.1 垂直扩容:最简单,但天花板和成本都摆在眼前
垂直扩容就是升级单机配置,加内存、换更强的CPU、增加SSD容量。这是最省事的方式,因为不需要改任何代码,不需要动数据分布,只需要在云控制台里点几下“升级实例”,或者运维同学去机房插几条内存条。
垂直扩容的适用场景是:数据量还在一个可控范围内,且未来增长率不高;或者当前瓶颈确实只是内存不足,而CPU和磁盘还有余量。比如一套500万条向量、HNSW索引、32GB内存的Milvus单机实例,查询P99还能稳定在30毫秒内,只是内存使用率到了92%。这种情况垂直扩容到64GB,代价最小,收益最直接。
但垂直扩容有两个明显问题。第一个问题是单机上限,再强的物理机也有内存上限,几百GB已经是云服务器里的顶配,但向量检索领域亿级数据配上高维向量,几百GB也未必够用。第二个问题是成本曲线,单机规格越高,单价非线性上涨,64GB内存的机器绝非32GB的两倍价格,经常是1.8倍性能卖你2.5倍的钱。
如果你能判断出数据量在未来三个月内还会继续翻倍,那我就建议不要优先垂直扩容。垂直扩容只适合“临时应急”,它解决的是眼前的焦虑,不是结构性的容量问题。
2.2 水平扩容:分片与一致性哈希的落地细节
水平扩容就是加节点,把数据分散到多台机器上共同承担。这是向量检索系统面对大规模数据时的最终归宿,但也是最容易踩坑的路线。
水平扩容的核心是分片策略。一种常见做法是基于哈希分片,对向量的主键ID做一致性哈希,路由到不同的shard。这种方式的好处是数据分布均匀,写入时可以并行分发;坏处是一致性哈希需要维护虚拟节点和路由表,新增节点时需要处理数据重分布,如果分片数量定死了,后期加节点就得依赖集群自带的重平衡机制。
另一种做法是基于分区(partition)的思维,按业务维度把数据切分到不同节点。比如一个多租户系统,每个租户的数据独立放在一个分区,查询时通过租户ID直接路由到对应分区。这种方案查询隔离性最好,一个租户的查询慢不会拖垮其他租户,但前提是单个租户的数据量不能超过一台机器的承载能力。
具体到产品,Milvus的架构里shard是按照主键哈希落到不同的数据节点,Qdrant的shard机制类似,每个collection拆成多个分片分布在不同节点上;pgvector的话要靠PostgreSQL原生的分区表,或者干脆用Citus这类分布式扩展。无论用哪个,扩容过程中最核心的問題是数据重分布。重分布期间新旧副本同时存在,磁盘和网络都会被打满,所以一旦决定水平扩容,最好在业务低峰期操作,并且提前验证rollback方案。
调整这几个参数是水平扩容前后的必修课:分片数、副本数、路由一致性、查询超时时间。从单机迁移到集群后,原来一个连接直连搞定的事,现在变成了查询协调器、分片路由、结果合并多个环节,延迟一定会有所增加,需要用实际数据重新压测一遍。
2.3 软扩容:换索引、做量化、降精度
说到扩容,很多人第一反应就是“加机器”,但在加机器之前,其实有一个成本更低的路线,那就是把现有数据的占用空间压下来。这就是“软扩容”。它不是真的增加容量,而是让同样一批数据在系统里占更少的空间,从而在现有硬件上塞下更多数据。
最常见的软扩容手段是向量量化。float32降成float16,内存占用直接减半,大部分场景下召回率几乎不受影响,因为向量本身的各种模型输出精度远没有到float32的敏感度。如果想更激进,可以做int8量化,每条向量从4字节降到1字节,内存变为原来的四分之一。但int8量化对精度的影响要测过才能用,不同的模型、不同的数据分布差异很大,有的项目降完召回率掉了2个点没有任何业务影响,有的项目直接掉了15个点导致业务不可用了。
还有一种手段是换索引类型,从HNSW换到IVF系列。IVF的基本思路是先用聚类把向量空间划分成nlist个区域,查询时只搜其中nprobe个区域,索引本身不需要像HNSW那样维护庞大的图结构。IVF的内存占用比HNSW低不少,代价是召回率有轻微损失,而且调参复杂度上来了,nlist和nprobe的取值直接关系召回和延迟,需要反复实验。
PQ(乘积量化)又是另一个维度。它把高维向量切分成若干子向量,每个子向量用码本中心索引代替,存储时每条向量只需要几十到几百字节。PQ的压缩比极高,适合超大规模甚至“向量索引放不下内存”的场景,但PQ本身不做精确距离计算,而是用查表近似,召回率损失是需要重点评估的。
我的建议是:在决定加机器之前,先在你的数据集上做一个量化实验。写一小段脚本,用同样的测试集分别跑float32、float16、int8三种精度的召回率对比。如果召回率下降可以接受,那就直接改精度,一分钱不用花,容量问题先解决一半。这是扩容序列里性价比最高的操作。
2.4 冷热分离与数据归档:内存不够,磁盘来凑
如果说量化和换索引是“降占用”,冷热分离就是“只让一部分数据住在内存里”。这在真实业务里太常见了:一个电商平台的向量检索,新增的商品会被频繁搜索,三个月前的商品几乎没人搜;一个知识库RAG,热门的文档天天被引用,归档的文档一年都不见得被查一次。
冷热分离的思路是把热数据放在内存索引里提供低延迟查询,把冷数据放在磁盘索引或对象存储中,只有偶尔的查询才需要访问。很多向量数据库已经原生支持这种模式,Milvus的磁盘索引、Qdrant的磁盘模式、以及Elasticsearch的那套本地缓存体系,本质上都是在做类似的事。
需要注意的是,冷热分离不是简单的“把不用的数据删掉”。你要设计数据生命周期策略,定义多热算热;要处理冷数据被查询时的延迟惩罚,比如一个用户搜到一个很旧但相关的商品,系统等了两秒才返回,体验上能否接受;还要处理冷数据重新加热的情况,比如某个文档突然被大量引用,这时需要把它从磁盘索引加载回内存索引,这个过程有可能引起瞬时资源竞争。
冷热分离适合数据量本身就大、但访问有明显时间衰减规律的场景。如果你的业务是“所有数据等概率被查询”,那冷热分离的意义就大打折扣,不如老老实实做水平扩容。
3. 选型不只是看性能:索引、数据库、部署形态的多维权衡
3.1 索引类型选型:HNSW、IVF、PQ、DiskANN,怎么选
索引选型本质上是延迟、召回率、内存三者的三角权衡,没有一项技术能同时做到极致。一个粗暴的分类表可以参考:
| 索引类型 | 内存占用 | 召回率 | 查询延迟 | 适合场景 |
|---|---|---|---|---|
| HNSW | 高 | 高 | 低 | 百万到千万级,内存能覆盖,追求召回和延迟 |
| IVF-Flat | 中 | 中高 | 中 | 千万级以上,可接受调参,懂nprobe的作用 |
| IVF-PQ | 低 | 中 | 中 | 上亿级,内存受限,能接受召回损失 |
| DiskANN/磁盘索引 | 极低 | 中高 | 中高(依赖磁盘) | 数据远超内存,访问稀疏,热数据可缓存 |
HNSW永远是我在项目初期最推荐的索引。它的构建不需要训练,插入即建索引,查询效果和延迟都很稳定,就是吃内存。如果你的数据规模在几百万到几千万,且机器内存宽裕,无脑用HNSW不会出错。数据量再往上,内存成本会变得不切实际,这时候IVF系列或者PQ就值得投入时间去调。
选索引还要考虑你的更新频率。HNSW对增量插入的支持相对自然,IVF在插入量大的时候需要周期性重建聚类中心,否则聚类中心和数据分布脱节,召回率会慢慢劣化。如果你的数据是日更百万级,IVF的训练和重建流程需要设计到自动化任务里,这部分的运维成本很容易被低估。
3.2 向量数据库选型:开源、商业、还是从零构建
在这轮选型里,我没有“银弹”能给你。真正的关键指标是你团队能运维什么样的系统。
如果你们的项目基于PostgreSQL,pgvector是最低门槛的方案。它就是一个PostgreSQL扩展,SQL语法直接支持向量运算,业务团队上手成本极低。但pgvector目前并不是专门为大规模向量检索设计的,索引能力和横向扩展能力相对有限,适合数据量在百万级到千万级初期、不想额外引入重组件的项目。
如果数据量已经明确会到亿级,或者需要非常丰富的过滤条件配合向量检索,我建议调研Milvus或者Qdrant。Milvus的架构偏向分布式重武器,有独立的coordinator、datanode、querynode等组件,适合有专用运维精力的团队。Qdrant则偏轻,Rust写的,单机性能好,集群模式比较简洁,SDK体验舒服。Weaviate、Elasticsearch(带向量插件)也都可以,Elasticsearch的好处是你们如果已经在用ES做全文检索,向量检索直接接入同一套体系,省掉一个组件。
我一直提醒团队的一件事是:不要为了“多一个组件”而多一个组件。向量检索如果只是你们系统里一个次要功能,为了它单独运维一套Milvus集群,成本可能会拖垮整个迭代节奏。有时候用pgvector扛到千万级,反而比引入分布式数据库更划算。
3.3 部署形态:单机、分布式集群、还是Serverless
部署形态和你们的业务生命周期强相关。
原型验证阶段,单机部署就够了。Milvus Lite、Qdrant单机、甚至直接用Faiss接一个Python服务,都行。千万不要在这个阶段为了“将来能扩展”而上一套三节点集群,那只会拖慢迭代。验证业务价值远比验证架构弹性重要。
业务上线后,如果QPS和数据量同时涨,分布式集群就提上日程。这里要规划好:分片数、副本数、节点的CPU型号同一性(混用新老CPU会导致性能差异)、持久化存储的IOPS指标。向量检索不是纯内存操作,数据的持久化和温数据回写都依赖磁盘,SSD的IOPS不够,查询P99会很不稳定。
Serverless形态是最近几年才成熟起来的,最大的优点是免运维、按量计费。适合流量波动大、时不时有突发查询的场景。缺点也明显:如果长期高并发固定负载,包年包月的预留实例成本可能比Serverless低得多;而且Serverless形态下,你对底层资源、索引参数的控制力会弱不少。选Serverless还是自建,核心还是算账,把未来12个月的容量曲线和费用曲线拉出来对比,数字会帮你做决定。
4. 实操复盘:一次真实的容量诊断与扩容迁移记录
4.1 案例背景:从内存告警到选型决策
我这里讲一个相对典型的案例。这个项目做的是企业内部文档RAG助手,向量用768维float32,索引是HNSW(M=16,efConstruction=200),索引构建走Milvus单机部署,16C/64GB内存。上线三个月后数据量从300万涨到2200万,内存使用率长期在95%,查询P99从25ms恶化到180ms,频繁出现“system OOM killed”的告警。
当时摆在桌上的选项有三个:A. 直接升级到128GB内存的机器;B. 把向量精度降到float16,配合HNSW参数调整;C. 迁移到三节点Milvus集群。我们最终做决定之前,先把容量估算模板完整跑了一遍,结论是:2200万条768维float32向量,光原始向量就60多GB,加上HNSW图结构、元数据和系统开销,64GB内存根本覆盖不了,就算升到128GB也只剩50%的余量——而项目预期半年后会到4000万条,届时128GB又会顶满。
于是我们把A方案否了。垂直扩容在这里只能买时间,不能解决问题。
4.2 扩容方案与执行细节:量化、换参数、再加节点
最终我们走的是“先软后硬”的两阶段方案。第一阶段是软扩容,先把向量精度从float32降到float16,这个操作在Milvus里是重建collection时的参数变化,不需要改业务代码。同时我们把元数据里几个大JSON字段做了一次瘦身,把完整文本切片挪到对象存储,只在向量库里保留摘要和引用ID,单条元数据从平均500字节压到120字节。这一轮下来,同样的2200万条数据,总内存占用从62GB降到约38GB,查询P99回到45ms左右。
期间我们做了严格的召回率对比测试。用线上真实查询采样了1000条,float32和float16的结果重合度在98.6%,业务上完全可接受。这个测试必须做,而且最好记录成CI回归用例,防止以后改了精度没人发现召回率退化。
第二阶段是硬扩容,在数据量继续增长的预期下,把单机Milvus迁移到三节点集群。这一步的操作顺序是:先搭好新集群,用Milvus的备份工具把collection数据导出再导入,等增量数据追平后进行流量切换。全量导入约花了5个小时,切换过程有10分钟只读窗口。我们预留了旧单机实例不销毁,回滚预案就是切回旧实例,虽然旧实例已经内存吃紧,但至少能在紧急情况下保证服务不中断。
4.3 迁移后验证与回滚预案
集群切完之后,我们做了两轮验证。第一轮是功能性验证,跑全部核心查询用例,确认结果集数量和Top10内容与迁移前一致。第二轮是性能压测,用jmeter混入80%的读和20%的写,观察新集群在分片路由下的P99延迟和吞吐。得出的典型数据是:三节点集群下,P99稳定在25ms以内,比单机64GB时还低,而且各节点内存使用率只有65%左右。
回滚预案这件事很多人觉得麻烦,甚至觉得“为了面子也得迁完就删旧的”,我非常不建议。回滚不一定要保留整份旧数据,至少要把旧实例的镜像或磁盘快照保留几天,等到新集群稳定运行一周再清理。数据迁移里的隐藏风险往往出现在第二天、第三天的某个未验证查询上,如果没有回滚能力,到时候只能干着急。
5. 常见问题与排查技巧实录
容量规划和扩容这件事,很多坑不去踩一遍是想不到的。整理几个最常见的现场问题,你可以直接对照排查。
5.1 估算时漏了索引开销,上线后OOM
这是最经典的失误,我第一版容量表也犯过这个错。只算了“原始向量+元数据”,没算HNSW图结构,导致实际内存需求是估算值的1.8倍。排查方式也很直接:如果集群OOM,先用reserved内存和heap内存监控确认是索引结构占的还是查询过程临时申请导致的。如果是前者,说明估算模型漏了索引结构;如果是后者,多半是并发查询太多,每条查询都会复制一部分图结构用于搜索,这种情况要限制并发或者优化efSearch。
5.2 水平扩容后数据倾斜,某些节点负载特别高
分片策略需要提前设计好,哈希分片虽然概率上均匀,但如果主键本身有规律(比如全是递增数字,UUID模式),某些hash区间的数据量可能被放大。检查方式是在集群管理界面看每个shard的doc count,发现倾斜严重的话,只能重新设计分片键。我的建议是主键尽量用随机分布的值,或者在分片路由层增加虚拟节点,让哈希更方便打散。
5.3 量化之后召回率崩了,比预期低20个点
float16通常不太会出问题,但int8量化对原始向量分布很敏感,特别是数据分布不均衡、存在少量离群向量时,量化带来的误差会被放大。遇到这种情况,先别急着回滚精度,看两件事:一是是否先做了归一化,很多模型输出的向量模长差别很大,不归一化就量化,损失会异常;二是码本训练的迭代次数是否太少,让码本没充分拟合数据分布。如果这两项都调过还是不行,那再回到float16。
5.4 扩容之后查询延迟反而变高了
从单机切到分布式集群后,查询链路变成“协调器-分片-合并”,中间多了一层网络开销。延迟变高有时候不是系统慢了,而是架构变复杂了。排查重点放在三处:分片数是否比节点数多太多(查询会串行访问若干分片再合并);是否每个分片都有足够的副本(否则某个节点故障后查询压力全打到一个分片上);协调节点是否需要单独扩容(它负责汇聚结果,海量结果集合并时CPU会吃紧)。
6. 容量不是一个数字,是一套不断校准的流程
最后分享一个我自己的体会:向量容量规划不是一次性的计算题,它是一套需要持续校准的流程。每次数据量跨一个量级,都要重新跑一遍容量估算模板,重新测一下召回率,重新审视索引参数是否有优化的余地。我习惯把这些动作沉淀成一套脚本,每次变更数据规模时自动跑一遍,输出一张“容量健康度”报表,包含当前内存使用率、预估下个月内存使用率、建议动作三行。这个方法帮我们在多个项目里躲过了OOM事故,也替团队省过不少升级配额的费用。
如果你的项目正在为“是否要扩容”“该选哪种方案”纠结,我建议你先别急着换硬件或者迁库,花一个下午把容量估算模板跑一遍,再用真实查询做一次精度对比测试。很多时候答案不是加机器,而是把数据和索引的每个字节都盘清楚。这篇就到这,接下来这个系列如果继续写,我想聊聊向量检索的召回率评估体系建设,那个话题比容量更玄,也更有意思。