一、业务背景与选型困境

我们团队负责电商平台的拍照搜商品功能,底层是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的坑

  1. HNSW参数要调:默认m=16, ef_construct=100在500万数据下召回率只有92%。我把ef_construct调到200后,召回率提升到99.2%,但构建时间从42分钟变成58分钟。线上查询时ef参数(搜索宽度)也需要注意,我最终设了ef=128平衡延迟和召回。

  2. 内存泄漏问题:Qdrant 1.12.4版本在连续导入超过200万条数据后,内存会缓慢增长。查看GitHub issue发现是payload索引的bug。升级到1.12.5后解决。

6.2 Milvus的坑

  1. etcd和MinIO的版本兼容:Milvus 2.4.5默认的docker-compose里etcd是3.5.5,MinIO是RELEASE.2023-03-20T20-16-18Z。千万别随便升级这些组件的版本,否则Milvus会报"etcd server version mismatch"错误。

  2. 内存占用高的根本原因:Milvus需要把segment索引加载到内存(collection.load()),且HNSW的图结构是原样加载的。Qdrant支持mmap映射磁盘,所以内存占用低。要优化Milvus内存,可以设置param={"max_scan_count": 100}限制搜索范围,但会牺牲召回率。

  3. 查询超时:默认timeout=10s,但高峰时搜索可能超过这个时间。我设置timeout=30s,但注意这会导致线程阻塞,需要配合连接池使用。

七、最终选型与总结

我的决策:最终选择了Qdrant,理由如下:

  1. 性能优势:QPS高14%,P99延迟低33%,在同等硬件下性能完胜。
  2. 运维成本:单二进制文件,systemd管理即可。Milvus需要3个组件,出了问题排查链路长。
  3. 内存友好:6.2GB vs 18.2GB,省下来的内存可以给业务服务用。
  4. 文档质量:Qdrant的Python SDK文档更清晰,示例代码可以直接跑通。Milvus的文档经常更新不及时,示例代码有坑。

什么情况选Milvus:如果你需要分布式部署(数据量超过1亿)、需要对接Hadoop生态、或者需要内置的监控面板(Milvus有自带的Prometheus指标),那选Milvus。另外Milvus对GPU加速支持更好,如果你们有CUDA环境可以试试。

最后说一句:选型不是比谁功能多,而是比谁能在你的场景下跑得稳。向量数据库这块,Qdrant用极简的设计赢得了我们的信任。你们的场景如果数据量在千万级以下,强烈建议先试试Qdrant,部署10分钟就能跑起来做验证。