尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vearch大规模向量相似性搜索:架构、IVFPQ调参与扩容实践

Vearch大规模向量相似性搜索:架构、IVFPQ调参与扩容实践 做向量相似性搜索的人大概都经历过同一个阶段本地拿单机索引库跑十万条数据毫秒级返回感觉这事不过如此等数据涨到几千万、业务要求在线增删改查、还要保证服务不抖才发现前面那套玩法基本要推倒重来。vearch 就是我在这个阶段找到的一套大规模向量相似性搜索系统它的定位很明确把向量检索做成一个可水平扩展的在线服务而不是跑在笔记本上的算法脚本。整个系统由 Master、Router、PartitionServer 三层组成底层用 Gamma 引擎负责索引构建、查询执行和落盘。我在这上面建过表、调过 IVF 和 IVFPQ 的参数、也经历过扩容之后延迟反而变高的窘境。下面这些内容都是拿真实流量试出来的不保证和你手上的版本文档完全一致但路径是可复现的。1. 大规模向量相似性搜索的三道坎内存、延迟、召回1.1 最先撞的墙从来不是算法新手做向量检索容易有个错觉觉得难点在于选哪个距离度量、要不要归一化。真到大体量上先出问题的是内存。按 1024 维、float32 算一条向量占 4 KB一亿条就是 400 GB 上下这还只是向量本体没算 ID、标量字段、倒排表和图结构。即便你机器够豪暴力检索一次要算一亿次距离单核每秒能处理的浮点距离计算量级也就那样一次查询直接奔着秒级去。所以大规模场景必然走近似最近邻ANN而 ANN 的代价是召回率不再是 100%。于是形成了一个绕不开的三角想省内存就得压缩向量压缩就丢精度想降延迟就得少扫倒排桶少扫桶就掉召回想把召回拉回来就得加大扫描范围延迟跟着涨。后面所有参数调优本质都是在这三条边之间挪位置。我把这三条边记成一句话——内存、延迟、召回任何一次调参都是拿其中两个换第三个没有白捡的便宜。1.2 单机方案会断在四个具体位置单机索引库不是不好用是它有明确的断裂点我在项目里依次撞过这四个内存天花板。向量全量常驻内存的方案在大几千万条时会直接 OOM此时唯一的出路是换压缩索引而换索引意味着重建重建期间服务怎么办。索引训练是一次性的。基于聚类IVF 系的索引在训练完成后聚类中心就固定了。业务数据分布会漂移比如新品类上线、用户兴趣变化漂移之后原来的中心覆盖不到新数据召回会无声无息地掉这种掉法最坑因为没报错。在线变更的锁与停顿。删除、更新标量字段、调整过滤条件都会触碰索引结构。单机实现往往用一把大锁顶住写入高峰时查询 p99 直接起飞。扩容不是线性的。加机器不能把已有索引切成两半单机方案要么整份重建要么做上层分片而上层分片要自己写路由、自己处理副本一致性工作量比想象中大得多。1.3 vearch 在这条链路里的位置把边界想清楚后面很多设计就不会纠结。vearch 承担的是向量和标量混在一起的存储、索引与在线检索集群元数据、请求路由、分区与副本、索引构建、过滤执行、结果归并这些是它的活。它不负责的部分同样重要——向量怎么生成是你的模型的事召回之后怎么做业务重排和去重是应用层的事跨空间做复杂分析查询也不是它的强项。所以我在架构图里给它画的位置是上游接特征服务下游接重排服务中间这一段它当向量数据库用同时带一点轻量检索能力标量过滤。搞清楚这一点你就不会指望它帮你做 biz 层的逻辑也不会因为它不支持某个 SQL 语义而失望。2. 三个组件各管一段一次写入和一次检索在集群里怎么走2.1 Master 与 etcd谁在线、谁该搬数据Master 是集群的大脑但它的状态很轻真正的元数据放在 etcd 这类协调组件里。Master 干的事情主要有三件感知 PartitionServer 节点的上下线、维护空间与分区的分配关系、在节点变化时触发数据迁移和负载均衡。理解这一点的实际价值在于排查问题时能定位层级——如果你的建表请求超时、节点列表不刷新、迁移迟迟不动问题大概率在 Master 或 etcd 这一层跟索引参数无关。我遇到过一次新增节点后一直不参与服务的情况最后发现是节点注册信息进了 etcd 但 Master 的调度周期还没轮到它等了一个调度周期就好了。这类问题不需要改任何配置知道它在哪一层就够。2.2 Router无状态的一层负责广播和归并Router 是请求入口本身不存数据理论上可以随便多起几个做读扩展。它做三件事解析请求体、按空间的分区分布把检索请求分发下去、把各分区返回的候选合并成最终结果。写入请求则通常按文档 ID 算出目标分区直接投递到对应分区。这里有个很多人没意识到的点检索请求是扇出到该空间所有分区的。因为向量是按分区切的一个查询向量可能和任意分区里的数据最相似谁都不能漏。这就意味着分区数直接等于单次查询要打的后端数量尾部延迟会被最慢的那个分区拖住。我后来调分区数的时候这条规则是主要约束不是只考虑单分区数据量。2.3 PartitionServer 与 Gamma索引、缓存、落盘都在这里PartitionServerPS是真正干重活的节点一个 PS 上会承载多个分区。每个分区内部由 Gamma 引擎管理索引和数据Gamma 负责几件事把文档写进存储引擎可落盘、维护向量索引、按请求执行检索、以及与同分区的其他副本保持同步。Gamma 里的向量索引通常借助成熟的 ANN 库实现所以你在配置里看到的 IVF、IVFPQ、HNSW 这些名词语义和业界通用实现基本一致。一个容易忽略的配置是索引常驻内存的条目数上限。这个值决定有多少向量索引在内存里常驻、多少要读盘。它不是越大越好内存会被吃光也不是越小越好超出的部分每次检索都要落盘读延迟会突然抬起来。我的经验是把它设在热点数据规模 × 1.2左右然后盯住磁盘读 IO 作为校验。2.4 写入链路和检索链路的关键差别两条链路的差异决定了它们在故障时的表现完全不同。写入链路Router 按 ID 定位分区 → 打到该分区的 leader 副本 → leader 写本地存储引擎与索引 → 同步到 follower 副本。这条链路是单点投递的所以某个分区所在节点出问题只影响落到这个分区的写入。检索链路Router 广播到所有分区 → 每个分区各自跑一次 ANN 检索并返回 topK → Router 做一次全局归并。这条链路是扇出的任何一个分区慢整个请求就慢。基于这个差别我的运维动作也分成两类写入抖动先查是不是有分区在批量重建索引或做后台合并检索抖动先查是不是某个分区的内存索引缓存命中率掉了。下面第 5 节会把这两条排查路线展开。3. 建表这一步就决定了后面能不能省心Space 建模的取舍3.1 partition_num 是一次性决定别随手填分区数在空间创建时确定之后基本不动。它同时影响四件事单分区数据量影响训练和重建耗时、检索扇出影响 p99、数据迁移的粒度分区是搬迁的最小单位、以及并行度分区多配合更多节点能提高并发。我在第一个项目里随手填了个 4数据涨到两千万条时单分区五百万重建索引要跑很久扩容也只能以 1/4 为单位搬非常笨。后来重做时按单分区目标两千万条以内倒推五百亿条数据直接取 32 个分区这个问题就消失了。分区数还建议取能被节点数和副本数整除的数否则均衡时会出现某些节点多分一块的情况。3.2 replica_num 买的是读吞吐不是安全感很多人把副本理解成备份其实在大规模向量检索里副本的第一价值是读扩展检索请求可以分散到多个副本上执行同时副本还能在单节点故障时继续提供读服务。代价是写放大——每次写入要同步到所有副本副本越多单次写入的完成时间越长副本落后的风险也越大。我的取舍逻辑是写入 QoS 要求高、数据可重算能离线重灌的场景副本给 1 到 2如果是检索 QPS 极高且数据量可控副本给 2 到 3 来换读吞吐。没有一个通用答案但一定不要因为怕丢数据就把副本拉到 3 以上然后回去抱怨写慢。3.3 向量字段的索引类型怎么选把常见选项摊开对比选型就不玄学了索引类型单向量内存开销召回水平构建与训练成本增量写入典型场景暴力/精确检索维度 × 4 B100%无直接追加百万级以内、作为召回基线IVF 加原始向量维度 × 4 B 加倒排开销高需要训练训练后可追加千万级、精度优先IVF 加 PQ 压缩子量化器数量 B中等靠扫描范围拉回需要训练训练后可追加亿级、内存敏感HNSW 图索引维度 × 4 B 加 30% 到 70% 图开销高构建慢、吃内存支持但写入变慢千万级以内、低延迟优先一条实用判断先看内存能不能装下再看延迟能不能达标。内存装不下就别犹豫直接上 PQ然后靠加扫描范围把召回补回来内存充裕且延迟敏感HNSW 是更省心的选择但要有心理准备它的构建时间和内存水位都比 IVF 高。3.4 一份可以直接改的 Space 定义下面这份定义是我做商品以图搜图场景时的骨架字段含义写在代码块后面{ name: product_space, partition_num: 8, replica_num: 2, properties: { title: { type: string, index: {name: title_idx, type: SCALAR} }, category_id: { type: integer, index: {name: cate_idx, type: SCALAR} }, price: { type: float }, create_time: { type: date }, feature: { type: vector, dimension: 768, index: { name: feature_idx, type: IVFPQ, params: { ncentroids: 2048, nprobe: 64, nsubquantizers: 96, nbits: 8, metric_type: InnerProduct, training_threshold: 200000 } } } } }逐项说一下我为什么这么填。partition_num: 8对应的是预计数据量在千万级、单分区目标百万到千万replica_num: 2是为了让检索请求能分摊到两个副本上。feature字段里dimension必须和特征服务产出的向量长度严格一致写错的话写入会直接报维度不匹配。ncentroids是每个分区的聚类中心数注意是每个分区各训一份不是全局共享。nsubquantizers: 96配上 768 维正好每个子量化器管 8 维维度必须能被它整除否则建表就会失败。training_threshold是攒够多少条数据才触发索引训练设小了索引质量差设大了首次检索要等很久才发现索引还没建好。3.5 建完表才会发现改不动的几项这是我踩得最疼的一类问题。空间建好之后下面这些改动基本都要走重建或者新空间迁移分区数量、向量维度、向量字段的索引类型和量化参数、字段类型本身。能相对平滑改的一般是标量字段上的增补和部分检索侧参数比如扫描范围这种只影响查询的。所以建表前一定要把三件事定下来向量维度会不会变、数据量三年后大概多少、有没有可能从 IVF 换到别的索引。只要第三件事的答案不是绝无可能就提前规划好迁移方案别等业务来了再临时想。4. 索引参数不是拍脑袋ncentroids、nprobe、PQ 的定量算法4.1 训练样本和聚类中心之间的 39 倍关系聚类这件事有统计上的下限。业界通行的经验是单个聚类中心至少要分到 39 条训练样本低于这个数中心位置估不准检索时会出现最近的桶里没东西的情况。反过来说ncentroids的上限就是训练样本数除以 39。假如一个分区有一百万条数据参与训练那ncentroids理论上不要超过两万五实际我会压在两千到八千之间因为中心数过多会让倒排表变碎、查询时要扫的桶变多。还有一个下界问题中心太少每个桶里的候选太多扫描成本飙升召回虽然高但延迟难看。我的做法是把中心数控制在样本量的平方根到四倍平方根之间试几档配合召回率曲线选一个拐点而不是照抄别人的配置。4.2 nprobe 从 1% 起步往回收nprobe是查询时要探查的桶数量直接决定扫描成本是延迟和召回之间最灵敏的旋钮。我的习惯是先从中心数的百分之一左右起步比如 2048 个中心就从 20 开始然后画一条曲线横轴是nprobe两条纵轴分别是召回率和 p99 延迟。曲线上通常会出现一个明显的拐点——过了拐点之后召回涨得很慢而延迟涨得很快那个拐点就是你应该待的位置。这里有个非常容易忽略的坑召回率必须用真实业务向量测不能用随机向量测。随机向量的分布太均匀测出来的召回曲线特别好看上线之后业务数据分布集中实际召回会低一截。我当时就是用随机向量自测通过了上线后靠人工抽检才发现问题。4.3 用 PQ 换内存把压缩比算清楚再动手PQ 的原理是把一个长向量切成若干段每段各自用一个码本做近似最终每段只存一个码本下标。内存账很好算每个向量占用的字节数约等于子量化器数量乘以每个子量化器的位宽再除以 8。还是用 768 维举例原始 float32 是 3072 字节切成 96 段、每段 8 位压缩后就是 96 字节压缩比 32 倍。这个比例意味着原本需要 300 GB 内存的数据集压缩后不到 10 GB差距就是这么来的。代价是精度损失而且损失不是均匀的子量化器分得越细段数越多每段编码的维度越少精度越好但压缩比下降。我的经验值是把每段控制在 4 到 16 维之间太高精度掉得厉害太低压缩收益不明显。另外 PQ 是有损的它不适合作为唯一索引再指望精确重排通常做法是先用 PQ 快速捞出较大的候选集比最终返回数量大 5 到 10 倍再用原始向量做一次精排这样能把精度大部分找回来。4.4 标量过滤叠加向量检索时的两条执行路径带过滤条件的向量检索是实践里最容易出问题的场景因为过滤的执行顺序会显著改变结果质量。存在两条路径先过滤再检索先用标量条件筛出候选集合再在这个集合里做向量比较。当过滤条件很严命中比例不到百分之几时这条路又快又准。边检索边过滤照常走倒排桶扫到候选时再判断标量条件是否符合。当过滤条件很宽命中比例过半时这条路更省因为它能靠倒排结构快速裁剪。真正坑人的地方在中间地带过滤条件不宽不严命中百分之十到三十两条路都不理想。走第一条候选集太小导致向量检索的索引形同虚设走第二条扫了很多桶最后大部分被过滤掉等于白扫。我处理这类查询的办法是把过滤条件命中比例当成一个监控指标打出来命中比例落在中间地带就考虑调整业务侧的查询方式比如把过滤维度拆到更粗的粒度上或者干脆把它做成独立的空间。有的版本会给过滤暴露类似快速过滤的开关语义就是在遍历过程中做判断效果与全量过滤差异很大。遇到这种开关两条路各测一遍再决定别默认开或者默认关。5. 上线之后才是正题扩容、重建索引和指标看什么5.1 加节点为什么不是插上就能用新增 PartitionServer 之后集群会进入一个数据迁移阶段把部分分区从旧节点搬到新节点。这里有两个现实约束。第一搬迁的最小单位是分区不是向量条数所以如果你的分区粒度很粗一共 4 个分区加 3 个节点也只能搬走 1 个分区利用率极低。第二迁移过程本身会消耗磁盘 IO 和网络带宽如果业务正在高峰迁移可能把检索延迟拉高。我现在的扩容流程固定成三步先在低峰期加节点观察迁移进度和检索 p99迁移完成后跑一轮召回抽检确认数据没有异常最后再考虑要不要调整负载策略。顺序别反先调策略再扩容迁移行为会变得不好预测。5.2 换索引类型只能走新空间加双写加灰度切流想把 IVF 换成 HNSW或者想改 PQ 的段数这些参数在建表后基本改不了。社区版本有的提供了索引重建入口可以在不丢数据的前提下重建但重建期间资源占用很高而且要确认你手上的版本确实支持。我用得最多的方案还是老实的四步迁移建一个新空间参数按新方案配置好。打开双写新老空间同时接收增量数据同时用离线任务把历史数据刷进新空间。用同一批真实查询在两套空间上做召回对比确认新方案达标这一步不能省参数变了召回一定变。灰度切读流量从百分之一开始观察延迟和业务指标再逐步放大最后停掉老空间。整个过程最耗时的是第 2 步的数据回灌。这里有个技巧回灌时把写入批量调大、并发调高但不要拉满留出一部分带宽给在线业务否则在线写入会被挤到超时。5.3 该盯的几个指标和它们的异常含义监控不用多但下面这几个必须有而且要知道异常时意味着什么指标异常表现大概率原因检索 p99 延迟突然抬高且持续某个分区的内存索引缓存命中率下降开始读盘各分区文档数长时间不均衡迁移卡住或分区粒度太粗索引训练状态长时间停在待训练数据量没到训练阈值或训练任务堆积副本同步延迟持续上涨写入压力大或网络带宽被迁移占用磁盘读吞吐检索延迟同向波动索引常驻内存的条目数设小了我的做法是给检索延迟加一条分区维度的拆解能看到是全局变慢还是个别分区变慢。这个拆解比总延迟有用得多因为向量检索的扇出特性会把个别分区的慢放大成全局的 p99。5.4 三类常见故障的排查顺序写入变慢。第一个怀疑对象是索引训练或后台合并这两个操作会大量占用磁盘第二个怀疑对象是某个分区的副本同步落后第三步才去看节点本身的资源水位。顺序别跳因为最常见的根因就是第一种。检索抖动。先看是不是读盘也就是常驻内存索引容量和实际数据量是否匹配再看是不是有分区在迁移最后看单个查询的过滤条件是不是落在前面说的中间地带扫了一堆桶最后大部分被过滤掉。副本持续落后。先确认是全局现象还是单个副本全局的话看写入 QPS 是否超预期单个的话看那台节点的磁盘和网络顺便确认是不是正好赶上它在做迁移。副本落后本身不可怕可怕的是它长期不收敛那意味着写入速率已经超过集群处理能力了。6. 容量估算模板与上线前的自检清单6.1 从原始向量推算内存、磁盘和分区规模我把估算做成固定套路每次新场景都在上面套。举个例子五亿条向量、512 维、float32单副本原始向量体量5 亿 × 512 × 4 字节 ≈ 953 GB。这个数字只代表如果全部以原始精度常驻内存的规模。上 IVFPQ、子量化器取 64每向量压缩到 64 字节单副本降到约 32 GB两个副本约 64 GB。这才是实际可以放进内存的量级。再乘一个 1.5 到 2 的系数留给 ID、标量字段、倒排表、缓存和内存碎片。分区规划单分区目标两千万条以内五亿除以两千万等于 25向上取到 32 个分区方便被节点数和副本数整除。节点规划按每节点承载内存量的 70% 水位倒推节点数同时确认 32 个分区能比较均匀地铺在节点上。这套算法的价值不在数字精确而在于让要买多少机器这件事在评审会上有据可依而不是拍脑袋。6.2 上线前我一定会跑的那几张测试表压测不是打个高 QPS 就完事向量检索的压测必须带召回验证否则你测的只是一个很快但搜不准的系统。我固定跑四张表召回率对照表用一批真实查询向量分别在近似索引和暴力检索上跑比较前 K 个结果的交集比例。这是所有参数调优的基准线没有它后面的数字都没意义。延迟分位表记录 p50、p95、p99重点看 p99 和 p50 的比值。比值很大说明尾部有慢分区该去查扇出和分区均衡。过滤条件敏感性表把过滤命中比例从 1% 扫到 90%每个点记录延迟和召回找出那个中间地带落在哪里。写入混合表在读压力下按比例注入写入和更新看检索延迟的劣化幅度。很多系统单测读没问题读写混合之后就崩了。6.3 几个反直觉的坑最后分享几个和直觉相反的点都是我实际撞过的。第一分区数不是越多越好。分区多能提高并行度但检索请求要扇出到所有分区分区数翻倍意味着单次查询的后端调用数翻倍尾部延迟通常跟着变差。找到单分区大小和扇出数量的平衡点比单纯追求分区细粒度更重要。第二召回率掉的时候先怀疑数据分布再怀疑参数。参数没动、配置没改召回却慢慢下降八成是数据分布漂移导致原有聚类中心覆盖不足。这时候调nprobe只是治标真正的解法是重建索引。第三首次写入峰值可能是最难的一关。索引还没训练的时候数据是先攒着的攒到阈值才训练训练完才真正可检索。如果你的业务一上线就要灌几千万条数据务必把首次训练的耗时和资源占用提前测出来别等上线当天才发现数据灌进去了但搜不出来。第四压测用的向量一定要来自真实分布。我见过不止一次用均匀随机向量做的压测指标全绿换成真实业务向量之后延迟涨了一倍多原因就是真实数据的向量分布更集中落到的倒排桶更热。说到底向量检索系统的复杂度八成不是花在怎么算相似度上而是花在数据分布会变、容量会涨、故障会来这些事情上。要不要上分布式、分区数取多少、副本给几个这些问题在写第一行代码之前想清楚后面就能少重建几次索引。
返回列表