日期:2026-09-07
某平台商品库过亿,做以图搜图和相似推荐时,向量检索延迟高、召回不稳。团队最早用单机 pgvector,数据量到千万级就顶不住,查询要几秒。后来直接上 Milvus,又因为分片与索引参数没调好,压测一上来就 OOM,集群频繁重启。我们进场时,相似推荐模块基本处于半瘫痪状态,运营不敢在大促开这个入口,怕一开就把数据库打挂。
更尴尬的是,没有人说得清问题到底在哪。是索引选错了,还是分片太少,还是机器内存不够,团队各执一词。我们做的第一件事不是调参,而是搭一套可复现的压测,把每个变量单独拉出来看,否则争论永远没结论。
我们把向量检索拆成写入和查询两条线来优化。亿级向量写入走批量建库,按商品类目分集合,避免单一超大集合带来的管理负担和单点风险。索引选型上对比了 HNSW 和 IVF,以图搜图这种低延迟高召回的场景最终选了 HNSW,配合合理的 M 和 efConstruction。分片与副本按查询并发和可用性要求设置,读写分离,写入高峰不影响查询。近邻检索性能调优重点在 efSearch 的取值,太小召回掉、太大延迟涨,我们按业务可接受的延迟上限反推,而不是照抄网上的推荐值。
案例片段(已脱敏): Milvus 集合与索引参数配置片段(示意):
collection: shard_num: 4 index_type: HNSW params: { M: 16, efConstruction: 256 } efSearch: 128 replica: 2压测下该配置支撑约 1200 QPS,P99 延迟约 35 毫秒,召回率约 0.95。对比初版 efConstruction=64、efSearch=32 的配置,P99 从约 90 毫秒降到 35 毫秒,召回从约 0.82 提到 0.95。
第一个坑是 OOM。初版把全部数据灌进一个集合,索引构建时内存直接打满。我们改成按类目分集合,并给每个集合设资源组,构建和查询错峰。这里有个判断,分太细会增加查询路由复杂度,我们按类目热度做了两级划分,热类目独立集合、长尾类目合并,平衡了管理和性能。这个两级划分后来成了我们做向量库的默认起手式。
第二个难点是召回和延迟的拉扯。HNSW 的 efSearch 是这个矛盾的核心。我们没有拍脑袋,而是抽样了一批真实查询,在延迟上限约 50 毫秒的约束下扫描 efSearch 取值,找到拐点。超过那个点延迟涨得快但召回几乎不涨,所以锁定在拐点附近。这一步花了一天,但把后续的争论全堵死了,因为数据摆在那。
第三个点是版本升级的兼容。Milvus 大版本之间索引格式会变,我们建了灰度回滚流程,新版本先在长尾集合试跑一周,观测召回和延迟无异常再全量。我们早年间吃过一次直接升级导致索引失效的亏,那次回滚花了大半天,自此之后任何向量库升级都走灰度。
优化后相似推荐入口稳定支撑大促,P99 延迟约 35 毫秒,召回率约 0.95,写入吞吐约每分钟 20 万向量,集群资源利用率从约四成提到约七成,OOM 事故归零。运营终于敢在大促挂出以图搜图入口,活动期间该入口贡献了约一成的加购。我们内部复盘时一致认为,这波优化的关键不是换了多牛的参数,而是先有了可复现的压测。(数据均为脱敏示意值)
回过头看,写入侧也值得单独说。亿级向量不是一次性灌进去就行,我们做了分批构建加索引渐进加载,避免建库期间查询全挂,构建任务错峰到凌晨,白天只做增量,线上几乎无感。资源隔离也关键,我们给查询和构建分了不同的资源组,构建吃满内存也不会挤掉查询的算力,这层隔离让大促前集中建库时线上稳得住。监控上我们把每个集合的查询延迟和召回都单独盯,哪个类目异常一眼能看到,不用等运营投诉,这套监控后来成了日常巡盘的标配。我们还顺手做了数据生命周期管理,冷类目的向量降配存储,热类目保高性能,整体成本又压下来一截。
我们在压测中还发现一个容易被忽视的点,就是向量检索的缓存命中率。高热度的商品向量查询重复度很高,我们在网关层加了一层短时结果缓存,把重复查询直接拦在数据库外面,整体负载又降了一截。这个优化不体现在索引参数上,但实打实减轻了底座压力。我们也因此养成一个习惯,凡是高频只读的查询都先想清楚能不能缓存,而不是无脑打到向量库,这个思路后来用到了好几个读多写少的场景。
向量库不是装上就能快,索引类型和参数得按数据规模和查询模式反推,照搬别人的配置,量级一变就翻车。分集合要讲策略,按类目热度两级划分比一刀切合理。召回和延迟的拐点一定要用真实查询扫出来,别靠经验估。版本升级别莽,长尾集合灰度一周再全量,我们吃过格式不兼容的亏。说到底,向量检索的调优是数据驱动的活,不是配置复制。我后来觉得,做向量库最该先投的精力是压测体系,而不是调参本身。