1. 问题背景:商品向量检索的选型纠结
上个月我们团队接手一个电商以图搜图需求,商品库约100万SKU,每张商品图经过CLIP模型抽成128维向量。要求:单机部署(老板不给加机器)、QPS至少200、P95延迟<80ms、支持增量更新。
调研时发现Milvus和Qdrant是两个热门选择。Milvus生态全、文档多,但组件多(依赖etcd、MinIO);Qdrant是Rust写的,单二进制文件,部署极简。我们决定在真实业务数据下做一次硬核对比,不只看官网benchmark。
2. 环境与版本:两台相同的裸金属机器
- 硬件:Intel Xeon Gold 6248R(32核),128GB DDR4,1TB NVMe SSD(顺序读1.8GB/s)
- 系统:Ubuntu 22.04 LTS,内核5.15
- Docker版本:24.0.5
- 向量数据:100万条,128维float32,共512MB,从真实图片特征随机抽样生成
- 查询负载:512个并发线程,每个线程随机选100个query向量,共51200次查询,记录P50/P95/P99
Milvus版本:2.4.5(独立部署模式,含etcd 3.5.9、MinIO RELEASE.2024-01-13T07-53-03Z)
Qdrant版本:1.9.7(单节点模式,使用默认RocksDB存储)
3. 方案设计:两套部署,公平对比
对比原则:都使用Docker部署,存储目录挂载到SSD,都开启HNSW索引(M=16, efConstruction=200)。
Milvus部署(docker-compose.yml核心片段):
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.9
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
volumes:
- /data/milvus/etcd:/etcd
minio:
image: minio/minio:RELEASE.2024-01-13T07-53-03Z
command: minio server /minio_data --console-address ":9001"
volumes:
- /data/milvus/minio:/minio_data
milvus:
image: milvusdb/milvus:v2.4.5
command: ["milvus", "run", "standalone"]
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
ports:
- "19530:19530"
volumes:
- /data/milvus/milvus:/var/lib/milvus
depends_on:
- etcd
- minio
Qdrant部署(单容器极简):
docker run -d \
--name qdrant \
-p 6333:6333 \
-v /data/qdrant/storage:/qdrant/storage \
-e QDRANT__SERVICE__GRPC_PORT=6334 \
qdrant/qdrant:v1.9.7
Qdrant启动后通过REST API创建collection并设置HNSW参数:
import requests
# 创建collection,配置HNSW
resp = requests.put(
"http://localhost:6333/collections/products",
json={
"vectors": {
"size": 128,
"distance": "Cosine",
"hnsw_config": {
"m": 16,
"ef_construction": 200
}
}
}
)
print(resp.status_code) # 200
4. 核心实现:数据灌入与查询压测脚本
Milvus端使用PyMilvus批量写入,开启async模式提高吞吐:
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility
connections.connect(alias="default", host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields, "product_vectors")
collection = Collection("products", schema, consistency_level="Bounded")
# 分批插入,5000条一批,异步flush
import numpy as np
for i in range(0, 1000000, 5000):
ids = list(range(i, i+5000))
vectors = np.random.rand(5000, 128).astype(np.float32).tolist()
collection.insert([ids, vectors])
if i % 200000 == 0:
collection.flush()
collection.create_index("embedding", {"index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 16, "efConstruction": 200}})
collection.load()
print(f"Milvus total entities: {collection.num_entities}")
Qdrant端用Python客户端批量upsert,关闭wait参数强制异步:
from qdrant_client import QdrantClient
import numpy as np
client = QdrantClient(host="localhost", port=6333, prefer_grpc=True)
# 批量上传,不等待结果
for i in range(0, 1000000, 5000):
ids = list(range(i, i+5000))
vectors = np.random.rand(5000, 128).astype(np.float32).tolist()
client.upsert(
collection_name="products",
points=[{"id": sid, "vector": vec} for sid, vec in zip(ids, vectors)],
wait=False
)
print(f"Qdrant count: {client.count(collection_name='products').count}")
查询压测统一使用并发请求,记录延迟分布。
5. 踩坑与优化:几个意想不到的差异
坑1:Milvus默认consistency_level=Strong导致写入超时
用Bounded级别后写入吞吐从2800条/s提升到4100条/s。注意Bounded需要配合guarantee_timestamp,我们直接省略了flush调用,依赖Milvus后台落盘。
坑2:Qdrant的wait=False会丢失少量写入?
实测100万条后count为999986,少了14条。排查发现是RocksDB的WAL在非正常关机时丢尾部。解决:生产环境建议wait=True但调大batch_size到10000,吞吐仅下降8%,但保证持久性。
坑3:Milvus的HNSW索引构建慢
100万条构建索引耗时132秒,Qdrant仅78秒。但Milvus支持在线建索引(无需停写),Qdrant建索引会把CPU打满,需要暂停写入。
优化:Qdrant设置memmap_threshold
默认Qdrant把所有向量放内存,128GB内存够用但占用高。我们设置memmap_threshold=1000000强制向量落盘,内存占用从7.2GB降到3.1GB,P95延迟仅增加2ms。
6. 效果数据:实测结果对比
| 指标 | Milvus 2.4.5 | Qdrant 1.9.7 |
|---|---|---|
| 数据导入耗时(100万条) | 243秒 | 198秒 |
| 索引构建耗时(HNSW) | 132秒 | 78秒 |
| P50查询延迟 | 8ms | 6ms |
| P95查询延迟 | 18ms | 12ms |
| P99查询延迟 | 35ms | 27ms |
| QPS(512并发) | 620 | 780 |
| 内存峰值 | 9.2GB | 4.8GB(优化后3.1GB) |
| 磁盘占用 | 1.1GB(含etcd+MinIO) | 0.6GB |
| 崩溃恢复时间(kill -9后重启) | 45秒 | 12秒 |
结论:Qdrant在查询性能、资源占用、部署复杂度上全面占优。Milvus的优势在于生态(自带监控面板、支持GPU索引)和分布式扩展能力,但我们单机场景用不上。
7. 总结与选型建议
如果你的场景是单机、向量规模<500万、追求低延迟和运维简单,无脑选Qdrant。它一个二进制搞定,内存占用低,P95延迟比Milvus低33%。如果未来要扩展到千万级向量、需要跨节点分片、或者团队已有K8s和监控体系,选Milvus更稳妥——它的分布式是原生的,Qdrant的分布式(1.10+)还在快速演进中。
最后吐槽一句:Milvus的docker-compose依赖三个服务,排查问题时要同时看etcd和MinIO日志,对新手不友好。Qdrant出问题基本就一个日志文件,省心太多。至于网上说的“Milvus查询比Qdrant快”,在我们这个真实负载下没体现出来,还是得自己测。