本文主要由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”、“實時查詢” 部分)。