最近在搭一套基于规范条文的 RAG 知识库,向量库选了 Milvus。单机版部署本身不复杂,docker compose up -d一条命令的事,但我发现很多人(包括最开始的我)起完之后其实并不清楚那三个容器各自在干嘛,遇到端口冲突、连不上、数据写不进去之类的问题就只能干瞪眼。
这篇把两件事讲清楚:Milvus standalone 是怎么跑起来的,以及部署时实际会踩到的坑。不讲那些官网抄一遍就有的东西,只说自己装的时候真正需要搞明白的部分。
一、为什么单机版要起三个容器
你docker compose up -d之后docker ps会看到三个容器:milvus-standalone、milvus-etcd、milvus-minio。第一次见到的人通常会愣一下——我不就装个数据库吗,怎么冒出来仨。
原因是 Milvus 走的是存算分离的架构。它自己不负责持久化数据,而是把不同种类的状态分别甩给专门的组件去存。这个设计在分布式集群里是为了各部分能独立扩缩容,到了单机版,虽然计算角色都压进了一个进程,但依赖关系没变,所以 etcd 和 minio 还是得拉起来陪跑。
说白了就是:standalone 是干活的,etcd 和 minio 是存东西的,而且这俩存的是完全不同的东西。
standalone:本体,但不存数据
这是 Milvus 真正干活的进程。在集群版里 proxy(接请求)、querynode(查询)、datanode(写数据)、indexnode(建索引)是分开的不同节点,单机版把它们全打包进了这一个容器。你代码里连的19530端口就是它。
关键点:它本身不持久化任何数据。这个容器删了重建,里面是空的,所有数据得靠下面两个组件捞回来。所以你会发现重启 standalone 很快,因为它只是个计算层。
etcd:存元数据
etcd 里放的是"目录信息"——有哪些 collection、每个 collection 的 schema 长什么样、字段定义、索引参数、分区信息、各组件状态和时间戳。
数据量很小,但极其关键。Milvus 要知道"这个库里有什么、结构是怎样、索引建在哪",全靠问 etcd。可以把它理解成整个库的账本。
minio:存数据本体
minio 是个 S3 兼容的对象存储,真正的大块数据都堆在这——向量、标量字段的值、建好的索引文件、各种日志。你插进去的向量和 HNSW 索引,最终都以对象的形式落在 minio 里。
一个不太严谨但好懂的类比
把 standalone 想成图书馆管理员,干活但不藏书;etcd 是借阅目录的卡片柜,记着每本书叫什么、在哪个架子上;minio 是真正堆书的书库。管理员下班重启了无所谓,只要卡片柜和书库还在,他回来查一下目录就能把书找回来。
二、insert 和 search 时,三者怎么配合
理解了分工,数据流就很顺了。
写入(insert):请求打到 standalone → 这批数据属于哪个 collection、对应什么 schema,这类元数据记进 etcd → 向量和索引文件这些实体数据写进 minio。
检索(search):请求到 standalone → 先问 etcd “这个 collection 的索引在哪、什么类型” → 再从 minio 把索引和数据读出来,在内存里算相似度 → 返回结果。
这里有个容易忽略的细节:这三个容器之间走的是容器内网,用服务名互相寻址。standalone 的配置里连的是etcd:2379和minio:9000,而不是宿主机端口。这点等下排端口冲突时有用。
三、部署步骤
前置条件:装好 Docker 和 Docker ComposeV2。注意是docker compose(中间空格),不是老的docker-compose。docker compose version能正常输出版本号就行。
下载 compose 文件并启动
# 钉死版本,别用会自己变的 latestwgethttps://github.com/milvus-io/milvus/releases/download/v2.6.18/milvus-standalone-docker-compose.yml\-Odocker-compose.ymldockercompose up-d起来之后三个容器都应该是 Up 状态,standalone 对外暴露 19530:
dockercomposeps验证
直接用 Python 连一下最直观:
frompymilvusimportMilvusClient client=MilvusClient(uri="http://localhost:19530")print(client.list_collections())# 能返回(哪怕是空列表)就说明通了四、踩坑:端口冲突
我第一次docker compose up -d直接报了这个:
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint milvus-minio: failed to bind host port for 0.0.0.0:9001 ... address already in use这跟 Milvus 本身没关系,是宿主机的 9001 端口被别的进程占了,minio 绑不上。先看是谁占的:
sudoss-ltnp|grep:9001大概率两种情况。
一种是之前已经起过一次 Milvus,旧的 minio 容器还在跑。这种最常见,把整套停干净再重起:
dockercompose downdockerps-a|grepmilvus# 确认没有残留的 milvus-* 容器dockercompose up-d要是还看到游离的容器(可能是上次用别的方式起的),手动清掉:
dockerrm-fmilvus-minio milvus-etcd milvus-standalonedockercompose up-d另一种是 9001 被非 Milvus 的进程占了。那就改 compose,把 minio 映射到宿主机的端口换一个,容器内端口不要动:
ports:-"9011:9001"# 只改冒号左边(宿主机侧)-"9010:9000"为什么只改左边?回到前面讲的:Milvus 内部是通过容器网络minio:9000连 minio 的,跟你映射到宿主机的端口没半点关系。冒号右边是容器内端口,改了反而连不上。
顺手做个简化
其实 minio 的 9000/9001、etcd 的 2379根本没必要绑到宿主机,因为 Milvus 走的是容器内网。你真正需要从宿主机访问的只有 standalone 的19530(以及可选的 WebUI 9091)。
所以想彻底避免这类端口冲突、顺便更安全一点,可以直接把 minio 和 etcd 那几行ports:注释掉,只留 19530 对外。装多套环境、或者机器上服务比较杂的时候,这一步能省不少事。
五、几点实战判断
数据的真值不在 Milvus 里。三个容器的数据落在 compose 同目录的volumes/下(volumes/etcd、volumes/minio、volumes/milvus)。但如果你的向量是从某份源文件(比如我这边是清洗好的 jsonl)灌进去的,那真值是那份源文件,Milvus 里的东西随时能重建。我的做法是源文件进 git,volumes 目录根本不备份——这点数据量,重灌比恢复还快。当然,如果你的向量是花了大代价算出来、源头丢了就没了,那另说,该备份备份。
排障时按组件对症下药。理解了三者分工,出问题能快速定位:连不上 19530 是 standalone 的事;报 collection / schema 相关的诡异错误,常是 etcd 状态乱了;报存储、写不进数据、failed to put object这类,基本是 minio 的问题(最常见是磁盘满了)。
Milvus 单机版部署这事,命令是真简单,但把这三个容器的关系搞明白之后,遇到问题才知道往哪查,而不是每次都重装一遍碰运气。后面我会接着写向量化服务和检索这部分怎么和它对接,有需要的可以关注一下。