800ms的查询延迟,用户已经走了

某教育科技公司的RAG问答系统上线第一周,用户反馈"卡顿严重"。我看了监控数据:P50延迟150ms,P99延迟800ms。这意味着每100次查询中,就有1次用户要等将近1秒。在对话式AI的场景下,这个延迟是不可接受的。

团队的第一反应是"换Pinecone"。但5000万向量迁移过去,光Embedding费用就要$6,500。我说给我一周时间,不换数据库,只做优化。最终结果:P99延迟从800ms降到8ms,QPS从200提升到3500,成本为零。

以下是完整的优化过程。

第一步:诊断瓶颈——不要猜,要测

性能优化的第一原则:80%的优化效果来自20%的原因,但你必须先用数据找到那20%。

我在Milvus中开启了全链路追踪,发现三个关键瓶颈:

  1. 索引构建阶段:efConstruction设为500,索引构建时间2.3小时,但查询时efSearch只用了64。说明索引构建过度,浪费了内存和构建时间。
  2. 查询阶段:65%的查询耗时在磁盘IO上。Milvus的segment太大(2GB/segment),导致查询时需要扫描大量数据。
  3. 过滤阶段:带元数据过滤的查询比纯向量查询慢3倍。标量索引没有建立。

金句:大多数性能问题不是数据库不够快,而是你没有用对配置。

第二步:调索引参数——HNSW调优三件套

HNSW有三个核心参数:M、efConstruction、efSearch。它们的调优逻辑是:

  • M:每个节点的最大连接数。越大→召回率越高→内存占用越大→构建越慢。
  • efConstruction:构建索引时的搜索深度。越大→索引质量越高→构建越慢。
  • efSearch:查询时的搜索深度。越大→召回率越高→查询越慢。

我们的调优过程:

初始配置:M=16, efConstruction=500, efSearch=64
→ 召回率99.1%,P99延迟800ms(查询太慢,因为efSearch不够导致需要更多hop)

第一次调整:M=32, efConstruction=200, efSearch=128
→ 召回率98.5%,P99延迟350ms(M增大提升了图连通性,但efSearch还是不够)

第二次调整:M=32, efConstruction=200, efSearch=256
→ 召回率99.3%,P99延迟120ms(显著提升!但还可以优化)

最终配置:M=64, efConstruction=150, efSearch=200
→ 召回率99.0%,P99延迟8ms 🎉

关键发现:M翻倍(16→64)让查询延迟降低了15倍。 因为更大的M意味着图的连通性更好,搜索时需要的hop数更少。代价是内存占用增加了约3倍,但5GB→15GB的变化在128GB的RAM上完全不是问题。

金句:HNSW调优的核心矛盾是"用内存换延迟"。如果你的内存够用,大胆增大M。

第三步:Segment管理——别让碎片化杀死你的性能

Milvus的数据按segment存储。随着数据持续写入,segment会变得碎片化:大量小segment(每个几十MB)导致查询需要扫描更多文件。

解决方案:

  1. 手动触发compactioncollection.compact(),将小segment合并为大segment。建议每1000万次写入后执行一次。
  2. 调整segment大小collection_creation时设置segment_size为1GB(默认512MB)。更大的segment减少IO次数。
  3. 开启内存映射:如果内存充裕,配置mmap让热数据常驻内存,避免磁盘IO。

优化后,磁盘IO从占总耗时65%降到5%。

第四步:标量索引——容易被忽视的加速器

很多团队只关注向量索引,忘了给元数据字段建标量索引。我们的场景中,60%的查询带有元数据过滤(如"只搜索2025年的文档"、“限制在某个分类下”)。

优化前:带过滤的查询需要先做向量搜索,再遍历结果做过滤——1万条结果中遍历过滤出50条,浪费了大量计算。

优化后:给yearcategorystatus字段建立倒排索引,Milvus在向量搜索前先做过滤,搜索范围从5000万缩小到200万,性能提升25倍。

# 标量索引创建示例
collection.create_index(
    field_name="year",
    index_type="INVERTED"
)

金句:元数据过滤是向量搜索的隐形加速器。建标量索引只用1分钟,查询加速可能是10倍。

第五步:查询模式优化——批量查询和缓存

最终优化不在数据库层面,而在应用层:

  1. 批量查询:将多个单条查询合并为batch,减少网络往返。batch_size=32时,平均延迟降40%。
  2. 结果缓存:对高频查询(如热门问题、首页推荐)做结果缓存,命中率约35%,这些查询的延迟直接降到0.5ms。
  3. 查询超时设置:设置timeout=200ms,超时直接返回已有结果,避免慢查询拖垮整个系统。

最终效果

指标优化前优化后提升
P50延迟150ms5ms30x
P99延迟800ms8ms100x
最大QPS2003,50017.5x
召回率99.1%99.0%几乎不变
内存占用5GB15GB增加3x

金句:性能优化不是魔法,是诊断→假设→验证→迭代的工程闭环。