1. 问题背景:电商图片检索的选型困局
去年接手一个以图搜图需求:从200万张vivo手机壳图片中,通过ResNet50提取768维特征向量,完成实时检索。初期使用Faiss离线索引,但增量更新困难。转向开源向量数据库时,Milvus和Qdrant是最热的两个选项。
团队需要回答三个问题:
- 部署复杂度能否接受?(运维只有2人)
- 百万级数据下P99延迟能否低于50ms?
- 4C8G的云服务器能否扛住?
2. 环境与版本
硬件清一色腾讯云轻量服务器:
- CPU:4核(Intel Xeon Platinum 8255C)
- 内存:8GB
- 磁盘:50GB SSD
- 操作系统:Ubuntu 20.04 LTS
软件版本:
- Milvus:2.3.3(Standalone模式,Docker Compose部署)
- Qdrant:1.8.0(Docker单节点)
- 向量维度:768(ResNet50输出)
- 索引类型:HNSW(参数:M=16, ef_construction=200)
- 数据量:2,034,567条
3. 方案设计:两种部署架构对比
Milvus部署(含踩坑)
官方推荐Docker Compose,但第一次部署就踩坑——必须同时启动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
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
volumes:
- /data/minio:/data
standalone:
image: milvusdb/milvus:v2.3.3
command: ["milvus", "run", "standalone"]
depends_on:
- etcd
- minio
ports:
- "19530:19530"
启动后三个容器吃掉2.1GB内存,其中etcd占用470MB。这是第一个意外——文档说Standalone模式“轻量”,但实际并不轻。
Qdrant部署(真·单二进制)
Qdrant官方Docker镜像只有20MB,且无外部依赖:
docker run -d \
-p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
--name qdrant \
qdrant/qdrant:v1.8.0
启动后内存占用仅280MB。更激进的做法是直接用静态二进制文件(官方提供Linux x86_64的编译版本),连Docker都能省。
4. 核心实现:数据导入与检索
4.1 数据导入代码对比
Milvus端(使用pymilvus 2.3.3)
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
connections.connect(host='localhost', port='19530')
# 创建集合
fields = [
FieldSchema(name='id', dtype=DataType.INT64, is_primary=True),
FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, dim=768),
FieldSchema(name='product_id', dtype=DataType.VARCHAR, max_length=64)
]
schema = CollectionSchema(fields, description='vivo phone case images')
collection = Collection(name='phone_cases', schema=schema)
# 创建HNSW索引(这一步巨慢)
index_params = {
'metric_type': 'IP',
'index_type': 'HNSW',
'params': {'M': 16, 'efConstruction': 200}
}
collection.create_index('vector', index_params)
# 批量插入(每次10000条)
import numpy as np
batch_size = 10000
for i in range(0, len(vectors), batch_size):
batch_vectors = vectors[i:i+batch_size]
batch_ids = list(range(i, i+len(batch_vectors)))
batch_pids = product_ids[i:i+batch_size]
entities = [batch_ids, batch_vectors, batch_pids]
collection.insert(entities)
print(f'Inserted {i+len(batch_vectors)} records')
Qdrant端(使用qdrant-client 1.8.0)
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
client = QdrantClient(host='localhost', port=6333)
# 创建集合(索引参数直接在创建时指定)
client.recreate_collection(
collection_name='phone_cases',
vectors_config=VectorParams(
size=768,
distance=Distance.DOT,
hnsw_config={'m': 16, 'ef_construct': 200}
)
)
# 批量上传(同样10000条,但支持异步)
from qdrant_client.http import models
points = [
PointStruct(id=idx, vector=vec.tolist(), payload={'product_id': pid})
for idx, vec, pid in zip(range(len(vectors)), vectors, product_ids)
]
client.upload_points(
collection_name='phone_cases',
points=points,
batch_size=10000,
wait=True
)
注意Qdrant的upload_points方法内部自动处理分批和重试,而Milvus需要手动循环。Qdrant在200万数据全量导入耗时约7分钟,Milvus因为索引构建策略问题耗时23分钟。
4.2 检索性能对比
Milvus查询
search_params = {'metric_type': 'IP', 'params': {'ef': 64}}
results = collection.search(
data=[query_vector],
anns_field='vector',
param=search_params,
limit=10,
output_fields=['product_id']
)
Qdrant查询
results = client.search(
collection_name='phone_cases',
query_vector=query_vector,
limit=10,
with_payload=['product_id'],
search_params={'hnsw_ef': 64}
)
5. 踩坑与优化记录
5.1 Milvus的“内存泄漏”假象
部署后内存从1.2GB逐渐涨到3.8GB,以为是内存泄漏。后来发现是Milvus的knowhere引擎会预分配内存池,可以通过cache.cache_size限制:
# milvus.yaml 配置节选
cache:
cache_size: 1024 # 限制为1GB
但修改后重启,索引重建耗时翻倍。最终结论:Milvus的内存池设计对内存敏感场景不友好。
5.2 Qdrant的磁盘占用优化
Qdrant默认开启WAL(预写日志),200万数据占用磁盘12GB,而Milvus仅8.5GB。优化方案:
# 创建集合时关闭WAL(生产环境慎用)
client.recreate_collection(
collection_name='phone_cases',
vectors_config=VectorParams(
size=768,
distance=Distance.DOT,
hnsw_config={'m': 16, 'ef_construct': 200}
),
wal_config={'enabled': False} # 关闭WAL
)
关闭后磁盘降至9.2GB,但牺牲了故障恢复能力。最终我们选择保留WAL,因为电商场景不能丢数据。
6. 效果数据:实测性能对比
测试条件:并发100个请求,每个请求返回top-10,连续跑30分钟。
| 指标 | Milvus 2.3.3 | Qdrant 1.8.0 |
|---|---|---|
| P50延迟 | 8ms | 5ms |
| P95延迟 | 28ms | 12ms |
| P99延迟 | 67ms | 22ms |
| 内存占用(稳态) | 3.2GB | 1.9GB |
| 磁盘占用 | 8.5GB | 12GB |
| 索引构建时间 | 23min | 7min |
| 容器数量 | 3 | 1 |
| 首条查询时间 | 41s(需等索引加载) | 2s(即时可用) |
值得注意的点:
- Qdrant的P99延迟明显更稳定,得益于Rust实现的零拷贝设计
- Milvus在高并发下偶发超时(1.2%请求超500ms),Qdrant未出现
- 两者召回率一致(在相同HNSW参数下均为99.2%)
7. 总结:选型建议
如果团队面临类似场景,我的建议是:
选Qdrant当:
- 服务器内存≤8GB
- 要求快速部署(1个Docker容器搞定)
- 对P99延迟敏感(比如实时搜图)
- 数据量在千万级以下
选Milvus当:
- 已有K8s集群,需要分布式扩展
- 数据量过亿,需要分片+负载均衡
- 需要多向量字段(如同时存图片和文本向量)
- 团队有专门的运维支持
最后说句实话:这次对比让我对Rust重写基础设施有了信心。Qdrant的5000+行Rust代码确实比Milvus的C+++Go组合更可控。但Milvus的社区活跃度和文档丰富度仍是优势。选型没有银弹,只有最适合当前团队和场景的方案。