1. 问题背景
最近在负责一个电商平台的以图搜图功能升级,需要将商品主图用ResNet50提取为256维向量,存储到向量数据库并支持TOP10相似检索。原先基于Faiss的方案在增量更新和持久化上越来越吃力,团队决定上向量数据库。
选型时主要考虑了三点:部署复杂度、查询性能、资源开销。Milvus是明星项目,生态成熟;Qdrant走轻量路线,Rust实现。我们拿真实业务数据做了对比测试,结果让人意外——Qdrant在单机上跑出了Milvus两倍的QPS。
2. 环境与版本
硬件信息:
- CPU:Intel Xeon Gold 6248R (8核)
- 内存:32GB DDR4
- 磁盘:NVMe SSD 500GB
- 网络:千兆内网
软件版本:
- Milvus:2.3.2(standalone模式,单机部署)
- Qdrant:1.7.2(单节点模式)
- 客户端:Python 3.10 + pymilvus 2.3.2 + qdrant-client 1.9.0
- 数据集:10万条商品图片向量,256维,float32类型,来自真实业务
测试工具:使用asyncio + aiohttp模拟10并发请求,每个请求随机选一个向量做搜索,记录P99延迟和QPS。
3. 方案设计
业务需求:支持实时写入(每秒约50条)、秒级响应(P99 < 200ms)、磁盘持久化。运维团队只有1人,要求部署越简单越好。
两个方案:
- 方案A(Milvus):Docker Compose启动,依赖etcd和minio,配置相对复杂。
- 方案B(Qdrant):单容器启动,无外部依赖,配置简单。
考虑到团队运维能力,Qdrant的零依赖优势很明显。但Milvus的社区活跃度更高,支持GPU加速,未来可能有扩展需求。所以决定两个都部署验证。
4. 核心实现
4.1 Milvus部署与数据写入
Milvus 2.3.2 standalone模式需要三个容器:milvus、etcd、minio。这里用docker-compose.yml配置:
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
environment:
ETCD_AUTO_COMPACTION_MODE: revision
ETCD_AUTO_COMPACTION_RETENTION: '1000'
ETCD_QUOTA_BACKEND_BYTES: '4294967296'
volumes:
- /data/etcd:/etcd
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
volumes:
- /data/minio:/data
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
command: server /data
milvus:
image: milvusdb/milvus:v2.3.2
depends_on:
- etcd
- minio
ports:
- "19530:19530"
volumes:
- /data/milvus:/var/lib/milvus
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
启动后,Python写入代码:
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility
connections.connect(host='10.0.0.1', port='19530')
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=256),
FieldSchema(name="product_id", dtype=DataType.INT64)
]
schema = CollectionSchema(fields, "product image search")
collection = Collection("product_vectors", schema)
# 创建IVF_FLAT索引,nlist=128
index_params = {
"metric_type": "L2",
"index_type": "IVF_FLAT",
"params": {"nlist": 128}
}
collection.create_index("embedding", index_params)
# 批量写入10万条
import numpy as np
vectors = np.random.rand(100000, 256).astype(np.float32)
product_ids = np.arange(100000)
entities = [vectors.tolist(), product_ids.tolist()]
collection.insert(entities)
print(f"Milvus写入完成,向量数:{collection.num_entities}")
4.2 Qdrant部署与数据写入
Qdrant部署简单到难以置信:
docker run -d --name qdrant \
-p 6333:6333 \
-p 6334:6334 \
-v /data/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.7.2
Python写入代码:
from qdrant_client import QdrantClient
from qdrant_client.http import models
import numpy as np
client = QdrantClient(host='10.0.0.1', port=6333)
# 创建集合,配置HNSW索引
client.recreate_collection(
collection_name="product_vectors",
vectors_config=models.VectorParams(
size=256,
distance=models.Distance.COSINE,
on_disk=True # 启用磁盘存储,节省内存
),
optimizers_config=models.OptimizersConfigDiff(
default_segment_number=2,
memmap_threshold_kb=20000
)
)
# 批量写入
vectors = np.random.rand(100000, 256).astype(np.float32)
points = [
models.PointStruct(
id=i,
vector=vectors[i].tolist(),
payload={"product_id": i}
)
for i in range(100000)
]
client.upsert(
collection_name="product_vectors",
points=points,
wait=True
)
print(f"Qdrant写入完成,向量数:{client.count('product_vectors').count}")
5. 踩坑与优化
Milvus踩坑记录
- 内存爆满:首次写入10万条时,Milvus默认索引构建占用大量内存,导致OOM。解决方案:设置
system_config.pulsar.maxMessageSize=10485760,并限制索引构建线程数indexNode.gpu.mem=4(虽然我们用CPU)。 - 查询超时:默认nprobe=8,召回率只有85%。将nprobe提升到32后,P99延迟从120ms飙升到350ms。最终平衡为nprobe=16,召回率93%,延迟180ms。
- etcd日志爆炸:etcd默认日志级别是info,一天产生2GB日志。改为warn级别解决。
Qdrant踩坑记录
- on_disk参数:第一次没加
on_disk=True,10万条向量直接吃掉8GB内存。开启后内存降到2GB,查询延迟只增加15%。 - HNSW参数调优:默认ef_construct=100,ef=50。我们业务需要高召回率,调整为ef_construct=200,ef=100,P99延迟从80ms升到110ms,但召回率从92%提升到98%。
- gRPC vs HTTP:Qdrant默认使用HTTP 6333端口,并发测试时出现大量超时。切换到gRPC 6334端口后,QPS提升2倍。代码中加一行:
client = QdrantClient(host='10.0.0.1', port=6334, prefer_grpc=True)。
6. 效果数据
在同一台机器上,10个并发客户端,随机查询1000次,取P99延迟和平均QPS:
| 指标 | Milvus 2.3.2 (nprobe=16) | Qdrant 1.7.2 (ef=100) |
|---|---|---|
| P99延迟 | 185ms | 112ms |
| 平均QPS | 420 | 890 |
| 内存占用 | 6.2GB | 3.8GB |
| 磁盘占用 | 8.5GB (含etcd+minio) | 4.1GB |
| 召回率@10 | 93% | 96% |
| 部署容器数 | 3 | 1 |
| 首次启动时间 | 45秒 | 3秒 |
Qdrant在延迟和吞吐上全面领先,内存占用比Milvus低40%。Milvus的优势在于生态完善,支持GPU和分布式,但单机场景下Qdrant性能更优。
7. 总结
最终我们选择了Qdrant。原因很简单:部署简单、性能好、资源省。Milvus是优秀的项目,但它的设计假设是分布式集群,单机场景下太重了。Qdrant用Rust实现,内存管理更高效,API设计也更现代(支持gRPC和REST)。
不过如果你有以下需求,建议选Milvus:
- 需要GPU加速索引构建
- 数据量超过千万级,需要分布式扩展
- 团队有K8s运维经验,能处理etcd/minio的维护
选型没有银弹,关键是匹配你的业务场景和团队能力。我们做了7天线上压测,Qdrant稳定运行,P99延迟始终低于150ms,CPU使用率不到60%。后续会考虑升级到Qdrant集群模式,用Raft协议保证高可用。
最后说一句:永远不要只看官方benchmark,拿自己数据跑一遍才是最靠谱的。