1. 业务场景与选型困惑
我们团队负责一个UGC社区的“低俗内容AI拦截”系统。流程很简单:用户发帖 -> 文本向量化(SGPT-125M模型,768维)-> 存入向量库 -> 检索相似违规样本。起初用FAISS单机搞定,但数据量从百万涨到千万级后,单机内存吃紧,且需要支持实时增量。
于是开始评估Milvus和Qdrant。我个人的初步感知是:Milvus功能全、生态大,但架构重(依赖etcd、MinIO);Qdrant轻量,Rust写的,单二进制文件。但网上的对比文章大多是“玩具级”数据量,缺乏参考价值。于是决定在相同硬件上做一轮实测。
2. 环境与版本锁定
为了避免“版本不同导致结果差异”的争论,我固定了如下环境:
- 硬件:2台物理机(1台部署,1台压测),每台配置:Intel Xeon Gold 6330(16核分配)、64GB RAM、NVMe SSD 1TB。
- OS:Ubuntu 22.04 LTS,内核5.15。
- Docker:24.0.7,docker-compose v2.24。
- Milvus:2.4.5(standalone模式,内置etcd和MinIO)。
- Qdrant:1.9.7(单节点,使用
qdrant/qdrant镜像)。 - 数据:500万条随机向量(768维,float32),外加10万条“违规样本”作为查询目标,距离度量使用余弦相似度。
为什么选这两个版本? Milvus 2.4.x是当前稳定版,支持GPU索引(虽然我没用);Qdrant 1.9.x引入了新的二进制量化,但这次没启用,保持公平。
3. 方案设计:两套部署文件与压测口径
我的压测脚本用Python concurrent.futures模拟100并发,持续10分钟,分别测试:
- 写入测试:批量插入(每批100条),观察QPS和延迟。
- 查询测试:固定
topk=10,搜索10万条预置query向量。
关键设计:两个系统都使用HNSW索引(M=16, efConstruction=200)。但注意,Milvus需要先建collection再建索引,而Qdrant是在创建collection时指定。
Milvus部署(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
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls=http://0.0.0.0:2379 --data-dir /etcd
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data
standalone:
image: milvusdb/milvus:v2.4.5
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
ports:
- "19530:19530"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
depends_on:
- etcd
- minio
Qdrant部署(docker-compose.yml):
version: '3.5'
services:
qdrant:
image: qdrant/qdrant:v1.9.7
ports:
- "6333:6333"
volumes:
- ./qdrant_storage:/qdrant/storage
command: ["qdrant", "--disable-telemetry"]
核心实现(Python写入示例):
# 以Milvus为例,Qdrant类似
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType, utility
connections.connect(alias="default", host="localhost", port="19530")
# 删除旧collection(如果存在)
if utility.has_collection("content_audit"):
utility.drop_collection("content_audit")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768)
]
schema = CollectionSchema(fields, description="content audit vectors")
col = Collection("content_audit", schema)
# 创建HNSW索引,注意这里的参数
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200}
}
col.create_index("embedding", index_params)
# 批量写入(100条一批)
import numpy as np
for i in range(0, 5000000, 100):
vectors = np.random.rand(100, 768).astype(np.float32)
ids = list(range(i, i+100))
col.insert([ids, vectors])
col.flush()
print("写入完成,实体数:", col.num_entities)
4. 踩坑与优化:两个系统各有一个大坑
Milvus的坑:索引参数不生效。我最初在创建索引时写了efConstruction=200,但查询时发现延迟高达80ms。后来检查Milvus的get_index_params,发现它默认用了ef=64(查询时的动态参数)。必须要在查询前显式设置search_params里的ef,否则会用默认值。而ef直接影响查询精度和延迟,我最终把ef设为128,延迟从80ms降到37ms,召回率从89%升到97%。
Qdrant的坑:内存爆掉。Qdrant默认会把所有向量加载到内存(on_disk=False)。500万条768维float32向量约15GB,加上HNSW图结构,轻松超过32GB。后来在创建collection时必须指定"on_disk": True,但这样查询延迟会从18ms飙升到45ms。最终折中方案:将HNSW的m从16降到8,并启用mmap模式,延迟回到23ms,内存稳定在12GB。
调优后的Qdrant collection配置:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, HnswConfigDiff
client = QdrantClient(host="localhost", port=6333)
client.recreate_collection(
collection_name="content_audit",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
hnsw_config=HnswConfigDiff(
m=8,
ef_construct=200,
full_scan_threshold=10000,
on_disk=True # 关键:使用磁盘存储
)
)
5. 效果数据:延迟、吞吐、资源占用对比
压测结果(100并发,10分钟稳定值):
| 指标 | Milvus 2.4.5 | Qdrant 1.9.7 | 差异 |
|---|---|---|---|
| P99 查询延迟 | 37ms | 23ms | Qdrant 快 38% |
| P50 查询延迟 | 21ms | 14ms | Qdrant 快 33% |
| 批量写入吞吐 | 8.2k QPS | 4.8k QPS | Milvus 高 42% |
| 单条写入延迟(P99) | 180ms | 320ms | Milvus 低 43% |
| 内存占用(空闲) | 8.2GB | 5.1GB | Qdrant 低 38% |
| 内存占用(压测时) | 18.5GB | 12.3GB | Qdrant 低 34% |
| 磁盘占用 | 21GB(含etcd/minio) | 16GB | Qdrant 低 24% |
| 部署组件数 | 3个容器(etcd/minio/standalone) | 1个容器 | Qdrant 简单 |
查询召回率:在ef=128(Milvus)和ef_construct=200(Qdrant)下,两者召回率均超过97%,无明显差异。
资源占用细看:Milvus的空闲内存8.2GB主要被etcd缓存和MinIO占用,实际向量引擎只用了4GB左右。Qdrant的5.1GB则纯粹是向量+HNSW图。如果数据量再翻倍到1000万,Qdrant可能更需要关注mmap的IO瓶颈,而Milvus则要小心etcd的磁盘空间。
6. 总结与最终选型
我的结论可能和部分“评测文”相反:
- 如果侧重查询低延迟且数据量在千万级以下,选Qdrant。部署简单、内存可控,单容器就能跑,适合微服务架构。
- 如果侧重写入吞吐、需要丰富的数据类型支持(如动态字段、分区),选Milvus。它虽然重,但原生支持批量写入的流式处理,且自带etcd保证了元数据一致性。
- 如果团队已有Kafka/Pulsar数据管道,Milvus更友好,因为它有内置的消息队列抽象。
我们最终选择了Qdrant。原因很现实:查询延迟是业务硬指标(必须低于30ms),而写入可以通过增加消费者实例来缓解QPS差距。另外,运维只维护一个容器,省心多了。
最后提醒一句:向量数据库选型没有“银弹”,务必用真实业务数据量(至少百万级)和真实查询模式(比如带过滤条件)去压测。我这次没做带标量过滤的测试,如果你的场景有“分类ID + 向量”的混合检索,结论可能会反过来。