当前位置: 首页 > news >正文

我把向量数据库从 Milvus 切到 pgvector 后,检索 P99 从 230ms 压到 18ms:这 4 个取舍要注意

我把向量数据库从 Milvus 切到 pgvector 后,检索 P99 从 230ms 压到 18ms:这 4 个取舍要注意

RAG 上线三个月后,检索 P99 突然从 80ms 涨到 230ms。

排查了一圈,问题不在 embedding 模型,也不在向量维度,而是每次查询都要跨一次网络去 Milvus。我们的主服务连着 PostgreSQL,向量库却单独跑在一套 K8s 里,中间隔着 DNS、负载均衡、序列化反序列化,延迟想低都难。

后来我把向量检索整体迁到了 pgvector,和业务库共用一个 PostgreSQL。改造完后 P99 压到 18ms,架构也清爽了不少。但这不是无脑替换,有几个取舍必须提前想清楚。

老架构:Milvus 确实强,但对我们太重

最开始选型 Milvus 是合理的。它专为向量设计,支持十亿级规模、分布式、GPU 索引,文档也成熟。我们的场景是:把 100 多万条文档切片向量化,供 RAG 检索。

架构是:

应用服务 → PostgreSQL(业务数据) → Milvus(向量检索)

问题慢慢暴露:

  • 多一套基础设施:Milvus 需要独立集群、独立监控、独立备份。
  • 跨网络查询:每次 RAG 检索要先去 Milvus 拿 top-k,再回 PostgreSQL 查业务字段。
  • 运维成本高:一次 Milvus 升级导致部分 collection 不可见,排查了两个小时。
  • 资源利用率低:我们的向量量级其实远没触达 Milvus 的优势区间。

我算了笔账:我们日均向量查询大概 50 万次,数据量不到 200 万条,向量维度 768。Milvus 的猛兽级能力,我们只用到了它 10% 的功能。为这 10% 的功能多养一套集群,性价比不高。

切到 pgvector:把向量存回业务库

pgvector 是 PostgreSQL 的向量扩展。安装就是一句CREATE EXTENSION vector;,然后表结构可以长这样:

CREATEEXTENSIONIFNOTEXISTSvector;CREATETABLEdocument_chunks(id BIGSERIALPRIMARYKEY,doc_idBIGINTNOTNULL,chunk_indexINTNOTNULL,contentTEXT,embedding VECTOR(768));CREATEINDEXONdocument_chunksUSINGhnsw(embedding vector_cosine_ops)WITH(m=16,ef_construction=64);

把向量字段和业务字段放在同一张表里,最直接的好处是:检索结果自带业务字段,不需要跨库 JOIN。一个 SQL 就能把向量和业务上下文一起带出来。

查询语句:

SELECTdoc_id,chunk_index,content,embedding<=>:query_vecASdistanceFROMdocument_chunksWHEREdoc_idIN(SELECTdoc_idFROMuser_docsWHEREuser_id=:uid)ORDERBYembedding<=>:query_vecLIMIT10;

这里WHERE doc_id IN (...)是 RAG 里很常见的业务过滤。在 Milvus 里,这种过滤要通过标量过滤或表达式,复杂一点就报错;在 PostgreSQL 里,直接写 SQL,爽多了。

压测方法:不能只盯平均延迟

切之前我做了两轮压测。第一轮只看平均延迟,Milvus 和 pgvector 差距不大,差点让我放弃。

后来发现真正的痛点是长尾。Milvus 的 P99 在业务过滤复杂时会突然跳到 200ms 以上,因为标量过滤和向量检索不是同一套执行路径,协调层会把请求拆成多段。pgvector 走的是 PostgreSQL 自己的执行器,计划和索引可以统一优化。

压测脚本我用的是 Python + asyncio,并发 100 个请求,每个请求随机抽 100 条向量做 top-10 检索:

importasyncio,time,random,psycopgfromcollectionsimportdeque DSN="postgresql://user:pass@localhost/db"latencies=deque()asyncdefquery_one(pool,qvec):t0=time.perf_counter()asyncwithpool.connection()asconn:asyncwithconn.cursor()ascur:awaitcur.execute("""SELECT doc_id, content FROM document_chunks ORDER BY embedding <=> %s LIMIT 10""",(qvec,))awaitcur.fetchall()latencies.append((time.perf_counter()-t0)*1000)asyncdefmain():pool=psycopg.AsyncConnectionPool(DSN,min_size=5,max_size=20)qvecs=[...]# 100 条查询向量awaitasyncio.gather(*[query_one(pool,random.choice(qvecs))for_inrange(1000)])sorted_lat=sorted(latencies)print(f"P50={sorted_lat[500]:.1f}ms P99={sorted_lat[990]:.1f}ms")asyncio.run(main())

注意:压测时要把 PostgreSQL 连接池和客户端连接池分开。pgvector 的 HNSW 查询主要是 CPU 计算,连接池太小会把 PG 的并行度压死。

性能对比:P99 230ms → 18ms

迁移前后的数据:

指标Milvus 方案pgvector 方案备注
平均检索延迟45ms6ms同机房,100 并发
P99 检索延迟230ms18ms含业务过滤
网络往返2 次(服务→Milvus→PG)1 次同库
索引构建时间约 15 分钟约 8 分钟200 万条,HNSW
运维组件Milvus + Etcd + MinIOPostgreSQL 本身组件少很多
内存占用约 16 GB约 19 GBHNSW 索引在 PG shared_buffers 外

P99 的提升主要是消除了跨服务的网络抖动。业务过滤在 PostgreSQL 里走 B+ 树,比在 Milvus 里做标量过滤更稳定。内存虽然多了 3 GB,但省掉一套独立集群的 overhead,整体成本反而更低。

4 个取舍:不是银弹

1. 索引类型:HNSW 还是 IVFFlat?

pgvector 支持 IVFFlat 和 HNSW。IVFFlat 省内存,但高维数据召回和延迟都不如 HNSW;HNSW 查询快、召回高,但构建慢、内存占用大。

我们测过:

  • IVFFlat(lists=100):P99 约 55ms,召回 92%
  • HNSW(m=16, ef=64):P99 约 18ms,召回 97%

最后选了 HNSW。内存多用了 20%,但延迟和召回都更稳。如果你的数据量小、查询频率低,IVFFlat 也能用。

关键参数是m(每个节点最大出度)和ef_construction(构建时搜索宽度)。维度 768 的话,m=16ef_construction=64是常见起点;数据量超过 500 万可以适当调大。

2. 和业务库共存:资源会打架吗?

最担心的问题是:向量检索把 PostgreSQL 的 CPU/IO 吃光,影响业务事务。

我们的做法:

  • 单独表空间:把document_chunks放在独立的 SSD 表空间,避免和事务表争 IOPS。
  • 连接池隔离:RAG 查询走只读副本,写入走主库。
  • autovacuum 调优:大表频繁更新时,autovacuum 会抢锁。我们加了autovacuum_vacuum_scale_factor = 0.02
ALTERTABLEdocument_chunksSET(autovacuum_vacuum_scale_factor=0.02);

另外,HNSW 索引在查询时几乎不锁表,但CREATE INDEX会长时间加锁。上线时可以用CREATE INDEX CONCURRENTLY避免阻塞业务。

3. 向量更新:批量写入还是实时更新?

pgvector 的 HNSW 索引在更新时不是完全无锁的。高频单条INSERTUPDATE会导致索引页竞争,写入延迟飙升。

我们的做法是:业务端先把向量变更写到 staging 表,每分钟批量INSERT到主表,并用pg_advisory_lock串行化索引更新。

importpsycopg2defbatch_insert_embeddings(rows):withpsycopg2.connect(DSN)asconn:withconn.cursor()ascur:cur.execute("SELECT pg_advisory_lock(42)")cur.executemany("""INSERT INTO document_chunks (doc_id, chunk_index, content, embedding) VALUES (%s, %s, %s, %s)""",rows)cur.execute("SELECT pg_advisory_unlock(42)")conn.commit()

实际写入峰值从单条 200ms 降到了批量 15ms。如果更新频率不高,也可以直接单条写,但要把事务控制在合理的并发数内。

4. 扩展天花板:什么时候该回退?

pgvector 不是替代 Milvus 的万能药。如果未来向量规模涨到几千万、几亿,或者需要多租户隔离、混合查询极其复杂,我们可能还要重新考虑专用向量数据库。

目前定的阈值是:

  • 数据量 < 500 万条:pgvector 够用。
  • 日均查询 < 100 万:pgvector 能扛。
  • 超过任一阈值:评估重新拆分出专用向量库。

这里的数据量不是纯向量数,而是“活跃索引中的向量数”。如果旧数据可以归档,pgvector 的寿命会更长。

监控看板:我们盯了哪些指标

切过去后,我搭了一个简单的监控看板,核心指标就三个:

  • pg_stat_user_tables.n_tup_insn_tup_upd:观察向量写入速率,防止单条更新把索引拖垮。
  • pg_stat_activitystate = active的查询数:确认连接池没有被打满。
  • 业务层的 P99 检索延迟:用 Prometheus histogram 直接埋点,比看数据库平均延迟更真实。
SELECTschemaname,relname,n_tup_ins,n_tup_upd,n_live_tupFROMpg_stat_user_tablesWHERErelname='document_chunks';

这几个指标够用,不用整太复杂。

迁移踩坑记录

1.pgvector扩展版本问题

旧版 PostgreSQL 13 的 pgvector 版本太低,不支持 HNSW。我们先升级到了 PostgreSQL 15,再装扩展。

2. 距离函数选错

我们默认用了<->(L2),但 embedding 模型用的是 cosine 归一化,应该用<=>。换到 cosine 后,召回率提升了 4%。

3. 索引参数没调

默认 HNSW 参数m=16, ef_construction=64对 768 维数据够用,但ef查询参数(在 SQL 里设置SET hnsw.ef = 32)对 P99 影响很大。调到 64 后召回和延迟达到我们想要的平衡点。

SEThnsw.ef=64;

4. 并发写入时索引膨胀

一开始用多线程同时写,结果表和索引都膨胀得厉害。改成单连接批量 +pg_advisory_lock后稳定下来。

写在最后

把 Milvus 切到 pgvector,不是因为它更好,而是因为它更适合我们当前的数据规模和团队栈。

省掉一套独立中间件,意味着少一套监控、少一份 on-call 压力、少一堆跨服务问题。代价是你要接受 PostgreSQL 的资源边界,并学会在 HNSW 索引、批量写入、查询参数之间做权衡。

如果你的向量量级和我差不多,主库已经是 PostgreSQL,不妨先试试 pgvector。别急着上猛兽,合适比先进更重要。

http://www.cnnetsun.cn/news/3542885.html

相关文章:

  • 用群晖给 ESXi 自动续期 Let‘s Encrypt 证书:三个官方文档没写的坑
  • 一文读懂PARD2-Llama-3.1-8B的Confidence-Adaptive Token技术:提升模型接受率的关键
  • ReAct 和 Plan and Solve 理解
  • 【Bug已解决】macOS detects Codex Computer Use.app as malware and deletes it! 解决方案
  • 鸿蒙Flutter Center与Align:组件对齐方式
  • 如何用Path of Building 2精准规划PoE2角色构建?3大核心功能深度解析
  • 逆变器芯片失效分析与防护设计实践
  • AI数据基础设施预计有1984亿规模?爱分析拆解七大细分市场构成
  • 163MusicLyrics:跨平台云音乐歌词获取与处理工具的深度解析
  • 旧款Mac免费升级macOS终极指南:用OpenCore Legacy Patcher重获新生
  • Markdown-Edit终极指南:Windows平台最简洁的Markdown编辑器完全解析
  • git-pr-release安全配置:保护你的GitHub Token和API访问完整指南
  • Codex全套科研技能汇总
  • React Native 跨平台图片浏览器开发:iOS 与 Android 兼容性指南
  • 如何3分钟打造专业级foobar2000美化方案:终极视觉与功能升级指南
  • C++字符编码转换实战:libiconv解决乱码问题
  • 成都网站建设木木科技:踩坑无数后,我为什么最终选了这家?
  • 怎么在网站上建设投票统计:从混乱到清晰的实战指南
  • 网站建设四段合一:告别割裂式开发,一次搞定设计与落地
  • 营销网站建设都是专业技术人员吗?别被忽悠了,这行水很深
  • Avalonia:3个核心模块实现跨平台.NET桌面应用的终极解决方案
  • Mac Mouse Fix:让你的普通鼠标在macOS上超越苹果触控板的终极方案
  • ISO 13355:2016是什么标准,ISO 13355随机振动试验包装测试
  • 075、语义分割驱动的局部调优:场景感知的影像增强
  • 数据库的概述--常用SQL命令
  • Runway官方未公布的12个生产力捷径:Ctrl+Shift+X触发的隐藏功能,仅限Beta测试者知晓
  • AI接口熔断与重试机制实战:Spring Retry与Sentinel应用
  • 进阶 XXE 漏洞攻防:从基础探测到高阶利用,附防御修复思路
  • VSCode配置python虚拟环境路径
  • Rust条件控制进阶:从if/else到模式匹配与函数式编程