一、业务背景与选型困境
我们团队负责电商平台的拍照搜商品功能,底层是CLIP模型输出的768维向量。数据量从年初的80万涨到了500万,原来的暴力检索(NumPy+Faiss)已经撑不住了——单次查询要200ms+,内存直接爆掉。当时摆在我面前的是两个热门选项:Milvus和Qdrant。
Milvus的生态最成熟,社区文档多,但组件多(etcd、MinIO、Pulsar),运维重。Qdrant是Rust写的单二进制文件,部署极简,有内置压缩。我们团队只有3个后端,没有专职运维,所以我的首要诉求是能快速跑起来、别半夜报警。本文记录了我从部署到压测的全过程,用真实数据帮大家做判断。
二、测试环境与版本锁定
所有测试都在同一台物理机上进行,避免硬件差异干扰:
| 配置项 | 参数 |
|---|---|
| CPU | Intel Xeon 8488C @ 2.4GHz (32核) |
| 内存 | 128GB DDR5 |
| 磁盘 | NVMe SSD (2TB, RAID0) |
| 操作系统 | Ubuntu 22.04 LTS (内核 5.15.0) |
| 向量维度 | 768维 |
| 数据量 | 500万条(float32,约15GB原始数据) |
| 距离函数 | Cosine相似度 |
| 索引类型 | Milvus: HNSW |
| 版本 | Milvus 2.4.5 (standalone模式) |
我特意把Milvus跑在standalone模式(单机),因为Qdrant也只有单机,这样对比才公平。Milvus的分布式(K8s)部署后面单独说,那玩意儿配置起来太痛苦了。
三、部署步骤:一个简单一个复杂
3.1 Qdrant:一条命令搞定
Qdrant的部署简单到让人感动,官方提供Docker镜像,一条命令启动:
# 拉取镜像并启动,端口6333是gRPC,6334是HTTP
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v /data/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.12.4 \
--config-path /qdrant/config/config.yaml
如果你不想用Docker,官方还提供静态编译的二进制文件,直接./qdrant就能跑。我最初用的就是二进制方式,方便systemd托管,内存管理更直观。
3.2 Milvus:全家桶套餐
Milvus 2.4.5的standalone模式虽然叫"单机",但依赖组件一个不少:etcd(元数据)、MinIO(对象存储)、而且还需要单独的milvus-server进程。官方推荐用Docker Compose启动:
# 从官方仓库下载docker-compose.yml
wget https://github.com/milvus-io/milvus/releases/download/v2.4.5/milvus-standalone-docker-compose.yml
docker-compose up -d
启动后检查一下:
# 确认所有组件都healthy
docker ps
# 等1分钟,等etcd和MinIO就绪
curl http://localhost:9091/healthz -v
# 应该返回 {"status":"OK"}
踩坑记录:Milvus的standalone模式默认在etcd未就绪时不会启动milvus-server,经常是docker ps显示容器运行中,但实际gRPC端口不通。解决办法是加--wait参数等健康检查,或者干脆写个脚本轮询端口。我就因为这个问题排查了半小时,一度以为装坏了。
四、数据导入与索引构建
4.1 Qdrant:用Python SDK批量导入
# 安装依赖
# pip install qdrant-client==1.12.4
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
import numpy as np
# 连接,注意Qdrant默认用http 6333端口
client = QdrantClient(host="localhost", port=6333)
# 创建collection,HNSW参数直接写在VectorParams里
client.create_collection(
collection_name="products",
vectors_config=VectorParams(
size=768,
distance=Distance.COSINE,
hnsw_config={
"m": 16, # HNSW图的每个节点的最大连接数
"ef_construct": 200, # 构建时扫描的候选节点数
}
)
)
# 分批导入(每批5000条)
# 假设vectors是np.ndarray, ids是list
batch_size = 5000
for i in range(0, len(ids), batch_size):
batch_points = [
PointStruct(
id=int(ids[j]),
vector=vectors[j].tolist(),
payload={"product_id": i, "category": "electronics"}
)
for j in range(i, min(i+batch_size, len(ids)))
]
client.upsert(
collection_name="products",
points=batch_points
)
if i % 100000 == 0:
print(f"导入进度: {i}/{len(ids)}")
4.2 Milvus:rpc超时的坑
Milvus的Python SDK逻辑类似,但要注意timeout参数。默认100ms的超时对批量导入来说完全不够用,我直接踩了这个坑:
# pip install pymilvus==2.4.5
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
# 连接Milvus
connections.connect(alias="default", host="localhost", port="19530")
# 定义schema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=768)
]
schema = CollectionSchema(fields, description="product embeddings")
collection = Collection(name="products_500w", schema=schema)
# 创建HNSW索引
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200}
}
collection.create_index(field_name="vector", index_params=index_params)
# 导入数据 —— 这里必须加大timeout
from pymilvus import utility
for i in range(0, len(ids), 5000):
# 注意Milvus要求批量插入必须用list of list
batch_vectors = [vectors[j].tolist() for j in range(i, min(i+5000, len(ids)))]
batch_ids = [ids[j] for j in range(i, min(i+5000, len(ids)))]
# timeout必须设置,默认100ms会超时
collection.insert([batch_ids, batch_vectors], timeout=300)
if i % 100000 == 0:
print(f"导入进度: {i}/{len(ids)}")
踩坑记录:Milvus的insert操作默认timeout是100ms,批量导入时几乎必超时。我在文档里翻了半天才找到timeout参数。另外,Milvus的索引构建是异步的,导入完成后要等collection.flush()执行完,然后collection.load()加载到内存,否则查询会报"collection not loaded"错误。
导入耗时对比(500万条数据):
| 指标 | Qdrant | Milvus |
|---|---|---|
| 纯导入时间 | 42分钟 | 38分钟 |
| 索引构建 | 自动伴随导入 | 需手动触发(flush+load) |
| 峰值内存 | 4.2GB | 8.5GB |
| 磁盘占用 | 12.3GB | 17.8GB |
Milvus的原始数据存储是15GB,但额外需要etcd快照和MinIO分段存储,实际磁盘占用多出5GB。
五、查询性能实测(关键部分)
5.1 压测方法
我写了Python压测脚本,用500个真实查询向量做循环,模拟线上流量。统一设置top_k=10,并开启缓存预热(预跑1000次查询)。
5.2 Qdrant性能数据
Qdrant默认配置直接跑,没有额外调优:
- 单线程延迟:P50 = 3ms,P99 = 8ms
- 并发压测(32线程):QPS = 3200,P99 = 12ms
- 内存占用:稳定在6.2GB(包含HNSW图结构)
- CPU占用:峰值1200%(8核满载)
5.3 Milvus性能数据
Milvus需要调整一个关键参数:query_node的数量和search_list大小。
# 关键优化:设置search_list参数并加载到内存
collection.set_properties({"query_node_num": 3}) # 多节点并行
query_params = {"metric_type": "COSINE", "params": {"ef": 128}} # HNSW搜索宽度
# 压测查询
results = collection.search(
data=[query_vector],
anns_field="vector",
param=query_params,
limit=10,
output_fields=["product_id"]
)
- 单线程延迟:P50 = 5ms,P99 = 12ms
- 并发压测(32线程):QPS = 2800,P99 = 16ms
- 内存占用:18.2GB(Milvus把全量索引加载到内存)
- CPU占用:峰值1600%(10核)
5.4 结果分析
| 指标 | Qdrant | Milvus | 差异 |
|---|---|---|---|
| QPS | 3200 | 2800 | Qdrant快14% |
| P99延迟 | 8ms | 12ms | Qdrant低33% |
| 内存占用 | 6.2GB | 18.2GB | Qdrant省下70% |
| 部署组件数 | 1个 | 3个(etcd+MinIO+server) | Qdrant更简单 |
性能瓶颈分析:Milvus的查询路径更长——query节点先查segment,再通过etcd做分布式协调。单机模式下这些额外开销全部成为瓶颈。Qdrant的Rust实现零拷贝读取,HNSW图的缓存更紧凑。
六、踩坑与优化记录
6.1 Qdrant的坑
-
HNSW参数要调:默认
m=16, ef_construct=100在500万数据下召回率只有92%。我把ef_construct调到200后,召回率提升到99.2%,但构建时间从42分钟变成58分钟。线上查询时ef参数(搜索宽度)也需要注意,我最终设了ef=128平衡延迟和召回。 -
内存泄漏问题:Qdrant 1.12.4版本在连续导入超过200万条数据后,内存会缓慢增长。查看GitHub issue发现是
payload索引的bug。升级到1.12.5后解决。
6.2 Milvus的坑
-
etcd和MinIO的版本兼容:Milvus 2.4.5默认的docker-compose里etcd是3.5.5,MinIO是RELEASE.2023-03-20T20-16-18Z。千万别随便升级这些组件的版本,否则Milvus会报"etcd server version mismatch"错误。
-
内存占用高的根本原因:Milvus需要把segment索引加载到内存(
collection.load()),且HNSW的图结构是原样加载的。Qdrant支持mmap映射磁盘,所以内存占用低。要优化Milvus内存,可以设置param={"max_scan_count": 100}限制搜索范围,但会牺牲召回率。 -
查询超时:默认
timeout=10s,但高峰时搜索可能超过这个时间。我设置timeout=30s,但注意这会导致线程阻塞,需要配合连接池使用。
七、最终选型与总结
我的决策:最终选择了Qdrant,理由如下:
- 性能优势:QPS高14%,P99延迟低33%,在同等硬件下性能完胜。
- 运维成本:单二进制文件,systemd管理即可。Milvus需要3个组件,出了问题排查链路长。
- 内存友好:6.2GB vs 18.2GB,省下来的内存可以给业务服务用。
- 文档质量:Qdrant的Python SDK文档更清晰,示例代码可以直接跑通。Milvus的文档经常更新不及时,示例代码有坑。
什么情况选Milvus:如果你需要分布式部署(数据量超过1亿)、需要对接Hadoop生态、或者需要内置的监控面板(Milvus有自带的Prometheus指标),那选Milvus。另外Milvus对GPU加速支持更好,如果你们有CUDA环境可以试试。
最后说一句:选型不是比谁功能多,而是比谁能在你的场景下跑得稳。向量数据库这块,Qdrant用极简的设计赢得了我们的信任。你们的场景如果数据量在千万级以下,强烈建议先试试Qdrant,部署10分钟就能跑起来做验证。