当你的向量数据从百万级飙到亿级,单机 Milvus 开始喘气了?本文手把手带你部署 Milvus 集群,并实战混合检索(向量 + 标量过滤),让推荐系统、RAG 应用轻松抗住高并发。
为什么要上 Milvus 集群?
去年我给一个电商客户做商品推荐系统,初期 50 万 SKU,单机 Milvus 跑得飞起。结果双十一前 SKU 膨胀到 8000 万,向量维度 768,单机查询延迟从 20ms 飙到 1200ms,CPU 直接打满。
单机瓶颈在哪?
- 内存不够:8000 万条 768 维 float32 向量 ≈ 220GB,还没算索引
- 查询并发上不去:单实例只能吃满一个 CPU 核的查询线程
- 无高可用:实例挂了,整个推荐系统凉凉
集群化之后的好处:
| 能力 | 单机 | 集群 |
|---|---|---|
| 数据容量 | < 5 亿条(受内存限制) | 理论上无限(可横向扩展) |
| 查询 QPS | 500~2000 | 10000+ |
| 高可用 | 无 | 多副本自动故障转移 |
| 写入吞吐 | 5000 条/秒 | 50000+ 条/秒 |
架构一览:Milvus 集群长什么样?
Milvus 2.x 是存算分离架构,核心组件:
- Proxy:查询入口,无状态,可水平扩展
- QueryNode:执行向量检索,CPU/GPU 混合
- DataNode:处理写入和增量数据
- IndexNode:构建索引(IVF、HNSW 等)
- MinIO / S3:向量数据持久化存储
- etcd:元数据管理
- Pulsar / Kafka:消息队列(写吞吐削峰)
┌─────────────────────────────────────┐
│ Load Balancer │
└──────────────┬──────────────────────┘
│
┌──────────────┼──────────────────────┐
│ Proxy 实例 × N │ ← 无状态,随便扩
└──────────────┬──────────────────────┘
│
┌──────────────┼──────────────────────┐
│ QueryNode × N DataNode × N │ ← 有状态,扩之前要迁移
└──────────────┬──────────────────────┘
│
┌──────────────┴──────────────────────┐
│ MinIO(S3) etcd Pulsar │ ← 基础设施,建议独立集群
└─────────────────────────────────────┘
实战:Docker Compose 快速拉起 3 节点测试集群
生产建议用 K8s + Helm,但本地验证先用 Docker Compose 把架构跑通。
docker-compose.yml
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data
ports:
- "9000:9000"
- "9001:9001"
pulsar:
container_name: milvus-pulsar
image: apachepulsar/pulsar:2.8.2
command: bin/pulsar standalone
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/pulsar:/pulsar/data
# 3个 Proxy 实例(无状态,可扩展)
milvus-proxy-1:
container_name: milvus-proxy-1
image: milvusdb/milvus:v2.4.17
command: ["milvus", "run", "proxy"]
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
- PULSAR_ADDRESS=pulsar:6650
depends_on:
- etcd
- minio
- pulsar
ports:
- "19530:19530" # 客户端连接端口
milvus-proxy-2:
container_name: milvus-proxy-2
image: milvusdb/milvus:v2.4.17
command: ["milvus", "run", "proxy"]
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
- PULSAR_ADDRESS=pulsar:6650
depends_on:
- etcd
- minio
- pulsar
# 2个 QueryNode(负责向量检索)
milvus-querynode-1:
container_name: milvus-querynode-1
image: milvusdb/milvus:v2.4.17
command: ["milvus", "run", "querynode"]
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
- PULSAR_ADDRESS=pulsar:6650
depends_on:
- etcd
- minio
- pulsar
milvus-querynode-2:
container_name: milvus-querynode-2
image: milvusdb/milvus:v2.4.17
command: ["milvus", "run", "querynode"]
environment:
- ETCD_ENDPOINTS=etcd:2379
- MINIO_ADDRESS=minio:9000
- PULSAR_ADDRESS=pulsar:6650
depends_on:
- etcd
- minio
- pulsar
networks:
default:
name: milvus-cluster
启动:
docker-compose up -d
docker ps | grep milvus # 确认 4 个 Milvus 实例都起来了
Python 实战:插入亿级向量 + 混合检索
1. 连接集群并创建 Collection
from pymilvus import (
connections, Collection, FieldSchema, CollectionSchema,
DataType, utility
)
import numpy as np
# 连接集群(指向任意一个 Proxy 实例)
connections.connect(
alias="default",
host="localhost",
port="19530"
)
# 定义 Schema:主键 + 向量 + 标量字段
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=False),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64), # 标量过滤字段
FieldSchema(name="price", dtype=DataType.FLOAT), # 标量过滤字段
FieldSchema(name="create_time", dtype=DataType.INT64),
]
schema = CollectionSchema(fields, description="电商商品向量库")
# 创建 Collection,设置 2 个分片(数据分布到不同 QueryNode)
collection = Collection(
name="ecommerce_products",
schema=schema,
shards_num=2 # 关键:分片数 >= QueryNode 数量,才能并行查询
)
print(f"Collection 创建成功,分片数:{collection.num_shards}")
2. 批量插入 100 万条测试数据
import random
from tqdm import tqdm
BATCH_SIZE = 5000
TOTAL = 1_000_000
for start in tqdm(range(0, TOTAL, BATCH_SIZE), desc="插入向量中"):
end = min(start + BATCH_SIZE, TOTAL)
batch_size = end - start
entities = [
[i for i in range(start, end)], # id
np.random.rand(batch_size, 768).tolist(), # embedding
[f"category_{random.randint(1,20)}" for _ in range(batch_size)], # category
[round(random.uniform(10.0, 9999.0), 2) for _ in range(batch_size)], # price
[1715000000 + random.randint(0, 86400*365) for _ in range(batch_size)], # create_time
]
collection.insert(entities)
print(f"✅ 插入完成,共 {collection.num_entities} 条")
3. 创建索引(IVF_SQ8 适合大规模数据)
# 向量索引
collection.create_index(
field_name="embedding",
index_params={
"metric_type": "IP", # 内积(余弦相似度需先归一化)
"index_type": "IVF_SQ8", # 量化索引,节省内存
"params": {"nlist": 2048} # 聚类中心数量,经验值:sqrt(N) ~ N/1000
}
)
# 标量索引(加速混合检索中的过滤)
collection.create_index(field_name="category", index_params={"index_type": "Trie"})
collection.create_index(field_name="price", index_params={"index_type": "STL_SORT"})
collection.load() # 加载到内存,才能查询
print("✅ 索引创建完成,Collection 已加载")
4. 混合检索实战(向量 + 标量过滤)
纯向量检索(不推荐,召回不准):
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param={"metric_type": "IP", "params": {"nprobe": 16}},
limit=10
)
混合检索(精准!):
# 表达式语法和 SQL WHERE 类似
expr = "category == 'category_3' and price >= 100 and price <= 500"
results = collection.search(
data=[query_embedding],
anns_field="embedding",
expr=expr, # 👈 混合检索核心:标量过滤 + 向量检索
param={
"metric_type": "IP",
"params": {"nprobe": 32} # nprobe 越大越准但越慢
},
limit=10,
output_fields=["category", "price"] # 返回标量字段
)
for hit in results[0]:
print(f"ID: {hit.id}, 距离: {hit.distance:.4f}, "
f"分类: {hit.entity.get('category')}, 价格: {hit.entity.get('price')}")
性能调优 5 个关键参数
| 参数 | 默认值 | 推荐值(亿级数据) | 说明 |
|---|---|---|---|
nlist | 16384 | 4096~8192 | 太大索引构建慢,太小查询精度低 |
nprobe | 8 | 16~64 | 查询时扫描的聚类中心数,精度和速度的平衡点 |
shards_num | 1 | 2~4 | 分片数,建议等于 QueryNode 数量 |
index_type | IVF_FLAT | IVF_SQ8 / HNSW | 亿级数据用 SQ8(量化压缩),内存不够用 HNSW |
M(HNSW 专用) | 16 | 48~64 | HNSW 的图连接数,越大召回率越高但占用内存越多 |
实测数据(8000 万条 768 维):
- IVF_SQ8,nprobe=32:查询延迟 45ms,召回率 94%
- HNSW,M=64:查询延迟 18ms,召回率 98%,但内存占用是 IVF 的 3 倍
生产环境部署建议
- QueryNode 内存计算:每 1 亿条 768 维向量(IVF_SQ8)约占用 4GB 内存,预留 50% 缓冲
- Proxy 前置负载均衡:用 Nginx 或云 LB 把流量分到多个 Proxy
- Kubernetes 部署:官方 Helm Chart 支持,QueryNode 用 HPA 根据 CPU 自动扩缩
- 监控指标:重点关注
milvus_proxy_sq_latency(查询延迟)和milvus_querynode_cache_hit_rate(缓存命中率)
小结
Milvus 集群部署的核心要点:
- Proxy 随便扩(无状态),QueryNode 谨慎扩(涉及数据迁移)
- 混合检索用
expr参数,标量字段一定要建索引 - 亿级数据优先选 IVF_SQ8(内存友好),延迟敏感场景用 HNSW
- 分片数设置 = QueryNode 数量,才能把查询并行化
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~