milvus学习笔记
本文主要由AI生成,请注意自己查看源代码校验。
Milvus v2.4+ 系统架构概览
Milvus 采用分布式微服务架构,将计算层(Proxy、QueryCoord、QueryNode、IndexCoord、DataCoord、DataNode 等)与存储层(Pulsar、MinIO/S3、etcd)分离,实现高并发、高可用和水平扩展。在查询场景下,查询请求首先经过前端 Proxy,由 Proxy 调度到后端查询服务,再由 QueryNode 在数据分片(segment)上并行执行向量检索,最后汇总结果返还给客户端。总体架构示意如图所示,重点标出了 Proxy、QueryCoord、QueryNode、DataCoord 等模块及它们之间通过 gRPC 和消息队列(如 Pulsar)通信的路径。
图1:Milvus 存算分离架构示意(Proxy -> 消息队列 -> DataNode -> 对象存储 -> QueryNode)
整体而言,Milvus 将写流程和读流程分离:写入时,客户端经由 Proxy 将数据写入日志流(Pulsar),DataNode 消费写日志并生成 segment(上传到对象存储),DataCoord/QueryCoord 收集和管理 segment;查询时,客户端经由 Proxy 发起检索请求,Proxy 将查询信息作为消息放入查询通道(QueryChannel),各个 QueryNode 订阅该通道接收查询。在查询前,QueryCoord 会根据 DataCoord 提供的 segment 信息对 QueryNode 做负载均衡(按 segment 或通道分配)。整个系统中,etcd 用于存储元数据和服务发现,Pulsar 用作日志 broker(可替换为 Kafka 等),MinIO/S3 作为大文件存储(segment、索引等)。
Proxy 模块源码结构
Proxy 作为客户端访问层,负责接收各种客户端请求(DML、查询、DDL 等),做校验和预处理后分发给后端服务。其核心结构体在 internal/proxy
包中定义,包含如下主要成员:
type Server struct {ctx context.Contextproxy types.ProxyComponent // 实现 proxy 功能的接口(对外暴露 gRPC 服务)rootCoordClient types.RootCoordClient // gRPC 客户端:RootCoorddataCoordClient types.DataCoordClient // gRPC 客户端:DataCoordqueryCoordClient types.QueryCoordClient // gRPC 客户端:QueryCoord// ... 其他成员 ...
}
其中,types.ProxyComponent
接口由 internal/proxy/proxy.go
中的 Proxy
结构体实现。Server
的 Run()
方法启动 gRPC 服务并注册各类任务处理入口。对于向量检索(Search)请求,Proxy 内部会生成一个 SearchTask
(位于 internal/proxy/task_search.go
),将用户的查询参数(如集合 ID、向量、Top-K、过滤表达式等)封装成任务,然后将该任务发送给调度器或下游组件。在新版 Milvus 中,Proxy 并不直接执行搜索逻辑,而是根据查询信息将查询消息发往 Pulsar 的查询通道(QueryChannel)。同时,Proxy 维护对 RootCoord/DataCoord/QueryCoord 的客户端,用于获取集合元数据、segment 分布、时间戳等信息。例如,在 LoadCollection 操作中,Proxy 会先调用 RootCoord 创建加载任务,再通过 QueryCoord 获取需要加载的 segment 信息。总之,Proxy 起到“入口和汇聚”作用,无状态地对外提供统一服务。
QueryCoord 模块源码结构
QueryCoord 是查询协调节点,负责管理整个查询集群的拓扑和负载均衡,以及查询使用的 segment 分配和增量数据(Growing Segment)切换。其核心结构体位于 internal/querycoord
包中,主要成员包括:
type Server struct {queryCoord types.QueryCoordComponent // 实现 querycoord 功能的接口dataCoord types.DataCoordClient // gRPC 客户端:DataCoordrootCoord types.RootCoordClient // gRPC 客户端:RootCoord// ... 其他成员 ...
}
其中,types.QueryCoordComponent
接口由 internal/querycoord/query_coord.go
中的 QueryCoord
结构体实现。Server.Run()
启动 gRPC 服务,处理来自 Proxy 或客户端的查询加载等请求。典型的功能包括:LoadCollection(预加载指定集合到查询集群)和Search(调度查询任务)等。当用户调用 collection.load()
时,Proxy 会将加载请求转发给 QueryCoord。QueryCoord 首先通过 dataCoord.GetRecoveryInfo()
获取集合在存储中的已封存段(Sealed Segment)及对应的检查点;然后根据配置选择“按段分配”或“按通道分配”策略,将不同的封存段 (或日志通道) 分配到各个 QueryNode 上。QueryCoord 的任务调度器 (segment allocator
和 channel allocator
) 负责这一步骤。此外,QueryCoord 还负责监控 QueryNode 的负载,触发流式到封存段的转换(handoff),以及均衡重分配。官方文档指出:“QueryCoord 管理查询节点的拓扑和负载平衡,并处理从增长段到封存段的切换”。在代码层面,internal/querycoord
下的 querycoord
、segment_allocator
、channel_allocator
等包实现了以上逻辑。
QueryNode 模块源码结构
QueryNode 是实际执行查询计算的工作节点,负责在加载到本地的 segment 数据上运行向量检索。其核心代码位于 internal/querynodev2
(Milvus 2.x 采用 v2 版本实现)。一个 QueryNode 进程启动时,会初始化以下主要组件:流图(FlowGraph)用于处理增量数据,索引服务用于处理已封存的数据检索,和 Segment Manager 管理加载的 segment。Milvus 文档中描述:“QueryNode 订阅日志代理获取增量日志,将其转换为增长段,并从对象存储加载历史数据,在向量和标量数据之间运行混合搜索”。
在实现上,QueryNode 包括:server.go
定义了 QueryNode
结构体(实现 types.QueryNodeComponent
接口),负责启动 gRPC 服务和管理子模块;flowgraph
子包实现了增长段(Growing Segment)的流式数据接收与过滤;delegate
子包负责封装和分发具体的搜索请求到适当的 segment 或索引;segcore
(SegCore)则提供 C++ 的向量检索核心调用。QueryNode 的主要方法包括 Search()
(接受 QueryChannel 的查询消息)、LoadSegments()
(加载指定 segment 的数据到内存),以及定期从 DataCoord 读取全局时间戳和消费位置,实现读取增量数据并触发持久化/封存。在并发执行向量检索时,QueryNode 会对本地所有加载的封存段并行搜索,然后与增长段(当前写入的数据)一起做融合,最后本地归约(去重)输出结果。可见,QueryNode 相对复杂,涉及流式处理和检索执行两大功能。
一次完整的向量检索流程
下面按步骤说明客户端一次查询请求在 Milvus 中的流转路径:
-
客户端请求 -> Proxy:客户端通过 SDK 发起 Search 请求到 Proxy(gRPC)。Proxy 接收后,将请求信息(包括集合 ID、查询向量、搜索参数等)封装成一个查询消息,并写入日志中间件(Pulsar)的查询通道。同时,Proxy 可从 RootCoord/DataCoord 获取集合的元数据和可用 segment 列表,为后续的调度做准备。
-
(预先)Load Collection -> QueryCoord:在发起搜索前,如果用户调用了
collection.load()
,Proxy 会触发 QueryCoord 的加载操作。QueryCoord 向 DataCoord 查询集合所有已封存段(和各段的消费检查点),然后执行负载分配。例如,按段分配时将不同封存段分配给不同 QueryNode;按通道分配时让 QueryNode 订阅不同的 DMChannel。在此步骤结束后,相关 QueryNode 已从对象存储加载了对应的历史数据段(封存段),并订阅了增量日志通道(收到新的写入数据)。 -
QueryCoord 分配 -> QueryNode 订阅:QueryCoord 调度后,各 QueryNode 会执行对应的
LoadSegments
和WatchChannels
操作,准备好查询环境。每个 QueryNode 都在其本地维持了若干封存段(Sealed Segment)和对应的增长段(Growing Segment)(参见图2)。
图2:QueryCoord 为每个 QueryNode 分配封存段和日志通道示意。QueryNode1 加载了 S1、S3 等历史段,并订阅了 DMChannel1;QueryNode2 加载了 S2、S4,并订阅 DMChannel2。最终每个节点在本地同时拥有历史数据和增量流数据。
-
QueryProxy 将查询推送至查询通道:客户端的查询请求已经写入 Pulsar 查询通道后,各 QueryNode 会监听此通道并取出查询消息。在消息中包含了执行时间戳等信息。Milvus 首先比较当前服务时间戳(service_ts)与查询消息中的保证时间戳(guarantee_ts)。只有当
service_ts >= guarantee_ts
时,才执行查询;否则该查询消息会暂时“悬挂”直到达到条件。 -
QueryNode 执行检索并归约:当查询消息符合执行条件后,每个 QueryNode 并行地在本地的历史数据和增量数据上执行搜索。这包括对已经加载的封存段(离线历史数据)和正接收写入的增长段(在线流数据)分别进行搜索。由于两者可能重叠,QueryNode 内部会做一次“本地归约”(Local Reduce)去重。搜索完成后,各 QueryNode 将本地结果发布到结果通道(ResultChannel)。此时,结果消息中包含了本节点所搜索的封存段和通道信息。
-
Proxy 聚合返回结果:Proxy 订阅结果通道,收集来自所有 QueryNode 的结果集。收到后,Proxy 会做一次全局归约(Global Reduce),去除不同节点间的重复结果。为确保结果完整性,Proxy 通过结果消息中的字段跟踪是否所有封存段和通道的数据都已返回。当条件满足后,Proxy 将最终合并排序后的 Top-K 结果返回给客户端。整个流程中,Proxy 仅负责路由和汇总;QueryCoord 只在查询准备阶段调度;真正的搜索计算由各 QueryNode 执行。
模块间调用链示意
以下伪代码描述了上述交互的主要调用顺序(忽略错误处理和并发细节):
// Proxy 接收到客户端的搜索请求
func ProxyHandleSearch(req QueryRequest) {// 1. 向 RootCoord 请求集合元数据(例如分片、索引信息)collectionInfo := RootCoord.GetCollectionInfo(req.CollectionID)// 2. 将查询信息封装为消息写入 Pulsar 查询通道Pulsar.Publish(QueryChannel, req)// 3. 监听结果通道,收集 QueryNode 返回结果results := collectResultsFromChannel(ResultChannel)// 4. 对所有 QueryNode 的结果做归约并返回merged := globalReduce(results)return merged
}// QueryCoord 预加载流程(LoadCollection)
func QueryCoordLoad(collectionID) {// 从 DataCoord 获取所有已封存段和检查点segments := DataCoord.GetRecoveryInfo(collectionID)// 选择分配策略(按段或按通道)// 将 segment 列表分配给不同的 QueryNodeassignments := allocateSegmentsToNodes(segments)// 通知各 QueryNode 加载数据或订阅通道for each node, segs in assignments:node.LoadSegments(segs)
}// QueryNode 搜索处理
func QueryNodeOnQueryMessage(msg QueryRequest) {if serviceTS < msg.GuaranteeTS {// 不满足时间条件,等待return}// 在本地封存段和增长段上并行检索resultsOffline := searchSealedSegments(msg.Vector, req)resultsStream := searchGrowingSegments(msg.Vector, req)localResults := localReduce(resultsOffline, resultsStream)// 发布到结果通道Pulsar.Publish(ResultChannel, localResults)
}
该调用链中,关键的是 Proxy、QueryCoord、QueryNode 三层协作:Proxy 负责接收请求与返回结果,QueryCoord 负责查询准备与调度,QueryNode 负责具体检索执行。
代码目录与主要文件(参考)
Milvus 仓库 internal
目录下包含各组件的实现子目录,其中与查询相关的主要路径有:
-
internal/proxy/
:Proxy 服务实现,包括proxy.go
(Proxy 结构体、接口)、task_search.go
(SearchTask 实现)等。 -
internal/querycoord/
:QueryCoord 服务实现,包括query_coord.go
(QueryCoord 结构体、接口)、segment_allocator/
、channel_allocator/
等调度逻辑。 -
internal/querynodev2/
:QueryNode 服务实现,包括server.go
(QueryNode 结构体、接口)、flowgraph/
(流式处理)、delegate/
(查询代理)、segments/
(本地 segment 管理)等。
下图为 Milvus 源码(部分)目录结构示意:
internal/
├── proxy/
│ ├── proxy.go # ProxyComponent 实现(types.ProxyComponent)
│ ├── task_search.go # SearchTask 逻辑
│ └── ...
├── querycoord/
│ ├── query_coord.go # QueryCoordComponent 实现(types.QueryCoordComponent)
│ ├── segment_allocator/
│ └── channel_allocator/
│ └── ...
├── querynodev2/
│ ├── server.go # QueryNodeComponent 实现(types.QueryNodeComponent)
│ ├── flowgraph/ # 增量数据流式处理
│ ├── delegate/ # 查询请求分发
│ ├── segments/ # 本地 Segment 管理
│ └── ...
└── datacoord/ # DataCoord 服务(管理存储相关)└── ...
从以上目录可见,每个组件都用一个 Server 结构体启动 GRPC 服务,并持有对应的客户端接口,如 ProxyServer 包含 rootCoordClient
、queryCoordClient
等。组件间通过 gRPC 和 Pulsar 消息进行协同,如 Proxy 调用 QueryCoord.LoadCollection 获取 segment 信息,QueryCoord 调用 DataCoord.GetRecoveryInfo 获取段信息,QueryNode 从 Pulsar 查询通道接收 Search 请求等。
参考资料
-
Milvus 官方文档与博客:《Milvus 架构概览》《QueryCoord 相关配置》《实时查询解析》等。
-
Zilliz/Milvus GitHub 代码:
internal/proxy
、internal/querycoord
、internal/querynodev2
包。 -
开源社区文章:Milvus 源码解析博客(见 “SearchTask”、“实时查询” 部分)。