一、问题背景:为什么要在 Milvus 和 Qdrant 之间纠结
先说业务。我们做的是一个内容平台的图文去重系统:用户上传图片后,用 CLIP 提取 768 维向量,去向量库里找相似度 > 0.92 的历史内容,命中就拦截。日均新增约 40 万条,历史存量 1200 万条,峰值 QPS 大概 300,P99 要求 100ms 以内。
需求很明确:高维向量 + 标量过滤(按业务线、时间范围)+ 中等规模 + 成本敏感。
一开始用的是 FAISS 自建,但过滤和增量更新太痛苦,决定换专业的向量数据库。调研一圈后剩两个候选:
- Milvus 2.4.4:国内生态好,文档全,支持多种索引(HNSW、IVF、DiskANN),分布式架构成熟,但组件多(etcd、MinIO、Pulsar),运维重。
- Qdrant 1.9.2:Rust 写的,单二进制部署,过滤性能是官方主打卖点,内存占用低,但分布式版本(集群模式)需要额外配置,生态相对小。
网上评测大多是"玩具数据"跑出来的,我决定自己动手,用真实数据量测一遍。
二、环境与版本
硬件:3 台 16C 64G,SSD 1TB,万兆内网,Ubuntu 22.04。
软件版本固定如下,避免版本差异导致结论失真:
| 组件 | 版本 |
|---|---|
| Milvus | 2.4.4 (standalone + 3 节点集群各测一轮) |
| Qdrant | 1.9.2 |
| Python SDK | pymilvus 2.4.4 / qdrant-client 1.9.1 |
| 数据集 | 1200 万条 768 维 float32 向量 + 3 个标量字段 |
| 压测工具 | 自写 asyncio 脚本 + locust |
Milvus 用官方 docker-compose 起 standalone,Qdrant 用 docker run 单机起,先保证公平。
三、方案设计
测试分四块:
- 部署复杂度:记录从零到可用的步骤数和踩坑。
- 写入性能:1200 万条分批灌入,batch=1000,记录总耗时和内存峰值。
- 查询性能:随机抽 1 万条 query,测 TopK=10 的 P50/P99;再测带
business_id == X AND ts > T过滤的查询。 - 资源占用:用
docker stats和pidstat采样,记录稳定态 CPU/内存。
索引参数统一用 HNSW,M=16,efConstruction=200,查询 ef=64。Milvus 用 MILVUS 索引类型,Qdrant 用默认 HNSW 配置。
四、核心实现(含部署与压测代码)
4.1 Milvus 部署
# 下载官方 compose
wget https://github.com/milvus-io/milvus/releases/download/v2.4.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
# 关键:调大 etcd 和 minio 的资源限制,默认配置在 1200 万数据下会 OOM
# 修改 docker-compose.yml 中 standalone 的 deploy.resources.limits.memory: 32G
docker compose up -d
# 验证
docker compose ps
curl http://localhost:9091/healthz
建集合和索引:
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType
connections.connect("default", host="127.0.0.1", port="19530")
fields = [
FieldSchema("id", DataType.INT64, is_primary=True, auto_id=False),
FieldSchema("vec", DataType.FLOAT_VECTOR, dim=768),
FieldSchema("business_id", DataType.INT64),
FieldSchema("ts", DataType.INT64),
]
schema = CollectionSchema(fields, description="image_dedup")
col = Collection("image_dedup", schema)
# HNSW 索引
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {"M": 16, "efConstruction": 200},
}
col.create_index("vec", index_params)
col.load()
4.2 Qdrant 部署
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
-e QDRANT__STORAGE__ON_DISK_PAYLOAD=true \
qdrant/qdrant:v1.9.2
建集合:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
client = QdrantClient(host="127.0.0.1", port=6333)
client.recreate_collection(
collection_name="image_dedup",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
hnsw_config={"m": 16, "ef_construct": 200},
)
4.3 压测脚本(关键部分)
import asyncio, random, time, numpy as np
from qdrant_client import AsyncQdrantClient
from pymilvus import AsyncMilvusClient
N_QUERY = 10000
TOPK = 10
async def bench_qdrant():
cli = AsyncQdrantClient(host="127.0.0.1", port=6333)
lat = []
for _ in range(N_QUERY):
q = np.random.rand(768).astype(np.float32).tolist()
t0 = time.perf_counter()
await cli.search(
collection_name="image_dedup",
query_vector=q,
limit=TOPK,
query_filter={"must": [{"key": "business_id", "match": {"value": 7}}]},
)
lat.append(time.perf_counter() - t0)
lat.sort()
print(f"Qdrant P50={lat[len(lat)//2]*1000:.1f}ms P99={lat[int(len(lat)*0.99)]*1000:.1f}ms")
async def bench_milvus():
cli = AsyncMilvusClient(uri="http://127.0.0.1:19530")
lat = []
for _ in range(N_QUERY):
q = np.random.rand(768).astype(np.float32).tolist()
t0 = time.perf_counter()
await cli.search(
collection_name="image_dedup",
data=[q],
limit=TOPK,
filter='business_id == 7',
output_fields=["id"],
)
lat.append(time.perf_counter() - t0)
lat.sort()
print(f"Milvus P50={lat[len(lat)//2]*1000:.1f}ms P99={lat[int(len(lat)*0.99)]*1000:.1f}ms")
asyncio.run(bench_qdrant())
asyncio.run(bench_milvus())
五、踩坑与优化
坑 1:Milvus standalone 默认内存不够。 compose 里 standalone 默认 limit 8G,灌到 800 万条时开始频繁 GC,写入吞吐从 3500 条/s 掉到 800 条/s。调到 32G 后恢复。生产环境建议直接上集群。
坑 2:Qdrant 的 payload 索引。 一开始过滤查询 P99 高达 400ms,原因是 business_id 没建 payload index。加上 client.create_payload_index("image_dedup", "business_id", "integer") 后降到 60ms 以内。这是 Qdrant 过滤快的前提,官方文档写得不显眼。
坑 3:两者 batch size 差异大。 Milvus 单次 insert 超过 5000 条容易触发 segment flush 抖动,建议 1000-2000;Qdrant 的 upsert 可以到 5000,再大反而慢。压测时统一 batch=1000 才公平。
坑 4:Milvus 的 ef 参数在 search 时要显式传。 不传会用默认值,和索引里的 efConstruction 不是一回事,我一开始漏了,P99 虚高 30ms。
六、效果数据
1200 万条数据、单机部署下的实测结果:
| 指标 | Milvus 2.4.4 | Qdrant 1.9.2 |
|---|---|---|
| 写入总耗时(batch=1000) | 58 min | 25 min |
| 写入吞吐峰值 | 3500 条/s | 8200 条/s |
| 纯向量查询 P50 / P99 | 18ms / 62ms | 12ms / 48ms |
| 带过滤查询 P50 / P99 | 22ms / 71ms | 15ms / 55ms |
| 500 并发 P99 | 110ms | 240ms(开始抖动) |
| 稳定态内存占用 | 41 GB | 24 GB |
| 稳定态 CPU(空载) | 6% | 2% |
| 部署步骤 | 5 步(含 3 个依赖组件) | 1 步 |
结论有点反直觉:低并发下 Qdrant 全面占优,高并发下 Milvus 更稳。Qdrant 单机在 500 并发时 P99 从 55ms 飙到 240ms,原因是单节点没有分片,请求排队;Milvus 因为 segment 并行查询,反而扛住了。
过滤查询这块,我原本以为 Qdrant 会碾压,实际差距没那么大(1.3 倍左右),前提是 payload index 建对了。Milvus 2.4 的标量过滤优化比 2.3 好不少。
七、总结
选型建议按场景分:
- 数据量 300 或有强过滤需求、需要水平扩展:选 Milvus。集群模式成熟,高并发更稳,代价是运维复杂度。
- 成本敏感:Qdrant 内存占用低 40%,长期跑下来电费和机器成本差距明显。
我们最后选了 Qdrant,因为业务峰值 QPS 实际只有 180,且团队没有专职运维。但如果你在做的是搜索类高并发场景,Milvus 的分布式能力更值得投入。
最后提醒一句:所有评测都要用自己的数据和查询模式跑一遍,向量维度和过滤字段的选择性对结果影响极大,别直接抄别人的 benchmark。