一、问题背景:为什么需要向量数据库选型
去年下半年我们团队接手了一个电商图片搜索功能,要求对100万张商品图提取特征向量(512维),实现以图搜图。最初用Faiss本地方案,但维护索引版本、支持增量更新、高并发查询都很痛苦。于是开始调研主流的向量数据库。
业务核心需求:单机部署(初期数据量不大)、支持高并发读(QPS 100+)、增量数据实时可见、内存占用可控。最终候选锁定Milvus和Qdrant,两者都是当前社区活跃度高的开源方案。
二、环境与版本
测试服务器配置:
- CPU: Intel Xeon Platinum 8260 4核
- 内存: 8GB
- 磁盘: 100GB SSD(云盘)
- OS: Ubuntu 22.04 LTS, Kernel 5.15
- Docker: 24.0.7
- Docker Compose: v2.23.0
软件版本:
- Milvus: 2.3.0(standalone模式)
- Qdrant: 1.7.3(单节点模式)
- Python客户端: pymilvus 2.3.0 / qdrant-client 1.7.3
- 测试数据:100,000条512维float向量,随机生成
三、方案设计:两套部署对比
3.1 Milvus部署
Milvus 2.3采用了存算分离架构,但standalone模式把所有组件打包在一个容器里。直接上docker-compose.yml:
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
container_name: milvus-etcd
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- /data/milvus/etcd:/etcd
minio:
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
container_name: milvus-minio
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- /data/milvus/minio:/data
command: minio server /data
standalone:
image: milvusdb/milvus:v2.3.0
container_name: milvus-standalone
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
ports:
- "19530:19530"
volumes:
- /data/milvus/data:/var/lib/milvus
启动:docker-compose up -d。注意首次启动需要拉三个镜像,约2GB。
3.2 Qdrant部署
Qdrant就轻量多了,单容器搞定:
version: '3.5'
services:
qdrant:
image: qdrant/qdrant:v1.7.3
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- /data/qdrant/storage:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334
command: ["./qdrant", "--uri", "http://0.0.0.0:6333"]
启动:docker-compose up -d。镜像约200MB,启动速度比Milvus快一倍。
四、核心实现:数据写入与查询
4.1 数据准备
生成100,000条512维float向量,每条关联一个随机商品ID。下面是用Python批量写入Qdrant的代码:
from qdrant_client import QdrantClient
from qdrant_client.http import models
import numpy as np
client = QdrantClient(host="localhost", port=6333)
# 创建集合,使用Cosine距离
client.recreate_collection(
collection_name="products",
vectors_config=models.VectorParams(
size=512,
distance=models.Distance.COSINE
),
optimizers_config=models.OptimizersConfigDiff(
default_segment_number=2, # 控制内存分段数
)
)
# 批量写入100,000条
BATCH_SIZE = 256
for i in range(0, 100000, BATCH_SIZE):
vectors = np.random.rand(BATCH_SIZE, 512).astype(np.float32)
payloads = [{"product_id": f"P{j:06d}"} for j in range(i, i+BATCH_SIZE)]
ids = list(range(i, i+BATCH_SIZE))
client.upsert(
collection_name="products",
points=models.Batch(
ids=ids,
vectors=vectors.tolist(),
payloads=payloads
)
)
if i % 10000 == 0:
print(f"Inserted {i} records")
Milvus的写法类似,但需要先创建索引。注意Milvus默认不会自动建索引,必须手动调用collection.create_index(),否则查询会全量扫描。
4.2 查询性能测试
写一个简单的压测函数,分别查询1000次,记录延迟分布:
import time
from pymilvus import connections, Collection
# Milvus查询示例
connections.connect(host="localhost", port="19530")
collection = Collection("products")
collection.load() # 加载到内存
query_vector = np.random.rand(512).astype(np.float32).tolist()
latencies = []
for _ in range(1000):
start = time.perf_counter()
results = collection.search(
data=[query_vector],
anns_field="embedding",
param={"metric_type": "COSINE", "params": {"nprobe": 8}},
limit=10,
)
latencies.append((time.perf_counter() - start) * 1000) # ms
print(f"Milvus p50: {np.percentile(latencies, 50):.2f}ms, p99: {np.percentile(latencies, 99):.2f}ms")
Qdrant的查询代码类似,这里不重复贴了。
五、踩坑与优化
5.1 Milvus的「内存暴涨」问题
第一次测试时,往Milvus写入50万向量后,内存直接从2GB飙升到7.9GB,然后OOM被杀。排查发现Milvus的load()操作会把整个集合加载到内存,且默认的index_type是IVF_FLAT,内存占用高。解决办法:
- 改用
HNSW索引,内存占用降低40% - 设置
collection.load(replica_number=1),控制加载副本数 - 核心配置:
index_param = {"M": 16, "efConstruction": 200}
5.2 Qdrant的「分段管理」
Qdrant默认每个分段(segment)会独立构建索引,如果分段数过多(>10),查询时会遍历所有分段,导致延迟飙升。优化方式:
- 写入后手动触发
client.update_collection(optimizers_config={"default_segment_number": 1})合并分段 - 或者设置
optimizers_config中的memmap_threshold_kb为0,强制使用mmap减少内存占用
5.3 数据一致性差异
一个坑:Milvus在插入后立即查询可能查不到最新数据,因为数据先写入消息队列再批量刷盘。而Qdrant写入时默认wait=True则保证实时可见。这对我们的增量更新场景很重要,最终选择了Qdrant。
六、效果数据:实测数字说话
测试条件:100,000条512维向量,查询top-10,并发10线程,持续压测5分钟。
| 指标 | Milvus 2.3.0 (HNSW) | Qdrant 1.7.3 (HNSW) |
|---|---|---|
| 部署镜像大小 | 2.1 GB (3容器) | 198 MB (1容器) |
| 内存占用 (空闲) | 1.2 GB | 380 MB |
| 内存占用 (加载后) | 3.8 GB | 2.6 GB |
| QPS (并发10) | 2350 | 1980 |
| p50 延迟 | 4.1 ms | 5.0 ms |
| p99 延迟 | 42.7 ms | 68.3 ms |
| 写入速度 (1000条/秒) | 2.8秒 | 1.2秒 |
| 增量数据实时可见 | 需等待2-5秒 | 实时(写后即可查) |
关键发现:
1. Qdrant在资源受限场景更友好:内存占用低30%,镜像体积小一个数量级
2. Milvus高并发下更稳:p99延迟控制在50ms内,Qdrant偶有毛刺到100ms+
3. 写入速度Qdrant快一倍:因为Milvus有etcd和消息队列的写入开销
4. 部署复杂度:Qdrant一个docker-compose文件搞定,Milvus需要三个服务配合
七、总结与选型建议
经过实测,我给出的选型建议:
- 选Qdrant:如果你的场景是中小规模(<500万向量)、单机部署、对内存敏感、需要增量数据实时可见。Rust写的,启动快、部署简单。
- 选Milvus:如果你有大规模集群需求(千万级以上)、需要高QPS稳定服务、团队有Kubernetes运维能力。Milvus的分布式架构在水平扩展上更成熟。
我们最终选择了Qdrant,因为初期数据量只有100万,云服务器成本敏感,而且需要支持运营后台实时上传图片后立即能搜到。Qdrant 1.7.3版本在单个4核8G服务器上跑得很稳,后续如果数据涨到500万以上,可以考虑迁移到Milvus集群。
最后一句:选型没有银弹,拿自己真实的数据量跑一遍,比看一百篇对比文章都有用。