☰
Milvus vs Chroma vs Qdrant:生产环境向量数据库选型与Docker部署实战
2026/11/4 9:48:05 网站建设 项目流程

1. 为什么我最终选了 Milvus 而不是 Chroma 或 Qdrant

向量数据库这个赛道这两年热闹得不行,Chroma、Qdrant、Weaviate、Milvus 各有各的拥趸。我最早做知识库检索原型的时候用的是 Chroma,图它轻量、几行代码就能跑起来,本地开发体验确实好。但项目一旦往生产环境推,问题就来了:数据量上到百万级向量之后,Chroma 的查询延迟开始肉眼可见地抖动,而且它原生不支持分布式部署,横向扩展基本靠手动分片,运维成本陡增。

后来我花了一周时间把 Chroma、Qdrant 和 Milvus 做了个横向对比测试,场景是 200 万条 768 维向量、TopK=10 的近似最近邻检索。测试环境是一台 16 核 64G 内存的机器,三者都用 Docker 部署单机版。结果 Qdrant 在纯检索延迟上表现最好,P99 大概在 12ms 左右;Milvus 稍慢一点,P99 约 18ms;Chroma 在数据量上去之后 P99 直接飙到 80ms 以上。但 Milvus 的优势在于它的架构设计——存算分离、支持多种索引类型(IVF_FLAT、HNSW、DiskANN 等)、原生支持水平扩展,而且社区活跃度和文档完善程度明显高一个档次。

最终让我下定决心的是一次数据迁移的踩坑经历。当时用 Chroma 存了大概 50 万条向量,想迁移到另一台机器上,发现它的持久化文件格式在不同版本之间居然不兼容,升级版本后旧数据直接读不出来。Milvus 在这方面规范得多,它有独立的 etcd 做元数据管理、MinIO 做对象存储,数据迁移和备份都有标准流程。所以如果你和我一样,项目会从原型走向生产,Milvus 是更稳妥的选择。

提示:选型没有绝对的对错。如果只是做本地小规模实验、数据量在十万级以内,Chroma 完全够用且更省心。但一旦涉及生产环境、数据量预期会增长、需要高可用,Milvus 的架构优势就体现出来了。

2. Docker 部署 Milvus 单机版的完整操作链路

2.1 环境准备:Docker 与 Docker Compose 的安装确认

Milvus 官方推荐的部署方式就是 Docker Compose,因为它依赖 etcd 和 MinIO 两个外部组件,手动一个个装太折腾。所以第一步是确认你的机器上 Docker 和 Docker Compose 都能正常工作。

在 Linux 上,用下面命令确认:

docker --version docker compose version

如果docker compose version报错说找不到命令,说明你装的是老版本的docker-compose(带横杠),需要单独安装 Compose V2 插件。Ubuntu 下可以这样补装:

sudo apt-get update sudo apt-get install docker-compose-plugin

Windows 用户如果用 Docker Desktop,Compose V2 是自带的,直接在 PowerShell 里跑docker compose version就行。这里有个我踩过的坑:Windows 上 Docker Desktop 启动时报 “Virtualization support not detected”,大概率是 BIOS 里的虚拟化选项没开,或者和 Hyper-V / WSL2 的配置冲突。解决办法是进 BIOS 打开 Intel VT-x 或 AMD-V,然后在 Windows 功能里确认 “虚拟机平台” 和 “适用于 Linux 的 Windows 子系统” 都勾上了。

另外,Milvus 对内存有硬性要求,官方建议至少 8GB 可用内存,低于这个值 etcd 可能起不来。我实测 4GB 内存的机器上跑 standalone 模式,MinIO 会频繁 OOM。所以部署前先用free -h确认一下可用内存。

2.2 拉取 Milvus 的 Docker Compose 配置文件

Milvus 官方在 GitHub 上维护了 standalone 模式的 compose 文件,直接下载就行:

wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml

如果你网络环境访问 GitHub 慢,也可以手动创建这个文件。核心内容包含三个服务:etcd(元数据存储)、minio(对象存储)、standalone(Milvus 本体)。我建议把版本号固定住,不要用latest,因为 Milvus 不同大版本之间的 API 有 breaking change,用 latest 哪天自动升级了可能直接把你的代码跑挂。

下载完之后,看一眼文件里的端口映射。默认情况下:

服务容器内端口宿主机端口用途
Milvus1953019530gRPC 和 RESTful API
Milvus90919091健康检查与指标
MinIO90009000对象存储 API
MinIO90019001MinIO 控制台
etcd23792379元数据服务

如果你宿主机上这些端口已经被占用了(比如 9000 被别的服务占了),需要改 compose 文件里左边的宿主机端口映射。改完之后记得同步修改 Milvus 配置里连接 MinIO 的地址。

2.3 启动服务与验证运行状态

配置文件就绪后,一条命令拉起所有服务:

docker compose up -d

-d是后台运行。执行完之后用docker compose ps看一下三个容器的状态,正常应该是running或者healthy。Milvus 的 standalone 容器启动会慢一些,因为它要等 etcd 和 MinIO 都就绪之后才能完成初始化,大概需要 30 秒到 1 分钟。

验证服务是否真正可用,最直接的方式是看健康检查接口:

curl http://localhost:9091/healthz

返回OK就说明 Milvus 已经正常工作了。如果返回连接拒绝,先docker compose logs standalone看日志,常见问题是 etcd 连接超时或者 MinIO 的 access key 配置不对。

注意:第一次docker compose up的时候,Docker 需要拉取 etcd、MinIO 和 Milvus 三个镜像,总共大概 1.5GB 左右。如果拉取速度慢,可以配置国内镜像加速器,在 Docker Desktop 的设置里或者/etc/docker/daemon.json里加上 registry-mirrors。

2.4 数据持久化目录的规划

默认的 compose 文件会把数据存在 Docker 的 volume 里,路径类似/var/lib/docker/volumes/milvus_volumes/。这样做的好处是 Docker 帮你管理,坏处是你想备份或者迁移数据的时候得先找到这个路径。

我的做法是在宿主机上建一个明确的数据目录,比如/data/milvus/,然后在 compose 文件里把 volume 映射改成 bind mount:

volumes: - /data/milvus/etcd:/etcd - /data/milvus/minio:/minio_data - /data/milvus/milvus:/var/lib/milvus

这样数据文件一目了然,备份的时候直接打包/data/milvus/目录就行。但要注意目录权限问题,Milvus 容器内的进程是以非 root 用户运行的,如果宿主机目录权限不对,容器会报 permission denied。解决办法是提前把目录权限放开:

sudo mkdir -p /data/milvus/{etcd,minio,milvus} sudo chmod -R 777 /data/milvus

生产环境当然不建议 777,可以根据容器内用户的 UID 来精确设置,但开发环境图省事就这么干了。

3. 用 Python SDK 跑通第一个向量检索 Demo

3.1 安装 pymilvus 并建立连接

Milvus 提供了多种语言的 SDK,Python 这边是pymilvus。安装很简单:

pip install pymilvus

截至我写这篇的时候,pymilvus 2.4.x 对应 Milvus 2.4.x,版本号最好对齐,避免 API 不兼容。安装完之后,连接 Milvus 服务:

from pymilvus import connections, utility connections.connect( alias="default", host="localhost", port="19530" ) print(utility.get_server_version())

如果打印出了版本号,说明连接成功。这里有个细节:connections.connect是全局的,同一个进程里多次调用会复用连接。如果你要连多个 Milvus 实例,需要用不同的 alias 区分。

3.2 定义 Collection Schema 与索引参数

Milvus 里的数据组织单位叫 Collection,类比关系型数据库里的表。定义一个 Collection 需要指定字段结构,其中必须有一个主键字段和一个向量字段。

from pymilvus import CollectionSchema, FieldSchema, DataType, Collection # 定义字段 book_id = FieldSchema( name="book_id", dtype=DataType.INT64, is_primary=True, auto_id=False ) book_title = FieldSchema( name="book_title", dtype=DataType.VARCHAR, max_length=512 ) book_vector = FieldSchema( name="book_vector", dtype=DataType.FLOAT_VECTOR, dim=768 ) schema = CollectionSchema( fields=[book_id, book_title, book_vector], description="图书向量检索测试" ) collection = Collection( name="book_collection", schema=schema )

这里dim=768是因为我用的 embedding 模型输出是 768 维。如果你用的是 OpenAI 的 text-embedding-3-small,维度是 1536;如果用 BGE 系列中文模型,常见的是 768 或 1024。维度必须和实际生成的向量一致,否则插入时会报错。

建完 Collection 之后要创建索引,否则 Milvus 不会对向量字段做任何加速,查询会退化成暴力扫描。索引类型的选择直接影响检索速度和召回率:

索引类型适用场景内存占用检索速度召回率
FLAT小数据集,要求 100% 召回高慢100%
IVF_FLAT中等数据集,可接受少量精度损失中中可调
HNSW大数据集,追求低延迟高快高
DiskANN超大数据集,内存有限低中高

我一般用 HNSW,因为它在延迟和召回率之间平衡得最好。创建索引的代码:

index_params = { "index_type": "HNSW", "metric_type": "COSINE", "params": { "M": 16, "efConstruction": 200 } } collection.create_index( field_name="book_vector", index_params=index_params )

M控制每个节点的最大连接数,越大索引越精确但内存占用越高;efConstruction是构建时的搜索范围,越大构建越慢但索引质量越好。这两个参数没有绝对的最优值,需要根据你的数据规模和精度要求来调。

3.3 插入数据与执行相似度检索

索引建好之后,往 Collection 里插数据:

import random # 模拟生成向量数据 entities = [ [i for i in range(100)], # book_id [f"图书标题_{i}" for i in range(100)], # book_title [[random.random() for _ in range(768)] for _ in range(100)] # book_vector ] collection.insert(entities) collection.flush()

flush()这一步很关键。Milvus 是异步写入的,数据先进入内存中的 segment,flush 之后才会持久化到磁盘并变得可搜索。如果不 flush 就查询,可能查不到刚插入的数据。

检索的时候,先准备好查询向量,然后调用search:

search_params = { "metric_type": "COSINE", "params": {"ef": 64} } query_vector = [[random.random() for _ in range(768)]] results = collection.search( data=query_vector, anns_field="book_vector", param=search_params, limit=5, output_fields=["book_title"] ) for hits in results: for hit in hits: print(f"ID: {hit.id}, 距离: {hit.distance}, 标题: {hit.entity.get('book_title')}")

ef参数控制检索时的搜索范围,越大越精确但越慢。一般设置为limit的 2 到 4 倍比较合适。

3.4 加载 Collection 与释放资源的正确姿势

有一个新手特别容易踩的坑:Milvus 的 Collection 在查询之前必须先load到内存里。如果你建完 Collection 直接 search,会报 “collection not loaded” 的错误。

collection.load()

load()会把索引文件和部分数据加载到内存中,这个过程可能耗时几秒到几十秒,取决于数据量大小。加载完成后才能执行 search 和 query 操作。

对应的,如果你暂时不用这个 Collection 了,可以release()释放内存:

collection.release()

但注意 release 之后再次查询需要重新 load。在生产环境中,通常不会频繁 release,因为 load 的开销不小。

4. 实际使用中绕不开的几个坑与应对方案

4.1 内存溢出:load 大数据集时的 OOM 问题

Milvus 的 standalone 模式把所有数据都加载到内存里,这是它查询快的原因,但也是最大的风险点。我遇到过最典型的情况是:Collection 里有 500 万条 768 维向量,load()的时候直接把 32GB 内存的机器打爆了。

算一笔账:500 万条 × 768 维 × 4 字节(float32)= 约 15GB 纯向量数据。加上 HNSW 索引本身的图结构开销(通常是原始数据的 1.5 到 2 倍),总内存需求轻松超过 30GB。这还没算上 etcd 和 MinIO 的占用。

应对方案有几个:一是换用 DiskANN 索引,它把大部分索引数据放在磁盘上,内存占用可以降低到原来的十分之一左右,代价是查询延迟会高一些;二是用 Milvus 的集群模式,把数据分片到多个 query node 上;三是在业务层面做分区(partition),按时间或类别把数据切分,查询时只 load 相关的分区。

# 按类别分区,查询时只加载特定分区 collection.create_partition("category_tech") collection.create_partition("category_literature") collection.insert(entities, partition_name="category_tech") collection.load(partition_names=["category_tech"])

4.2 向量维度不匹配与 metric_type 选错的排查

维度不匹配是最常见的报错之一,错误信息通常是 “dimension mismatch” 或者 “vector dimension not equal”。排查思路很简单:确认你生成 embedding 的模型输出维度,和 Collection schema 里定义的dim是否一致。我见过有人用 OpenAI 的 embedding 接口,默认返回 1536 维,但 schema 里写的是 768,插入时直接报错。

metric_type 选错则更隐蔽,因为它不会报错,只是检索结果不准。Milvus 支持 COSINE、L2、IP 三种度量方式:

  • COSINE:余弦相似度,适合文本语义检索,值越大越相似
  • L2:欧氏距离,适合图像特征检索,值越小越相似
  • IP:内积,适合推荐系统场景,值越大越相似

关键点是:索引的 metric_type 和查询时的 metric_type 必须一致。如果你建索引时用了 COSINE,查询时写 L2,Milvus 会报错。另外,如果你的向量已经做了归一化(L2 norm = 1),那么 COSINE 和 IP 是等价的,用哪个都行。

4.3 Docker 网络不通导致 SDK 连接失败的定位过程

这个坑我在 Windows 和 Linux 上都遇到过。现象是:docker compose ps显示容器都正常运行,curl http://localhost:9091/healthz也返回 OK,但 Python SDK 连接 19530 端口就是超时。

排查链路是这样的:先确认端口有没有真正映射出来,用docker port milvus-standalone看容器的端口映射情况。如果显示19530/tcp -> 0.0.0.0:19530,说明映射没问题。然后从宿主机上telnet localhost 19530测试连通性,如果 telnet 不通,说明是防火墙或者 iptables 规则拦截了。

Linux 上常见的原因是 firewalld 或者 ufw 没放行 19530 端口。Windows 上则可能是 Docker Desktop 的 WSL2 网络模式和宿主机网络之间的隔离导致的,解决办法是在 Docker Desktop 设置里开启 “Expose daemon on tcp://localhost:2375 without TLS”,或者直接用host.docker.internal作为主机名来连接。

还有一种情况是 Milvus 容器虽然启动了,但内部服务还没完全就绪。这时候 SDK 连接会报 “fail to connect to server”。解决办法是等健康检查通过之后再连,或者在代码里加重试逻辑:

import time from pymilvus import connections for i in range(10): try: connections.connect(host="localhost", port="19530") print("连接成功") break except Exception as e: print(f"第 {i+1} 次连接失败,重试...") time.sleep(3)

4.4 数据持久化与容器重启后的数据恢复验证

Docker 容器重启后数据还在不在,取决于你有没有正确配置 volume。默认的 compose 文件用了 named volume,数据是持久的,但如果你手动docker compose down -v,-v参数会删除 volume,数据就没了。这个参数在清理环境的时候很好用,但千万别在生产环境手滑。

验证数据持久化的方法:插入一批数据,flush 之后docker compose restart,等容器重新起来后再查询,看数据是否还在。如果数据丢了,检查 compose 文件里的 volume 配置,确认 etcd、MinIO、Milvus 三个服务的数据目录都做了持久化映射。

另外,Milvus 的数据是分散在 etcd(元数据)、MinIO(向量数据和索引文件)两处的,备份的时候要一起备份,只备份其中一个恢复不出来完整数据。

5. 从 Demo 到生产:几个值得提前考虑的设计决策

5.1 Collection 的字段设计:哪些该存、哪些该外挂

Milvus 支持标量字段(INT64、VARCHAR、BOOL 等)和向量字段混存,但它的标量字段查询能力相比关系型数据库还是弱不少。我的经验是:只把检索时必须用到的过滤字段放在 Milvus 里,比如类别、时间戳、标签这类用于expr过滤的字段。完整的业务数据(比如文章正文、用户信息)还是放在 MySQL 或者 PostgreSQL 里,Milvus 只存一个关联 ID。

这样做的好处是:一是减少 Milvus 的存储压力,标量字段也是要占内存的;二是避免把 Milvus 当主数据库用,它的数据一致性保证没有关系型数据库那么强;三是后续如果要换向量数据库,迁移成本更低,因为核心业务数据不在 Milvus 里。

# Milvus 里只存必要字段 fields = [ FieldSchema(name="doc_id", dtype=DataType.INT64, is_primary=True), FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64), FieldSchema(name="create_time", dtype=DataType.INT64), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768) ]

查询的时候用expr做标量过滤:

results = collection.search( data=query_vector, anns_field="embedding", param=search_params, limit=10, expr='category == "tech" and create_time > 1700000000', output_fields=["doc_id", "category"] )

5.2 批量插入的性能调优与 flush 策略

往 Milvus 里灌数据的时候,如果一条一条 insert,性能会非常差。我实测过,单条插入的吞吐大概只有几百 TPS,而批量插入(每批 1000 条)可以到几万 TPS。

但批量也不是越大越好。批次太大(比如一次 10 万条)会导致单次请求超时,而且内存占用飙升。我的经验值是每批 500 到 2000 条比较合适,具体取决于单条向量的大小。

flush 策略也很关键。频繁 flush 会产生大量小的 segment,影响查询性能;长时间不 flush 则数据一直在内存里,容器重启会丢。Milvus 有一个自动 flush 的机制,默认会根据数据量触发。但在批量导入场景下,我通常会在所有数据插入完成后手动 flush 一次,确保数据落盘。

batch_size = 1000 for i in range(0, len(all_vectors), batch_size): batch = all_vectors[i:i+batch_size] collection.insert(batch) collection.flush()

5.3 索引构建时机:先插数据还是先建索引

这个问题我纠结过一阵子。Milvus 的官方文档建议是先建索引再插数据,但实际上两种顺序都支持。区别在于:

先建索引再插数据:每次插入新数据时,Milvus 需要增量更新索引,插入速度会慢一些,但数据插入后立即可搜索。

先插数据再建索引:插入速度最快,但建索引的时候会有一个较长的阻塞期,期间 Collection 不可搜索。对于一次性导入大量历史数据的场景,这种方式更合适。

我的做法是:初始化导入用“先插后建”,日常增量更新用“先建后插”。如果增量数据量很大,可以攒一批之后重建索引,而不是每条都触发索引更新。

5.4 监控指标:哪些 Milvus 指标值得盯

Milvus 在 9091 端口暴露了 Prometheus 格式的指标,生产环境建议接入监控。几个我重点关注的指标:

  • milvus_proxy_search_vectors_count:累计检索向量次数,用来评估负载
  • milvus_querynode_sq_queue_latency:查询队列延迟,超过 100ms 就要警惕
  • milvus_datacoord_stored_binlog_size:已存储的 binlog 大小,用来评估磁盘占用
  • milvus_rootcoord_collection_num:Collection 数量,太多会影响元数据管理性能

如果不想自己搭 Prometheus + Grafana,Milvus 也支持通过curl http://localhost:9091/metrics直接拉取指标文本,写个定时脚本解析也行。

6. 一些零散但实用的经验补充

Milvus 的 Docker 部署看起来简单,但细节很多。比如docker compose down和docker compose down -v的区别,前者只停容器保留数据,后者连数据一起删。我在测试环境经常用-v来快速重置,但有一次在预发布环境手快用了-v,把测试数据全清了,还好不是生产库。

还有一点是关于 Milvus 的版本升级。Docker 部署的好处是升级方便,改一下 compose 文件里的镜像 tag,重新docker compose up -d就行。但跨大版本升级(比如 2.3 到 2.4)之前一定要看官方的 release notes,确认有没有 breaking change。我有一次从 2.2 升到 2.3,发现search接口的output_fields参数行为变了,之前不传会返回所有标量字段,升级后默认只返回主键,导致业务代码拿不到数据。

最后说一个关于资源清理的。Milvus 的 Collection 删除之后,磁盘上的数据不会立即释放,而是等后台的 compaction 任务来清理。如果你频繁创建和删除 Collection,磁盘占用会持续增长。手动触发清理可以用compact()方法,或者等 Milvus 的 GC 周期自动处理。在磁盘紧张的机器上,这个行为需要特别注意。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询