Milvus 核心概念
这是理解 Milvus 的基石,它们定义了 Milvus 处理的对象和基本单位。
- 非结构化数据 (Unstructured Data) :指那些不符合预定义数据模型、不便于用数据库二维逻辑表来表现的数据,如图像、视频、音频和自然语言文本等。传统数据库难以高效处理这类数据的相似性检索,这正是 Milvus 擅长解决的问题。
- 向量 (Vector) :非结构化数据的“数学翻译”。它本质上是一个由浮点数或二进制数组成的列表(数组),用于描述原始数据的特征。
- 密集向量 (Dense Vector) :维度通常为数百,每个位置都有非零值,适合表达包含丰富语义的文本或图像整体特征。
- 稀疏向量 (Sparse Vector) :维度可能高达数十万,但大部分位置为0,适合用于关键词匹配或在推荐系统中表达用户长尾兴趣等场景。
- 嵌入 (Embedding) :指将非结构化数据通过模型(如BERT、ResNet)转化为向量的过程。Milvus 2.6.x 引入了嵌入函数 (Embedding Function) 功能,你只需提供原始文本,它便能直接调用 OpenAI、Hugging Face 等模型自动完成向量化,并存储到指定向量字段,整个过程更加平滑。
- 实体 (Entity) :类似于关系数据库中的“行”。它由一个主键和一组字段(包含标量字段和向量字段)组成,构成了Collection中的一条完整数据记录。
- 字段 (Field) :实体的组成部分,类似于关系数据库中的“列”。Milvus 2.6.x 的字段分为两类:
- 标量字段 (Scalar Field) :存储元数据(如
author、price),用于数据过滤和查询。2.6.x版本大幅增强了标量字段能力,支持动态添加新字段而无需重建集合,并新增了GEOMETRY(地理空间) 类型,用于存储和查询空间数据。 - 向量字段 (Vector Field) :核心数据所在,存储由嵌入生成的向量。
- 标量字段 (Scalar Field) :存储元数据(如
- 主键 (Primary Key) :实体的唯一标识符,类似于关系数据库的主键列。数据类型可以是
INT64或VARCHAR。- AutoID :是主键的一个属性。若在创建集合时设置
auto_id=True,则插入数据时无需指定主键值,Milvus会自动生成并管理它。
- AutoID :是主键的一个属性。若在创建集合时设置
- 集合 (Collection) :类似于关系数据库中的“表”。它是所有实体和字段的容器,我们正是通过在集合上定义结构(Schema)、建立索引和发起搜索来完成工作。
数据建模与管理
这部分涉及如何为数据设计蓝图,并进行物理分区管理。
- 模式 (Schema) :数据结构的“蓝图”,定义了Collection包含哪些字段、字段类型及约束。在Milvus 2.6.x中,你可以随时向已有Schema添加新字段,实现不停机的模式演进。
- 动态模式 (Dynamic Schema) :一种灵活的Schema设计,允许你在插入数据时,定义Schema之外的字段。Milvus 会自动将这些未定义的字段放入一个名为
$meta的保留字段中存储。这在处理JSON等半结构化数据时非常有用。
- 动态模式 (Dynamic Schema) :一种灵活的Schema设计,允许你在插入数据时,定义Schema之外的字段。Milvus 会自动将这些未定义的字段放入一个名为
- 索引 (Index) :为加速字段(尤其是向量字段)搜索而构建的数据结构。Milvus 支持IVF_FLAT、HNSW等多种索引。
- 自动索引 (Auto-Index) :一种智能索引策略。Milvus会根据数据分布自动选择最合适的索引类型和参数,免去手动调参的繁琐。例如,配置向量字段自动索引会根据经验自动调整配置以优化性能。
- 度量类型 (Metric Type) :衡量两个向量间相似度或距离的方法,需与索引类型匹配。Milvus 2.6.x 支持的主要类型如下:
- 欧氏距离 (L2) :计算两点间的直线距离,值越小越相似。
- 内积 (IP) :推荐系统中的常用指标,值越大越相似。
- 余弦相似度 (COSINE) :关注方向而非大小,值越大越相似。
- 分区 (Partition) :Collection内部的逻辑划分。将实体按一定规则(如日期、类别)放入不同分区,查询时指定分区可显著提升效率。
- 分片 (Shard) :用于横向扩展的物理分割。一个Collection的数据会被均分到多个分片,分布在多个计算节点上,以实现并行写入和存储。
- 分区键 (Partition Key) :从
is_partition_key=True的字段取值,自动将记录路由到对应分区。将分片、分区、分区键结合,可在不同粒度上实现数据隔离,常用于多租户场景。
- 分区键 (Partition Key) :从
核心操作
这部分是Milvus为用户提供的基本数据操作能力。
- 插入 (Insert) :向指定Collection中添加一条或多条实体。
- 批量写入工具 (Bulk Writer) :一种辅助工具,用于将原始数据集(如JSON、NumPy文件)高效转换成Milvus可导入的格式。
- 批量插入 (Bulk Insert) :一个API接口,允许你通过单个请求导入多个文件(如JSON、NumPy),从而高效地将海量数据一次性写入Milvus。批量插入需要先将数据文件上传到对象存储。
- Upsert :一种数据操作,若记录不存在则插入(Insert),若存在则更新(Update)。
- 搜索 (Search) :Milvus的核心操作,通过查询向量,找到Collection中与之最相似的Top-K向量( 近似最近邻搜索 ,ANN),并返回其主键和标量字段值。
- 查询 (Query) :基于标量字段条件的精确查询,与SQL的
SELECT语句类似。此操作不涉及向量相似度计算。 - 过滤搜索 (Filtered Search) :最常见的搜索模式。在向量搜索中加入标量过滤条件
expr,实现语义匹配与元数据过滤的结合。 - 范围搜索 (Range Search) :向量搜索的一个变种。不限定返回结果数量(K),而是搜索查询向量距离在指定半径内的所有向量。
- 混合搜索 (Hybrid Search) :多向量检索与重排序。2.6.x版本极大增强了混合检索能力,核心功能如下:
hybrid_search()API :支持在多个向量字段上同时发起ANN搜索(多路召回),然后通过RRF、Weighted Ranker等策略对结果进行重排序。- 文本+向量混合检索 :原生集成了基于BM25的全文检索和向量检索能力,支持短语匹配等精确度控制。
- 空间+向量混合检索 :原生支持 几何搜索 ,如上例所示,能在一个查询中完成空间范围过滤(地理围栏)和语义相关性搜索。
Milvus核心架构
客户端
各 SDK 官方地址:
| SDK | 语言 | 官方仓库 / 文档 |
|---|---|---|
| PyMilvus | Python | https://github.com/milvus-io/pymilvus(pip install pymilvus) |
| Milvus Go SDK | Go | https://github.com/milvus-io/milvus-sdk-go(go get github.com/milvus-io/milvus-sdk-go/v2) |
| Milvus Java SDK | Java | https://github.com/milvus-io/milvus-sdk-java(Maven / Gradle 引入) |
| Milvus Node.js SDK | JavaScript | https://github.com/milvus-io/milvus-sdk-node(npm install @zilliz/milvus2-sdk-node) |
| RESTful API | 通用 | 内嵌于 Proxy 组件,通过 HTTP 端口访问,SDK 中以 uri="http://..."方式连接 |
RESTful API 并非独立仓库,而是由 Proxy 进程内部启动的 HTTP Gateway 提供。
所有语言 SDK 底层均通过 gRPC(默认端口 19530)与 Proxy 通信,Proxy 同时对外暴露 RESTful 端点(默认端口 9091),方便非 gRPC 语言直接通过 HTTP 调用。
各 SDK 共享同一套 Protobuf 定义:https://github.com/milvus-io/milvus-proto 。
访问层(Access Layer)
访问层是Milvus系统的最前端,负责接收和处理所有客户端请求。它是用户与Milvus系统交互的入口点,扮演着至关重要的网关角色。
从 Docker 容器认识访问层
在 Milvus Standalone 部署中,访问层(Proxy)作为 Milvus 容器内的核心组件运行。通过 Docker 命令可以直接观察到 Proxy 的端口监听、请求处理和服务状态。
# 1. 查看 Milvus 容器及其端口映射
$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"
# 关键发现:
# 端口 19530 → gRPC 端口(SDK 必须连接这个端口!)
# 端口 9091 → HTTP 监控/管理端口
# etcd 的端口(2379-2380/tcp)

Proxy 启动日志
# windows
docker logs milvus-standalone 2>&1 | findstr /i "proxy access layer"
# linux
docker logs milvus-standalone 2>&1 | grep -i "proxy\|access layer"

访问层核心职责
1) 请求接收与路由
访问层通过统一的API接口(gRPC/HTTP)接收来自客户端的请求,并根据请求类型将其路由到相应的协调器组件。当客户端发起连接时,访问层首先验证连接的有效性,然后根据操作类型(插入、搜索、查询等)将请求分发到对应的处理模块。

2) 认证与授权
访问层集成了完整的用户认证和权限管理机制。Milvus支持基于角色的访问控制(RBAC),可以为不同的用户或用户组分配不同的权限级别。认证过程采用业界标准的JWT令牌机制,确保只有经过验证的请求才能访问系统资源。
flowchart LR
classDef client fill:#E3F2FD,stroke:#1976D2,color:#000;
classDef auth fill:#FFF3E0,stroke:#F57C00,color:#000;
classDef store fill:#E8F5E9,stroke:#43A047,color:#000;
classDef jwt fill:#F3E5F5,stroke:#8E24AA,color:#000;
classDef result fill:#E1F5FE,stroke:#0288D1,color:#000;
Client["客户端请求"]:::client
Auth["访问层<br/>认证模块"]:::auth
Etcd["etcd<br/>用户库"]:::store
Result["验证结果"]:::result
JWT["JWT Token<br/>验证"]:::jwt
Client --> Auth
Auth --> Etcd
Etcd --> Result
Auth --> JWT
Result --> JWT
3) 负载均衡
访问层(Proxy)实现了智能负载均衡策略,能够根据各工作节点的当前负载情况,将请求分发到最合适的节点处理。Milvus 支持多种负载均衡策略,确保系统在高并发场景下仍能保持稳定的性能表现。
负载均衡的策略类型:
Proxy 内部在路由查询请求到 Query Node 时,通过 LoadBalancer 接口选择目标节点。源码中实现了以下策略(位于 internal/proxy/ 目录):
| 策略 | 类名 | 工作原理 |
|---|---|---|
| 轮询(Round Robin) | RoundRobinBalancer |
依次轮流选择可用 Query Node,每个节点轮流承担请求 |
| 最少代价(Least Cost) | LeastCostBalancer |
实时采集各 Query Node 的 CPU/内存/请求数指标,选择当前负载最轻的节点 |
| 旁路(Look Aside) | LookAsideBalancer |
查询时先检查本地缓存的路由表,命中则直接转发,未命中则查询 QueryCoord 获取最新拓扑 |
配置方式(milvus.yaml):
负载均衡策略通过 Milvus 配置文件中的 Proxy 和 QueryCoord 相关参数进行配置:
# milvus.yaml — Proxy 负载均衡相关配置
proxy:
port: 19530 # gRPC 端口
http:
port: 9091 # HTTP/RESTful 端口
enabled: true
timeTickInterval: 200 # 时间滴答间隔(毫秒)
# 查询节点选择相关参数
queryNode:
selector: "RoundRobin" # 节点选择策略:RoundRobin / LeastCost
failoverRetryTimes: 3 # 故障转移重试次数
# QueryCoord 相关配置
queryCoord:
autoBalance: true # 是否开启自动负载均衡
balanceIntervalSeconds: 60 # 负载均衡检查间隔(秒)
overloadedMemoryWatermark: 0.85 # 内存过载水位线(超过 85% 触发均衡)
# Segment 分配策略
globalRowCountFactor: 0.25 # 全局行数因子
scoreBasedBalance: true # 基于评分的均衡(CPU+内存综合打分)
场景举例:
场景 1:多 Query Node 轮询分发
假设有 3 个 Query Node(QN1、QN2、QN3),配置 selector: "RoundRobin":
请求序列:
请求1 → QN1 请求4 → QN1 请求7 → QN1
请求2 → QN2 请求5 → QN2 请求8 → QN2
请求3 → QN3 请求6 → QN3 请求9 → QN3
适用场景:各节点配置相同、数据均质分布
场景 2:基于实际负载的智能分发(LeastCost)
假设 3 个 Query Node 当前状态:
QN1: CPU 85%,内存 90%,当前排队请求 120 ← 负载高
QN2: CPU 30%,内存 45%,当前排队请求 20 ← 负载低
QN3: CPU 50%,内存 60%,当前排队请求 50 ← 负载中
配置 selector: "LeastCost" 时:
新请求 → QN2(代价最低,优先选择)
适用场景:节点配置不均、数据热点分布、弹性扩缩容过程中
场景 3:自动负载再均衡(QueryCoord 触发)
当某 Query Node 内存超过 overloadedMemoryWatermark (85%):
1. QueryCoord 检测到 QN1 内存 90% > 85% 水位线
2. 触发自动再均衡(balanceIntervalSeconds: 60s 周期检查)
3. 将 QN1 上的部分 Sealed Segment 迁移到 QN2/QN3
4. 迁移完成后,QN1 内存降至 60%,恢复健康状态
无需人工介入,全程自动完成
生产环境建议:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 节点同配、数据均匀 | RoundRobin |
简单高效,无额外采集开销 |
| 节点异构、负载波动大 | LeastCost |
动态感知负载,避免”热点节点” |
| 高并发 + 低延迟要求 | LeastCost+autoBalance: true |
配合自动再均衡持续优化分布 |
| 调试/测试环境 | RoundRobin |
行为可预测,便于排查问题 |
# 客户端侧无法直接配置 Proxy 负载均衡策略(策略在服务端 milvus.yaml 中配置),
# 但可以通过以下方式影响负载分布:
from pymilvus import connections, Collection
connections.connect(alias="default", address="localhost:19530")
# 方式1:创建多个副本(replica),Proxy 会自动将查询负载分散到各副本
collection = Collection("my_collection")
collection.load(replica_number=3) # 3 个内存副本 = 3 个 Query Node 可并行查询
# 方式2:合理使用 Partition Key,将数据分散到不同分区
# 不同分区可能由不同 Query Node 负责,天然分散查询负载
# 方式3:连接池(SDK 侧)
# PyMilvus 内部维护 gRPC 连接池,并发请求会自动多路复用
# 无需额外配置,SDK 默认开启
协调器组件详解
协调器是 Milvus 的”大脑”,负责调度和管理所有工作节点。四种协调器各司其职,共同组成系统的控制平面(Control Plane)。
| 协调器 | 代码位置 | 操作对象 | 核心职责 | 对外接口 |
|---|---|---|---|---|
| Root Coord | internal/rootcoord/ |
元数据(DB/Collection/Partition) | DDL 操作、TSO 时间戳分配 | CreateCollection、DropCollection |
| Query Coord | internal/querycoordv2/ |
Query Node 集群 | 查询分发、结果合并、Segment 分配 | Search、Query、HybridSearch |
| Data Coord | internal/datacoord/ |
Data Node 集群 | 数据持久化调度、Segment 生命周期 | Insert、Delete、Flush |
| Index Coord | internal/indexcoord/ |
Index Node 集群 | 索引构建调度、进度监控 | CreateIndex、DropIndex |
技术实现与类比:
flowchart LR
subgraph Kitchen["餐厅后厨"]
K1["👨🍳 厨师长<br/>(分配桌子)"]
K2["🚶 传菜主管<br/>(调度传菜)"]
K3["🔪 切配主管<br/>(管理食材)"]
K4["🍽️ 摆盘主管<br/>(成品把关)"]
end
subgraph Milvus["Milvus 协调器"]
M1["Root Coord<br/>管理全局菜单(元数据)"]
M2["Query Coord<br/>调度 Query Node"]
M3["Data Coord<br/>调度 Data Node"]
M4["Index Coord<br/>调度 Index Node"]
end
K1 <--> M1
K2 <--> M2
K3 <--> M3
K4 <--> M4
classDef kitchen fill:#FFF8E1,stroke:#F9A825,stroke-width:2px;
classDef milvus fill:#E3F2FD,stroke:#1E88E5,stroke-width:2px;
class K1,K2,K3,K4 kitchen;
class M1,M2,M3,M4 milvus;
1) Root Coordinator(根协调器)—— 全局总管
Root Coord 是唯一拥有 TSO(Timestamp Oracle)能力的组件,所有时间戳由它统一分配,是整个系统时序一致性的根基。
请求路径(以创建 Collection 为例):
graph TD
%% 定义节点样式
classDef component fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
classDef database fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
classDef action fill:#fff3e0,stroke:#e65100,stroke-width:1px,stroke-dasharray: 5 5;
%% 节点定义
Client([Client]):::component
Proxy([Proxy]):::component
RootCoord([Root Coord]):::component
Etcd[(etcd)]:::database
TSO[分配时间戳 TSO]:::action
DataCoord[通知 Data Coord 初始化 Segment]:::action
%% 连线关系
Client --> Proxy
Proxy --> RootCoord
%% Root Coord 的水平/垂直操作
RootCoord --> |写入元数据| Etcd
RootCoord --> TSO
TSO --> DataCoord
2) Query Coordinator(查询协调器)—— 查询调度员
Query Coord 负责将查询请求路由到最优的 Query Node,并合并多个节点的搜索结果。它监听各 Query Node 的负载指标,在节点间动态迁移 Segment 以实现负载均衡。请求路径(以 Search 为例):
graph TD
Client --> Proxy
Proxy --> QueryCoord[Query Coord]
QueryCoord --> Step1[查询 localMetaCache<br>哪个 QN 持有目标 Segment?]
QueryCoord --> Step2[选择负载最低的 Query Node]
Step1 --> QueryNode[Query Node]
Step2 --> QueryNode
QueryNode --> Search[执行向量搜索]
Search --> Return[返回结果]
Return --> Merge[Query Coord 合并多个 QN 的结果]
3) Data Coordinator(数据协调器)—— 数据大管家
Data Coord 负责决定”数据写到哪里”。它通过分配 vchannel(虚拟通道)和物理 Segment 来控制数据的流向和落地位置。请求路径(以 Insert 为例):
graph TD
Client --> Proxy
Proxy --> DataCoord[Data Coord]
DataCoord --> VChannel[分配 vchannel<br>数据路由通道]
DataCoord --> Segment[分配 Growing Segment<br>可写入的数据段]
VChannel --> DataNode[Data Node]
Segment --> DataNode
DataNode --> WAL[写 WAL]
WAL --> Storage[(落盘到对象存储)]
4) Index Coordinator(索引协调器)—— 索引调度员
Index Coord 监控所有”尚未建索引”的 Sealed Segment,为它们创建索引构建任务,并分发给 Index Node 执行。请求路径(以 CreateIndex 为例):
graph TD
Client --> Proxy
Proxy --> RootCoord[Root Coord]
RootCoord --> IndexCoord[Index Coord]
IndexCoord --> Scan[扫描待建索引的 Segment 列表]
IndexCoord --> Generate[生成索引构建任务]
Scan --> IndexNode[Index Node]
Generate --> IndexNode
IndexNode --> Build[构建索引]
Build --> Storage[(写入对象存储)]
Storage --> Notify[通知 Query Coord<br>'索引就绪,可加载到查询节点']
协调器相关配置(milvus.yaml):
# 根协调器配置
rootCoord:
address: localhost:53100
port: 53100
dmlChannelNum: 16 # DML 通道数
maxPartitionNum: 4096 # 最大分区数
# 查询协调器配置
queryCoord:
address: localhost:19531
port: 19531
autoBalance: true # 自动 Segment 负载均衡
balanceIntervalSeconds: 60 # 均衡检查周期
overloadedMemoryWatermark: 0.85 # 内存过载水位线
# 数据协调器配置
dataCoord:
address: localhost:13333
port: 13333
segment:
maxSize: 1024 # 单个 Segment 最大容量(MB)
sealProportion: 0.12 # Sealed Segment 比例
compaction:
enableAutoCompaction: true # 自动压缩合并
# 索引协调器配置
indexCoord:
address: localhost:31000
port: 31000
bindIndexNodeMode:
enable: false # 是否绑定固定 Index Node
协调器健康状态监控:
# 通过 SDK 检查协调器管理的资源状态
from pymilvus import connections, Collection, utility
connections.connect(alias="default", address="localhost:19530")
# 查看 Root Coord 管理的资源
print(f"数据库数量: {len(utility.list_databases())}")
print(f"集合数量: {len(utility.list_collections())}")
# 查看 Index Coord 管理的索引状态
for coll_name in utility.list_collections():
collection = Collection(coll_name)
for idx in collection.indexes:
print(f"集合 '{coll_name}' 字段 '{idx.field_name}' 索引就绪")
# 查看 Data Coord 管理的分区状态
collection = Collection(utility.list_collections()[0])
for partition in collection.partitions:
print(f"分区 '{partition.name}': {partition.num_entities} 条数据")
connections.disconnect("default")
以上要点总结:
| 维度 | Root Coord | Query Coord | Data Coord | Index Coord |
|---|---|---|---|---|
| 类比 | 数据库 DBA | 查询调度员 | 存储管理员 | 索引管理员 |
| 处理的操作 | Create/Drop/Describe | Search/Query | Insert/Delete/Flush | CreateIndex/DropIndex |
| 调度对象 | 全局元数据 | Query Node | Data Node | Index Node |
| 核心机制 | TSO 时间戳 | Segment 负载均衡 | vchannel 分配 | 异步任务队列 |
| 如果挂了 | 无法创建/删除集合 | 无法执行搜索 | 无法写入新数据 | 新数据无法建索引 |
工作节点(Worker Nodes)
工作节点是 Milvus 中真正”干活”的角色——写入数据、构建索引、执行搜索都由它们完成。协调器只管”调度”,工作节点负责”执行”。工作节点采用无状态设计,支持水平扩展。
从 Docker 容器认识工作节点
以 Milvus Standalone(Docker Compose 部署)为例,所有组件运行在单个容器内,但内部进程模型与分布式一致。通过 Docker 命令可以直观看到各组件的真实面貌。
Milvus Standalone 的 docker-compose.yml 示例:
# 单机版 Milvus Standalone 的 Docker Compose 配置
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.5
# ...
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
# ...
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.4.0
ports:
- "19530:19530" # gRPC 端口(SDK 连接用)
- "9091:9091" # HTTP 监控端口
depends_on:
- etcd
- minio
# 在这个容器内部,Proxy、Coordinator、Worker Node 全部以 Go 协程方式运行
查看运行中的 Milvus 容器:
# 1. 查看 Docker 容器列表
$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
NAMES IMAGE STATUS PORTS
attu zilliz/attu:v3.0.0-beta.6 Up 59 minutes 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
milvus-standalone milvusdb/milvus:v2.6.17 Up About an hour (healthy) 0.0.0.0:9091->9091/tcp, [::]:9091->9091/tcp, 0.0.0.0:19530->19530/tcp, [::]:19530->19530/tcp
milvus-minio minio/minio:RELEASE.2024-12-18T13-15-44Z Up About an hour (healthy) 0.0.0.0:9000-9001->9000-9001/tcp, [::]:9000-9001->9000-9001/tcp
milvus-etcd quay.io/coreos/etcd:v3.5.25 Up About an hour (healthy) 2379-2380/tcp
# 2. 进入 Milvus 容器查看内部进程
$ docker exec -it milvus-standalone /bin/bash
# 3. 容器内查看 Milvus 进程(Standalone 模式下所有组件在同一进程内)
$ ps aux | grep milvus
root 1 0.0 0.2 19656 7772 ? Ssl 08:52 0:00 /tini -- milvus run standalone
root 8 5.7 10.0 2150216 374464 ? Sl 08:52 3:39 milvus run standalone
# 4. 查看 Milvus 日志 —— 日志中会打印各组件的角色标识
$ docker logs --tail 20 milvus-standalone
[INFO] "Welcome to Milvus"
[INFO] "Proxy successfully started" # ← Access Layer
[INFO] "RootCoord successfully started" # ← Coordinator
[INFO] "QueryCoord successfully started" # ← Coordinator
[INFO] "DataCoord successfully started" # ← Coordinator
[INFO] "IndexCoord successfully started" # ← Coordinator
[INFO] "QueryNode successfully started" # ← Worker Node ← 本章主角
[INFO] "DataNode successfully started" # ← Worker Node ← 本章主角
[INFO] "IndexNode successfully started" # ← Worker Node ← 本章主角
[INFO] "Milvus is ready"
关键认知: 从日志可以看到,Standalone 模式下每行 “successfully started” 都是一个 Go 协程/子模块,它们在同一个操作系统进程内通过 gRPC loopback 互相通信。在分布式模式下,每个 “Node” 独立部署为单独的 Pod/容器。
Docker 容器内部组件通信示意图:
graph TD
subgraph Standalone["Milvus Standalone 进程 (单机模式)"]
Proxy["Go 协程:Proxy ← 监听 0.0.0.0:19530 (gRPC)"]
RootCoord["Go 协程:RootCoord ← 与 etcd 通信 (milvus-etcd)"]
QueryCoord["Go 协程:QueryCoord ← 调度 QueryNode"]
DataCoord["Go 协程:DataCoord ← 调度 DataNode"]
IndexCoord["Go 协程:IndexCoord ← 调度 IndexNode"]
QueryNode["Go 协程:QueryNode ← 加载索引,执行搜索"]
DataNode["Go 协程:DataNode ← 写入 WAL,持久化数据"]
IndexNode["Go 协程:IndexNode ← 构建向量索引"]
Note>📢 所有协程通过本地 gRPC 通信(127.0.0.1),无需跨容器]
%% 使用隐式连线强制内部节点垂直整齐排列
Proxy ~~~ RootCoord ~~~ QueryCoord ~~~ DataCoord ~~~ IndexCoord ~~~ QueryNode ~~~ DataNode ~~~ IndexNode ~~~ Note
end
Etcd[(milvus-etcd<br>元数据存储)]
Minio[(milvus-minio<br>对象存储)]
%% 从整个进程容器指向外部存储依赖
Standalone --> Etcd
Standalone --> Minio
写节点 / 流节点(Stream Node / Write Node)
写节点(Milvus 内部称为 Stream Node)负责处理所有数据写入请求。在分布式部署中它是独立可扩展的组件,在 Standalone 模式下由 DataNode 协程兼任。
流节点的 Docker 视角:
# 分布式部署中,流节点/写节点作为独立组件运行
$ docker ps | grep stream
milvus-streamnode-1 milvusdb/milvus:v2.4.0 "milvus run streamnode"
# 查看写节点日志 —— 观察数据写入行为
$ docker logs --tail 10 milvus-streamnode-1
[INFO] "StreamNode register success"
[INFO] "Receive insert request, channel=by-dev-rootcoord-dml_0, size=1024"
[INFO] "Write to WAL completed, msgID=18446744073709551615"
[INFO] "Flush segment 440000000000000001 to MinIO"
# 在 Standalone 模式下,查看数据写入的实时日志
$ docker logs -f milvus-standalone | grep -E "DataNode|Flush|Insert"
[INFO] "DataNode received insert, collection=my_demo, rows=1000"
[INFO] "DataNode buffer flush triggered, size=128KB"
[INFO] "Segment sealed and persisted to MinIO"
流节点工作流程:
graph TD
Client[客户端] --> Proxy[Proxy]
Proxy --> DataNode["DataNode<br>(StreamNode)"]
DataNode --> Step1["1. 数据写入内存 Buffer"]
DataNode --> Step2["2. 同步写 WAL<br>(Pulsar/Kafka)"]
DataNode --> Step3["3. Buffer 满或定时触发 Flush"]
DataNode --> Step4["4. 数据落盘到对象存储<br>(MinIO/S3)"]
%% 用虚线表示这四步的先后逻辑顺序,让图表更具可读性
Step1 -.-> Step2 -.-> Step3 -.-> Step4
%% 为存储组件添加不同的样式区分
classDef storage fill:#f9f2f4,stroke:#d37295,stroke-width:2px;
class Step2,Step4 storage;
查看 WAL 和 Flush 状态:
docker exec milvus-standalone ls -la /var/lib/milvus/data/
docker exec milvus-minio ls -la /data/
写节点核心特性:
| 特性 | 描述 | Docker 验证方式 |
|---|---|---|
| 异步写入 | 数据先入内存,异步持久化 | docker logs milvus-standalone | grep Flush |
| 批量处理 | 支持批量插入,减少网络 IO | 观察日志中 rows=N的变化 |
| WAL 保障 | 崩溃后从 WAL 恢复未持久化数据 | 查看 /var/lib/milvus/wal/目录 |
| 流量控制 | 内置写入速率限制 | 查看 milvus.yaml中 dataNode.flowGraph.maxQueueLength |
查询节点(Query Node)
查询节点是搜索性能的关键。它从对象存储加载索引到本地内存,在内存中执行向量相似度计算。
查询节点的 Docker 视角:
# 分布式部署中查看 Query Node 容器
$ docker ps | grep querynode
milvus-querynode-1 milvusdb/milvus:v2.4.0 "milvus run querynode"
# 查看 Query Node 加载的 Segment 信息
$ docker logs milvus-querynode-1 2>&1 | grep -E "load|segment|memory"
[INFO] "QueryNode loading segment 440000000000000002, size=128MB"
[INFO] "Segment loaded into memory, collection=my_demo"
[INFO] "QueryNode memory usage: 256MB / 1024MB"
# 查看容器内存使用(观察加载索引前后的变化)
$ docker stats milvus-standalone --no-stream
CONTAINER CPU % MEM USAGE / LIMIT MEM %
milvus-standalone 12.5% 512.3MiB / 2GiB 25.01%
# 在 Python 中 load 数据后再看
$ docker stats milvus-standalone --no-stream
CONTAINER CPU % MEM USAGE / LIMIT MEM %
milvus-standalone 25.0% 890.5MiB / 2GiB 43.48% ← 内存涨了!
教学技巧: 上面的
docker stats对比是课堂上最直观的演示——让学生先记下 load 前的内存,再 load 后看内存增长,立即理解 “Query Node 是将索引加载到内存做搜索” 这句话的含义。
查询节点搜索流程:
graph TD
Request([查询请求]) --> QueryCoord[Query Coord]
%% 定义 Query Node 集群子图
subgraph Cluster [Query Node 集群]
direction LR
QN1["QN-1<br>Seg: A, B<br>Mem: 30%"]
QN2["QN-2<br>Seg: C, D<br>Mem: 55%"]
QN3["QN-3<br>Seg: E, F<br>Mem: 85%<br>(Load 高会被绕过)"]
end
%% 下发查询请求(路由与绕过)
QueryCoord -->|分发| QN1
QueryCoord -->|分发| QN2
QueryCoord -.->|绕过不分发| QN3
Merge[结果合并与排序]
%% 结果汇总
QN1 --> Merge
QN2 --> Merge
%% QN3 被绕过所以没有返回箭头
Merge --> Output([返回 Top-K 结果])
%% 样式区分(为高负载节点添加虚线边框标注)
style QN3 stroke:#f44336,stroke-width:2px,stroke-dasharray: 5 5
索引节点(Index Node)
索引节点是 CPU 密集型组件,负责计算和构建向量索引。在大规模数据场景下,索引构建可能占用大量 CPU 时间。
索引节点的 Docker 视角:
# 观察索引构建时的 CPU 飙升
$ docker stats milvus-standalone --no-stream
CONTAINER CPU % MEM USAGE / LIMIT
milvus-sa 8.2% 412MiB / 2GiB
# Python 代码创建索引后立即观察
# >>> collection.create_index("vector", {"index_type": "HNSW", ...})
$ docker stats milvus-standalone --no-stream
CONTAINER CPU % MEM USAGE / LIMIT
milvus-sa 145.3% 890MiB / 2GiB ← CPU 飙升,正在构建索引!
# 索引构建完成后 CPU 回落
$ docker stats milvus-standalone --no-stream
CONTAINER CPU % MEM USAGE / LIMIT
milvus-sa 6.5% 780MiB / 2GiB ← 索引构建完成,CPU 恢复正常
# 查看索引文件是否已写入 MinIO
$ docker exec milvus-minio ls -la /data/default/my_collection/
-rw-r--r-- index_440000000000000001.hnsw 24567890 ← HNSW 索引文件已生成
数据节点(Data Node)
数据节点是数据的”管家”,管理数据从内存到磁盘的完整生命周期。它的核心工作单元是 Segment(数据段)。
数据节点的 Docker 视角:
# 查看 DataNode 管理的 Segment 信息
$ docker exec milvus-standalone ls /var/lib/milvus/data/
440000000000000001/ # ← 这是一个 Sealed Segment 的本地缓存目录
440000000000000002/
# 查看 DataNode 的日志 —— 追踪 Segment 的生命周期
$ docker logs milvus-standalone 2>&1 | grep -E "Segment|seal|compact"
[INFO] "DataNode created growing segment 440000000000000003"
[INFO] "Segment 440000000000000003 sealed, size=512MB"
[INFO] "Compaction start: merging segments [440000000000000001, 440000000000000002]"
[INFO] "Compaction done: new segment 440000000000000004 created"
# 查看 MinIO 中持久化的 Segment 文件
$ docker exec milvus-minio ls -laR /data/default/my_collection/
/data/default/my_collection/:
insert_log/ # DML 日志(原始向量数据)
index_files/ # 索引文件
binlog/ # Binlog 文件(数据快照)
Segment 生命周期(DataNode 视角):
flowchart LR
%% 定义卡片节点内容
Growing["<b>Growing Segment(可写入)</b><br/>━━━━━━━━━━━━━━<br/>• 新数据持续写入<br/>• 驻留在 DataNode 内存<br/>• 同时写 WAL"]
Sealed["<b>Sealed Segment(已封存)</b><br/>━━━━━━━━━━━━━━<br/>• 不再接受写入<br/>• 等待 IndexNode 建索引<br/>• 已持久化到对象存储"]
Compacted["<b>合并后的大 Segment</b><br/>━━━━━━━━━━━━━━<br/>• 减少文件碎片<br/>• 提高查询效率"]
%% 定义包含换行文本的横向流转箭头
Growing -->|"满了<br>(seal)"| Sealed
Sealed -->|"多个小 Segment 合并<br>(Compaction)"| Compacted
%% 让文本靠左对齐,样式更像卡片
classDef default fill:#f8fcff,stroke:#87bdec,stroke-width:2px,text-align:left;
工作节点综合
以下是一个完整的课堂演示流程,使用 Docker 命令配合 Python SDK,让学生直观感受各组件的协作:
# ==================== Shell 终端 ====================
# 步骤1: 启动前记录基线
docker stats milvus-standalone --no-stream
# 步骤2: 观察插入数据时 DataNode 的日志
docker logs -f milvus-standalone &
# ==================== Python 终端 ====================
from pymilvus import MilvusClient, Collection
import numpy as np
client = MilvusClient(uri="http://localhost:19530")
# 步骤3: 创建集合 + 插入数据 —— 触发 DataNode
client.create_collection("demo", dimension=128, metric_type="L2")
client.insert("demo", [
{"id": i, "vector": np.random.rand(128).tolist()}
for i in range(10000)
])
client.flush("demo")
print("数据写入完成,查看 Docker 日志应看到 Flush 记录")
# ==================== Shell 终端 ====================
# 步骤4: 查看 DataNode 的 Flush 日志
docker logs milvus-standalone --tail 20 | grep -i "flush\|seal"
# 预期输出: "Segment sealed and persisted to MinIO"
# 步骤5: 查看 MinIO 中是否有文件生成
docker exec milvus-minio ls -la /data/default/demo/
# 预期输出: insert_log/ binlog/ 目录
# ==================== Python 终端 ====================
# 步骤6: 创建索引 —— 触发 IndexNode,观察 CPU 飙升
client.create_index("demo", "vector", {
"index_type": "HNSW",
"metric_type": "L2",
"params": {"M": 16, "efConstruction": 200}
})
# ==================== Shell 终端 ====================
# 步骤7: 索引构建时观察 CPU(应立即执行)
docker stats milvus-standalone --no-stream
# 预期: CPU > 100%,说明 IndexNode 正在密集计算
# 步骤8: 查看索引文件
docker exec milvus-minio ls -la /data/default/demo/index_files/
# 预期: .hnsw 索引文件
# ==================== Python 终端 ====================
# 步骤9: Load 数据 —— 触发 QueryNode 加载索引到内存
client.load_collection("demo")
# ==================== Shell 终端 ====================
# 步骤10: 立即观察内存增长(触目惊心的对比!)
docker stats milvus-standalone --no-stream
# 内存应从 ~400MB 增长到 ~800MB+
# 步骤11: 执行搜索,观察 QueryNode 工作
docker logs milvus-standalone --tail 5 | grep -i "search\|query"
# ==================== Python 终端 ====================
# 步骤12: 最终搜索验证
results = client.search("demo", [np.random.rand(128).tolist()], limit=5)
print("搜索成功!", [(r["id"], round(r["distance"], 4)) for r in results[0]])
以上代码注意如下几点:
- **Docker 日志是”活教材”**:每个操作在日志中都有对应记录,让学生”看见”架构不再是抽象概念。
docker stats对比法:插入前后、建索引前后、Load 前后的 CPU/内存对比,比任何文字描述都有说服力。- 写节点写入:使用批量写入可以显著提高写入性能,日志中的
rows=N能直观反映批量大小。 - 查询节点搜索:搜索参数(如
nprobe、ef)直接影响搜索性能,可通过日志观察耗时。 - 索引节点构建:索引构建是异步过程,日志中会打印
Index building progress状态更新。 - 数据节点段管理:理解 Growing/Sealed/Compacted 三种 Segment 状态,是性能调优的基础。
存储层(Storage)
存储层是 Milvus 的”地基”,所有数据最终都落在这里。它由三个核心组件构成:元存储(etcd)、对象存储(MinIO/S3)和 WAL 存储(Pulsar/Kafka/RocksMQ)。
从 Docker 容器认识存储层
在 Milvus Standalone 部署中,存储层的每个组件都运行在独立 Docker 容器中,可以直接进入容器查看内部数据。
存储层 Docker 容器概览:
# 查看存储层相关容器
$ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \
| grep -E "etcd|minio|pulsar|milvus"
NAMES IMAGE STATUS PORTS
milvus-standalone milvusdb/milvus:v2.4.0 Up 3 hours 19530, 9091
milvus-minio minio/minio:RELEASE.2023-03-20 Up 3 hours 9000-9001
milvus-etcd quay.io/coreos/etcd:v3.5.5 Up 3 hours 2379-2380
# 查看各存储容器的磁盘占用
$ docker system df -v | grep milvus
milvus-minio 2.5GB # ← 向量数据 + 索引文件(最大的那个!)
milvus-etcd 45MB # ← 元数据(很小)
milvus-standalone 380MB # ← 本地缓存(Segment 临时数据)
存储层总览示意图(容器视角):
flowchart LR
%% 定义节点内部左对齐的卡片样式,并按组件给不同颜色
classDef standalone fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,text-align:left;
classDef etcd fill:#e8f5e9,stroke:#43a047,stroke-width:2px,text-align:left;
classDef minio fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,text-align:left;
%% 1. 核心运行容器
Standalone["<b>milvus-standalone 容器</b><br/>━━━━━━━━━━━━━━━━━━<br/>• Proxy / Coordinators<br/>• QueryNode / DataNode<br/>• IndexNode<br/>• (本地缓存: Segment 暂存)"]:::standalone
%% 2. 元数据存储容器 (使用空格树状展现路径)
Etcd["<b>milvus-etcd 容器</b><br/>━━━━━━━━━━━━━━━━━━<br/>/milvus/<br/> ├─ collections/<br/> ├─ partitions/<br/> ├─ segments/<br/> └─ coordinators/"]:::etcd
%% 3. 对象存储容器 (使用空格树状展现路径)
Minio["<b>milvus-minio 容器</b><br/>━━━━━━━━━━━━━━━━━━<br/>/data/default/<br/> ├─ collection_1/<br/> │ ├─ insert_log/ (向量数据)<br/> │ ├─ index_files/ (索引文件)<br/> │ └─ binlog/ (数据快照)<br/> └─ collection_2/"]:::minio
%% 定义容器间的交互关系
Standalone <-->|"读写元数据"| Etcd
Standalone -->|"数据落盘与加载"| Minio
元存储(Meta Store)—— etcd
etcd 是 Milvus 的”配置中心 + 注册中心 + 元数据库”。它不存向量数据,只存”描述数据的数据”(Schema、集合定义、节点状态等)。
从 Docker 直接查看 etcd 中的元数据:
# 1. 进入 etcd 容器
$ docker exec -it milvus-etcd /bin/sh
# 2. 查看 Milvus 写入的元数据 key
$ etcdctl get --prefix "/milvus/" --keys-only
/milvus/collections/454444359156859000 # 集合元数据 ID
/milvus/partitions/454444359156859001 # 分区元数据
/milvus/segments/454444359156859002 # Segment 元数据
/milvus/rootcoord/session # Root Coord 注册信息
/milvus/querycoord/session # Query Coord 注册信息
/milvus/datacoord/session # Data Coord 注册信息
/milvus/indexcoord/session # Index Coord 注册信息
# 3. 查看某个集合的具体 Schema(解码 Protobuf 后的人类可读格式)
$ etcdctl get /milvus/collections/454444359156859000
# 输出包含:collection_name, fields[], primary_key, dimension, metric_type 等
# 4. 查看当前活跃的协调器节点(服务发现功能)
$ etcdctl get /milvus/rootcoord/session
# 输出包含:ServerID, Address=localhost:53100, LeaseID, 心跳时间
etcd 中存储的关键数据:
/milvus/
├── collections/{coll_id} # 每个集合的 Schema 定义(字段名、类型、维度)
├── partitions/{partition_id} # 分区元数据
├── segments/{segment_id} # Segment 状态:Growing / Sealed / Flushed / Dropped
├── rootcoord/session # Root Coord 的注册 session + 心跳
├── querycoord/session # Query Coord 的注册 session
├── datacoord/session # Data Coord 的注册 session
├── indexcoord/session # Index Coord 的注册 session
├── querynodes/{node_id} # 各 Query Node 的负载信息
├── datanodes/{node_id} # 各 Data Node 的 channel 分配信息
└── credentials/ # 用户名/加密密码(RBAC)
元存储核心特性:
| 特性 | 描述 | Docker 验证方式 |
|---|---|---|
| 高可用 | Raft 协议,3 节点集群 | etcdctl endpoint health |
| 强一致 | 线性一致性读 | etcdctl get保证读到最新 |
| 低延迟 | 内存级访问 | etcdctl check perf |
| 服务发现 | 各组件注册 session + 心跳续约 | etcdctl get /milvus/rootcoord/session |
# 演示元存储的元数据管理功能
from pymilvus import connections, Collection, utility
connections.connect(alias="default", address="localhost:19530")
print("=" * 60)
print("【Python API 视角 vs Docker 视角对比】")
print("=" * 60)
# Python SDK 查看(等价于 etcdctl get /milvus/collections/)
print("\n【集合列表】")
collections = utility.list_collections()
for coll in collections:
print(f" - {coll}")
# 查看集合详细信息
collection = Collection(coll)
schema = collection.schema
for field in schema.fields:
print(f" 字段: {field.name} | 类型: {field.dtype} | 主键: {field.is_primary}")
connections.disconnect("default")
# 此时让学员在另一个终端执行:
# docker exec milvus-etcd etcdctl get --prefix "/milvus/collections/" --keys-only
# 对比发现:Python API 看到的集合 = etcd 中存储的集合元数据
对象存储(Object Storage)—— MinIO / S3
对象存储是 Milvus 最大的存储消费者,存放向量原始数据和索引文件。在 Standalone 部署中默认使用 MinIO。
从 Docker 直接查看 MinIO 中的文件:
# 1. 查看 MinIO 容器状态
$ docker ps | grep minio
milvus-minio minio/minio:RELEASE.2023-03-20 Up 3 hours 0.0.0.0:9000-9001->9000-9001/tcp
# 2. 进入 MinIO 容器,查看 Milvus 写入的数据
$ docker exec -it milvus-minio /bin/sh
# 3. 查看 MinIO 存储目录结构
$ ls -la /data/
drwxr-xr-x a-bucket/ # Milvus 使用的默认 bucket
# 4. 查看某个集合的存储文件(最直观的"数据库文件在哪"的答案)
$ find /data/a-bucket/ -type f | head -20
/data/a-bucket/default/my_collection/
├── insert_log/
│ ├── 454444359156860000/
│ │ ├── 454444359156860001 # 向量数据文件(每条记录的向量值)
│ │ ├── 454444359156860002
│ │ └── 454444359156860003
├── index_files/
│ ├── 454444359156860100/
│ │ ├── 454444359156860101 # HNSW 索引文件
│ │ └── 454444359156860102
├── binlog/
│ ├── 454444359156860200/
│ │ ├── 454444359156860201 # 数据快照文件
│ │ └── 454444359156860202
└── delta_log/
├── 454444359156860300/
│ └── 454444359156860301 # 删除记录文件
# 5. 查看文件大小 —— 理解"向量数据库占多少磁盘"
$ du -sh /data/a-bucket/default/my_collection/
insert_log/ 1.2GB # 原始向量数据(128维 × Float32 × 4字节 × 行数)
index_files/ 856MB # 索引文件(HNSW 额外索引结构)
binlog/ 1.2GB # 数据快照
delta_log/ 2.1MB # 删除记录(几乎可以忽略)
Total: ~3.2GB
# 6. 通过浏览器访问 MinIO 管理界面(更直观)
# 浏览器打开: http://localhost:9001
# 默认账号: minioadmin / minioadmin
# 进入 milvus bucket → 看到所有文件,可直接下载查看
MinIO Web 控制台是最佳的教学辅助工具。登录
http://localhost:9001后,让学生亲眼看到insert_log/、index_files/、binlog/这些目录和他们刚刚写入的数据一一对应。
MinIO 控制台操作演示(课堂截图级内容):
浏览器访问 http://localhost:9001 → 登录
→ Buckets → a-bucket → default/ → my_collection/
→ insert_log/ ← "老师,这就是我们刚 insert 的数据吗?"(是的!)
→ index_files/ ← "create_index 后出现的"(正确!)
→ binlog/ ← "flush 后持久化的快照"
→ delta_log/ ← "delete 操作的记录"
对象存储后端对比:
| 存储后端 | 适用场景 | 优点 | 缺点 | Docker 部署 |
|---|---|---|---|---|
| MinIO | 本地/私有部署 | 部署简单,与 S3 兼容 | 需自行运维 | minio/minio(Standalone 默认) |
| AWS S3 | AWS 云 | 全托管,无限扩展 | 云厂商绑定 | 云端,通过 milvus.yaml配置 |
| GCS | GCP 云 | 全托管 | 云厂商绑定 | 云端 |
| Azure Blob | Azure 云 | 企业级 | 配置复杂 | 云端 |
| 阿里云 OSS | 国内云 | 国内速度快 | 云厂商绑定 | 云端 |
| 腾讯 COS | 腾讯云 | 低成本 | 云厂商绑定 | 云端 |
# 演示向量数据在对象存储中的概念
from pymilvus import connections, Collection
import numpy as np
connections.connect(alias="default", address="localhost:19530")
collection_name = "object_storage_demo"
if collection_name in utility.list_collections():
utility.drop_collection(collection_name)
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=200)
]
schema = CollectionSchema(fields=fields, description="对象存储演示")
collection = Collection(name=collection_name, schema=schema)
batch_count = 500
print(f"准备存储 {batch_count} 条向量数据...")
entities = [{
"id": i,
"vector": np.random.rand(128).astype(np.float32).tolist(),
"title": f"文档_{i}"
} for i in range(batch_count)]
insert_result = collection.insert(entities)
print(f"写入成功!共写入 {len(insert_result.primary_keys)} 条数据")
# Flush 后,数据从 WAL 持久化到 MinIO
collection.flush()
print("Flush 完成,数据已写入 MinIO")
print(f" 此时在 MinIO 控制台应能看到 insert_log/ 目录下出现新文件")
print(f" 存储大小估算: {batch_count * 128 * 4} 字节 ≈ {batch_count * 128 * 4 / 1024 / 1024:.2f} MB (未压缩)")
connections.disconnect("default")
WAL存储(Write-Ahead Log)—— Pulsar / Kafka / RocksMQ
WAL 是数据写入的”安全气囊”——写入请求先在 WAL 中记录(写日志极快),再异步批量写入对象存储(写文件较慢)。如果系统崩溃,可以从 WAL 恢复未持久化的数据。
WAL 存储的 Docker 视角:
# 1. Standalone 默认使用 RocksMQ(内置在 milvus 进程中,无需独立容器)
# 分布式部署通常使用 Pulsar 或 Kafka
# 2. 查看 Standalone 模式下 RocksMQ 的存储位置
$ docker exec milvus-standalone ls -la /var/lib/milvus/rocksmq/
-rw-r--r-- 100MB rocksmq_data.db # RocksMQ 持久化文件
-rw-r--r-- 4KB MANIFEST
# 3. 如果使用 Pulsar(分布式部署)
$ docker ps | grep pulsar
milvus-pulsar apachepulsar/pulsar:2.8.2 Up 3 hours
$ docker exec milvus-pulsar ls -la /pulsar/data/bookkeeper/
# BookKeeper 的 ledger 文件 = Milvus 的 WAL 记录
# 4. 观察 WAL 的写入和消费(站在消息队列角度)
# Producer 端:DataNode 把数据写入 WAL
# Consumer 端:IndexNode 从 WAL 读取数据建索引
$ docker logs milvus-standalone 2>&1 | grep -i "channel\|publish\|subscribe"
[INFO] "DataNode publish to channel by-dev-rootcoord-dml_0, msg count=1000"
[INFO] "IndexNode subscribe channel by-dev-rootcoord-dml_0"
[INFO] "QueryNode subscribe channel by-dev-rootcoord-dml_0, consume 1000 msgs"
WAL 端到端工作原理:
flowchart TD
subgraph 正常写入流程
A[Client 写入请求] --> B[Proxy 转发]
B --> C[DataNode 写 WAL]
C --> D[返回 Client OK]
D --> E[异步 Flush 到 MinIO]
end
subgraph 崩溃发生
E -.->|未完成| F[⚠️ 系统宕机]
F --> G[数据在 WAL 中确认<br/>但未持久化到 MinIO]
end
subgraph 重启恢复流程
G --> H[Milvus 重启]
H --> I[DataNode 读取 WAL<br/>未消费消息]
I --> J[重放 Replay 写入操作]
J --> K[数据写入 MinIO]
K --> L[✅ 数据完整,一致]
end
关键理解:
- 写 WAL 是同步的(一定要等 WAL 确认)
- 写 MinIO 是异步的(批量做,效率高)
- 崩溃后从 WAL 恢复:回放所有”已写 WAL” 但 “未写 MinIO” 的数据
WAL 存储核心特性:
| 特性 | 描述 | Docker 验证方式 |
|---|---|---|
| 数据持久化保证 | 写入 WAL 即视为已持久化 | 查看 RocksMQ 文件大小变化:ls -la /var/lib/milvus/rocksmq/ |
| 故障恢复 | 崩溃后从 WAL 回放未完成的操作 | 查看启动日志中的 recovery/replay关键字 |
| 异步处理 | WAL 写入(同步)+ MinIO 写入(异步) | 对比 insert 返回速度和 MinIO 文件出现时间差 |
| 多后端支持 | Pulsar / Kafka / RocksMQ | Standalone 默认 RocksMQ(内置),分布式推荐 Pulsar |
# 演示 WAL 的数据一致性保证
from pymilvus import connections, Collection, utility
connections.connect(alias="default", address="localhost:19530")
collection_name = "wal_demo"
if collection_name in utility.list_collections():
utility.drop_collection(collection_name)
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields=fields, description="WAL演示集合")
collection = Collection(name=collection_name, schema=schema)
print("写入数据——此时数据已写 WAL,客户端收到 OK")
entities = [{"id": i, "vector": np.random.rand(128).astype(np.float32).tolist()}
for i in range(100)]
insert_result = collection.insert(entities)
print(f"写入完成,返回 ID: {insert_result.primary_keys[:5]}...")
print(f"此时数据已安全记录在 WAL 中,即使系统崩溃也不会丢失")
# Flush —— 将 WAL 中的数据批量写入 MinIO
print("Flush 数据到 MinIO...")
collection.flush()
# 加载并验证
collection.load()
loaded = collection.query(expr="id >= 0", output_fields=["id"])
print(f"查询到 {len(loaded)} 条数据 —— 一致性验证: {'通过' if len(loaded) == 100 else '失败'}")
connections.disconnect("default")
存储层综合实验(Docker 实操)
以下实验让学生亲眼看到三层存储的数据流转:
# ==================== 实验:追踪一条数据的存储路径 ====================
# 步骤1: 查看插入前的 MinIO 状态
docker exec milvus-minio du -sh /data/a-bucket/default/
# 步骤2: Python 插入 10000 条数据并 flush
# (执行 Python 代码)
# 步骤3: 查看 MinIO 文件增长
docker exec milvus-minio du -sh /data/a-bucket/default/
# 预期:明显增大(insert_log/ 和 binlog/ 目录有新文件)
# 步骤4: 创建索引后再次查看
# (执行 Python collection.create_index)
docker exec milvus-minio du -sh /data/a-bucket/default/
# 预期:再次增大(index_files/ 目录出现 .hnsw 文件)
# 步骤5: 查看 etcd 中的集合注册
docker exec milvus-etcd etcdctl get --prefix "/milvus/collections/" --keys-only
# 预期:看到集合 ID key
# 步骤6: 查看 RocksMQ 中的消息(Standalone 模式)
docker exec milvus-standalone ls -lh /var/lib/milvus/rocksmq/
# 预期:rocksmq_data.db 文件存在
# 步骤7: 查看总磁盘占用
docker system df -v | grep milvus
# 综合报告:MinIO(最大) + etcd(小) + RocksMQ(中)
实验结论:三层存储各有其责
| 存储层 | 容器 | 存储内容 | 大小特征 |
|---|---|---|---|
| 元存储 | milvus-etcd | Schema、分区、Segment 状态、节点注册 | KB~MB 级(极小) |
| 对象存储 | milvus-minio | 向量数据、索引文件、Binlog 快照 | GB~TB 级(最大) |
| WAL 存储 | RocksMQ(内置)或 milvus-pulsar | 待持久化的写入记录 | MB~GB 级 |
以上代码注意如下几点:
- 元存储高可用:etcd 的强一致性保证了元数据的可靠访问,生产环境建议部署 etcd 3 节点集群。
- 对象存储扩展性:MinIO 数据量 > 其余所有组件之和,根据数据量选择合适的存储后端。
- WAL 数据安全:这是防止数据丢失的最后一道防线,
insert返回成功 = 数据已安全写入 WAL。 - Docker exec 教学法:让学生亲手进入容器查看文件,比任何 PPT 都更能建立对”存算分离”的直观理解。