资讯详情

资讯详情

Vearch十亿级向量检索实战:分布式架构、Faiss索引调优与避坑

1. 向量检索这件事为什么值得单独拎出来讲第一次接触十亿级别向量检索的需求时我脑子里第一反应是这玩意儿不就是给每个向量算个距离然后排序吗。真动手才发现暴力计算在千万级往上就彻底不可接受了——假设你有 1 亿条 128 维的向量单次全量比对大约需要 128 亿次浮点乘加就算用上 SIMD 优化单机跑一遍也得几秒起步同时把整份向量所占用内存全部读一遍延迟和吞吐都撑不住真实业务。Vearch 要解决的就是这个矛盾在召回质量和响应时间之间找一个能工程落地的平衡点并且把它做成可水平扩展、可运维的分布式系统。很多人会把 Vearch 简单理解成一个向量数据库。这么说不算错但不够准确。它更贴切的定位是带向量索引能力的分布式文档检索系统——既支持传统结构化字段的过滤也支持过滤 向量近邻的混合查询底层用 Faiss 做核心索引用分片加副本的方式做水平扩展。这套能力最有价值的场景一个是图文/视频的相似内容召回一个是推荐系统里用户向量找相似物品还有一个是 RAG 场景下把知识库切片向量化之后做语义检索。如果你正在做这几类业务或者团队正准备自建一套向量检索服务那接下来这些坑我踩过的、绕过的、想明白的应该都能帮你少走点弯路。先给还不熟悉这个领域的朋友一个最简心智模型。给定一个查询向量系统要返回和它最接近的 K 个向量。匹配的度量方式常见有两种内积InnerProduct和欧氏距离L2前者更关注方向一致性后者更关注绝对距离。工程上用 IVF、HNSW 这类**近似最近邻ANN**索引本质是放弃100% 精确用少量召回损失换几十上百倍的性能提升。Vearch 的价值不在于它发明了新索引而在于它把索引、分片、存储、一致性这些工程细节拼成了一个能直接跑生产的东西。2. Vearch 整体架构与核心组件拆解2.1 四个核心角色的职责边界Vearch 集群里主要跑四类进程理解它们的职责边界是排查一切问题的前提。Master是这个集群的大脑负责元数据管理、空间Space的创建与删除、分区Partition的分配、节点上下线的感知。它本身不存数据只维护谁在哪、有哪些表、表怎么切这类信息。Router是请求入口所有外部查询和写入都先到它这里。它根据 Master 维护的路由表把请求转发到对应的 Partition Server再把多个分片的返回结果做一次归并排序最终返回给客户端。你可以把它理解成一个智能网关。Partition ServerPS是真正干活的角色数据存这里、索引建这里、召回算这里。一个空间会被切成若干分区每个分区有若干副本PS 负责承载这些分区的读写。Meta 存储一般用 Raft 保证一致性维护集群的元信息和变更历史。为什么要这么拆因为读写放大和水平扩展是两条独立的路。Router 无状态可以随便加PS 有状态但可以按分区迁移。如果你把路由和存储耦合在一起扩容时数据搬迁会非常痛苦。我第一次搭一个小集群时偷懒只起了单 PS后来数据涨到几千万才发现没法平滑加机器只能重新灌数据——这个教训很贵。2.2 存储与索引引擎Gamma 与 Faiss 的分工Vearch 3.x 之后存储层换成了自研的Gamma 引擎底层基于 RocksDB 做持久化。Gamma 核心解决两件事一是把文档的正排数据、标量字段、向量字段统一管理二是支持实时写入的同时增量构建索引避免传统 Faiss 那种必须全量重训的尴尬。向量索引依然交给Faiss来做支持 IVF、IVFPQ、HNSW、FLAT 等主流类型。Gamma 负责把文档写进 RocksDB同时在后台维护一份 Faiss 索引结构。写入的时候按 batch 攒批到一定量再触发索引合并查询的时候走内存中的索引标量过滤走倒排或行存扫描。这套设计的关键取舍是实时性与索引质量的平衡。刚写入的向量还没进 Faiss 索引就先放在一个小的实时结构里后台 periodically 把它合并进主索引。所以你会看到刚写进去的数据搜不到或者不稳的现象这不是 bug是设计使然。理解了这一点很多看起来诡异的行为就解释得通了。2.3 数据模型Space、Partition 与 DocumentVearch 的数据组织是三层Database → Space → Document。Space 类似关系数据库里的表Document 类似行。Space 在建的时候就要定义好 Schema包括每个字段是 scalar 还是 vector、向量的维度是多少、用哪种索引类型、分多少个分区。分区数partition_num是个非常关键的参数。分区太多Router 归并压力大、元数据开销高分区太少单 PS 上数据太集中扩容和迁移的粒度都很粗。经验上让单个分区控制在几百万到千万级文档比较舒服具体要结合单向量维度和内存来算。下面这张表是我整理的分区数选择参考数据规模建议分区数单分区文档量级说明100 万级1-250-100 万单机可扛几乎不用分片1000 万级4-8100-300 万便于按 PS 扩展1 亿级16-32300-600 万需要多 PS注意归并开销10 亿级641000 万左右单分区别太大否则迁移慢3. 动手搭一个能跑起来的 Vearch 服务3.1 部署准备与集群启动先说环境。Vearch 的部署方式有好几种单机 Docker 跑通最快生产一般用多机集群部署。下面我用一个三节点的小集群做演示这也是我建议新手第一次练手的规模既能体会分布式行为又不至于把本地机器搞炸。准备工作分几步。第一确认机器时间同步Raft 对时间漂移比较敏感。第二规划端口Master 默认 8811Router 默认 9001PS 默认 8817这几个端口别和本机其他服务冲突。第三准备好数据目录最好单独挂盘别和系统盘混在一起否则数据量一上来 IO 打架。# 单机快速验证仅用于学习和测试 docker run -d --name vearch \ -p 9001:9001 -p 8817:8817 -p 8811:8811 \ -v /data/vearch:/vearch-data \ vearch/vearch:latest生产集群的启动顺序很讲究必须先起 Master再起 PS最后起 Router。因为 PS 启动后要向 Master 注册自己、上报自己的资源信息Router 启动后要拉取路由表。顺序错了会出现 PS 注册失败、Router 拿不到路由的情况届时你只能重启对应进程。启动后验证是否健康直接打 Master 的健康检查接口就行curl http://127.0.0.1:8811/_cluster/health返回里有集群整体状态、节点列表和分区分配情况。如果这里看到某个 PS 是 dead 状态先别急着做业务把 PS 的日志翻出来看——八成是数据目录权限或者端口占用的问题。3.2 建库、建表与 Schema 设计Vearch 的 Schema 设计是整件事里最需要动脑子的一步尤其是索引类型的选择。先看一个最典型的 Space 创建请求{ name: image_search, partition_num: 4, replica_num: 2, engine: { name: gamma, index_size: 200000, id_type: String, retrieval_type: IVFPQ, retrieval_param: { metric_type: InnerProduct, ncentroids: 2048, nsubvector: 32 } }, properties: { title: { type: string, index: true }, category: { type: keyword, index: true }, feature: { type: vector, dimension: 512, store_type: MemoryOnly, index: true } } }这里有几个参数需要重点解释因为它们直接决定性能和召回。ncentroids是 IVF 聚类的中心点数量。这个值太小每个簇太大查询时扫描的簇内向量多性能下降太大训练开销和内存开销都上去了而且每个簇太瘦查询时可能需要访问更多簇才能保证召回。经验公式是ncentroids ≈ 4 * sqrt(N)其中 N 是向量总数同时不建议超过总向量数的 1/39。比如 1000 万向量一个合理的量级是 8192 到 16384。nsubvector是 PQ 的切分子向量个数直接影响压缩率。128 维向量切 32 个子向量每个子向量 4 维通常能压到原始大小的 1/16 到 1/8。这个值越大压缩越狠但精度损失也越大。一个常见错误是把 nsubvector 设成不能整除维度的值比如 128 维设 30Faiss 会在训练阶段直接报错。必须是维度的因子。metric_type的选择要匹配你的向量产生方式。模型如果做了 L2 归一化用 InnerProduct 和 L2 是等价的归一化后内积等于余弦相似度没归一化就老实按语义选。选错度量召回会莫名其妙地差但不报错最难排查。3.3 写入数据与增量索引写入接口支持单条和批量生产环境务必用批量写。原因是每次写入都会触发一次 WAL 落盘和小批量的内存索引更新单条写的 IO 放大和索引更新开销都很难看。curl -X POST http://127.0.0.1:9001/document/upsert \ -H Content-Type: application/json \ -d { db_name: my_db, space_name: image_search, documents: [ { _id: doc_001, title: 示例图片, category: landscape, feature: [0.12, -0.34, 0.56, ...] } ] }批量大小怎么定我的经验是单批 200 到 1000 条比较稳。批太小网络往返和事务开销占比高批太大单次请求超时风险大而且一旦失败整批重试代价高。如果写入吞吐还上不去可以开多个客户端并发写但要注意别把 PS 的写入队列打满。写完数据别急着查等一会儿。前面说过新写入的向量先进实时结构后台合并进主索引是需要时间的。你可以通过index_size参数控制攒批的阈值或者通过接口手动触发索引刷新。线上排查写完搜不到时先确认是不是索引还没合并别一上来就怀疑数据丢了。4. 查询链路与性能调优实战4.1 一次查询到底走过哪些环节一个找相似向量的请求链路上会依次经过Router 收到请求根据 db 和 space 找到所有目标分区把查询并发发到各个 PS每个 PS 在本地的 Faiss 索引里做 ANN 检索同时对标量过滤条件做筛选PS 返回局部 TopKRouter 把 M 个分区的结果合并成全局 TopK 返回。这个链路里有两个容易忽略的性能杀手。一是标量过滤和向量检索的顺序。如果过滤条件选择性很强比如筛出 0.1% 的数据理想情况是先过滤再在候选集里做向量检索但重写了过滤条件如果没法下推就变成先暴力检索一批再过滤很容易出现召回率够了但结果被过滤没了的情况。二是归并阶段放大。每个分区都要返回足够的候选给 Router如果分区数很多Router 的归并开销会线性增长。所以大集群里 K 不能随便设太大一般业务场景 K 在 10 到 100 之间足够。4.2 索引参数的调优思路调参这件事最忌讳瞎试得先明确你的目标是什么是要极致的召回还是要极致的低延迟还是吞吐优先。下面这张表是我总结的参数调优方向调优目标关键参数调整方向副作用提召回nprobe 增大查询扫描更多簇延迟上升降延迟nprobe 减小少扫簇召回下降省内存nsubvector 增大PQ 压缩更狠精度下降提吞吐批量查询 并发摊薄单次开销内存峰值高提精度换 HNSW 或 IVFFLAT减少量化损失内存暴涨nprobe是查询阶段最常用的旋钮。它决定了每次查询扫描多少个倒排簇。默认值往往偏小实际业务里建议从 32 开始往上调边测召回边看延迟。我一般会画一条nprobe-召回率曲线找一个召回能到 95% 以上、延迟还能接受的拐点。如果你追求高召回且内存够可以上HNSW。它本质是分层图索引查询走的是图上的贪心导航召回和延迟通常都优于 IVF 系列代价是索引内存大得多——HNSW 基本上要常驻内存数据量上千万元素就得认真估算机器配置。而IVFPQ省内存但精度有损适合预算受限的场景。选型没有标准答案看你业务对成本的敏感度。4.3 压测与容量规划上线前一定要压测而且要用接近真实分布的数据。用随机向量压测是最常见的误区——随机向量分布均匀索引结构和真实业务里的聚类分布差别很大压出来的召回和延迟参考价值有限。压测关注三个指标P99 延迟、QPS 和召回率。三者互相牵制你要做的是找出在当前机器配置下三者能同时接受的工作点。容量规划时记住一句经验话向量检索是内存密集型的。按每条向量维度 × 4 字节算原始大小加上索引结构的开销通常要再乘以 1.5 到 2 倍来估算内存。内存不够时系统会开始换页延迟会断崖式恶化。5. 常见问题与排查技巧实录5.1 索引构建失败与内存溢出这是新手最常撞的墙。表现是 PS 日志里出现 Faiss 训练相关的报错或者进程直接被 OOM Killer 干掉。根因基本是两点一是索引参数设置不合理比如 ncentroids 设得比总向量数还大Faiss 训练时直接崩二是内存估算不足训练 IVF 时需要额外内存来放聚类过程的中间结果这个峰值往往被忽略。排查顺序我建议这样走先看日志里报错的具体阶段是训练阶段还是合并阶段再算一遍理论内存需求和机器实际可用内存对照如果是参数问题先把 ncentroids 调回推荐值把 nsubvector 确保整除维度。注意调整索引参数后已经建好的索引不会自动重训。要么删除 Space 重建要么按业务允许的方式重建索引。生产环境删 Space 前一定确认没有依赖它的上游服务。5.2 召回结果偏差大搜出来的结果和预期差距大先别怀疑算法按这个顺序查第一确认查询向量和入库向量的处理方式一致。如果入库是归一化过的查询没归一化内积结果会完全乱套。这种问题非常隐蔽因为系统不会报任何错。第二确认度量方式选对了。前面提过InnerProduct 和 L2 在未归一化时结果差别很大。第三调大 nprobe 复测。如果调大后召回明显改善就说明是索引精度问题可以从参数和索引类型上再优化。第四检查标量过滤是否把正确结果排除了。有次我排查一个搜不到的问题最后发现是过滤条件里时间字段的类型写成了字符串而入库是整型比较逻辑直接导致候选全被过滤掉了。这种低级错误在真实项目里的出现频率远超你想象。5.3 集群节点异常与数据迁移PS 掉线后Master 会检测到并把它的分区副本调度到其他健康节点上这个过程叫副本重建。期间集群的可用性受影响主要是 IO 和网络会被数据搬迁占满查询延迟会明显抖动。排查节点异常时我习惯按这个清单逐项过排查项检查方法典型现象进程存活ps/ 容器状态进程没了或反复重启端口占用lsof -i:8817启动报 bind 失败磁盘空间df -h写 WAL 失败、索引合并中断内存水位free -h/ 监控频繁 GC、OOM网络连通节点间互 pingRaft 心跳超时时间同步ntpq -p选主异常、日志时间错乱一次真实事故里集群三个 PS 里有一个持续被判死日志显示 Raft 心跳超时。查了半天发现是那台机器的时间和另外两台差了十几秒而它的时钟源配置错了。改掉时钟源、做一次校准问题当场消失。分布式系统里时间同步这行的坑永远排得进前十。再补一个关于扩容的经验。给集群加 PS 节点后Master 不会自动把已有分区均衡过去你需要主动触发分区迁移或者重建。我一般选业务低峰期做这个操作同时把迁移限速打开避免把在线查询拖垮。6. 我踩过的坑和一些实在建议先说一个关于向量格式的坑。Vearch 的向量字段接收的是浮点数组但如果你的特征生成脚本输出的是整型或者做了量化一定记得在入库前统一转成 float32。有次我们上游把特征存成了 uint8 节省空间直接用的时候没转类型结果内积算出来全是错的排查了一整天才定位。数据类型的隐性不一致是这类系统里最难抓的 bug 之一。第二个是关于版本兼容。Vearch 的大版本之间有比较明显的架构变化尤其是 3.x 引入 Gamma 引擎之后2.x 的数据不能直接平滑升级。所以选型定版本时要把升级路径也考虑进去别只顾着当前跑得通。建生产集群前先在测试环境完整跑一遍版本升级演练心里有底才踏实。第三个是关于监控。Vearch 本身提供的监控指标够排查基本问题但线上建议额外采集 Faiss 索引大小、各分区文档数、Raft 状态这些自定义指标。尤其是分区文档数的分布如果不均衡说明你的数据分布有问题查询时会表现为某些 PS 特别忙、整体 P99 被拖高。最后聊聊什么时候该用 Vearch什么时候别硬上。如果你数据量在百万以内、查询延迟要求也不苛刻直接用单机版的向量库反而更省心分布式系统的运维成本不是小团队能轻松扛住的。Vearch 真正发挥价值的场景是数据量上千万、需要水平扩容、同时带结构化过滤需求的中大型业务。选它之前先想清楚你有没有一个能持续投入的运维或后端同学来盯着集群的健康和容量。这个问题的答案往往比参数怎么调更重要。另外提一句关于分区设计的心得。见过太多团队一开始把 partition_num 设得过大觉得分得细灵活结果单分区只有几十万数据Router 归并十个分区的开销比真正检索还大。分区数的本质是扩展粒度和归并开销之间的权衡宁可一开始分少点后面数据涨上来再重建扩容也比一上来就分得太散要好。这行没有一招鲜多测、多观察、多按业务真实数据来验证才是把向量检索这个事做稳的唯一途径。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →