4.2 分布式扩展机制 组件图有了,本节回答扩载问题:内存装不下、查询扛不住时,系统靠什么继续长大。承接 4.1 的索引节点层,本节讲清两个基本手段——分片管容量、副本管可用与吞吐——以及它们带来的新问题:路由怎么定、扩容怎么搬数据、跨分片查询怎么归并。多模态与亿级场景(1.3 的推荐召回)从这里获得水平扩展的能力。 分片:把数据切开 分片把全集按规则拆到多个节点,每片持有索引的一个子集。切法主要两种:哈希分片按主键或向量 ID 的哈希落桶,优点是分布均匀、路由简单,缺点是范围查询不友好——不过向量检索本来就很少按范围扫,这让哈希分片成为默认选择;范围分片按键的区间切,适合有时间属性、经常按时间窗过滤的负载,代价是热尾问题(最新区间永远最热)。
组件图有了,本节回答扩载问题:内存装不下、查询扛不住时,系统靠什么继续长大。承接 4.1 的索引节点层,本节讲清两个基本手段——分片管容量、副本管可用与吞吐——以及它们带来的新问题:路由怎么定、扩容怎么搬数据、跨分片查询怎么归并。多模态与亿级场景(1.3 的推荐召回)从这里获得水平扩展的能力。
分片把全集按规则拆到多个节点,每片持有索引的一个子集。切法主要两种:哈希分片按主键或向量 ID 的哈希落桶,优点是分布均匀、路由简单,缺点是范围查询不友好——不过向量检索本来就很少按范围扫,这让哈希分片成为默认选择;范围分片按键的区间切,适合有时间属性、经常按时间窗过滤的负载,代价是热尾问题(最新区间永远最热)。选分片键是低频但极难更改的决定:向量库常见的做法是按集合切分片、集合内按 ID 哈希;多租户场景则常常租户优先——让一个租户的数据尽量落在同一批分片上,过滤条件才能下推成"只查某些片",这比全局均匀分布省得多。

副本解决的是分片解决不了的两件事:节点挂了数据还在(可用性),读流量太大时分担压力(读扩展)。常规布局是每个分片一主多从,写入走主、同步到从,读可分散到从副本。向量场景有个特殊考量:内存即容量,副本数量翻倍意味着内存成本翻倍,而向量库的内存账本来就紧(第三章量化压缩省的就是这个)。所以副本数通常取二或三——二份保可用、三份保"一台坏了还能修"期间仍有双份冗余——再多就要拿业务价值说服财务了。
主从切换的一致性也值得说清:向量检索对"读到稍旧数据"通常宽容(晚一秒可见的新向量不影响业务),这给了系统优化空间——异步复制加自动切换,换取写入低延迟。但切换瞬间可能有少量已确认写入丢失,对账务级严格场景要用同步复制或确认写双副本的选项。这不是技术高下之分,而是业务容忍度问题,选型时(第六章)要拿真实场景去对。
集群扩容时,最朴素的重哈希会把几乎所有数据搬一遍;一致性哈希的思想是把桶挂在环上、节点也挂在环上,扩容时只有新节点两侧区间的数据需要搬家,搬迁量从"全部"降到"若干分之一"。用一个可运行的模拟看清差别:
import hashlib def bucket(key: str, rings: int) -> int: return int(hashlib.md5(key.encode()).hexdigest(), 16) % rings nodes_old, nodes_new = ["n1", "n2", "n3"], ["n1", "n2", "n3", "n4"] VBUCKETS = 1024 keys = [f"vec-{i}" for i in range(20000)] move = 0 for k in keys: if bucket(k, len(nodes_old)) != bucket(k, len(nodes_new)): move += 1 print(f"朴素重哈希需搬迁 {move/len(keys):.0%}") # 虚拟桶方案:先固定 1024 个虚拟桶到物理节点的映射 import random assign_old = {v: nodes_old[i % 3] for v, i in ((v, i) for i, v in enumerate(random.Random(1).sample(range(VBUCKETS), VBUCKETS)))} assign_new = {v: nodes_new[i % 4] for v, i in ((v, i) for i, v in enumerate(random.Random(1).sample(range(VBUCKETS), VBUCKETS)))} moved = sum(1 for v in assign_old if assign_old[v] != assign_new[v]) print(f"虚拟桶方案仅迁移被重新分配的桶,约 {moved/VBUCKETS:.0%}")
工程含义有两层:其一,扩容前先算搬迁量与带宽窗口,把迁移限速排进低峰;其二,搬迁期间新旧分片并存服务,协调器按段清单路由,这正是 3.3 双索引思想的集群版。缩容同理反向操作。多租户场景还有第三条路:以租户为分片单位,扩容=把某些租户整体搬走,逻辑清晰但可能倾斜,超大盘租户需要内部再切。
用目标倒推而不是拍数字。容量维度:总分片内存等于单分片目标内存乘分片数,按未来一年的容量水位除以"单分片舒适容量"(通常几十 GB 量级)得到下限;延迟维度:分片越多单分片越小、查询越快,但扇出归并开销越大,P99 的甜点区通常在"单分片扫描十毫秒内"对应的分片粒度。两个约束取大者,再预留扩容余量(分片数翻倍比减半容易得多,宁小勿大)。多租户系统另有一条优先级更高的约束:先按租户边界定分片布局,再在租户内部应用上述计算。
会漏,如果每个分片只返回 K 条。全局第 K 名可能排在某分片的第 K 加一位之后,而另一个分片贡献了前面一大半名次。工程解法是放大归并采样:每个分片返回 K 加冗余条(常见 K 的两倍或加固定余量),协调器归并后再截断,全局 TopK 的正确率即有概率保证。这个细节解释了一个常见现象——分片多的集群在同样召回目标下需要更高的单分片参数余量,本质是归并损耗要提前在分片侧补齐。
扩容方案写在文档里都很完美,检验它的唯一方式是演练:在预发环境把集群从三节点扩到五节点,量三件事——数据搬迁耗时与带宽峰值、搬迁期间在线 P99 的抬升幅度、搬迁后分片间的负载均衡度。三组数字回填到容量规划公式里,扩容就从"理论可行"变成"有实测账本"。见过太多团队第一次真实扩容是在生产告警之夜,所有未经验证的假设一起爆雷——把扩容演练排进季度计划,成本远低于那次必然到来的深夜。
容量问题解决了,下一节跟着一条数据走完写入、更新、删除的一生。