Milvus 數據處理流程解剖

編者按:本文詳細解剖 Milvus 2.0 主要的數據處理流程以及訪問接入層( Access Layer)。

  • 主要數據處理流程

    • MsgStream 接口

    • 寫路徑

    • 讀路徑

    • DDL 流程

    • 建索引流程

  • Access Layer 代碼


主要數據處理流程

Milvus 2.0 中主要的數據處理流程包括讀寫路徑、建表等數據定義操作以及向量索引構建流程。

《前所未有的 Milvus 源碼架構解析》一文中曾提到 Milvus 2.0 依賴 Pub/sub 系統來做日誌的存儲和持久化。Pub/sub 系統是指類似 Kafka 或者 Pulsar 的消息隊列,採用該系統後,其他系統的角色就變成了日誌的消費者,以保證 Milvus 本身是沒有狀態的,進而提升故障恢復速度。同時可以依賴 Kafka 或者 Pulsar 來提升數據的可靠性。Pub/sub 系統的引入可以保證系統的擴展性,Milvus 可以與更多的系統做集成,而和這些交互的重要接口封裝就是MsgStream。

文章中出現的諸如 Collection、Shard、Partition 和 Segment 等概念,本文不再贅述。如果讀者朋友對這些概念不瞭解,請參考《前所未有的 Milvus 源碼架構解析》這篇綜述性文章。

MsgStream 接口

Milvus 2.0 中重要的接口之一就是 MsgStream。

MsgStream 的接口定義如上圖左半部分所示。通過 Start 和 Close 可以開啓和關閉 MsgStream 對象的後臺協程。一個 MsgStream 對象在被 Start 之後,後臺的 Go 協程會去處理將數據寫入到消息存儲系統裏或者從消息存儲系統訂閱和讀取數據等邏輯。

MsgStream 既可以作爲生產者(producer) 也可以作爲消費者(consumer)。AsProducer 接口將該 MsgStream 對象定義爲 producer。AsConsumer 接口將 MsgStream 定義爲 consumer。注意到這兩個接口都有名爲 channels 的參數。前面我們提到 collection 在創建時可以指定 shard 的數目。一個 shard 代表一個 virtual channel,每個 collection 可以有多個 virtual channel。對於 collection 的每一個 virutal channel 在消息存儲系統中都有一個 channel 與其對應,爲了做區分,我們將消息存儲系統中的 channel 稱之爲 physical channel。AsProducer 和 AsConsumer 的 channels 參數代表的就是消息存儲系統中 physical channel 的名字列表。這些 channels 限定了 MsgStream 對象的寫入或者消費的 physical channel 的範圍。

通過 Produce 方法將數據寫入到消息存儲系統中的 physical channel 裏。有兩種寫入模式:單一寫入模式和廣播寫入模式。單一寫入模式是通過寫入數據中 entity 的主鍵 hash 值確定的 shard(virtual channel)進而決定數據寫入的 physical channel。廣播寫入模式是指將數據寫入到 channels 參數指定的所有的 physical channel 裏。

Consume() 是一個阻塞式的接口。調用這個接口時如果 physical channel 裏沒有數據,協程會阻塞。

Chan() 返回的是 Go 語言定義的 channel,目的是提供一種非阻塞的消費數據的方式。比如使用 select 語句可以做到有數據可讀時纔會進入相應的數據讀取和處理邏輯裏,而當無數據可讀時協程可以去處理其他邏輯而不用阻塞等待。

Seek() 服務於宕機恢復。消費者消費到某個位置之後,會記錄當前消費到的位置。這個位置需要寫到 meta 裏的,當新起一個節點來接管工作後,它是可以調這個 Seek 接口,傳入宕機前消費的位置,接着上次的位置再接着消費。

寫路徑

接下來我們來看一下寫路徑。這裏寫路徑裏流經的是寫入到 collection 中的數據。寫入的數據既可以是 insert 消息也可以是 delete 消息。這些消息(entity) 會被寫入到不同的 virtual channel(shard)裏。對於這些 virtual channel,我們也稱之爲 DmChannels(data manipulation channels).

需要指出的是,不同的 collection 可能會共享消息存儲系統中的 physical channel。一個 collection 在創建時可以指定很多個 shard(virtual channel),因而該 collection 的數據也就會流經消息存儲中的多個 physical channel。這樣有個好處是可以在寫的時候可以大量並通過依賴消息存儲系統的併發的特性提高寫吞吐。我們的初步設定是每一個 collection 可以在底層複用相同的物理 channel,這樣物理 channel 維持在一個固定大小,然後 collection 級別的 virtual channel 可以很多,而且不同 collection 之間也可以共用 physical channel。

這裏需要指出的是 collection 在創建時不僅指定了 shard 的個數,也會確定 virutal channel 和消息存儲中 physical channel 之間的映射關係。

在寫路徑中,訪問接入層 proxy 作爲生成者會通過 MsgStream 對象的 produce 接口將數據寫入到消息存儲系統裏,同時 data node 作爲消費者消費數據之後,按照時間窗口以及每個 segment 的閾值大小,定期將這些消費到的數據轉換並存到對象存儲中。同時存儲的路徑是一個 meta 信息,需要通過 RPC 去通知 data coordinator,data coordinator 將這些 Binlog paths 記錄到 etcd 裏。

既然不同的 collection 可能共用消息存儲系統的 physical channel,那麼 data node/query node 消費數據是需要區分該 channel 中數據的歸屬問題。因此引入 flowgraph 這個對象,它可以負責對 physical channel 中的數據根據 collection 的 ID 做過濾。可以認爲一個 flowgraph 負責相應 collection 中的一個 shard(virtual channel)中的數據流。

什麼時候創建 MsgStream 呢?對於 proxy 來說,它是在處理 insert 請求時創建的。當 proxy 收到一個數據寫入請求時,它首先詢問 root coordinator 拿到 virtual channel 和 physical channel 的映射關係,然後構造一個 MsgStream 對象。

作爲消費者,data node 創建 MsgStream 對象的時機則在 data node 啓動之後。data coordinator 將 collection 的 virtual channel 在不同的 data node 做好分配後,會將分配信息寫入 etcd。data node 啓動之後可以讀取這個 etcd 中的分配信息,就可知其負責的 virutal channel 及相應的 physical channel 然後創建 MsgStream 對象。以上圖右半邊所示, data node 1 負責 V1、C1、V2、C2,data node 2 負責 V3、C5、V4、C6。

讀路徑

Milvus 是一個典型的 MPP 架構的系統。每個 query node 的搜索是並行執行的,proxy 聚合最終的結果返回給客戶端。在讀路徑中,查詢請求通過 DqRequestChannel 進行廣播,而查詢結果通過 gRPC 彙總到 proxy。

proxy 作爲生產者,將查詢請求寫入到 DqRequestChannel 中。query node 消費 DqRequestChannel 的方式比較特殊:每個 query node 的都會訂閱這個 channel,這樣該 channel 中的每條消息會廣播給所有的 query node。

query node 收到請求之後,本地做查詢,並以 segment 爲粒度做一次聚合,將聚合後的結果通過 gRPC 發送給相應的 proxy 。需要指出的是,在查詢請求裏有唯一的 ProxyID 標識查詢的發起方。query node 據此將不同查詢結果路由到相應的 proxy。

proxy 判定收集到所有 query node 的查詢結果後,做一次全局的聚合得到最終的查詢結果,並將查詢結果返回給客戶端。需要指出的是在查詢請求和查詢結果裏有相同且唯一 requestID 可以標記查詢本身,proxy 據此區分哪些查詢結果歸屬於同一個查詢請求。

Milvus 2.0 設計要求是流批統一攝取的,query node 等查詢節點也需要從消息存儲中攝取實時流數據。因此 query node 同樣需要引入 flowgraph 對象對數據做過濾,以對歸屬不同表的數據做隔離。

query node 是什麼時候創建這個 MsgStream 對象的呢?

在 Milvus 的用戶側,提供了一個 load collection 的操作接口,其含義分兩部分:第一是將批數據從對象存儲中加載到 query node 中;第二是對接到 MsgStream 裏能夠接收流式數據。這樣可以保證數據的完整性。表只有經過 load 之後才能執行讀操作。

proxy 收到一個關於表的 load 請求後,會將該請求轉發到 query coordinator。quary coordinator 來決策 shard(virtual channel)在不同的 query node 上的分配方式。這種分配信息以函數調用或者 RPC 的方式發送給 query node。query node 收到分配信息後,創建對應的 MsgStream 對象來消費數據。這些分配信息包括 vitural channel 名字及其和相應 physical channel 的映射關係。

在 query node 裏,查詢結果是來自兩部分:第一部分是批量數據查詢得到的結果。這些批量數據是從對象存儲中加載所有的 sealed segment。第二部分來自於從消息存儲中消費的實時數據的查詢結果。這些實時數據也會形成一些 segment,這些 segment 被稱爲 growing segment。query node 需要對這兩部分查詢結果做一個本地的聚合。本地聚合之後再將結果發送給相應的 proxy。

DDL流程

DDL 表示的是 data definition language。針對元數據操作的請求也分爲讀和寫兩類,不過處理這些請求的流程是一樣的,並不區分讀寫。讀類型的元數據操作包括,查詢表的 schema、查詢索引信息等;寫類型的元數據操作包括創建表、刪表、建索引和刪除索引等等。

客戶端將 DDL 請求發送至 proxy, proxy 需要對這些請求做一個定序並打上時間戳,然後將請求轉發到 root coordinator 並等待其返回結果。這裏的時間戳指的是 root coordinator 分配的全局混合時間戳。這意味着對於每個 DDL 的請求,proxy 都會從 root coordinator 申請一個時間戳。proxy 對於每個 DDL 請求的處理是串行執行的,每次只處理一個 DDL 請求,當前 DDL 請求處理完並且收到反饋結果後纔會執行下一個 DDL 請求。proxy 收到 root coordinator 的結果後,將其返回給客戶端。

root coordinator 主要做的工作就是對請求做一些動態檢查,檢查通過後執行相應的邏輯。

需要重點注意的是,root coordinator 在設計上要確保 DDL 操作按照時間戳升序順序執行。

舉個例子,我們可以看到上圖裏,root coordinator 的 task queue 包括 k 個操作,分別是 "ddl1、ddl3…… ddlk",數字代表時間戳。root coordinator 會對該 task queue 中的請求按照時間戳遞增的順序依次執行,並且記錄當前已經執行完畢的最大時間戳。在分佈式部署方式下,proxy 和 root coordinator 之間的通訊是通過 gRPC,兩個獨立組件,請求到達的順序不一定嚴格按照時間先後。假設當前 task queue 中執行完畢的最大時間戳爲 k,來自 proxy1 的 "ddl(K-1)" 到達時發現 "ddlK" 已經被執行了,那這個時候 "ddl(k-1)" 就會被拒絕進入 task queue,否則就會打破所有請求按照時間戳遞增順序執行的約定。而來自 proxy2 的 請求 "ddl(k+5)" 則被允許進入 task queue 中。

建索引流程

建索引的過程在 Milvus 系統內部來看,是一個長期的異步的過程。當客戶端發起建索引的請求之後,proxy 收到該請求首先做一些靜態檢查,通過後將該請求轉發到 root coordinator。root coordinator 將這些建索引的請求持久化到 KV 存儲中,就立馬返回給 proxy,proxy 返回給 SDK。既然是異步,那就需要有狀態,以便需要查詢索引建立的進度或者狀態。

在用戶的視角上,建索引針對的是向量 field,而向量 field 的數據在物理上是由一個個 segment 組成的。建索引是在 segment 粒度上進行的,因此 root coordinator 需要向 index coordinator 發起針對每個 sealed segment 的建索引請求。

上面這張圖是每個 segment 其上的 Index 狀態變化的一個過程。index coordinator 收到 root coordinator 發來的建索引的請求後,首先會將該任務持久化到 meta store 中。索引任務的初始狀態是  Unissued。index coordinator 維護一個記錄每個 index node 負載的優先級隊列,選擇一個負載比較低的 index node,將這個任務發送到 index node 去做。index node 建完索引後會把成功/失敗狀態寫入到 meta store 中。index coordinator 通過感知 meta store 中索引狀態的變化。如果由於系統資源或者 index node 失活等可恢復的失敗原因,index coordinator 會重新觸發這個流程,選擇另外一個 index node 重新做索引構建的任務。

index coordinator 還需要負責回收那些被標記刪除的索引任務及其相應的索引文件。這裏我們可以看到 一個名爲 recycleIndexFiles 接口,它的主要作用是將被標記刪除的索引任務相應的索引文件從對象存儲中刪除。

當客戶端發送索引的 drop 請求之後, root coordinator 會標記這個索引被 drop,然後立馬返回給客戶端。索引的 drop 也是一個異步的過程。root coordinator 通知 index coordinator 包含屬性 IndexID 的索引需要被標記刪除。每個 segment 的索引都記錄屬性 IndexID,它唯一標識表中向量 field 上的索引。index coordinator 根據這個 IndexID 爲過濾條件,將所有索引任務中匹配到屬性 IndexID 的索引任務標記爲刪除。index coordinator 有一個後臺的協程,逐漸將所有標記爲刪除的任務對應的索引文件從對象存儲中刪除,當該索引任務對應的 索引文件被全部刪除後,再將改索引任務的 meta 信息從 meta store 中移除。

Access Layer 代碼

proxy 把所有的請求分爲三類:

  • DdRequest(data definition request)
  • DmRequest(data manipulation request)
  • DqRequest(data quary request)

proxy 針對每個具體的請求封裝一個 task 類,實現通用的 preExecute、Execute、postExecute 三個標準流程,在標準流程裏,完成靜態檢查、預處理等。同時,proxy 會對每一個請求分配時間戳和全局 ID 標記請求。上方圖中右邊展示了 proxy 和其他系統所有主要組件的交互,以及交互中的數據。

proxy 的調度邏輯如下:proxy 把請求分爲三類,每一類都有一個對應的 task queue;來自 SDK 的請求都會被封裝成一個 task,並放入對應的 task queue 裏;針對不同的 task queue 後臺有不同的調度邏輯。

對於 data definition request 類型請求的隊列,其中的請求是串行執行的,流水線主要分爲五個步驟。首先是進隊(enqueue)操作,在這裏需要設置一個時間戳,給這個操作定序,同時設置 ID 唯一標識該請求,接着把它放入到一個待辦的 unissuted tasks 列表裏。而該 task queue 的 schedule 就發生在步驟 2 和 3 之間。

schedule 的過程就是將一個任務從 unissuted tasks 取出放置到 active task 列表中。當任務放置到 active task 之後,它裏面的每個任務都會順序執行 preExecute、Execute、postExecute 三個操作,最後 從 active task 列表中刪除。任何一個請求任務需要完整地處理完,其中任何一個環節發生錯誤,都會提前退出流水線並返回錯誤信息。

DmTaskQueue 的特點就是它可以併發執行。第一個 enqeue 的步驟和 DdTaskQueue 中 task 的 enque 邏輯相同,也會經歷設置時間戳、設置 ID 等步驟,區別點在於步驟二和步驟三,針對該 DmTaskQueue 的調度是一次取出多個任務,每個協程處理一個任務的後續流水線步驟。

proxy 需要緩存一些重要的對象和數據,Cache 功能的實現位於 GlobalMetaCache 這個類。它主要緩存兩大部分數據,第一部分是 name 到 ID 映射,客戶端看到的是 name 而系統中下游看到的都是相應對象的 ID,第二部分是每個 collection 的 schema 等重要元信息。proxy 需要大量做一些前期的靜態檢查,因此爲了避免經常向 root coordinator 詢問元數據,需要添加緩存。當然 Cache 也應該有清理機制,當 root coordinator 執行了一個表的元信息的更改操作,會通知所有 proxy 其上關於該表的元信息緩存失效。

ChannelMgr 這個類主要維護了 virtual channel 到 physical channel 的映射,以及管理相應的 MsgStream 對象。上圖右側主要列出了 ChannelMgr 的主要接口。

曹鎮山

Zilliz 高級研發

Milvus 項目 maintainer

畢業於華中科技大學,主要興趣方向包括分佈式系統、數據庫和大數據處理。在 Milvus 社區主要負責多個核心模塊的編寫和維護,是 Milvus 項目的 maintainer。除了敲代碼,他也喜歡鑽研心理學(尤其是犯罪心理學和積極心理學)和爬山。他的 Github 賬號:czs007。

視頻版講解請戳 👇


Z i l l i z  
Zilliz  Milvus Milvus  LF AI & Data  用。


閱讀原文,解鎖更多應用場景



本文分享自微信公衆號 - ZILLIZ(Zilliztech)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章