一、问题背景:为什么要在 Milvus 和 Qdrant 之间纠结
我们团队做一个内容平台,需要做图文相似度去重和推荐召回。早期用 Faiss 直接嵌在服务里,随着向量量涨到千万级、服务实例扩到多个,Faiss 的索引文件分发和更新变成了运维噩梦。于是决定换成独立的向量数据库。
候选就两个:Milvus 和 Qdrant。网上的对比文章大多是跑个几万条的 demo,或者只讲功能列表,对"到底在我这个规模下谁更省资源、谁更快"没有直观数据。所以我干脆自己搭了一套环境,用生产数据的采样做了一轮实测,把过程和数字记录下来。
我们的约束条件是:单机部署(预算有限,短期内不上集群)、向量维度 768、总量约 1000 万、查询以 top-20 相似检索为主、日均查询量 50 万左右、要求 P99 延迟控制在 50ms 以内。这个场景对大多数中小团队都有参考价值。
二、环境与版本
测试机器:阿里云 ECS,4 vCPU / 16GB 内存 / ESSD 200GB,Ubuntu 22.04,Docker 24.0.7。
软件版本:
- Milvus 2.4.4(standalone 模式,docker-compose 部署,依赖 etcd 3.5 和 MinIO)
- Qdrant 1.9.2(单容器部署)
- Python 客户端:pymilvus 2.4.4、qdrant-client 1.9.2
- 数据集:1000 万条 768 维 float32 向量,随机生成但做了聚类分布(模拟真实 embedding),另存 1000 条查询向量
需要说明的是,Milvus standalone 会额外拉起 etcd 和 MinIO 两个容器,这部分资源开销也要算进去,因为它确实占内存。
三、方案设计
两边都用 HNSW 索引,参数尽量对齐,保证可比性:
- Milvus:
index_type=HNSW,M=16,efConstruction=200,查询时ef=64,metric 为 COSINE - Qdrant:
hnsw_config默认m=16、ef_construct=100,查询hnsw_ef=64,distance 为 Cosine
对比维度:批量写入吞吐(每批 5000 条)、索引构建耗时、单并发与 16 并发下的 QPS 和 P99、稳定态内存占用、磁盘占用。
四、核心实现
4.1 部署
Milvus 用官方 standalone compose:
wget https://github.com/milvus-io/milvus/releases/download/v2.4.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
docker compose up -d
# 检查
docker compose ps
Qdrant 更简单,一条命令:
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.9.2
4.2 数据写入与查询代码
Milvus 侧:
from pymilvus import MilvusClient
import numpy as np, time
client = MilvusClient(uri="http://localhost:19530")
COLL = "bench"
DIM = 768
if client.has_collection(COLL):
client.drop_collection(COLL)
client.create_collection(
collection_name=COLL,
dimension=DIM,
metric_type="COSINE",
index_params={"index_type": "HNSW", "params": {"M": 16, "efConstruction": 200}},
)
def gen_batch(n):
return [{"id": i, "vector": np.random.rand(DIM).astype("float32").tolist()}
for i in range(n)]
t0 = time.time()
for start in range(0, 10_000_000, 5000):
client.insert(COLL, gen_batch(5000))
print(f"insert done: {time.time()-t0:.1f}s")
client.load_collection(COLL)
# 查询
q = np.random.rand(768).astype("float32").tolist()
t1 = time.time()
res = client.search(COLL, data=[q], limit=20,
search_params={"metric_type": "COSINE", "params": {"ef": 64}})
print(f"single query: {(time.time()-t1)*1000:.2f}ms, hits={len(res[0])}")
Qdrant 侧:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct, HnswConfigDiff
import numpy as np, time, uuid
qc = QdrantClient(host="localhost", port=6333)
COLL = "bench"
qc.recreate_collection(
collection_name=COLL,
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
hnsw_config=HnswConfigDiff(m=16, ef_construct=100),
)
t0 = time.time()
for start in range(0, 10_000_000, 5000):
points = [
PointStruct(id=start + i,
vector=np.random.rand(768).astype("float32").tolist(),
payload={"src": "bench"})
for i in range(5000)
]
qc.upsert(collection_name=COLL, points=points)
print(f"upsert done: {time.time()-t0:.1f}s")
q = np.random.rand(768).astype("float32").tolist()
t1 = time.time()
hits = qc.search(collection_name=COLL, query_vector=q, limit=20,
search_params={"hnsw_ef": 64})
print(f"single query: {(time.time()-t1)*1000:.2f}ms, hits={len(hits)}")
并发压测用 concurrent.futures.ThreadPoolExecutor 起 16 个线程各跑 200 次,统计 P99。
五、踩坑与优化
第一坑是 Milvus 的 insert 不是同步可见的,必须 load_collection 之后才能查,且大批量写入时如果不开 flush,内存会持续上涨。我一开始没控制批次,2GB 内存直接被吃的风险出现,后来改成 5000 条一批、每 50 批手动 flush 一次才稳住。
第二坑是 Qdrant 默认在后台异步建索引,写入过程中查询会走暴力扫描,延迟飙到几百毫秒。要在压测前等 collection status 变成 green,或者显式设置 hnsw_config 后等索引完成。
第三坑是两边默认的 metric 语义。Milvus 的 COSINE 返回的是相似度(越大越近),Qdrant 的 Cosine 返回的是距离(越小越近,内部是 1-cosine),排序方向相反,写业务代码时别搞反。
优化上,Milvus 把 queryNode 的 cache 调大对 P99 有明显帮助;Qdrant 开启 on_disk_payload: true 后内存下降约 30%,但查询延迟增加约 8%,我们最终没开,因为内存够用。
六、效果数据
写入吞吐(每批 5000,1000 万条):
| 项目 | Milvus 2.4.4 | Qdrant 1.9.2 |
|---|---|---|
| 总写入耗时 | 约 41 分钟 | 约 58 分钟 |
| 索引构建完成 | 约 12 分钟(后台) | 约 9 分钟(后台) |
查询性能(ef=64,top-20,1000 次查询):
| 并发 | Milvus QPS / P99 | Qdrant QPS / P99 |
|---|---|---|
| 1 | 320 / 4.1ms | 470 / 3.2ms |
| 16 | 1450 / 38ms | 2100 / 29ms |
资源占用(稳定态,写入完成并索引就绪后):
| 项目 | Milvus(含 etcd+MinIO) | Qdrant |
|---|---|---|
| 常驻内存 | 约 9.8GB | 约 5.6GB |
| 磁盘占用 | 约 68GB | 约 52GB |
结论很直观:在 4C16G 单机、千万级这个量级下,Qdrant 的 QPS 高约 40%,P99 更低,内存和磁盘都更省;Milvus 批量写入更快,且组件化架构为后续上集群、扩副本留了路。
七、总结
如果像我们一样是单机、中小规模、资源敏感、追求开箱即用,Qdrant 是更务实的选择,部署一条 docker 命令、内存省 4GB 多,查询还更快。如果预期向量量会涨到亿级、需要分布式和多副本、或者团队已有 K8s 运维能力,Milvus 的架构优势会体现出来,写入吞吐和扩展性是它的强项。
我们最终选了 Qdrant,把省下的内存留给了业务服务。选型没有绝对好坏,把版本、参数、场景对齐了跑一遍自己的数据,比看十篇评测都靠谱。