1. 问题背景:RAG应用扛不住并发,先别急着加机器
上个月我们给电商搜索团队做一个“以图搜图”功能,底层是标准的RAG架构:CLIP模型把商品图变成768维向量,丢进向量数据库,然后根据用户上传图做相似度检索。初期数据量只有200万,用ES的dense_vector凑合跑,延迟在80ms左右。
但业务方突然说要接入全量商品库——1000万条,ES直接崩了,查询P99飙到1.2秒。CTO给我一周时间选型并落地。我当时在Milvus和Qdrant之间犹豫,因为两者都支持HNSW索引、都提供Python SDK、都能部署在K8s上。但网上对比文章大多是“部署体验”层面的,没有我这种真实业务场景的压测数据。
所以我自己搭了两个集群,跑了三天三夜的压测。本文所有数据均来自我的测试环境,不是官方benchmark,因为官方测的是纯向量检索,而我需要的是“标签过滤 + 向量检索”这种混合查询。
2. 环境与版本:别用最新版,用稳定版
硬件配置(两台物理机,避免云上网络抖动干扰测试):
- CPU:Intel Xeon Gold 6330,32核64线程
- 内存:256GB DDR4
- 磁盘:NVMe SSD,顺序读2.8GB/s
- 网络:万兆内网
软件版本:
- Milvus 2.4.1(官方推荐稳定版,不是2.5那个beta)
- Qdrant 1.9.2(带--force-sync参数,因为单机模式)
- Python 3.10 + pymilvus 2.4.4 + qdrant-client 1.12.1
- 数据:1000万条,768维float32,每条附带category(10类)和price(浮点)两个过滤字段
部署方式都是Docker Compose,没用K8s。原因很简单:K8s operator的版本兼容问题太多,我上周刚被Milvus-operator 0.1.4坑过(它会强制拉取旧版etcd镜像)。生产环境如果是K8s,建议直接用Helm,但测试阶段Docker足够。
3. 方案设计:同样架构,不同配置
我的核心查询模式是:
SELECT id, vector FROM items WHERE category = 'electronics' AND price = 10000:
client.upsert(collection_name='product_vectors', points=points, wait=False)
points = []
性能差异初现:导入1000万条,Milvus耗时1小时52分钟(28k QPS),Qdrant耗时2小时17分钟(21k QPS)。但Milvus在导入过程中内存峰值到98GB,Qdrant只有72GB。因为Milvus的shards_num=4会创建4个独立的HNSW图,每个图维护独立的visit_list,内存开销是单图的4倍。
5. 踩坑与优化:一个让服务“假死”的Bug
压测查询是重点。我写了个脚本模拟50并发,混合查询(30%纯向量检索,70%带过滤条件)。跑了30分钟后,Qdrant突然所有查询超时(10秒无响应),但CPU只有45%——典型的“假死”现象。
排查发现:Qdrant的ef参数(搜索时动态调整HNSW探索宽度)默认是None,它会根据查询向量和索引规模自动调整。但我的过滤条件category='electronics'命中率只有10%(约100万条),Qdrant会退化成长表扫描——它要先过滤出100万条候选,再在这些候选里跑HNSW。
解决方案:在client.search里手动设置ef=64,并利用Qdrant的payload_selector字段把过滤下推到索引层:
# 优化后的Qdrant查询
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
filter_condition = Filter(
must=[
FieldCondition(key='category', match=MatchValue(value='electronics')),
FieldCondition(key='price', range=Range(lt=500))
]
)
results = client.search(
collection_name='product_vectors',
query_vector=query_vec,
query_filter=filter_condition, # 关键:把过滤下推到索引
limit=10,
ef=64, # 手动设置,避免长表扫描
with_payload=False
)
Milvus这边没遇到这种问题,因为它的partition by category天然做了数据分片,而price上的RangeIndex(默认就是)能快速过滤。但Milvus也有坑:如果efSearch参数设置过大(比如超过256),内存会暴涨。官方文档说efSearch范围是1-65535,我设了512后,单个查询的内存峰值从200MB涨到1.2GB,而且P99延迟从30ms涨到90ms——收益是召回率只提升0.8%,完全没必要。
最终调优参数:
- Milvus:efSearch=128,metric_type=IP,shards_num=4
- Qdrant:ef=128,payload_mapping="category"(对高基数字段建映射索引)
6. 效果数据:延迟、吞吐、资源占用全对比
压测结果(50并发,混合查询,运行2小时,P99统计):
| 指标 | Milvus 2.4.1 | Qdrant 1.9.2 | 差异 |
|---|---|---|---|
| 纯向量检索 P99 | 28ms | 35ms | Milvus快25% |
| 过滤+向量检索 P99 | 42ms | 105ms | Qdrant慢150% |
| 纯向量检索 QPS | 1800 | 1500 | Milvus高20% |
| 过滤+向量 QPS | 950 | 420 | Milvus高126% |
| 内存占用(稳态) | 87GB | 63GB | Milvus高38% |
| 磁盘占用(HNSW+原始) | 112GB | 98GB | Milvus高14% |
结论很明显:如果你和我一样,查询里离不开metadata过滤(比如商品类目、价格区间、用户标签),选Milvus。它的分区+标量索引组合拳更成熟。但如果你做的是纯向量检索(比如人脸比对、重复图片检测),Qdrant更省内存,单机就能扛住大模型,还能省一台服务器。
最后补充一个资源占用的细节:Milvus的querynode进程在空闲时也会保持约40GB的常驻内存(因为HNSW图常驻),而Qdrant在空闲时会释放到20GB(缓存LRU策略)。如果你的业务有明显的波峰波谷(比如白天高并发、晚上低流量),Qdrant更适合——它可以缩容到一台机器,Milvus做不到这么灵活。
7. 总结:别迷信官方benchmark,拿自己的数据说话
这次选型花了3天,但值得。我踩过的坑希望你避开:
1. Milvus的内存配置:shards_num不是越大越好,我试过8分片,导入吞吐没提升,内存却翻倍了。
2. Qdrant的过滤查询:一定搞清楚query_filter会不会走索引,用explain方法验证。
3. 压测脚本要混合查询:纯向量检索的benchmark没有参考价值,业务里一定带过滤。
4. 版本锁死:Milvus 2.4和Qdrant 1.9是当前最稳的组合,别急着升级2.5或1.10,等社区把坑填平再说。
如果你的数据量在500万以下,用Qdrant单机就够了;如果千万级且过滤查询多,直接上Milvus集群。至于官方说的“Qdrant比Milvus快3倍”,那是在他们自己的测试集上,我这边结论是反过来的。所以——拿自己的数据测,别信任何人的“性能对比”。