一、问题背景
我们做的是一个图文内容平台,每天新增约 30 万条图文素材,需要做「相似图片/文案去重」和「推荐召回」。向量维度 768(CLIP ViT-L/14 图像向量 + BGE-base 文本向量各一套),预计一年内单集合规模到 500 万条。
候选方案很快收敛到 Milvus 和 Qdrant 两个:
- Milvus:社区大、生态全、支持存算分离和多种索引,公司其他团队已有运维经验。
- Qdrant:Rust 写的,单机性能口碑好,filter 语法舒服,部署轻。
网上的对比文章大多停留在「Milvus 功能多、Qdrant 轻量」这种定性描述,缺少同机同数据下的数字。所以干脆自己搭一套压测环境,用数据说话。
二、环境与版本
| 项目 | 配置 |
|---|---|
| 服务器 | 16 vCPU / 64GB RAM / 1TB NVMe SSD |
| OS | Ubuntu 22.04,内核 5.15 |
| Docker | 24.0.7 |
| Milvus | 2.4.1 standalone(etcd + minio 内置) |
| Qdrant | 1.9.2 |
| 客户端 | pymilvus 2.4.3 / qdrant-client 1.9.1 |
| 数据集 | 500 万条 × 768 维 float32,约 14.3GB 原始向量 |
两份服务不要同时跑,压测时轮流启动,避免资源互相干扰。数据落盘都用 NVMe,关闭 swap。
三、方案设计
两边都使用 HNSW 索引,尽量对齐参数:
- Milvus:
M=16, efConstruction=200,查询ef=64,metric 用 COSINE。 - Qdrant:
m=16, ef_construct=200,查询hnsw_ef=64,distance 用 Cosine。
测试三类负载:
- 纯向量 TopK=10 检索,1000 次取 P50/P95/P99。
- 带标量过滤(category 字段等值过滤,过滤后约 10% 数据)的检索。
- 批量写入 100 万条,观察吞吐和内存曲线。
四、部署与核心实现
4.1 Milvus 2.4.1 部署
# 下载官方 standalone compose
wget https://github.com/milvus-io/milvus/releases/download/v2.4.1/milvus-standalone-docker-compose.yml -O docker-compose.yml
# 关键调参:给足内存,避免 mmap 抖动
cat >> docker-compose.yml <<'EOF'
# 在 standalone 服务的 environment 中追加
# QUERY_NODE_GRACEFUL_STOP_TIMEOUT: 60
EOF
docker compose up -d
docker compose ps # 确认 milvus-standalone / etcd / minio 三个容器 healthy
建集合与索引:
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
client.create_collection(
collection_name="img_vec",
dimension=768,
metric_type="COSINE",
auto_id=False,
)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 200},
)
client.create_index("img_vec", index_params)
client.load_collection("img_vec")
4.2 Qdrant 1.9.2 部署
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
-v /data/qdrant:/qdrant/storage \
-e QDRANT__SERVICE__GRPC_PORT=6334 \
qdrant/qdrant:v1.9.2
建集合与索引:
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, HnswConfigDiff
client = QdrantClient(host="localhost", port=6333)
client.recreate_collection(
collection_name="img_vec",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
hnsw_config=HnswConfigDiff(m=16, ef_construct=200),
)
4.3 压测脚本(两边共用逻辑)
import time, numpy as np, statistics
def bench_search(search_fn, queries, topk=10, rounds=1000):
lat = []
for i in range(rounds):
q = queries[i % len(queries)]
t0 = time.perf_counter()
search_fn(q, topk)
lat.append((time.perf_counter() - t0) * 1000) # ms
lat.sort()
return {
"p50": statistics.median(lat),
"p95": lat[int(len(lat) * 0.95)],
"p99": lat[int(len(lat) * 0.99)],
}
# Milvus 侧
def milvus_search(q, topk):
return mclient.search(
collection_name="img_vec",
data=[q.tolist()],
limit=topk,
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
)
# Qdrant 侧
def qdrant_search(q, topk):
return qclient.search(
collection_name="img_vec",
query_vector=q.tolist(),
limit=topk,
search_params={"hnsw_ef": 64},
)
五、踩坑与优化
-
Milvus standalone 的 mmap 陷阱:默认配置下 querynode 会积极 mmap,压测时 RSS 看起来不高但 page cache 飙到 40GB,导致 P99 抖动。后来显式限制
queryNode.cache.cacheSize并观察docker stats的 RSS + cache 总和才准确。 -
Qdrant 的 ef 参数名不一致:建索引时是
ef_construct,查询时是hnsw_ef,文档里分散在两处,第一次写错直接静默用了默认值,延迟数据偏乐观,重跑才修正。 -
批量写入的分批大小:Milvus 用
insert时单批 1 万条、并发 4 路最稳;超过 5 万条单批会触发 flush 阻塞。Qdrant 用upload_points,batch_size 设 256 反而比 1024 吞吐更高,因为 gRPC 单帧太大时序列化开销明显。 -
过滤查询的索引:Milvus 需要为标量字段单独建
INVERTED索引,否则过滤走全扫;Qdrant 1.9 对标量字段有自动 payload index,但要手动create_payload_index才会生效,别忘了。
六、效果数据
500 万条数据全部导入后(Milvus 索引构建约 22 分钟,Qdrant 约 17 分钟),稳态压测结果:
| 指标 | Milvus 2.4.1 | Qdrant 1.9.2 |
|---|---|---|
| 纯向量 P50 | 8.6 ms | 5.2 ms |
| 纯向量 P99 | 27.4 ms | 16.9 ms |
| 过滤查询 P99 | 41.2 ms | 58.7 ms |
| 写入 100 万条耗时 | 6 分 40 秒 | 9 分 15 秒 |
| 稳态 RSS(含 cache) | 21.3 GB | 13.2 GB |
| 磁盘占用 | 18.9 GB | 15.4 GB |
结论很清晰:纯向量检索 Qdrant 更快更省内存,P99 领先约 38%,内存只有 Milvus 的 62%;但带标量过滤的查询 Milvus 更稳,因为它对倒排索引和向量索引的融合执行做了优化,Qdrant 在过滤比例 10% 时 P99 反而被反超。
七、总结
如果业务是「纯语义召回 + 单机规模、内存敏感」,Qdrant 1.9.2 是更划算的选择,部署一个容器就完事,延迟和资源占用都更好。
如果业务有大量「向量 + 复杂标量过滤」的组合查询,或者未来要上分布式、存算分离、多租户,Milvus 2.4.1 的生态和稳定性更值得投入,代价是多吃 8GB 左右内存。
我们最终选了 Qdrant 做主召回,因为它 90% 的查询都是纯向量;把需要复杂过滤的分析型查询单独走离线任务。选型没有银弹,用自己业务的数据跑一遍,比看十篇评测都管用。